From 1e4ef7bafb3ea9aa02bbcb3ecfb5d3dea10c6c96 Mon Sep 17 00:00:00 2001 From: "@permadeath.com" Date: Tue, 25 Aug 2026 21:39:31 -0400 Subject: [PATCH] test(stack): pin the seams the marks work left untested A view whose patch blob is gone, a mark whose branch was deleted, --add-change-ids moving the marks with the commits under them, and pr diff on a member holding more than one commit. Change-Id: Ib78353c38ac59878becb54b661eb183df0ff7a8a --- TODO.md | 16 ++++++ tests/stack_flows.rs | 119 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 135 insertions(+) diff --git a/TODO.md b/TODO.md index b898bb2..458469d 100644 --- a/TODO.md +++ b/TODO.md @@ -1299,6 +1299,22 @@ agents file five pull requests for one change. `init`ed and its layers `add`ed, never guessed at. With no marks left, the cuts come back out of the records' change-id headers, so a fresh clone still reconciles +- [x] Not done, and it should not be: naming a mark after the member's + *bottom* commit, to match the pull title `stack create` takes from + there. The mark is named after the commit it lands on, which is the + member's top, so for a grouped member the branch name and the pull + title come from different commits. The fix does not exist: a member's + bottom commit is whatever the mark below it ends at, and that mark may + not have been placed yet — `stack mark part2 HEAD~1` after + `stack mark part1 HEAD~3` changes what part2's bottom is, retroactively. + Naming from the bottom would be wrong as often as it is right, and the + listings already print the subject next to the name, so nothing is + being hidden +- [x] Seams the stack work left untested, now pinned: `stack view`'s `?` for + a member whose patch blob is gone, a mark whose branch was deleted, + `--add-change-ids` carrying the marks through the rewrite that moves + every commit under them, and `pr diff` on a member holding more than + one commit - [x] `atgc stack up`, `down`, `top`, `bottom` and `checkout` — move between a stack's members. The marks already say where each one ends, and the branches are already there; what was missing was the reverse index, so diff --git a/tests/stack_flows.rs b/tests/stack_flows.rs index bd042be..5fede8d 100644 --- a/tests/stack_flows.rs +++ b/tests/stack_flows.rs @@ -2400,3 +2400,122 @@ fn navigating_an_unstacked_branch_is_refused_with_a_pointer() { assert!(run.stderr.contains("not part of a stack"), "{}", run.stderr); assert!(run.stderr.contains("stack mark"), "{}", run.stderr); } + +// --------------------------------------------------------------------------- +// seams +// --------------------------------------------------------------------------- + +/// `stack view` says `?` for a member whose patch it could not read, rather +/// than failing the whole listing. +/// +/// How many commits a member holds is written in exactly one place — its +/// latest round's patch — so the count is one blob read per member, and a +/// blob that has gone is the ordinary consequence of that. The listing is +/// most wanted when something is wrong with the records, so it degrades +/// instead of refusing. +#[test] +fn view_says_question_mark_for_a_member_whose_patch_is_gone() { + let world = published_stack("view-unreadable-patch"); + world.with(|w| w.blobs.clear()); + + let run = world.run(&["stack", "view"]).success(); + + assert!( + run.stdout.contains("? commits"), + "an unreadable patch should show as ?:\n{}", + run.stdout + ); + let json = world.run(&["stack", "view", "--json"]).success().json(); + assert_eq!( + json["members"][0]["commits"], + serde_json::Value::Null, + "{json:#}" + ); +} + +/// A mark whose branch somebody deleted is named as gone, and the stack it +/// cut falls back to the cuts that are left. +#[test] +fn a_mark_whose_branch_was_deleted_is_named_and_stops_cutting() { + let world = Scenario::new("mark-branch-deleted"); + three_commit_branch(&world); + world.run(&["stack", "mark", "part1", "HEAD~1"]).success(); + world.checkout.git(&["branch", "-D", "part1"]); + + let listed = world.run(&["stack", "mark"]).success(); + assert!( + listed.stdout.contains("branch is gone"), + "{}", + listed.stdout + ); + + // With no cut left in the range, the branch is one pull per commit — + // the unmarked shape — rather than a stack built around a mark that no + // longer exists. + world.run(&["stack", "create"]).success(); + assert_eq!(keys(&world).len(), 3, "{:#?}", world.pulls(ALICE)); +} + +/// `--add-change-ids` rewrites every commit in the range, which moves the +/// branches the marks are, and the cut has to survive it. +/// +/// This is the seam the rewrite is most able to break silently: the marks +/// are remapped inside the rewrite, before the stacked branch's own ref is +/// moved, and getting the order wrong leaves them pointing at commits that +/// are no longer in the range — a stack that quietly re-cuts itself into one +/// pull per commit. +#[test] +fn add_change_ids_carries_the_marks_through_the_rewrite() { + let world = Scenario::new("add-change-ids-marks"); + world.checkout.branch("feature"); + world + .checkout + .commit("one.txt", "one\n", "feat: bottom", None); + world + .checkout + .commit("two.txt", "two\n", "feat: middle", None); + world + .checkout + .commit("three.txt", "three\n", "feat: top", None); + world.run(&["stack", "mark", "part1", "HEAD~1"]).success(); + let before = world.checkout.git(&["rev-parse", "part1"]); + + world + .run(&["stack", "create", "--add-change-ids"]) + .success(); + + let after = world.checkout.git(&["rev-parse", "part1"]); + assert_ne!(before, after, "the rewrite should have moved the mark"); + let listed = world.run(&["stack", "mark"]).success(); + assert!( + listed.stdout.contains("2/3"), + "part1 should still cut after the middle commit:\n{}", + listed.stdout + ); + assert_eq!( + keys(&world).len(), + 2, + "the cut should have held: {:#?}", + world.pulls(ALICE) + ); +} + +/// `pr diff` on a member that carries several commits prints the whole +/// mailbox, not just the first message. +#[test] +fn pr_diff_on_a_multi_commit_member_prints_every_commit() { + let world = Scenario::new("diff-multi-commit"); + three_commit_branch(&world); + world.run(&["stack", "mark", "HEAD~1"]).success(); + world.run(&["stack", "create"]).success(); + + let bottom = keys(&world).remove(0); + let run = world.run(&["pr", "diff", &bottom]).success(); + + assert!(run.stdout.contains("feat: bottom"), "{}", run.stdout); + assert!( + run.stdout.contains("feat: middle"), + "a two-commit member's patch holds both:\n{}", + run.stdout + ); +} -- 2.51.2