1use omgbase_mutate::canonical_json;
21use serde_json::{Map, Value as Json, json};
22
23use crate::error::SurfaceError;
24
25pub const LOCATOR: &str = "resolve the locator to an id first: `blocks_resolve { locator }`, or a `query` whose hit is the block";
28
29#[derive(Clone, Debug, PartialEq)]
32pub struct Request {
33 pub select: Option<Json>,
34 pub effects: Json,
35 pub expect: Option<Json>,
36}
37
38impl Request {
39 #[must_use]
42 pub fn of(args: &Json) -> Option<Self> {
43 Some(Self {
44 select: args.get("select").filter(|s| !s.is_null()).cloned(),
45 effects: args.get("effects")?.clone(),
46 expect: args.get("expect").cloned(),
47 })
48 }
49
50 fn list(&self) -> Option<&Vec<Json>> {
52 self.effects.as_array()
53 }
54
55 fn with_effects(&self, effects: Vec<Json>) -> Self {
56 Self {
57 effects: Json::Array(effects),
58 ..self.clone()
59 }
60 }
61}
62
63#[must_use]
66pub fn fix_request(req: &Request) -> String {
67 let mut m = Map::new();
68 if let Some(s) = &req.select {
69 m.insert("select".to_owned(), s.clone());
70 }
71 m.insert("effects".to_owned(), req.effects.clone());
72 if let Some(e) = &req.expect {
73 m.insert("expect".to_owned(), e.clone());
74 }
75 canonical_json(&Json::Object(m))
76}
77
78#[must_use]
80pub fn fix_tool(tool: &str, args: &Json) -> String {
81 format!("{tool} {}", canonical_json(args))
82}
83
84#[must_use]
87pub fn narrowed_to(root: &str, id: &str) -> String {
88 format!("from {root} where id == {}", Json::String(id.to_owned()))
89}
90
91#[must_use]
93pub fn root_of_id(id: &str) -> &'static str {
94 if id.starts_with("d_") {
95 "docs"
96 } else if id.starts_with("n_") {
97 "nodes"
98 } else {
99 "blocks"
100 }
101}
102
103fn substitute(value: &Json, from: &str, to: &str) -> Json {
106 match value {
107 Json::String(s) if s == from => json!(to),
108 Json::Array(a) => Json::Array(a.iter().map(|x| substitute(x, from, to)).collect()),
109 Json::Object(o) => Json::Object(
110 o.iter()
111 .map(|(k, v)| (k.clone(), substitute(v, from, to)))
112 .collect(),
113 ),
114 other => other.clone(),
115 }
116}
117
118#[derive(Clone, Debug, PartialEq, Eq)]
121pub struct Origin {
122 pub row_id: String,
123 pub template_index: usize,
124}
125
126#[must_use]
133pub fn conflicting_effects(
134 req: &Request,
135 a: usize,
136 b: usize,
137 origins: Option<&[Origin]>,
138) -> Option<String> {
139 let effects = req.list()?;
140 let Some(origins) = origins else {
141 let kept = effects
142 .iter()
143 .enumerate()
144 .filter(|(k, _)| *k != a)
145 .map(|(_, e)| e.clone())
146 .collect();
147 return Some(fix_request(&Request {
148 select: None,
149 effects: Json::Array(kept),
150 expect: None,
151 }));
152 };
153 let (oa, ob) = (origins.get(a)?, origins.get(b)?);
154 if oa.row_id != ob.row_id {
155 return Some(fix_request(&Request {
156 select: Some(json!(narrowed_to(root_of_id(&ob.row_id), &ob.row_id))),
157 effects: req.effects.clone(),
158 expect: Some(json!({ "max": 1 })),
159 }));
160 }
161 let kept = effects
162 .iter()
163 .enumerate()
164 .filter(|(k, _)| *k != oa.template_index)
165 .map(|(_, e)| e.clone())
166 .collect();
167 Some(fix_request(&req.with_effects(kept)))
168}
169
170#[must_use]
174pub fn stale_revision(req: &Request, t: usize, current: &str) -> Option<String> {
175 let effects = req.list()?;
176 let e = effects.get(t)?.as_object()?;
177 let mut fixed = e.clone();
178 let mut expect = e
179 .get("expect")
180 .and_then(Json::as_object)
181 .cloned()
182 .unwrap_or_default();
183 expect.insert("revision".to_owned(), json!(current));
184 fixed.insert("expect".to_owned(), Json::Object(expect));
185 let mapped = effects
186 .iter()
187 .enumerate()
188 .map(|(k, x)| {
189 if k == t {
190 Json::Object(fixed.clone())
191 } else {
192 x.clone()
193 }
194 })
195 .collect();
196 Some(fix_request(&req.with_effects(mapped)))
197}
198
199#[must_use]
204pub fn create_first(req: &Request, i: usize, symbol: &str, allocated: usize) -> Option<String> {
205 let effects = req.list()?;
206 let c = effects
207 .iter()
208 .enumerate()
209 .skip(i + 1)
210 .find(|(_, e)| e.get("op").and_then(Json::as_str) == Some("create"))
211 .map(|(k, _)| k)?;
212 let renamed = format!("$new.{allocated}");
213 let mut reordered: Vec<Json> = effects
214 .iter()
215 .enumerate()
216 .filter(|(k, _)| *k != c)
217 .map(|(_, e)| e.clone())
218 .collect();
219 reordered.insert(i, effects[c].clone());
220 let fixed = reordered
221 .iter()
222 .enumerate()
223 .map(|(k, e)| {
224 if k == i + 1 {
225 substitute(e, symbol, &renamed)
226 } else {
227 e.clone()
228 }
229 })
230 .collect();
231 Some(fix_request(&Request {
232 select: None,
233 effects: Json::Array(fixed),
234 expect: None,
235 }))
236}
237
238#[must_use]
244pub fn ambiguous_slot(
245 req: &Request,
246 here: &str,
247 first: &str,
248 narrow: (&str, &str),
249) -> Option<String> {
250 let mut fixed = req.clone();
251 let path = here.strip_prefix("effects")?;
252 *walk_mut(&mut fixed.effects, path)? = json!(first);
253 if req.select.is_none() {
254 return Some(fix_request(&Request {
255 select: None,
256 effects: fixed.effects,
257 expect: None,
258 }));
259 }
260 let (root, id) = narrow;
261 Some(fix_request(&Request {
262 select: Some(json!(narrowed_to(root, id))),
263 effects: fixed.effects,
264 expect: Some(json!({ "max": 1 })),
265 }))
266}
267
268#[must_use]
273pub fn root_fix(req: &Request, select: &str, span: (usize, usize), links: bool) -> String {
274 let cps: Vec<char> = select.chars().collect();
275 let start = span.0.min(cps.len());
276 let end = span.1.clamp(start, cps.len());
277 let root = if links { "blocks" } else { "docs" };
278 let spliced: String =
279 cps[..start].iter().collect::<String>() + root + &cps[end..].iter().collect::<String>();
280 fix_request(&Request {
281 select: Some(json!(spliced)),
282 ..req.clone()
283 })
284}
285
286pub fn planning_fix(
293 req: &Request,
294 mut err: SurfaceError,
295 origins: Option<&[Origin]>,
296 allocated: impl FnOnce(usize) -> Option<usize>,
297) -> SurfaceError {
298 let Some(data) = err.data.as_ref().and_then(Json::as_object) else {
299 return err;
300 };
301 if data.contains_key("fix") {
302 return err;
303 }
304 let index = |v: Option<&Json>| {
305 v.and_then(Json::as_u64)
306 .and_then(|n| usize::try_from(n).ok())
307 };
308 let fix = match err.code.as_str() {
309 "conflicting_effects" => {
310 let pair = data.get("effects").and_then(Json::as_array);
311 match (
312 pair.and_then(|p| index(p.first())),
313 pair.and_then(|p| index(p.get(1))),
314 ) {
315 (Some(a), Some(b)) => conflicting_effects(req, a, b, origins),
316 _ => None,
317 }
318 }
319 "stale_plan" => {
320 let current = data
321 .get("current")
322 .and_then(|c| c.get("revision"))
323 .and_then(Json::as_str);
324 let t = match origins {
325 Some(_) => index(data.get("template_index")),
326 None => index(data.get("effect_index")),
327 };
328 match (current, t) {
329 (Some(current), Some(t)) => stale_revision(req, t, current),
330 _ => None,
331 }
332 }
333 "target_missing" if origins.is_none() => {
334 match (
335 data.get("symbol").and_then(Json::as_str),
336 index(data.get("effect_index")),
337 ) {
338 (Some(symbol), Some(i)) => {
339 let n = omgbase_mutate::parse_new_symbol(symbol);
340 match (n, allocated(i)) {
342 (Some(n), Some(count)) if n >= count => create_first(req, i, symbol, count),
343 _ => None,
344 }
345 }
346 _ => None,
347 }
348 }
349 _ => None,
350 };
351 if let Some(fix) = fix {
352 if let Some(d) = err.data.as_mut().and_then(Json::as_object_mut) {
353 d.insert("fix".to_owned(), json!(fix));
354 }
355 }
356 err
357}
358
359#[must_use]
366pub fn replan(plan: &Json) -> Option<String> {
367 if let Some(sel) = plan.get("selection") {
368 let expect = sel
369 .get("expect")
370 .cloned()
371 .or_else(|| sel.get("cardinality").map(|c| json!({ "max": c })));
372 return Some(fix_request(&Request {
373 select: Some(sel.get("query")?.clone()),
374 effects: sel.get("templates")?.clone(),
375 expect,
376 }));
377 }
378 let effects = plan.get("effects")?;
379 if effects.as_array().is_some_and(Vec::is_empty) {
380 return None;
381 }
382 Some(fix_request(&Request {
383 select: None,
384 effects: effects.clone(),
385 expect: None,
386 }))
387}
388
389#[must_use]
392pub fn bounded(req: &Request, cardinality: usize) -> String {
393 fix_request(&Request {
394 expect: Some(json!({ "max": cardinality })),
395 ..req.clone()
396 })
397}
398
399#[must_use]
403pub fn literal_slot(req: &Request, here: &str) -> Option<String> {
404 let mut fixed = req.clone();
405 let path = here.strip_prefix("effects")?;
406 let slot = walk_mut(&mut fixed.effects, path)?;
407 let literal = crate::select::literal_of(slot.as_str()?.strip_prefix("-> ")?);
408 *slot = json!(literal);
409 Some(fix_request(&fixed))
410}
411
412fn walk_mut<'a>(v: &'a mut Json, path: &str) -> Option<&'a mut Json> {
414 let mut cur = v;
415 for seg in path.split('.') {
416 let (name, indices) = match seg.find('[') {
417 Some(i) => (&seg[..i], &seg[i..]),
418 None => (seg, ""),
419 };
420 if !name.is_empty() {
421 cur = cur.get_mut(name)?;
422 }
423 for idx in indices
424 .split(']')
425 .filter(|s| !s.is_empty())
426 .map(|s| s.trim_start_matches('['))
427 {
428 cur = cur.get_mut(idx.parse::<usize>().ok()?)?;
429 }
430 }
431 Some(cur)
432}
433
434#[must_use]
440pub fn attach(
441 tool: &str,
442 args: &Json,
443 err: SurfaceError,
444 allocated: impl FnOnce(usize) -> Option<usize>,
445) -> SurfaceError {
446 if !matches!(tool, "apply" | "plan" | "execute") {
447 return err;
448 }
449 let Some(data) = err.data.as_ref().and_then(Json::as_object) else {
450 return err;
451 };
452 if data.contains_key("fix") {
453 return err;
454 }
455 if let Some(plan) = args.get("plan") {
457 let fix = match err.code.as_str() {
458 "stale_plan" | "plan_mismatch" => replan(plan),
459 _ => None,
460 };
461 return with_fix(err, fix);
462 }
463 let Some(req) = Request::of(args) else {
464 return err;
465 };
466 let fix = match err.code.as_str() {
467 "plan_mismatch" => Some(fix_request(&req)),
469 _ if req.select.is_some() => None,
472 _ => return planning_fix(&req, err, None, allocated),
473 };
474 with_fix(err, fix)
475}
476
477fn with_fix(mut err: SurfaceError, fix: Option<String>) -> SurfaceError {
478 if let Some(fix) = fix {
479 if let Some(d) = err.data.as_mut().and_then(Json::as_object_mut) {
480 d.insert("fix".to_owned(), json!(fix));
481 }
482 }
483 err
484}
485
486#[must_use]
492pub fn append_after(parent: &str, id: &str, text: &str) -> String {
493 fix_tool(
494 "blocks_insert",
495 &json!({ "to": parent, "at": { "after": id }, "markdown": text }),
496 )
497}
498
499#[must_use]
503pub fn append_without_section(id: Option<&str>, path: Option<&str>, text: &str) -> String {
504 let mut m = Map::new();
505 if let Some(id) = id {
506 m.insert("id".to_owned(), json!(id));
507 }
508 if let Some(path) = path {
509 m.insert("path".to_owned(), json!(path));
510 }
511 m.insert("text".to_owned(), json!(text));
512 fix_tool("docs_append", &Json::Object(m))
513}
514
515#[must_use]
518pub fn append_into_heading(heading_id: &str, text: &str) -> String {
519 fix_tool("blocks_append", &json!({ "id": heading_id, "text": text }))
520}
521
522#[cfg(test)]
523mod tests {
524 use super::*;
525
526 fn effects() -> Json {
527 json!([
528 { "op": "insert", "into": { "kind": "document", "path": "a.md" }, "markdown": "x" },
529 { "op": "set", "target": { "kind": "document", "path": "a.md" }, "field": "frontmatter.k", "value": 1 },
530 { "op": "create", "path": "c.md", "markdown": "# C" }
531 ])
532 }
533
534 fn err(code: &str, data: Json) -> SurfaceError {
535 SurfaceError::with_data(code, "m", data)
536 }
537
538 fn fix_of(e: &SurfaceError) -> Option<String> {
539 e.data
540 .as_ref()
541 .and_then(|d| d.get("fix"))
542 .and_then(Json::as_str)
543 .map(str::to_owned)
544 }
545
546 fn canon(v: Json) -> String {
547 canonical_json(&v)
548 }
549
550 fn none(_: usize) -> Option<usize> {
551 None
552 }
553
554 #[test]
555 fn a_fix_is_canonical_json_prefixed_only_for_another_tool() {
556 let req = Request {
557 select: Some(json!("from docs collect")),
558 effects: json!([{ "op": "set", "field": "f", "value": 1 }]),
559 expect: Some(json!({ "max": 2 })),
560 };
561 assert_eq!(
562 fix_request(&req),
563 "{\"effects\":[{\"field\":\"f\",\"op\":\"set\",\"value\":1}],\"expect\":{\"max\":2},\"select\":\"from docs collect\"}"
564 );
565 assert_eq!(
566 fix_tool("plan", &json!({ "effects": [] })),
567 "plan {\"effects\":[]}"
568 );
569 assert_eq!(narrowed_to("docs", "d_1"), "from docs where id == \"d_1\"");
570 assert_eq!(root_of_id("d_1"), "docs");
571 assert_eq!(root_of_id("n_1"), "nodes");
572 assert_eq!(root_of_id("b_1"), "blocks");
573 }
574
575 #[test]
576 fn conflicting_effects_keeps_the_later_effect() {
577 let args = json!({ "effects": effects(), "reason": "r" });
578 let e = attach(
579 "apply",
580 &args,
581 err("conflicting_effects", json!({ "effects": [0, 1] })),
582 none,
583 );
584 assert_eq!(
585 fix_of(&e).unwrap(),
586 canon(json!({ "effects": [effects()[1], effects()[2]] }))
587 );
588 let req = Request {
591 select: Some(json!("from docs collect")),
592 effects: json!([effects()[1], effects()[1]]),
593 expect: Some(json!({ "max": 5 })),
594 };
595 let origins = [
596 Origin {
597 row_id: "d_1".into(),
598 template_index: 0,
599 },
600 Origin {
601 row_id: "d_1".into(),
602 template_index: 1,
603 },
604 Origin {
605 row_id: "d_2".into(),
606 template_index: 0,
607 },
608 Origin {
609 row_id: "d_2".into(),
610 template_index: 1,
611 },
612 ];
613 assert_eq!(
614 conflicting_effects(&req, 1, 2, Some(&origins)).unwrap(),
615 canon(
616 json!({ "select": "from docs where id == \"d_2\"", "effects": [effects()[1], effects()[1]], "expect": { "max": 1 } })
617 )
618 );
619 assert_eq!(
620 conflicting_effects(&req, 0, 1, Some(&origins)).unwrap(),
621 canon(
622 json!({ "select": "from docs collect", "effects": [effects()[1]], "expect": { "max": 5 } })
623 )
624 );
625 }
626
627 #[test]
628 fn a_stale_revision_at_plan_time_is_repinned_to_the_current_one() {
629 let args = json!({ "effects": [
630 { "op": "set", "target": { "kind": "document", "path": "a.md" }, "field": "frontmatter.k", "value": 1, "expect": { "revision": "r_0" } },
631 effects()[0],
632 ] });
633 let e = attach(
634 "apply",
635 &args,
636 err(
637 "stale_plan",
638 json!({ "doc": "d_0", "expected_revision": "r_0", "current": { "revision": "r_1", "hash": null }, "retriable": true, "effect_index": 0 }),
639 ),
640 none,
641 );
642 let mut fixed = args["effects"].clone();
643 fixed[0]["expect"]["revision"] = json!("r_1");
644 assert_eq!(fix_of(&e).unwrap(), canon(json!({ "effects": fixed })));
645 let req = Request {
647 select: None,
648 effects: json!([effects()[1]]),
649 expect: None,
650 };
651 let mut with = effects()[1].clone();
652 with["expect"] = json!({ "revision": "r_9" });
653 assert_eq!(
654 stale_revision(&req, 0, "r_9").unwrap(),
655 canon(json!({ "effects": [with] }))
656 );
657 }
658
659 #[test]
660 fn an_unexecutable_plan_is_replanned_from_its_core() {
661 let plan = json!({ "plan_id": "p", "effects": effects(), "preconditions": {} });
662 let e = attach(
663 "execute",
664 &json!({ "plan": plan }),
665 err("stale_plan", json!({ "doc": "d_0", "retriable": true })),
666 none,
667 );
668 assert_eq!(fix_of(&e).unwrap(), canon(json!({ "effects": effects() })));
669 let e = attach(
670 "execute",
671 &json!({ "plan": plan }),
672 err("plan_mismatch", json!({ "expected": "a", "actual": "b" })),
673 none,
674 );
675 assert_eq!(fix_of(&e).unwrap(), canon(json!({ "effects": effects() })));
676 let selected = json!({ "effects": [], "selection": { "query": "from docs collect", "templates": [effects()[1]], "cardinality": 3, "identities": ["d_1"] } });
678 assert_eq!(
679 replan(&selected).unwrap(),
680 canon(
681 json!({ "select": "from docs collect", "effects": [effects()[1]], "expect": { "max": 3 } })
682 )
683 );
684 let bounded = json!({ "effects": [], "selection": { "query": "from docs collect", "expect": { "max": 9 }, "templates": [effects()[1]], "cardinality": 3 } });
685 assert_eq!(
686 replan(&bounded).unwrap(),
687 canon(
688 json!({ "select": "from docs collect", "effects": [effects()[1]], "expect": { "max": 9 } })
689 )
690 );
691 let e = attach(
693 "execute",
694 &json!({ "plan_id": "old", "effects": effects(), "reason": "r" }),
695 err(
696 "plan_mismatch",
697 json!({ "expected": "old", "actual": "new" }),
698 ),
699 none,
700 );
701 assert_eq!(fix_of(&e).unwrap(), canon(json!({ "effects": effects() })));
702 let e = attach(
703 "execute",
704 &json!({ "plan_id": "old", "select": "from docs collect", "effects": [effects()[1]], "expect": { "max": 2 } }),
705 err(
706 "plan_mismatch",
707 json!({ "expected": "old", "actual": "new" }),
708 ),
709 none,
710 );
711 assert_eq!(
712 fix_of(&e).unwrap(),
713 canon(
714 json!({ "select": "from docs collect", "effects": [effects()[1]], "expect": { "max": 2 } })
715 )
716 );
717 }
718
719 #[test]
720 fn a_symbol_before_its_create_moves_the_create_first_and_renames_it() {
721 let args = json!({ "effects": [
722 { "op": "insert", "into": { "kind": "document", "path": "a.md" }, "markdown": "x" },
723 { "op": "insert", "into": { "kind": "document", "id": "$new.3" }, "markdown": "y" },
724 { "op": "create", "path": "c.md", "markdown": "# C" }
725 ] });
726 let e = attach(
729 "plan",
730 &args,
731 err(
732 "target_missing",
733 json!({ "symbol": "$new.3", "effect_index": 1 }),
734 ),
735 Some,
736 );
737 let fixed = json!([
738 args["effects"][0],
739 args["effects"][2],
740 { "op": "insert", "into": { "kind": "document", "id": "$new.1" }, "markdown": "y" },
741 ]);
742 assert_eq!(fix_of(&e).unwrap(), canon(json!({ "effects": fixed })));
743 let e = attach(
745 "plan",
746 &args,
747 err(
748 "target_missing",
749 json!({ "symbol": "$new.0", "effect_index": 1 }),
750 ),
751 Some,
752 );
753 assert_eq!(fix_of(&e), None);
754 let e = attach(
756 "plan",
757 &json!({ "effects": [args["effects"][1]] }),
758 err(
759 "target_missing",
760 json!({ "symbol": "$new.3", "effect_index": 0 }),
761 ),
762 |_| Some(0),
763 );
764 assert_eq!(fix_of(&e), None);
765 }
766
767 #[test]
768 fn selector_requests_are_decorated_with_their_origins() {
769 let req = Request {
770 select: Some(json!("from docs collect")),
771 effects: json!([{ "op": "insert", "into": "-> project", "markdown": "x" }]),
772 expect: Some(json!({ "max": 5 })),
773 };
774 let origins = [Origin {
775 row_id: "d_2".into(),
776 template_index: 0,
777 }];
778 let e = attach(
780 "apply",
781 &json!({ "select": "from docs collect", "effects": req.effects, "expect": { "max": 5 } }),
782 err("conflicting_effects", json!({ "effects": [0, 1] })),
783 none,
784 );
785 assert_eq!(fix_of(&e), None);
786 let e = planning_fix(
788 &req,
789 err(
790 "stale_plan",
791 json!({ "current": { "revision": "r_2" }, "effect_index": 3, "template_index": 0, "row": { "id": "d_2", "path": "/w.md" } }),
792 ),
793 Some(&origins),
794 none,
795 );
796 let mut repinned = req.effects.clone();
797 repinned[0]["expect"] = json!({ "revision": "r_2" });
798 assert_eq!(
799 fix_of(&e).unwrap(),
800 canon(
801 json!({ "select": "from docs collect", "effects": repinned, "expect": { "max": 5 } })
802 )
803 );
804 }
805
806 #[test]
807 fn an_ambiguous_slot_takes_the_first_identity() {
808 let req = Request {
809 select: Some(json!("from docs collect")),
810 effects: json!([{ "op": "insert", "into": "-> project", "markdown": "x" }]),
811 expect: Some(json!({ "max": 5 })),
812 };
813 assert_eq!(
814 ambiguous_slot(&req, "effects[0].into", "d_7", ("docs", "d_2")).unwrap(),
815 canon(
816 json!({ "select": "from docs where id == \"d_2\"", "effects": [{ "op": "insert", "into": "d_7", "markdown": "x" }], "expect": { "max": 1 } })
817 )
818 );
819 let plain = Request {
820 select: None,
821 effects: json!([{ "op": "set", "target": { "kind": "document", "path": "-> project" }, "field": "frontmatter.k", "value": 1 }]),
822 expect: None,
823 };
824 assert_eq!(
825 ambiguous_slot(&plain, "effects[0].target.path", "/p.md", ("docs", "d_2")).unwrap(),
826 canon(
827 json!({ "effects": [{ "op": "set", "target": { "kind": "document", "path": "/p.md" }, "field": "frontmatter.k", "value": 1 }] })
828 )
829 );
830 assert_eq!(
831 ambiguous_slot(&plain, "effects[3].x", "y", ("docs", "d")),
832 None
833 );
834 }
835
836 #[test]
837 fn bounds_and_roots() {
838 let req = Request {
839 select: Some(json!("from docs collect")),
840 effects: json!([effects()[1]]),
841 expect: None,
842 };
843 assert_eq!(
844 bounded(&req, 3),
845 canon(
846 json!({ "select": "from docs collect", "effects": [effects()[1]], "expect": { "max": 3 } })
847 )
848 );
849 let links = Request {
851 select: Some(json!("from links where x == 1 collect")),
852 ..req.clone()
853 };
854 assert_eq!(
855 root_fix(&links, "from links where x == 1 collect", (5, 10), true),
856 canon(
857 json!({ "select": "from blocks where x == 1 collect", "effects": [effects()[1]] })
858 )
859 );
860 assert_eq!(
861 root_fix(&req, "from ★.y collect", (5, 8), false),
862 canon(json!({ "select": "from docs collect", "effects": [effects()[1]] }))
863 );
864 }
865
866 #[test]
867 fn a_leader_in_a_literal_slot_is_dropped_there() {
868 let req = Request {
869 select: None,
870 effects: json!([
871 { "op": "set", "target": { "kind": "block", "id": "b_1" }, "field": "markdown", "value": "x", "expect": { "content_hash": "-> hash" } }
872 ]),
873 expect: None,
874 };
875 let mut fixed = req.effects.clone();
876 fixed[0]["expect"]["content_hash"] = json!("hash");
877 assert_eq!(
878 literal_slot(&req, "effects[0].expect.content_hash").unwrap(),
879 canon(json!({ "effects": fixed }))
880 );
881 assert_eq!(literal_slot(&req, "effects[0].field"), None);
882 assert_eq!(
883 walk_mut(&mut json!({ "a": [[1, 2]] }), "a[0][1]").cloned(),
884 Some(json!(2))
885 );
886 }
887
888 #[test]
889 fn other_tools_and_other_errors_pass_through() {
890 let e = attach(
891 "blocks_read",
892 &json!({}),
893 err("doc_missing", json!({ "doc": "x" })),
894 none,
895 );
896 assert_eq!(fix_of(&e), None);
897 let e = attach(
898 "apply",
899 &json!({ "effects": [] }),
900 err("doc_missing", json!({ "doc": "x" })),
901 none,
902 );
903 assert_eq!(fix_of(&e), None);
904 let bare = attach(
905 "apply",
906 &json!({}),
907 SurfaceError::new("doc_missing", "m"),
908 none,
909 );
910 assert_eq!(bare.data, None);
911 let e = attach(
913 "apply",
914 &json!({ "effects": effects() }),
915 err(
916 "conflicting_effects",
917 json!({ "effects": [0, 1], "fix": "kept" }),
918 ),
919 none,
920 );
921 assert_eq!(fix_of(&e).unwrap(), "kept");
922 }
923
924 #[test]
925 fn the_append_tools_spell_their_calls() {
926 assert_eq!(
927 append_after("b_parent", "b_leaf", "more"),
928 "blocks_insert {\"at\":{\"after\":\"b_leaf\"},\"markdown\":\"more\",\"to\":\"b_parent\"}"
929 );
930 assert_eq!(
931 append_without_section(None, Some("/a.md"), "t"),
932 "docs_append {\"path\":\"/a.md\",\"text\":\"t\"}"
933 );
934 assert_eq!(
935 append_into_heading("b_h", "t"),
936 "blocks_append {\"id\":\"b_h\",\"text\":\"t\"}"
937 );
938 }
939}