mod common;
use common::Sandbox;
fn section(s: &str, title: &str) -> bool {
s.lines().any(|l| l.trim_start().starts_with(title))
}
#[test]
fn measures_since_the_last_stop_by_default() {
let c = Sandbox::new_seeded("default-boundary");
c.ok(&["push", "Old node", "--why", "already handled"]);
c.ok(&["pop", "done with it"]);
c.ok(&["save", "checkpoint"]);
c.ok(&["add", "New node", "--why", "born after the stop"]);
let s = c.ok(&["changes"]);
assert!(s.contains("the last stop"), "{s}");
assert!(s.contains("New node"), "{s}");
assert!(
!s.contains("Old node"),
"it went past its own boundary:\n{s}"
);
}
#[test]
fn right_after_a_stop_nothing_has_moved_and_the_header_still_prints() {
let c = Sandbox::new_seeded("nothing-moved");
c.ok(&["push", "Something", "--why", "opened and closed already"]);
c.ok(&["pop", "done"]);
c.ok(&["save", "checkpoint"]);
let s = c.ok(&["changes"]);
assert!(s.contains("CHANGES SINCE"), "no header:\n{s}");
assert!(s.contains("Nothing has moved since"), "{s}");
}
#[test]
fn an_older_since_includes_what_the_last_stop_no_longer_does() {
let c = Sandbox::new_seeded("older-since");
c.ok(&["push", "Node A", "--why", "opened early"]);
c.ok(&["pop", "closed A"]);
c.ok(&["push", "Node B", "--why", "opened later"]);
c.ok(&["pop", "closed B"]);
let last = c.ok(&["changes"]);
assert!(!last.contains("Node A"), "{last}");
assert!(!last.contains("Node B"), "{last}");
let older = c.ok(&["changes", "--since", "v1"]);
assert!(older.contains("Node A"), "{older}");
assert!(older.contains("Node B"), "{older}");
assert!(older.contains("stops ago"), "{older}");
assert!(!older.contains("the last stop"), "{older}");
}
#[test]
fn an_unknown_since_is_refused_and_prints_no_group() {
let c = Sandbox::new_seeded("unknown-since");
c.ok(&["push", "Something", "--why", "any reason"]);
let (out, code) = c.run(&["changes", "--since", "v999"]);
assert_eq!(code, 2, "{out}");
assert!(out.contains("v999"), "{out}");
for g in ["OPENED", "CLOSED", "FLAGGED", "MOVED"] {
assert!(!out.contains(g), "it printed a group anyway:\n{out}");
}
}
#[test]
fn a_node_born_and_closed_in_the_same_stretch_appears_in_both_groups() {
let c = Sandbox::new_seeded("born-and-closed");
c.ok(&["save", "checkpoint"]);
c.ok(&[
"push",
"Quick fix",
"--why",
"spotted and fixed immediately",
]);
c.ok(&["pop", "shipped"]);
let s = c.ok(&["changes", "--since", "v1"]);
assert!(section(&s, "OPENED"), "{s}");
assert!(section(&s, "CLOSED"), "{s}");
assert_eq!(s.matches("Quick fix").count(), 2, "{s}");
}
#[test]
fn a_forced_close_comes_out_marked() {
let c = Sandbox::new_seeded("forced-close");
c.ok(&["save", "checkpoint"]);
c.ok(&["push", "Audit", "--why", "it is due for review"]);
c.ok(&[
"add",
"Unfixed finding",
"--parent",
"1",
"--blocks",
"--why",
"came out of the audit, late",
]);
c.ok(&["done", "1", "closing anyway", "--force"]);
let s = c.ok(&["changes", "--since", "v1"]);
assert!(s.contains("forced: closing anyway"), "{s}");
}
#[test]
fn a_stretch_that_only_moved_the_focus_prints_the_tail_line() {
let c = Sandbox::new_seeded("focus-only");
c.ok(&["push", "Root", "--why", "top of the stack"]);
c.ok(&["push", "Child", "--why", "a detour under it"]);
c.ok(&["save", "checkpoint"]);
c.ok(&["focus", "1"]);
let s = c.ok(&["changes"]);
assert!(!s.contains("Nothing has moved"), "{s}");
assert!(s.contains("focus move"), "{s}");
}
#[test]
fn the_json_carries_the_limit_and_the_four_lists() {
let c = Sandbox::new_seeded("json");
c.ok(&["save", "checkpoint"]);
c.ok(&["add", "A goal", "--why", "it is needed"]);
let s = c.ok(&["changes", "--json"]);
let v: serde_json::Value = serde_json::from_str(&s).expect("valid json");
assert!(v["since"].is_object(), "{s}");
for k in ["opened", "closed", "flagged", "moved"] {
assert!(v[k].is_array(), "{k} missing:\n{s}");
}
for k in [
"focus_moves",
"notes",
"flags_cleared",
"edge_changes",
"unreadable",
] {
assert_eq!(v["tail"][k], 0, "{k} missing or not zero:\n{s}");
}
}
#[test]
fn with_no_stop_at_all_the_header_measures_from_the_beginning() {
let c = Sandbox::new_seeded("no-stops");
c.ok(&["add", "Something", "--why", "no stop has happened yet"]);
let s = c.ok(&["changes"]);
assert!(
s.contains("CHANGES SINCE THE BEGINNING - no stops yet"),
"{s}"
);
}
#[test]
fn since_manual_reaches_past_the_stops_the_hook_wrote() {
let c = Sandbox::new_seeded("since-manual");
c.ok(&["add", "Before the save", "--why", "opened earlier"]);
c.ok(&["save", "where I stopped looking"]);
c.ok(&[
"add",
"First after the save",
"--why",
"opened right after the stop was made",
]);
c.ok(&["push", "A detour", "--why", "it leaves a stop behind it"]);
c.ok(&["pop", "and another one on the way out"]);
c.ok(&[
"add",
"Second after the save",
"--why",
"opened after two more stops",
]);
let manual = c.ok(&["changes", "--since", "manual"]);
assert!(manual.contains("First after the save"), "{manual}");
assert!(manual.contains("Second after the save"), "{manual}");
assert!(!manual.contains("Before the save"), "{manual}");
let default = c.ok(&["changes"]);
assert!(
!default.contains("First after the save"),
"the last stop alone reached too far back:
{default}"
);
assert!(default.contains("Second after the save"), "{default}");
}
#[test]
fn since_manual_says_so_in_the_header() {
let c = Sandbox::new_seeded("since-manual-header");
c.ok(&["add", "Something", "--why", "so a stop has work behind it"]);
c.ok(&["save", "a stop made by hand"]);
c.ok(&[
"add",
"After it",
"--why",
"so the hook has something to see",
]);
let s = c.ok(&["changes", "--since", "manual"]);
assert!(s.contains("the last stop you made"), "{s}");
}
#[test]
fn since_manual_with_no_stop_made_by_hand_falls_back_to_the_beginning() {
let c = Sandbox::new_seeded("since-manual-none");
let (s, code) = c.run(&["changes", "--since", "manual"]);
assert_eq!(code, 0, "{s}");
assert!(s.contains("CHANGES SINCE THE BEGINNING"), "{s}");
assert!(s.contains("no stop here was made by hand"), "{s}");
}
#[test]
fn since_manual_with_nothing_after_it_says_nothing_moved() {
let c = Sandbox::new_seeded("since-manual-empty");
c.ok(&[
"add",
"Something",
"--why",
"so the stop has work behind it",
]);
c.ok(&["save", "nothing follows this"]);
let s = c.ok(&["changes", "--since", "manual"]);
assert!(s.contains("Nothing has moved since"), "{s}");
}
#[test]
fn the_json_names_which_boundary_answered() {
let c = Sandbox::new_seeded("since-manual-json");
c.ok(&[
"add",
"Something",
"--why",
"so the stop has work behind it",
]);
c.ok(&["save", "checkpoint"]);
let manual_out = c.ok(&["changes", "--since", "manual", "--json"]);
let manual_json: serde_json::Value = serde_json::from_str(&manual_out).expect("valid json");
assert_eq!(manual_json["since"]["kind"], "manual", "{manual_out}");
let stop_out = c.ok(&["changes", "--json"]);
let stop_json: serde_json::Value = serde_json::from_str(&stop_out).expect("valid json");
assert_eq!(stop_json["since"]["kind"], "stop", "{stop_out}");
assert!(stop_json["since"]["alias"].is_string(), "{stop_out}");
assert!(stop_json["since"]["ts"].is_string(), "{stop_out}");
assert!(stop_json["since"]["stops_since"].is_number(), "{stop_out}");
}
#[test]
fn an_unknown_since_still_names_manual_as_an_option() {
let c = Sandbox::new_seeded("unknown-since-mentions-manual");
let (out, code) = c.run(&["changes", "--since", "v999"]);
assert_eq!(code, 2, "{out}");
assert!(out.contains("manual"), "{out}");
}