Skip to main content

gap/
apply.rs

1//! Stateless apply engine — pure function that transforms artifacts.
2//!
3//! `apply(artifact?, envelope) → (Artifact, Handle)`
4//!
5//! Synthesize: creates artifact from body. Edit: applies ops to existing artifact.
6//! Both return the artifact and a handle envelope.
7
8use anyhow::{bail, Context, Result};
9
10use crate::aap::{
11    Artifact, EditOp, Envelope, HandleContentItem, Name, OpType, Meta, SynthesizeContentItem,
12    Target, TargetInfo, PROTOCOL_VERSION,
13};
14
15// ── Resolve trait ────────────────────────────────────────────────────────
16
17/// Content resolution — how to find and replace targeted regions.
18pub trait Resolve {
19    type Content: Clone;
20
21    fn find_by_id(&self, content: &Self::Content, id: &str) -> Result<(usize, usize)>;
22    fn find_by_id_inclusive(&self, content: &Self::Content, id: &str) -> Result<(usize, usize)>;
23    fn find_by_pointer(&self, content: &Self::Content, pointer: &str) -> Result<(usize, usize)>;
24    fn replace(&self, content: &mut Self::Content, start: usize, end: usize, replacement: &str);
25    fn insert(&self, content: &mut Self::Content, pos: usize, text: &str);
26    fn delete(&self, content: &mut Self::Content, start: usize, end: usize);
27    fn to_string(&self, content: &Self::Content) -> String;
28    fn from_string(&self, s: &str) -> Self::Content;
29}
30
31// ── Text resolver ────────────────────────────────────────────────────────
32
33/// Text-based resolver using `<aap:target id="...">` markers.
34pub struct TextResolver {
35    pub format: String,
36}
37
38impl Resolve for TextResolver {
39    type Content = String;
40
41    fn find_by_id(&self, content: &String, id: &str) -> Result<(usize, usize)> {
42        crate::markers::find_target_range(content, id, &self.format)
43    }
44
45    fn find_by_id_inclusive(&self, content: &String, id: &str) -> Result<(usize, usize)> {
46        crate::markers::find_target_range_inclusive(content, id, &self.format)
47    }
48
49    fn find_by_pointer(&self, content: &String, pointer: &str) -> Result<(usize, usize)> {
50        let value: serde_json::Value = serde_json::from_str(content)
51            .context("pointer targeting requires valid JSON content")?;
52        let serialized = serde_json::to_string_pretty(&value)?;
53        let _ = value
54            .pointer(pointer)
55            .with_context(|| format!("pointer not found: {pointer}"))?;
56        Ok((0, serialized.len()))
57    }
58
59    fn replace(&self, content: &mut String, start: usize, end: usize, replacement: &str) {
60        *content = format!("{}{}{}", &content[..start], replacement, &content[end..]);
61    }
62
63    fn insert(&self, content: &mut String, pos: usize, text: &str) {
64        *content = format!("{}{}{}", &content[..pos], text, &content[pos..]);
65    }
66
67    fn delete(&self, content: &mut String, start: usize, end: usize) {
68        *content = format!("{}{}", &content[..start], &content[end..]);
69    }
70
71    fn to_string(&self, content: &String) -> String {
72        content.clone()
73    }
74
75    fn from_string(&self, s: &str) -> String {
76        s.to_string()
77    }
78}
79
80// ── Apply engine ─────────────────────────────────────────────────────────
81
82fn extract_synthesize_item(envelope: &Envelope) -> Result<SynthesizeContentItem> {
83    serde_json::from_value(
84        envelope
85            .content
86            .first()
87            .context("synthesize: empty content array")?
88            .clone(),
89    )
90    .context("synthesize: failed to parse content item")
91}
92
93fn build_handle_envelope(artifact: &Artifact) -> Result<Envelope> {
94    let target_ids = crate::markers::extract_targets(&artifact.body, &artifact.format);
95    let targets = if target_ids.is_empty() {
96        None
97    } else {
98        Some(target_ids.into_iter().map(|id| TargetInfo {
99            id,
100            label: None,
101            accepts: None,
102        }).collect())
103    };
104    let handle = HandleContentItem {
105        id: artifact.id.clone(),
106        version: artifact.version,
107        token_count: Some(artifact.body.len() as u64 / 4), // rough estimate
108        state: None,
109        content: None,
110        targets,
111    };
112    Ok(Envelope {
113        protocol: PROTOCOL_VERSION.to_string(),
114        id: artifact.id.clone(),
115        version: artifact.version,
116        name: Name::Handle,
117        meta: Meta {
118            format: Some(artifact.format.clone()),
119            tokens_used: None,
120            checksum: None,
121            state: None,
122        },
123        content: vec![
124            serde_json::to_value(handle).context("failed to serialize handle")?
125        ],
126    })
127}
128
129/// Stateless apply: `f(artifact?, envelope) → (Artifact, Handle)`.
130pub fn apply(artifact: Option<&Artifact>, envelope: &Envelope) -> Result<(Artifact, Envelope)> {
131    let format = envelope
132        .meta
133        .format
134        .as_deref()
135        .unwrap_or("text/html");
136
137    let resolver = TextResolver {
138        format: format.to_string(),
139    };
140
141    let result_artifact = match envelope.name {
142        Name::Synthesize => {
143            let item = extract_synthesize_item(envelope)?;
144            Artifact {
145                id: envelope.id.clone(),
146                version: envelope.version,
147                format: format.to_string(),
148                body: item.body,
149            }
150        }
151        Name::Edit => {
152            let art = artifact.context("edit requires a base artifact")?;
153            let ops: Vec<EditOp> = envelope
154                .content
155                .iter()
156                .map(|v| serde_json::from_value(v.clone()))
157                .collect::<std::result::Result<Vec<_>, _>>()
158                .context("edit: failed to parse content items")?;
159
160            let has_pointer = ops.iter().any(|op| matches!(op.target, Target::Pointer(_)));
161            let body = if has_pointer {
162                apply_edit_pointers(&art.body, &ops)?
163            } else {
164                apply_edit(&resolver, &art.body, &ops)?
165            };
166
167            Artifact {
168                id: envelope.id.clone(),
169                version: envelope.version,
170                format: format.to_string(),
171                body,
172            }
173        }
174        Name::Handle => {
175            bail!("handle is an output envelope, not an input operation")
176        }
177    };
178
179    let handle = build_handle_envelope(&result_artifact)?;
180    Ok((result_artifact, handle))
181}
182
183/// Apply edit operations using the Resolve trait (ID-based targeting).
184pub fn apply_edit<R: Resolve<Content = String>>(
185    resolver: &R,
186    base: &str,
187    operations: &[EditOp],
188) -> Result<String> {
189    let mut content = resolver.from_string(base);
190
191    for (i, op) in operations.iter().enumerate() {
192        // All ops target content between markers (exclusive range).
193        // Delete clears the content but preserves markers per spec §4.2.
194        let (start, end) = resolve_target(resolver, &content, &op.target)
195            .with_context(|| format!("operation {i}: target not found"))?;
196        match op.op {
197            OpType::Replace => {
198                let replacement = op.content.as_deref().unwrap_or("");
199                resolver.replace(&mut content, start, end, replacement);
200            }
201            OpType::Delete => {
202                resolver.delete(&mut content, start, end);
203            }
204            OpType::InsertBefore => {
205                let text = op.content.as_deref().unwrap_or("");
206                resolver.insert(&mut content, start, text);
207            }
208            OpType::InsertAfter => {
209                let text = op.content.as_deref().unwrap_or("");
210                resolver.insert(&mut content, end, text);
211            }
212        }
213    }
214
215    Ok(resolver.to_string(&content))
216}
217
218fn resolve_target<R: Resolve<Content = String>>(
219    resolver: &R,
220    content: &String,
221    target: &Target,
222) -> Result<(usize, usize)> {
223    match target {
224        Target::Id(id) => resolver.find_by_id(content, id),
225        Target::Pointer(pointer) => resolver.find_by_pointer(content, pointer),
226    }
227}
228
229/// Apply edit operations using JSON Pointer targeting.
230fn apply_edit_pointers(base: &str, operations: &[EditOp]) -> Result<String> {
231    let mut value: serde_json::Value =
232        serde_json::from_str(base).context("pointer targeting requires valid JSON content")?;
233
234    for (i, op) in operations.iter().enumerate() {
235        let pointer = match &op.target {
236            Target::Pointer(p) => p.as_str(),
237            _ => bail!("operation {i}: expected pointer target"),
238        };
239
240        match op.op {
241            OpType::Replace => {
242                let content = op.content.as_deref().context("replace requires content")?;
243                let new_val: serde_json::Value =
244                    serde_json::from_str(content).context("content must be valid JSON")?;
245                let target = value
246                    .pointer_mut(pointer)
247                    .with_context(|| format!("pointer not found: {pointer}"))?;
248                *target = new_val;
249            }
250            OpType::Delete => {
251                let (parent_ptr, key) = split_pointer(pointer).context("cannot delete root")?;
252                let parent = value
253                    .pointer_mut(&parent_ptr)
254                    .with_context(|| format!("parent not found: {parent_ptr}"))?;
255                remove_child(parent, &key)?;
256            }
257            OpType::InsertBefore | OpType::InsertAfter => {
258                let content = op.content.as_deref().context("insert requires content")?;
259                let new_val: serde_json::Value =
260                    serde_json::from_str(content).context("content must be valid JSON")?;
261                let (parent_ptr, key) = split_pointer(pointer).context("cannot insert at root")?;
262                let parent = value
263                    .pointer_mut(&parent_ptr)
264                    .with_context(|| format!("parent not found: {parent_ptr}"))?;
265                let arr = parent
266                    .as_array_mut()
267                    .context("insert requires array parent")?;
268                let index: usize = key.parse().context("insert requires numeric array index")?;
269                let insert_at = if op.op == OpType::InsertAfter { index + 1 } else { index };
270                arr.insert(insert_at, new_val);
271            }
272        }
273    }
274
275    serde_json::to_string_pretty(&value).context("failed to re-serialize JSON")
276}
277
278fn split_pointer(pointer: &str) -> Result<(String, String)> {
279    if pointer.is_empty() || !pointer.starts_with('/') {
280        bail!("invalid JSON Pointer: {pointer:?}");
281    }
282    match pointer.rfind('/') {
283        Some(0) => Ok(("".to_string(), pointer[1..].to_string())),
284        Some(pos) => Ok((pointer[..pos].to_string(), pointer[pos + 1..].to_string())),
285        None => bail!("invalid JSON Pointer: {pointer:?}"),
286    }
287}
288
289fn remove_child(parent: &mut serde_json::Value, key: &str) -> Result<()> {
290    let unescaped = key.replace("~1", "/").replace("~0", "~");
291    if let Some(obj) = parent.as_object_mut() {
292        if obj.remove(&unescaped).is_none() {
293            bail!("key not found: {unescaped}");
294        }
295    } else if let Some(arr) = parent.as_array_mut() {
296        let index: usize = unescaped.parse().with_context(|| format!("expected array index: {unescaped}"))?;
297        if index >= arr.len() { bail!("array index out of bounds: {index}"); }
298        arr.remove(index);
299    } else {
300        bail!("parent is neither object nor array");
301    }
302    Ok(())
303}
304
305#[cfg(test)]
306mod tests {
307    use super::*;
308
309    fn synth_env(id: &str, version: u64, body: &str) -> Envelope {
310        Envelope {
311            protocol: PROTOCOL_VERSION.to_string(),
312            id: id.to_string(),
313            version,
314            name: Name::Synthesize,
315            meta: Meta {
316                    format: Some("text/html".to_string()),
317                tokens_used: None, checksum: None, state: None,
318            },
319            content: vec![serde_json::json!({ "body": body })],
320        }
321    }
322
323    fn edit_env(id: &str, version: u64, ops: Vec<EditOp>) -> Envelope {
324        Envelope {
325            protocol: PROTOCOL_VERSION.to_string(),
326            id: id.to_string(),
327            version,
328            name: Name::Edit,
329            meta: Meta {
330                    format: Some("text/html".to_string()),
331                tokens_used: None, checksum: None, state: None,
332            },
333            content: ops.iter().map(|o| serde_json::to_value(o).unwrap()).collect(),
334        }
335    }
336
337    fn id_target(id: &str) -> Target { Target::Id(id.to_string()) }
338    fn ptr_target(p: &str) -> Target { Target::Pointer(p.to_string()) }
339
340    #[test]
341    fn test_synthesize() {
342        let env = synth_env("test", 1, "<div>hello</div>");
343        let (art, handle) = apply(None, &env).unwrap();
344        assert_eq!(art.body, "<div>hello</div>");
345        assert_eq!(art.id, "test");
346        assert_eq!(art.version, 1);
347        assert_eq!(handle.name, Name::Handle);
348    }
349
350    #[test]
351    fn test_edit_replace_by_id() {
352        let env = synth_env("t", 1, r#"<aap:target id="rev">$12,340</aap:target>"#);
353        let (art, _) = apply(None, &env).unwrap();
354
355        let edit = edit_env("t", 2, vec![EditOp {
356            op: OpType::Replace,
357            target: id_target("rev"),
358            content: Some("$15,720".to_string()),
359        }]);
360        let (art2, _) = apply(Some(&art), &edit).unwrap();
361        assert!(art2.body.contains("$15,720"));
362        assert!(!art2.body.contains("$12,340"));
363        assert!(art2.body.contains(r#"<aap:target id="rev">"#));
364    }
365
366    #[test]
367    fn test_edit_delete_by_id() {
368        // Spec §4.2: "For delete, the content between markers is removed
369        // (markers are preserved). Markers themselves are never moved or
370        // removed by edit operations."
371        let env = synth_env("t", 1, r#"before<aap:target id="tmp">remove</aap:target>after"#);
372        let (art, _) = apply(None, &env).unwrap();
373
374        let edit = edit_env("t", 2, vec![EditOp {
375            op: OpType::Delete, target: id_target("tmp"), content: None,
376        }]);
377        let (art2, _) = apply(Some(&art), &edit).unwrap();
378        assert_eq!(art2.body, r#"before<aap:target id="tmp"></aap:target>after"#);
379    }
380
381    #[test]
382    fn test_edit_insert_after() {
383        let env = synth_env("t", 1, r#"<aap:target id="list">item1</aap:target>"#);
384        let (art, _) = apply(None, &env).unwrap();
385
386        let edit = edit_env("t", 2, vec![EditOp {
387            op: OpType::InsertAfter, target: id_target("list"),
388            content: Some(", item2".to_string()),
389        }]);
390        let (art2, _) = apply(Some(&art), &edit).unwrap();
391        assert!(art2.body.contains("item1, item2"));
392    }
393
394    #[test]
395    fn test_nested_targets() {
396        let body = r#"<aap:target id="outer"><h2>Stats</h2><aap:target id="val">100</aap:target></aap:target>"#;
397        let (art, _) = apply(None, &synth_env("t", 1, body)).unwrap();
398
399        let edit = edit_env("t", 2, vec![EditOp {
400            op: OpType::Replace, target: id_target("val"),
401            content: Some("200".to_string()),
402        }]);
403        let (art2, _) = apply(Some(&art), &edit).unwrap();
404        assert!(art2.body.contains("200"));
405        assert!(art2.body.contains("<h2>Stats</h2>"));
406    }
407
408    #[test]
409    fn test_target_serde_roundtrip() {
410        let t = Target::Id("revenue".to_string());
411        let json = serde_json::to_string(&t).unwrap();
412        let parsed: Target = serde_json::from_str(&json).unwrap();
413        assert!(matches!(parsed, Target::Id(ref s) if s == "revenue"));
414
415        let op = EditOp {
416            op: OpType::Replace,
417            target: Target::Id("rev".to_string()),
418            content: Some("new".to_string()),
419        };
420        let json = serde_json::to_string(&op).unwrap();
421        let parsed: EditOp = serde_json::from_str(&json).unwrap();
422        assert!(matches!(parsed.target, Target::Id(ref s) if s == "rev"));
423    }
424
425    #[test]
426    fn test_edit_from_json_string() {
427        let json = r#"{
428            "protocol": "aap/0.1", "id": "x", "version": 2, "name": "edit",
429            "meta": {"format": "text/html"},
430            "content": [{"op": "replace", "target": {"type": "id", "value": "rev"}, "content": "new"}]
431        }"#;
432        let env: Envelope = serde_json::from_str(json).unwrap();
433        let art_body = r#"<aap:target id="rev">old</aap:target>"#;
434        let (art, _) = apply(None, &synth_env("x", 1, art_body)).unwrap();
435        let (art2, _) = apply(Some(&art), &env).unwrap();
436        assert!(art2.body.contains("new"));
437        assert!(!art2.body.contains("old"));
438    }
439
440    #[test]
441    fn test_pointer_replace() {
442        let base = r#"{"name": "Alice", "age": 30}"#;
443        let (art, _) = apply(None, &synth_env("t", 1, base)).unwrap();
444
445        let mut edit = edit_env("t", 2, vec![EditOp {
446            op: OpType::Replace, target: ptr_target("/name"),
447            content: Some(r#""Bob""#.to_string()),
448        }]);
449        edit.meta.format = Some("application/json".to_string());
450        let (art2, _) = apply(Some(&art), &edit).unwrap();
451        let parsed: serde_json::Value = serde_json::from_str(&art2.body).unwrap();
452        assert_eq!(parsed["name"], "Bob");
453        assert_eq!(parsed["age"], 30);
454    }
455
456    #[test]
457    fn test_pointer_delete() {
458        let base = r#"{"name": "Alice", "temp": true}"#;
459        let (art, _) = apply(None, &synth_env("t", 1, base)).unwrap();
460
461        let mut edit = edit_env("t", 2, vec![EditOp {
462            op: OpType::Delete, target: ptr_target("/temp"), content: None,
463        }]);
464        edit.meta.format = Some("application/json".to_string());
465        let (art2, _) = apply(Some(&art), &edit).unwrap();
466        let parsed: serde_json::Value = serde_json::from_str(&art2.body).unwrap();
467        assert!(parsed.get("temp").is_none());
468    }
469
470    #[test]
471    fn test_handle_is_not_input() {
472        let env = Envelope {
473            protocol: PROTOCOL_VERSION.to_string(),
474            id: "t".to_string(), version: 1, name: Name::Handle,
475            meta: Meta {
476                format: None,
477                tokens_used: None, checksum: None, state: None,
478            },
479            content: vec![],
480        };
481        assert!(apply(None, &env).is_err());
482    }
483
484    #[test]
485    fn test_synthesize_returns_targets() {
486        let body = r#"<aap:target id="stats"><aap:target id="rev">$100</aap:target></aap:target>"#;
487        let (_, handle) = apply(None, &synth_env("t", 1, body)).unwrap();
488        let item: crate::aap::HandleContentItem =
489            serde_json::from_value(handle.content[0].clone()).unwrap();
490        let targets = item.targets.unwrap();
491        let ids: Vec<&str> = targets.iter().map(|t| t.id.as_str()).collect();
492        assert_eq!(ids, vec!["stats", "rev"]);
493    }
494
495    #[test]
496    fn test_nested_target_invalidation() {
497        let body = r#"<aap:target id="outer"><aap:target id="inner">v</aap:target></aap:target>"#;
498        let (art, _) = apply(None, &synth_env("t", 1, body)).unwrap();
499
500        // Replace outer with content that drops inner
501        let edit = edit_env("t", 2, vec![EditOp {
502            op: OpType::Replace, target: id_target("outer"),
503            content: Some("no nested targets here".to_string()),
504        }]);
505        let (_, handle) = apply(Some(&art), &edit).unwrap();
506        let item: crate::aap::HandleContentItem =
507            serde_json::from_value(handle.content[0].clone()).unwrap();
508        let targets = item.targets.unwrap();
509        let ids: Vec<&str> = targets.iter().map(|t| t.id.as_str()).collect();
510        // outer still exists (markers preserved), but inner is gone
511        assert_eq!(ids, vec!["outer"]);
512    }
513
514    #[test]
515    fn test_no_targets_for_json() {
516        let base = r#"{"key": "value"}"#;
517        let mut env = synth_env("t", 1, base);
518        env.meta.format = Some("application/json".to_string());
519        let (_, handle) = apply(None, &env).unwrap();
520        let item: crate::aap::HandleContentItem =
521            serde_json::from_value(handle.content[0].clone()).unwrap();
522        assert!(item.targets.is_none());
523    }
524
525    // ── ID-based edge cases ────────────────────────────────────────────
526
527    #[test]
528    fn test_edit_insert_before() {
529        let env = synth_env("t", 1, r#"<aap:target id="list">item1</aap:target>"#);
530        let (art, _) = apply(None, &env).unwrap();
531
532        let edit = edit_env("t", 2, vec![EditOp {
533            op: OpType::InsertBefore, target: id_target("list"),
534            content: Some("item0, ".to_string()),
535        }]);
536        let (art2, _) = apply(Some(&art), &edit).unwrap();
537        assert!(art2.body.contains("item0, item1"));
538        assert!(art2.body.contains(r#"<aap:target id="list">"#));
539    }
540
541    #[test]
542    fn test_replace_with_empty_string() {
543        let env = synth_env("t", 1, r#"<aap:target id="val">old</aap:target>"#);
544        let (art, _) = apply(None, &env).unwrap();
545
546        let edit = edit_env("t", 2, vec![EditOp {
547            op: OpType::Replace, target: id_target("val"),
548            content: Some("".to_string()),
549        }]);
550        let (art2, _) = apply(Some(&art), &edit).unwrap();
551        assert_eq!(art2.body, r#"<aap:target id="val"></aap:target>"#);
552    }
553
554    #[test]
555    fn test_replace_with_none_content() {
556        let env = synth_env("t", 1, r#"<aap:target id="val">old</aap:target>"#);
557        let (art, _) = apply(None, &env).unwrap();
558
559        let edit = edit_env("t", 2, vec![EditOp {
560            op: OpType::Replace, target: id_target("val"),
561            content: None,
562        }]);
563        let (art2, _) = apply(Some(&art), &edit).unwrap();
564        assert_eq!(art2.body, r#"<aap:target id="val"></aap:target>"#);
565    }
566
567    #[test]
568    fn test_delete_preserves_markers_for_reuse() {
569        // After delete, the target should still be addressable for future ops.
570        let body = r#"<aap:target id="msg">hello</aap:target>"#;
571        let (art, _) = apply(None, &synth_env("t", 1, body)).unwrap();
572
573        let delete = edit_env("t", 2, vec![EditOp {
574            op: OpType::Delete, target: id_target("msg"), content: None,
575        }]);
576        let (art2, _) = apply(Some(&art), &delete).unwrap();
577        assert!(art2.body.contains(r#"<aap:target id="msg">"#));
578        assert!(art2.body.contains("</aap:target>"));
579        assert!(!art2.body.contains("hello"));
580
581        // Can still replace into the now-empty target.
582        let replace = edit_env("t", 3, vec![EditOp {
583            op: OpType::Replace, target: id_target("msg"),
584            content: Some("world".to_string()),
585        }]);
586        let (art3, _) = apply(Some(&art2), &replace).unwrap();
587        assert!(art3.body.contains("world"));
588    }
589
590    #[test]
591    fn test_delete_target_still_in_handle() {
592        // Since markers are preserved, handle should still list the target.
593        let body = r#"<aap:target id="msg">hello</aap:target>"#;
594        let (art, _) = apply(None, &synth_env("t", 1, body)).unwrap();
595
596        let delete = edit_env("t", 2, vec![EditOp {
597            op: OpType::Delete, target: id_target("msg"), content: None,
598        }]);
599        let (_, handle) = apply(Some(&art), &delete).unwrap();
600        let item: crate::aap::HandleContentItem =
601            serde_json::from_value(handle.content[0].clone()).unwrap();
602        let targets = item.targets.unwrap();
603        let ids: Vec<&str> = targets.iter().map(|t| t.id.as_str()).collect();
604        assert_eq!(ids, vec!["msg"]);
605    }
606
607    #[test]
608    fn test_multiple_ops_same_target() {
609        // Delete content, then insert new content into same target.
610        let body = r#"<aap:target id="x">old</aap:target>"#;
611        let (art, _) = apply(None, &synth_env("t", 1, body)).unwrap();
612
613        let edit = edit_env("t", 2, vec![
614            EditOp { op: OpType::Delete, target: id_target("x"), content: None },
615            EditOp { op: OpType::InsertAfter, target: id_target("x"), content: Some("new".to_string()) },
616        ]);
617        let (art2, _) = apply(Some(&art), &edit).unwrap();
618        assert!(art2.body.contains("new"));
619        assert!(!art2.body.contains("old"));
620    }
621
622    #[test]
623    fn test_multiple_ops_different_targets() {
624        let body = r#"<aap:target id="a">1</aap:target><aap:target id="b">2</aap:target>"#;
625        let (art, _) = apply(None, &synth_env("t", 1, body)).unwrap();
626
627        let edit = edit_env("t", 2, vec![
628            EditOp { op: OpType::Replace, target: id_target("a"), content: Some("X".to_string()) },
629            EditOp { op: OpType::Replace, target: id_target("b"), content: Some("Y".to_string()) },
630        ]);
631        let (art2, _) = apply(Some(&art), &edit).unwrap();
632        assert!(art2.body.contains("X"));
633        assert!(art2.body.contains("Y"));
634        assert!(!art2.body.contains("1"));
635        assert!(!art2.body.contains("2"));
636    }
637
638    #[test]
639    fn test_nonexistent_target_fails() {
640        let body = r#"<aap:target id="a">val</aap:target>"#;
641        let (art, _) = apply(None, &synth_env("t", 1, body)).unwrap();
642
643        let edit = edit_env("t", 2, vec![EditOp {
644            op: OpType::Replace, target: id_target("nonexistent"),
645            content: Some("x".to_string()),
646        }]);
647        assert!(apply(Some(&art), &edit).is_err());
648    }
649
650    #[test]
651    fn test_deeply_nested_targets() {
652        let body = r#"<aap:target id="l1"><aap:target id="l2"><aap:target id="l3">deep</aap:target></aap:target></aap:target>"#;
653        let (art, _) = apply(None, &synth_env("t", 1, body)).unwrap();
654
655        // Edit the innermost target.
656        let edit = edit_env("t", 2, vec![EditOp {
657            op: OpType::Replace, target: id_target("l3"),
658            content: Some("shallow".to_string()),
659        }]);
660        let (art2, _) = apply(Some(&art), &edit).unwrap();
661        assert!(art2.body.contains("shallow"));
662        // Outer markers still intact.
663        assert!(art2.body.contains(r#"<aap:target id="l1">"#));
664        assert!(art2.body.contains(r#"<aap:target id="l2">"#));
665    }
666
667    #[test]
668    fn test_adjacent_sibling_targets() {
669        let body = r#"<aap:target id="a">1</aap:target><aap:target id="b">2</aap:target><aap:target id="c">3</aap:target>"#;
670        let (art, _) = apply(None, &synth_env("t", 1, body)).unwrap();
671
672        // Replace middle sibling.
673        let edit = edit_env("t", 2, vec![EditOp {
674            op: OpType::Replace, target: id_target("b"),
675            content: Some("X".to_string()),
676        }]);
677        let (art2, _) = apply(Some(&art), &edit).unwrap();
678        assert!(art2.body.contains(r#"<aap:target id="a">1</aap:target>"#));
679        assert!(art2.body.contains(r#"<aap:target id="b">X</aap:target>"#));
680        assert!(art2.body.contains(r#"<aap:target id="c">3</aap:target>"#));
681    }
682
683    #[test]
684    fn test_replace_with_content_containing_new_targets() {
685        let body = r#"<aap:target id="section">old</aap:target>"#;
686        let (art, _) = apply(None, &synth_env("t", 1, body)).unwrap();
687
688        let new_content = r#"<aap:target id="inner">nested</aap:target>"#;
689        let edit = edit_env("t", 2, vec![EditOp {
690            op: OpType::Replace, target: id_target("section"),
691            content: Some(new_content.to_string()),
692        }]);
693        let (_, handle) = apply(Some(&art), &edit).unwrap();
694        let item: crate::aap::HandleContentItem =
695            serde_json::from_value(handle.content[0].clone()).unwrap();
696        let targets = item.targets.unwrap();
697        let ids: Vec<&str> = targets.iter().map(|t| t.id.as_str()).collect();
698        assert_eq!(ids, vec!["section", "inner"]);
699    }
700
701    #[test]
702    fn test_empty_target_content() {
703        let body = r#"<aap:target id="empty"></aap:target>"#;
704        let (art, _) = apply(None, &synth_env("t", 1, body)).unwrap();
705
706        let edit = edit_env("t", 2, vec![EditOp {
707            op: OpType::InsertAfter, target: id_target("empty"),
708            content: Some("filled".to_string()),
709        }]);
710        let (art2, _) = apply(Some(&art), &edit).unwrap();
711        assert!(art2.body.contains("filled"));
712    }
713
714    #[test]
715    fn test_edit_without_base_artifact_fails() {
716        let edit = edit_env("t", 2, vec![EditOp {
717            op: OpType::Replace, target: id_target("x"),
718            content: Some("y".to_string()),
719        }]);
720        assert!(apply(None, &edit).is_err());
721    }
722
723    #[test]
724    fn test_synthesize_empty_content_array_fails() {
725        let env = Envelope {
726            protocol: PROTOCOL_VERSION.to_string(),
727            id: "t".to_string(), version: 1, name: Name::Synthesize,
728            meta: Meta { format: Some("text/html".to_string()),
729                tokens_used: None, checksum: None, state: None },
730            content: vec![],
731        };
732        assert!(apply(None, &env).is_err());
733    }
734
735    #[test]
736    fn test_edit_empty_ops_is_noop() {
737        let body = r#"<aap:target id="a">val</aap:target>"#;
738        let (art, _) = apply(None, &synth_env("t", 1, body)).unwrap();
739
740        let edit = edit_env("t", 2, vec![]);
741        let (art2, _) = apply(Some(&art), &edit).unwrap();
742        assert_eq!(art2.body, body);
743    }
744
745    #[test]
746    fn test_default_format_is_html() {
747        let env = Envelope {
748            protocol: PROTOCOL_VERSION.to_string(),
749            id: "t".to_string(), version: 1, name: Name::Synthesize,
750            meta: Meta { format: None, tokens_used: None, checksum: None, state: None },
751            content: vec![serde_json::json!({ "body": "<div>hi</div>" })],
752        };
753        let (art, _) = apply(None, &env).unwrap();
754        assert_eq!(art.format, "text/html");
755    }
756
757    #[test]
758    fn test_synthesize_overwrites_existing_artifact() {
759        let (art, _) = apply(None, &synth_env("t", 1, "v1")).unwrap();
760        let (art2, _) = apply(Some(&art), &synth_env("t", 2, "v2")).unwrap();
761        assert_eq!(art2.body, "v2");
762        assert_eq!(art2.version, 2);
763    }
764
765    #[test]
766    fn test_all_or_nothing_semantics() {
767        // Second op targets a nonexistent target — entire edit should fail.
768        let body = r#"<aap:target id="a">old</aap:target>"#;
769        let (art, _) = apply(None, &synth_env("t", 1, body)).unwrap();
770
771        let edit = edit_env("t", 2, vec![
772            EditOp { op: OpType::Replace, target: id_target("a"), content: Some("new".to_string()) },
773            EditOp { op: OpType::Replace, target: id_target("missing"), content: Some("x".to_string()) },
774        ]);
775        assert!(apply(Some(&art), &edit).is_err());
776        // Original artifact body is unchanged (we still have the immutable ref).
777        assert_eq!(art.body, body);
778    }
779
780    #[test]
781    fn test_sequential_ops_with_position_shift() {
782        // Two insert_after ops on the same target — both should work.
783        let body = r#"<aap:target id="list">a</aap:target>"#;
784        let (art, _) = apply(None, &synth_env("t", 1, body)).unwrap();
785
786        let edit = edit_env("t", 2, vec![
787            EditOp { op: OpType::InsertAfter, target: id_target("list"), content: Some("b".to_string()) },
788            EditOp { op: OpType::InsertAfter, target: id_target("list"), content: Some("c".to_string()) },
789        ]);
790        let (art2, _) = apply(Some(&art), &edit).unwrap();
791        assert!(art2.body.contains("abc"));
792    }
793
794    #[test]
795    fn test_insert_before_and_after_combined() {
796        let body = r#"<aap:target id="mid">M</aap:target>"#;
797        let (art, _) = apply(None, &synth_env("t", 1, body)).unwrap();
798
799        let edit = edit_env("t", 2, vec![
800            EditOp { op: OpType::InsertBefore, target: id_target("mid"), content: Some("B".to_string()) },
801            EditOp { op: OpType::InsertAfter, target: id_target("mid"), content: Some("A".to_string()) },
802        ]);
803        let (art2, _) = apply(Some(&art), &edit).unwrap();
804        assert!(art2.body.contains("BMA"));
805    }
806
807    #[test]
808    fn test_delete_nested_inner_preserves_outer() {
809        let body = r#"<aap:target id="outer">pre<aap:target id="inner">val</aap:target>post</aap:target>"#;
810        let (art, _) = apply(None, &synth_env("t", 1, body)).unwrap();
811
812        let edit = edit_env("t", 2, vec![EditOp {
813            op: OpType::Delete, target: id_target("inner"), content: None,
814        }]);
815        let (art2, _) = apply(Some(&art), &edit).unwrap();
816        // Inner markers preserved but content gone, outer intact.
817        assert!(art2.body.contains(r#"<aap:target id="inner"></aap:target>"#));
818        assert!(art2.body.contains("pre"));
819        assert!(art2.body.contains("post"));
820        assert!(art2.body.contains(r#"<aap:target id="outer">"#));
821    }
822
823    #[test]
824    fn test_handle_version_matches_envelope() {
825        let env = synth_env("t", 5, "<div>hi</div>");
826        let (art, handle) = apply(None, &env).unwrap();
827        assert_eq!(art.version, 5);
828        assert_eq!(handle.version, 5);
829    }
830
831    #[test]
832    fn test_handle_id_matches_envelope() {
833        let env = synth_env("my-artifact", 1, "body");
834        let (_, handle) = apply(None, &env).unwrap();
835        assert_eq!(handle.id, "my-artifact");
836    }
837
838    #[test]
839    fn test_multiline_content_in_targets() {
840        let body = "<aap:target id=\"code\">line1\nline2\nline3</aap:target>";
841        let (art, _) = apply(None, &synth_env("t", 1, body)).unwrap();
842
843        let edit = edit_env("t", 2, vec![EditOp {
844            op: OpType::Replace, target: id_target("code"),
845            content: Some("replaced\ncontent".to_string()),
846        }]);
847        let (art2, _) = apply(Some(&art), &edit).unwrap();
848        assert!(art2.body.contains("replaced\ncontent"));
849        assert!(!art2.body.contains("line1"));
850    }
851
852    // ── Pointer-based edge cases ───────────────────────────────────────
853
854    fn json_edit_env(id: &str, version: u64, ops: Vec<EditOp>) -> Envelope {
855        let mut env = edit_env(id, version, ops);
856        env.meta.format = Some("application/json".to_string());
857        env
858    }
859
860    fn json_synth_env(id: &str, version: u64, body: &str) -> Envelope {
861        let mut env = synth_env(id, version, body);
862        env.meta.format = Some("application/json".to_string());
863        env
864    }
865
866    #[test]
867    fn test_pointer_nested_path() {
868        let base = r#"{"a": {"b": {"c": 1}}}"#;
869        let (art, _) = apply(None, &json_synth_env("t", 1, base)).unwrap();
870
871        let edit = json_edit_env("t", 2, vec![EditOp {
872            op: OpType::Replace, target: ptr_target("/a/b/c"),
873            content: Some("42".to_string()),
874        }]);
875        let (art2, _) = apply(Some(&art), &edit).unwrap();
876        let v: serde_json::Value = serde_json::from_str(&art2.body).unwrap();
877        assert_eq!(v["a"]["b"]["c"], 42);
878    }
879
880    #[test]
881    fn test_pointer_replace_array_element() {
882        let base = r#"{"items": [10, 20, 30]}"#;
883        let (art, _) = apply(None, &json_synth_env("t", 1, base)).unwrap();
884
885        let edit = json_edit_env("t", 2, vec![EditOp {
886            op: OpType::Replace, target: ptr_target("/items/1"),
887            content: Some("99".to_string()),
888        }]);
889        let (art2, _) = apply(Some(&art), &edit).unwrap();
890        let v: serde_json::Value = serde_json::from_str(&art2.body).unwrap();
891        assert_eq!(v["items"], serde_json::json!([10, 99, 30]));
892    }
893
894    #[test]
895    fn test_pointer_delete_array_element() {
896        let base = r#"{"items": [1, 2, 3]}"#;
897        let (art, _) = apply(None, &json_synth_env("t", 1, base)).unwrap();
898
899        let edit = json_edit_env("t", 2, vec![EditOp {
900            op: OpType::Delete, target: ptr_target("/items/1"), content: None,
901        }]);
902        let (art2, _) = apply(Some(&art), &edit).unwrap();
903        let v: serde_json::Value = serde_json::from_str(&art2.body).unwrap();
904        assert_eq!(v["items"], serde_json::json!([1, 3]));
905    }
906
907    #[test]
908    fn test_pointer_insert_before_array() {
909        let base = r#"{"items": [1, 2, 3]}"#;
910        let (art, _) = apply(None, &json_synth_env("t", 1, base)).unwrap();
911
912        let edit = json_edit_env("t", 2, vec![EditOp {
913            op: OpType::InsertBefore, target: ptr_target("/items/1"),
914            content: Some("99".to_string()),
915        }]);
916        let (art2, _) = apply(Some(&art), &edit).unwrap();
917        let v: serde_json::Value = serde_json::from_str(&art2.body).unwrap();
918        assert_eq!(v["items"], serde_json::json!([1, 99, 2, 3]));
919    }
920
921    #[test]
922    fn test_pointer_insert_after_array() {
923        let base = r#"{"items": [1, 2, 3]}"#;
924        let (art, _) = apply(None, &json_synth_env("t", 1, base)).unwrap();
925
926        let edit = json_edit_env("t", 2, vec![EditOp {
927            op: OpType::InsertAfter, target: ptr_target("/items/1"),
928            content: Some("99".to_string()),
929        }]);
930        let (art2, _) = apply(Some(&art), &edit).unwrap();
931        let v: serde_json::Value = serde_json::from_str(&art2.body).unwrap();
932        assert_eq!(v["items"], serde_json::json!([1, 2, 99, 3]));
933    }
934
935    #[test]
936    fn test_pointer_multiple_ops() {
937        let base = r#"{"name": "Alice", "age": 30, "city": "NYC"}"#;
938        let (art, _) = apply(None, &json_synth_env("t", 1, base)).unwrap();
939
940        let edit = json_edit_env("t", 2, vec![
941            EditOp { op: OpType::Replace, target: ptr_target("/name"), content: Some(r#""Bob""#.to_string()) },
942            EditOp { op: OpType::Delete, target: ptr_target("/city"), content: None },
943        ]);
944        let (art2, _) = apply(Some(&art), &edit).unwrap();
945        let v: serde_json::Value = serde_json::from_str(&art2.body).unwrap();
946        assert_eq!(v["name"], "Bob");
947        assert_eq!(v["age"], 30);
948        assert!(v.get("city").is_none());
949    }
950
951    #[test]
952    fn test_pointer_nonexistent_path_fails() {
953        let base = r#"{"a": 1}"#;
954        let (art, _) = apply(None, &json_synth_env("t", 1, base)).unwrap();
955
956        let edit = json_edit_env("t", 2, vec![EditOp {
957            op: OpType::Replace, target: ptr_target("/nonexistent"),
958            content: Some("1".to_string()),
959        }]);
960        assert!(apply(Some(&art), &edit).is_err());
961    }
962
963    #[test]
964    fn test_pointer_rfc6901_escaping() {
965        // RFC 6901: ~0 = ~, ~1 = /
966        let base = r#"{"a/b": 1, "c~d": 2}"#;
967        let (art, _) = apply(None, &json_synth_env("t", 1, base)).unwrap();
968
969        let edit = json_edit_env("t", 2, vec![EditOp {
970            op: OpType::Replace, target: ptr_target("/a~1b"),
971            content: Some("10".to_string()),
972        }]);
973        let (art2, _) = apply(Some(&art), &edit).unwrap();
974        let v: serde_json::Value = serde_json::from_str(&art2.body).unwrap();
975        assert_eq!(v["a/b"], 10);
976
977        let edit2 = json_edit_env("t", 3, vec![EditOp {
978            op: OpType::Replace, target: ptr_target("/c~0d"),
979            content: Some("20".to_string()),
980        }]);
981        let (art3, _) = apply(Some(&art2), &edit2).unwrap();
982        let v2: serde_json::Value = serde_json::from_str(&art3.body).unwrap();
983        assert_eq!(v2["c~d"], 20);
984    }
985
986    #[test]
987    fn test_pointer_insert_on_object_fails() {
988        // Insert requires array parent per spec.
989        let base = r#"{"a": {"b": 1}}"#;
990        let (art, _) = apply(None, &json_synth_env("t", 1, base)).unwrap();
991
992        let edit = json_edit_env("t", 2, vec![EditOp {
993            op: OpType::InsertBefore, target: ptr_target("/a/b"),
994            content: Some("2".to_string()),
995        }]);
996        assert!(apply(Some(&art), &edit).is_err());
997    }
998
999    #[test]
1000    fn test_pointer_delete_root_fails() {
1001        let base = r#"{"a": 1}"#;
1002        let (art, _) = apply(None, &json_synth_env("t", 1, base)).unwrap();
1003
1004        let edit = json_edit_env("t", 2, vec![EditOp {
1005            op: OpType::Delete, target: ptr_target(""), content: None,
1006        }]);
1007        assert!(apply(Some(&art), &edit).is_err());
1008    }
1009
1010    #[test]
1011    fn test_pointer_array_out_of_bounds_fails() {
1012        let base = r#"{"items": [1, 2]}"#;
1013        let (art, _) = apply(None, &json_synth_env("t", 1, base)).unwrap();
1014
1015        let edit = json_edit_env("t", 2, vec![EditOp {
1016            op: OpType::Delete, target: ptr_target("/items/5"), content: None,
1017        }]);
1018        assert!(apply(Some(&art), &edit).is_err());
1019    }
1020
1021    #[test]
1022    fn test_pointer_replace_with_complex_value() {
1023        let base = r#"{"config": null}"#;
1024        let (art, _) = apply(None, &json_synth_env("t", 1, base)).unwrap();
1025
1026        let edit = json_edit_env("t", 2, vec![EditOp {
1027            op: OpType::Replace, target: ptr_target("/config"),
1028            content: Some(r#"{"host": "localhost", "port": 5432}"#.to_string()),
1029        }]);
1030        let (art2, _) = apply(Some(&art), &edit).unwrap();
1031        let v: serde_json::Value = serde_json::from_str(&art2.body).unwrap();
1032        assert_eq!(v["config"]["host"], "localhost");
1033        assert_eq!(v["config"]["port"], 5432);
1034    }
1035
1036    #[test]
1037    fn test_pointer_replace_invalid_json_content_fails() {
1038        let base = r#"{"a": 1}"#;
1039        let (art, _) = apply(None, &json_synth_env("t", 1, base)).unwrap();
1040
1041        let edit = json_edit_env("t", 2, vec![EditOp {
1042            op: OpType::Replace, target: ptr_target("/a"),
1043            content: Some("not valid json".to_string()),
1044        }]);
1045        assert!(apply(Some(&art), &edit).is_err());
1046    }
1047
1048    #[test]
1049    fn test_pointer_on_non_json_content_fails() {
1050        // Pointer ops require valid JSON body.
1051        let body = "not json at all";
1052        let (art, _) = apply(None, &synth_env("t", 1, body)).unwrap();
1053
1054        let edit = json_edit_env("t", 2, vec![EditOp {
1055            op: OpType::Replace, target: ptr_target("/field"),
1056            content: Some("1".to_string()),
1057        }]);
1058        assert!(apply(Some(&art), &edit).is_err());
1059    }
1060
1061    #[test]
1062    fn test_pointer_all_or_nothing() {
1063        let base = r#"{"a": 1, "b": 2}"#;
1064        let (art, _) = apply(None, &json_synth_env("t", 1, base)).unwrap();
1065
1066        let edit = json_edit_env("t", 2, vec![
1067            EditOp { op: OpType::Replace, target: ptr_target("/a"), content: Some("10".to_string()) },
1068            EditOp { op: OpType::Replace, target: ptr_target("/missing"), content: Some("1".to_string()) },
1069        ]);
1070        assert!(apply(Some(&art), &edit).is_err());
1071        // Original artifact unchanged.
1072        let v: serde_json::Value = serde_json::from_str(&art.body).unwrap();
1073        assert_eq!(v["a"], 1);
1074    }
1075
1076    // ── Format handling ────────────────────────────────────────────────
1077
1078    #[test]
1079    fn test_python_format_targets() {
1080        let body = r#"<aap:target id="imports">import os</aap:target>"#;
1081        let mut env = synth_env("t", 1, body);
1082        env.meta.format = Some("text/x-python".to_string());
1083        let (art, _) = apply(None, &env).unwrap();
1084        assert_eq!(art.format, "text/x-python");
1085
1086        let mut edit = edit_env("t", 2, vec![EditOp {
1087            op: OpType::Replace, target: id_target("imports"),
1088            content: Some("import sys".to_string()),
1089        }]);
1090        edit.meta.format = Some("text/x-python".to_string());
1091        let (art2, _) = apply(Some(&art), &edit).unwrap();
1092        assert!(art2.body.contains("import sys"));
1093    }
1094
1095    // ── Store edge cases ───────────────────────────────────────────────
1096
1097    #[test]
1098    fn test_store_edit_without_synthesize_fails() {
1099        let mut store = crate::store::ArtifactStore::new(10);
1100        let edit = edit_env("t", 2, vec![EditOp {
1101            op: OpType::Replace, target: id_target("x"),
1102            content: Some("y".to_string()),
1103        }]);
1104        assert!(store.apply(&edit).is_err());
1105    }
1106
1107    #[test]
1108    fn test_store_multiple_artifacts() {
1109        let mut store = crate::store::ArtifactStore::new(10);
1110        store.apply(&synth_env("a", 1, "artifact-a")).unwrap();
1111        store.apply(&synth_env("b", 1, "artifact-b")).unwrap();
1112        assert_eq!(store.get("a").unwrap().body, "artifact-a");
1113        assert_eq!(store.get("b").unwrap().body, "artifact-b");
1114    }
1115
1116    #[test]
1117    fn test_store_max_history_eviction() {
1118        let mut store = crate::store::ArtifactStore::new(2);
1119        store.apply(&synth_env("t", 1, "v1")).unwrap();
1120        store.apply(&synth_env("t", 2, "v2")).unwrap();
1121        store.apply(&synth_env("t", 3, "v3")).unwrap();
1122        // Only 2 most recent should remain — rollback to v1 should fail.
1123        assert!(store.rollback("t", 1).is_err());
1124        // v2 should still be available.
1125        let rolled = store.rollback("t", 2).unwrap();
1126        assert_eq!(rolled.body, "v2");
1127    }
1128
1129    #[test]
1130    fn test_store_synthesize_resets_chain() {
1131        // Synthesize doesn't require version continuity.
1132        let mut store = crate::store::ArtifactStore::new(10);
1133        store.apply(&synth_env("t", 1, "v1")).unwrap();
1134        // Jump to version 10 via synthesize — should succeed.
1135        store.apply(&synth_env("t", 10, "v10")).unwrap();
1136        assert_eq!(store.current_version("t"), Some(10));
1137    }
1138}