mod common;
use common::Sandbox;
use serde_json::Value;
fn stack_aliases(c: &Sandbox) -> Vec<String> {
let s = c.ok(&["stack", "--json"]);
let v: Value = serde_json::from_str(&s).unwrap_or_else(|e| panic!("not JSON: {e}\n{s}"));
v["stack"]
.as_array()
.expect("stack is an array")
.iter()
.map(|n| n["alias"].as_str().unwrap().to_string())
.collect()
}
fn why_json(c: &Sandbox, id: &str) -> Value {
let s = c.ok(&["why", id, "--json"]);
serde_json::from_str(&s).unwrap_or_else(|e| panic!("not JSON: {e}\n{s}"))
}
#[test]
fn done_in_the_middle_of_the_stack_does_not_unstack_it() {
let c = Sandbox::new_seeded("stack-done-middle");
c.ok(&["push", "Base goal", "--why", "it anchors the branch"]);
c.ok(&["push", "Task two", "--why", "the goal needs it"]);
c.ok(&["push", "Task three", "--why", "one more step"]);
assert_eq!(stack_aliases(&c), vec!["g1", "t2", "t3"]);
c.ok(&["done", "t2", "settled early"]);
assert_eq!(
stack_aliases(&c),
vec!["g1", "t2", "t3"],
"done in the middle popped the stack"
);
assert_eq!(why_json(&c, "t2")["node"]["state"], "done");
let brief = c.ok(&["brief"]);
assert!(brief.contains("depth 3"), "{brief}");
assert!(brief.contains("Task two [closed]"), "{brief}");
let out = c.ok(&["pop", "wrapped up"]);
assert!(out.contains("back to t2"), "{out}");
assert_eq!(stack_aliases(&c), vec!["g1", "t2"]);
}
#[test]
fn park_in_the_middle_of_the_stack_does_not_unstack_it() {
let c = Sandbox::new_seeded("stack-park-middle");
c.ok(&["push", "Base goal", "--why", "it anchors the branch"]);
c.ok(&["push", "Task two", "--why", "the goal needs it"]);
c.ok(&["push", "Task three", "--why", "one more step"]);
c.ok(&["park", "t2", "stuck on something else"]);
assert_eq!(
stack_aliases(&c),
vec!["g1", "t2", "t3"],
"park in the middle popped the stack"
);
assert_eq!(why_json(&c, "t2")["node"]["state"], "suspended");
let brief = c.ok(&["brief"]);
assert!(brief.contains("Task two [parked]"), "{brief}");
let parked = c.ok(&["parked"]);
assert!(parked.contains("Task two"), "{parked}");
}
#[test]
fn the_depth_advice_carries_the_bottom_mark_once_it_is_closed() {
let c = Sandbox::new_seeded("stack-depth-mark");
c.ok(&["push", "Base goal", "--why", "root of the branch"]);
c.ok(&["push", "A", "--why", "first step"]);
c.ok(&["push", "B", "--why", "second step"]);
c.ok(&["push", "C", "--why", "third step"]);
c.ok(&["push", "D", "--why", "fourth step"]);
assert_eq!(stack_aliases(&c), vec!["g1", "t2", "t3", "t4", "t5"]);
c.ok(&["done", "g1", "superseded already"]);
let out = c.ok(&["push", "E", "--why", "fifth step"]);
assert!(
out.contains("You are 6 levels away from g1 \"Base goal\" [achieved]."),
"{out}"
);
}
#[test]
fn a_pop_into_a_superseded_focus_leaves_it_as_it_was() {
let c = Sandbox::new_seeded("stack-pop-superseded");
c.ok(&["push", "Base goal", "--why", "root of the branch"]);
c.ok(&[
"push",
"Old approach",
"--why",
"the call being made",
"--type",
"decision",
]);
c.ok(&["push", "Follow-up", "--why", "work under the decision"]);
c.ok(&[
"decide",
"New approach",
"--reason",
"the old one did not hold",
"--supersedes",
"2",
]);
assert_eq!(why_json(&c, "d2")["node"]["state"], "superseded");
assert_eq!(why_json(&c, "d2")["node"]["outcome"], "superseded by d4");
c.ok(&["pop", "follow-up done"]);
assert_eq!(stack_aliases(&c), vec!["g1", "d2"]);
let out = c.ok(&["pop", "moving on"]);
assert!(
out.contains("d2 Old approach -> already superseded, left as it was"),
"{out}"
);
assert_eq!(why_json(&c, "d2")["node"]["state"], "superseded");
assert_eq!(
why_json(&c, "d2")["node"]["outcome"],
"superseded by d4",
"the pop overwrote the outcome the supersession wrote"
);
}
#[test]
fn restore_reports_closed_nodes_still_on_the_path_apart_from_what_left() {
let c = Sandbox::new_seeded("stack-restore-path");
c.ok(&["push", "Base goal", "--why", "root of the branch"]);
c.ok(&["push", "A", "--why", "first step"]);
c.ok(&["push", "B", "--why", "second step"]);
c.ok(&["push", "C", "--why", "third step"]);
let saved = c.ok(&["save", "checkpoint", "--next", "carry on"]);
let vivac_num = saved
.lines()
.next()
.unwrap()
.split_whitespace()
.next()
.unwrap()
.to_string();
c.ok(&["done", "g1", "closed from below"]);
let out = c.ok(&["restore", &vivac_num]);
assert!(
out.contains("still on the path: g1 Base goal [achieved]"),
"{out}"
);
assert!(
!out.contains("no longer on the stack"),
"nothing should have fallen off yet:\n{out}"
);
assert_eq!(stack_aliases(&c), vec!["g1", "t2", "t3", "t4"]);
c.ok(&["done", "t4", "closed the tip"]);
let out = c.ok(&["restore", &vivac_num]);
assert!(
out.contains("still on the path: g1 Base goal [achieved]"),
"{out}"
);
assert!(
out.contains("no longer on the stack: t4 C [closed]"),
"{out}"
);
assert_eq!(stack_aliases(&c), vec!["g1", "t2", "t3"]);
}