@@ -498,6 +498,36 @@ proptest! {
498498 }
499499 }
500500
501+ #[test]
502+ fn law_by_rebuilds_the_same_zipper((tree, focus, backend) in arb_repo()) {
503+ // `by` finds the commit first and then assembles the location in one
504+ // pass, instead of refocusing at every node it visits. The location it
505+ // produces must be the one a step-by-step walk produced: same focus,
506+ // same children, and a context that still climbs back to the top.
507+ let repo = repo_value(&tree, &focus);
508+ let stored: BTreeSet<String> = backend.metas.keys().cloned().collect();
509+ let (mut i, cfg) = make_interp(backend);
510+ for id in &stored {
511+ let got = j::repo::by_id(&repo, id).unwrap();
512+ prop_assert!(got.is_some(), "by {} found nothing", id);
513+ let got = got.unwrap();
514+ // the focus is the commit asked for
515+ match got.field("root").unwrap().field("id").unwrap() {
516+ Value::Id(f) => prop_assert_eq!(&*f, id),
517+ _ => prop_assert!(false, "focus has no id"),
518+ }
519+ // `goto` through the language agrees with it
520+ let viaz = eval_fn(&mut i, &cfg, &format!("by @{}", id), repo.clone());
521+ prop_assert!(viaz.is_ok(), "by @{} failed: {:?}", id, viaz.err());
522+ prop_assert!(repos_match(&viaz.unwrap(), &got, &stored));
523+ // climbing all the way up returns the whole history unchanged
524+ let up = eval_fn(&mut i, &cfg, "top", got.clone());
525+ prop_assert!(up.is_ok(), "top failed: {:?}", up.err());
526+ let from_repo = eval_fn(&mut i, &cfg, "top", repo.clone()).unwrap();
527+ prop_assert!(repos_match(&up.unwrap(), &from_repo, &stored));
528+ }
529+ }
530+
501531 #[test]
502532 fn law_top_top((tree, focus, backend) in arb_repo()) {
503533 let repo = repo_value(&tree, &focus);
@@ -522,3 +552,93 @@ proptest! {
522552 prop_assert!(value_eq(&a, &b).unwrap());
523553 }
524554}
555+
556+/// A linear repo root -> c0 -> c1 -> ... focused on the top, built directly.
557+fn linear_repo(n: usize) -> (Value, Vec<String>) {
558+ let ids: Vec<String> = (0..n).map(|k| format!("kaaa{:04}", k)).collect();
559+ let mut subtree = Value::record(&[
560+ ("children", Value::list(vec![])),
561+ (
562+ "root",
563+ Value::record(&[
564+ ("files", Value::list(vec![])),
565+ ("id", Value::Id(Rc::new(ids[n - 1].clone()))),
566+ ("labels", Value::list(vec![])),
567+ ("message", Value::text(format!("commit {}", n - 1))),
568+ ]),
569+ ),
570+ ]);
571+ for k in (0..n - 1).rev() {
572+ subtree = Value::record(&[
573+ ("children", Value::list(vec![subtree])),
574+ (
575+ "root",
576+ Value::record(&[
577+ ("files", Value::list(vec![])),
578+ ("id", Value::Id(Rc::new(ids[k].clone()))),
579+ ("labels", Value::list(vec![])),
580+ ("message", Value::text(format!("commit {}", k))),
581+ ]),
582+ ),
583+ ]);
584+ }
585+ let repo = Value::record(&[
586+ ("children", Value::list(vec![subtree])),
587+ ("context", Value::list(vec![])),
588+ (
589+ "root",
590+ Value::record(&[
591+ ("files", Value::list(vec![])),
592+ ("id", Value::Id(Rc::new(ROOT_ID.to_string()))),
593+ ("labels", Value::list(vec![])),
594+ ("message", Value::text("")),
595+ ]),
596+ ),
597+ ]);
598+ (repo, ids)
599+}
600+
601+#[test]
602+fn by_builds_the_context_innermost_first() {
603+ // `by` assembles the location in one pass now; the context must still run
604+ // from the immediate parent outwards, which is what `up` reads
605+ let (repo, ids) = linear_repo(8);
606+ for (depth, id) in ids.iter().enumerate() {
607+ let loc = j::repo::by_id(&repo, id).unwrap().expect("found");
608+ match loc.field("root").unwrap().field("id").unwrap() {
609+ Value::Id(f) => assert_eq!(&*f, id, "wrong focus"),
610+ _ => panic!("focus has no id"),
611+ }
612+ let ctx = loc.field("context").unwrap();
613+ let ctx = ctx.as_list().unwrap();
614+ assert_eq!(ctx.len(), depth + 1, "context depth for {}", id);
615+ // frame k names the ancestor k+1 levels up; the last is the root
616+ for (k, frame) in ctx.iter().enumerate() {
617+ let want = if depth >= k + 1 {
618+ ids[depth - k - 1].clone()
619+ } else {
620+ ROOT_ID.to_string()
621+ };
622+ match frame.field("parent").unwrap().field("id").unwrap() {
623+ Value::Id(p) => assert_eq!(&*p, &want, "frame {} of {}", k, id),
624+ _ => panic!("frame parent has no id"),
625+ }
626+ }
627+ }
628+}
629+
630+#[test]
631+fn by_then_climbing_back_restores_the_history() {
632+ let (repo, ids) = linear_repo(8);
633+ let (mut i, cfg) = make_interp(MemBackend::new());
634+ let whole = eval_fn(&mut i, &cfg, "top", repo.clone()).unwrap();
635+ for id in &ids {
636+ let loc = j::repo::by_id(&repo, id).unwrap().expect("found");
637+ let back = eval_fn(&mut i, &cfg, "top", loc).unwrap();
638+ assert!(
639+ value_eq(&back, &whole).unwrap_or(false),
640+ "climbing back from {} changed the history",
641+ id
642+ );
643+ }
644+}