use std::collections::BTreeMap;
use std::fs;
use std::path::Path;
use assert_cmd::Command;
use tempfile::TempDir;
fn tix() -> Command {
Command::cargo_bin("tix").unwrap()
}
const SCOPES: &str = "issues_dir = \"docs/issues\"\n\n\
[kanban.default]\ncolumns = [\"todo\", \"doing\", \"done\"]\n\n\
[kanban.default.wip.units]\nd = 1\nw = 5\n\n\
[teams.Platform]\nmembers = [\"ada\"]\n\n\
[cycles.\"Release A\"]\nstart_date = \"2026-01-01\"\n";
const SCHEDULE: &str = "\n[cycle_schedules.sprint]\n\
name = \"Weekly sprint\"\n\
cadence = { kind = \"weekly\" }\n\
duration = { count = 1, unit = \"weeks\" }\n\
start_date = \"2026-01-05\"\n\
timezone = \"UTC\"\n\
look_ahead = 1\n";
const INSTANCE_NAME: &str = "Weekly sprint 2026-01-05 000001";
const INSTANCE_FILE: &str = "weekly-sprint-2026-01-05-000001.md";
fn project(raw: &str) -> TempDir {
let dir = TempDir::new().unwrap();
fs::write(dir.path().join("tix.toml"), raw).unwrap();
dir
}
fn read(path: &Path) -> String {
fs::read_to_string(path).unwrap()
}
fn registry(dir: &Path) -> String {
read(&dir.join("tix.toml"))
}
fn stdout(dir: &Path, args: &[&str]) -> String {
let out = tix()
.current_dir(dir)
.args(args)
.assert()
.success()
.get_output()
.stdout
.clone();
String::from_utf8(out).unwrap()
}
fn refused(dir: &Path, args: &[&str]) -> (i32, String, String) {
let out = tix()
.current_dir(dir)
.args(args)
.assert()
.failure()
.get_output()
.clone();
(
out.status.code().unwrap_or(-1),
String::from_utf8(out.stdout).unwrap(),
String::from_utf8(out.stderr).unwrap(),
)
}
fn run(dir: &Path, args: &[&str]) -> (i32, String, String) {
let out = tix()
.current_dir(dir)
.args(args)
.assert()
.get_output()
.clone();
(
out.status.code().unwrap_or(-1),
String::from_utf8(out.stdout).unwrap(),
String::from_utf8(out.stderr).unwrap(),
)
}
fn tree(dir: &Path) -> BTreeMap<String, Vec<u8>> {
let mut out = BTreeMap::new();
let mut stack = vec![dir.to_path_buf()];
while let Some(current) = stack.pop() {
let Ok(entries) = fs::read_dir(¤t) else {
continue;
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
stack.push(path);
} else {
let name = path
.strip_prefix(dir)
.unwrap()
.to_string_lossy()
.to_string();
out.insert(name, fs::read(&path).unwrap());
}
}
}
out
}
fn generated_instance_with_capacity(dir: &Path, capacity: i64, unit: &str) -> String {
stdout(
dir,
&["cycle", "schedule", "generate", "sprint", "--confirm"],
);
let id = "sprint:000001".to_string();
stdout(
dir,
&[
"capacity",
"set",
"--scope",
"cycle",
"--id",
&id,
"--value",
&capacity.to_string(),
"--unit",
unit,
],
);
id
}
fn create_issue(dir: &Path, title: &str, estimate: &str, status: &str) {
stdout(
dir,
&[
"create",
"--title",
title,
"--estimate",
estimate,
"--cycles",
INSTANCE_NAME,
"--assignee",
"ada",
"--status",
status,
],
);
}
fn payload_keys(snapshot: &str) -> Vec<String> {
let mut keys = Vec::new();
let mut inside = false;
for line in snapshot.lines() {
if line.starts_with("measurements:") {
inside = true;
continue;
}
if !inside {
continue;
}
if line.starts_with("---") {
break;
}
let Some(body) = line.strip_prefix(" ") else {
continue;
};
if body.starts_with([' ', '-']) {
continue;
}
if let Some((key, _)) = body.split_once(':') {
keys.push(key.trim().to_string());
}
}
keys.sort();
keys
}
const PAYLOAD_KEYS: [&str; 21] = [
"capacity",
"closed_at",
"closed_by",
"committed",
"completed",
"counted_issues",
"cycle_id",
"diagnostics",
"overflow",
"remaining",
"scope_display",
"scope_kind",
"scope_reference",
"snapshot_id",
"source_issues",
"source_units",
"state",
"uncounted_issues",
"unestimated_issues",
"unit",
"utilization",
];
#[test]
fn capacity_round_trips_through_set_list_show_and_delete() {
let tmp = project(SCOPES);
let dir = tmp.path();
assert_eq!(
stdout(dir, &["capacity", "list"]),
"id\tscope\treference\tdisplay\tcapacity\tunit\tenabled\n"
);
let set = stdout(
dir,
&[
"capacity",
"set",
"--scope",
"cycle",
"--id",
"Release A",
"--value",
"40",
"--unit",
"d",
],
);
assert_eq!(set, "Set capacity for cycle `Release A`: 40d\n");
stdout(
dir,
&[
"capacity", "set", "--scope", "team", "--id", "Platform", "--value", "8", "--unit", "w",
],
);
stdout(
dir,
&[
"capacity", "set", "--scope", "assignee", "--id", "ada", "--value", "3", "--unit", "d",
],
);
let document = registry(dir);
for fragment in [
"[capacity.cycle-release-a]",
"cycle = \"Release A\"",
"[capacity.team-platform]",
"team = \"Platform\"",
"[capacity.assignee-ada]",
"assignee = \"ada\"",
"capacity = 40",
"unit = \"d\"",
] {
assert!(
document.contains(fragment),
"`{fragment}` missing: {document}"
);
}
let list = stdout(dir, &["capacity", "list"]);
let rows: Vec<Vec<&str>> = list
.lines()
.map(|line| line.split('\t').collect())
.collect();
assert_eq!(rows[0][0], "id");
assert_eq!(rows[0][5], "unit");
assert_eq!(rows[0][6], "enabled");
assert_eq!(rows.len(), 4, "{list}");
assert_eq!(
rows[1],
[
"cycle-release-a",
"cycle",
"Release A",
"Release A",
"40",
"d",
"true"
]
);
assert_eq!(
rows[2],
[
"team-platform",
"team",
"Platform",
"Platform",
"8",
"w",
"true"
]
);
assert_eq!(
rows[3],
["assignee-ada", "assignee", "ada", "ada", "3", "d", "true"]
);
let show = stdout(dir, &["capacity", "show", "Release A"]);
assert_eq!(
show,
"id: cycle-release-a\nscope: cycle\nreference: Release A\ndisplay: Release A\n\
capacity: 40\nunit: d\nenabled: true\n"
);
assert!(
stdout(dir, &["capacity", "show", "ada", "--scope", "assignee"]).contains("capacity: 3")
);
stdout(
dir,
&[
"capacity",
"set",
"--scope",
"cycle",
"--id",
"Release A",
"--value",
"50",
"--unit",
"d",
],
);
assert_eq!(stdout(dir, &["capacity", "list"]).lines().count(), 4);
assert!(stdout(dir, &["capacity", "show", "Release A"]).contains("capacity: 50"));
assert_eq!(
stdout(
dir,
&["capacity", "delete", "--scope", "team", "--id", "Platform"]
),
"Removed capacity for team `Platform`\n"
);
let list = stdout(dir, &["capacity", "list"]);
assert!(!list.contains("team-platform"), "{list}");
assert!(!registry(dir).contains("[capacity.team-platform]"));
}
#[test]
fn a_disabled_entry_keeps_its_numbers_and_enforces_nothing() {
let tmp = project(SCOPES);
let dir = tmp.path();
stdout(
dir,
&[
"capacity",
"set",
"--scope",
"cycle",
"--id",
"Release A",
"--value",
"10",
"--unit",
"d",
"--disabled",
],
);
let document = registry(dir);
assert!(document.contains("enabled = false"), "{document}");
assert!(document.contains("capacity = 10"), "{document}");
assert!(document.contains("unit = \"d\""), "{document}");
assert!(stdout(dir, &["capacity", "list"]).contains("\tfalse"));
let calculation = stdout(
dir,
&[
"capacity",
"calculate",
"--scope",
"cycle",
"--id",
"Release A",
],
);
assert!(calculation.contains("state: unconfigured"), "{calculation}");
assert!(calculation.contains("capacity: -"), "{calculation}");
}
#[test]
fn every_bad_field_is_refused_by_name_and_writes_nothing() {
let tmp = project(SCOPES);
let dir = tmp.path();
let before = tree(dir);
let cases: [(&[&str], &str); 5] = [
(
&[
"capacity",
"set",
"--scope",
"cycle",
"--id",
"Release A",
"--value",
"10",
"--unit",
"points",
],
"is not a declared estimate unit",
),
(
&[
"capacity", "set", "--scope", "cycle", "--id", "nope", "--value", "10", "--unit",
"d",
],
"nope",
),
(
&[
"capacity",
"set",
"--scope",
"cycle",
"--id",
"Release A",
"--value",
"-1",
"--unit",
"d",
],
"must not be negative",
),
(
&[
"capacity",
"set",
"--scope",
"cycle",
"--id",
"Release A",
"--value",
"10",
"--unit",
" ",
],
"--unit",
),
(
&[
"capacity", "set", "--scope", "cycle", "--id", " ", "--value", "10", "--unit", "d",
],
"--id",
),
];
for (args, expected) in cases {
let (code, _, err) = refused(dir, args);
assert_eq!(code, 1, "{args:?}: {err}");
assert!(err.contains(expected), "{args:?}: {err}");
assert_eq!(tree(dir), before, "{args:?} must write nothing");
}
let (code, _, err) = refused(
dir,
&[
"capacity",
"calculate",
"--scope",
"sprint",
"--id",
"Release A",
],
);
assert_eq!(code, 1, "{err}");
assert!(
err.contains("expected one of `cycle`, `team`, `assignee`"),
"{err}"
);
let before = tree(dir);
let (code, _, err) = refused(
dir,
&[
"capacity",
"set",
"--scope",
"cycle",
"--id",
"Release A",
"--value",
"10",
],
);
assert_eq!(code, 2, "a missing required flag is a usage error: {err}");
assert!(err.contains("--unit"), "{err}");
assert_eq!(tree(dir), before);
}
#[test]
fn a_repeated_scope_is_refused_and_a_hand_written_entry_is_reached_by_scope() {
let raw = format!(
"{SCOPES}\n[capacity.handwritten]\ncycle = \"Release A\"\ncapacity = 40\nunit = \"d\"\n"
);
let tmp = project(&raw);
let dir = tmp.path();
let before = tree(dir);
let (code, _, err) = refused(
dir,
&[
"capacity",
"set",
"--scope",
"cycle",
"--id",
"Release A",
"--value",
"50",
"--unit",
"d",
],
);
assert_eq!(code, 1, "{err}");
assert!(err.contains("one ceiling per scope"), "{err}");
assert_eq!(tree(dir), before, "a repeated scope writes nothing");
assert!(stdout(dir, &["capacity", "show", "Release A"]).contains("id: handwritten"));
assert_eq!(
stdout(
dir,
&[
"capacity",
"delete",
"--scope",
"cycle",
"--id",
"Release A"
]
),
"Removed capacity for cycle `Release A`\n"
);
assert!(!registry(dir).contains("[capacity.handwritten]"));
}
#[test]
fn a_generated_instances_id_and_name_are_one_scope_for_set_show_and_delete() {
let raw = format!("{SCOPES}{SCHEDULE}");
let tmp = project(&raw);
let dir = tmp.path();
stdout(
dir,
&["cycle", "schedule", "generate", "sprint", "--confirm"],
);
let id = "sprint:000001";
stdout(
dir,
&[
"capacity", "set", "--scope", "cycle", "--id", id, "--value", "10", "--unit", "d",
],
);
let show = stdout(dir, &["capacity", "show", INSTANCE_NAME]);
assert!(show.contains("reference: sprint:000001"), "{show}");
assert!(
show.contains("display: Weekly sprint 2026-01-05 000001"),
"{show}"
);
assert!(show.contains("capacity: 10"), "{show}");
assert!(
registry(dir).contains("[capacity.cycle-weekly-sprint-2026-01-05-000001]"),
"{}",
registry(dir)
);
assert_eq!(
stdout(
dir,
&[
"capacity",
"delete",
"--scope",
"cycle",
"--id",
INSTANCE_NAME,
]
),
"Removed capacity for cycle `Weekly sprint 2026-01-05 000001`\n"
);
assert!(!registry(dir).contains("capacity.cycle-"));
stdout(
dir,
&[
"capacity",
"set",
"--scope",
"cycle",
"--id",
INSTANCE_NAME,
"--value",
"12",
"--unit",
"d",
],
);
let show = stdout(dir, &["capacity", "show", id]);
assert!(show.contains("capacity: 12"), "{show}");
let before_update = registry(dir);
stdout(
dir,
&[
"capacity", "set", "--scope", "cycle", "--id", id, "--value", "20", "--unit", "d",
],
);
assert_ne!(registry(dir), before_update, "the update is written");
let list = stdout(dir, &["capacity", "list"]);
assert_eq!(list.lines().count(), 2, "header plus one entry: {list}");
assert!(
list.contains("cycle-weekly-sprint-2026-01-05-000001\tcycle\tsprint:000001"),
"{list}"
);
assert_eq!(
stdout(dir, &["capacity", "delete", "--scope", "cycle", "--id", id]),
"Removed capacity for cycle `sprint:000001`\n"
);
assert_eq!(
stdout(dir, &["capacity", "list"]),
"id\tscope\treference\tdisplay\tcapacity\tunit\tenabled\n"
);
stdout(
dir,
&[
"capacity", "set", "--scope", "assignee", "--id", "@ada", "--value", "3", "--unit", "d",
],
);
assert!(stdout(dir, &["capacity", "show", "ada"]).contains("reference: @ada"));
assert_eq!(
stdout(
dir,
&["capacity", "delete", "--scope", "assignee", "--id", "ada"]
),
"Removed capacity for assignee `ada`\n"
);
}
#[test]
fn a_handle_collision_between_two_scopes_is_refused_rather_than_replacing_a_ceiling() {
let raw = SCOPES.replace(
"[teams.Platform]\nmembers = [\"ada\"]",
"[teams.Platform]\nmembers = [\"ada\"]\n\n[teams.platform]\nmembers = [\"bob\"]",
);
assert!(raw.contains("[teams.platform]"), "{raw}");
let tmp = project(&raw);
let dir = tmp.path();
stdout(
dir,
&[
"capacity", "set", "--scope", "team", "--id", "Platform", "--value", "40", "--unit",
"d",
],
);
let before = tree(dir);
let (code, _, err) = refused(
dir,
&[
"capacity", "set", "--scope", "team", "--id", "platform", "--value", "8", "--unit", "d",
],
);
assert_eq!(code, 1, "{err}");
for fragment in [
"capacity.team-platform",
"already configures team `Platform`",
"would replace that ceiling",
"`--id Platform`",
"tix capacity delete --scope team --id Platform",
] {
assert!(err.contains(fragment), "`{fragment}` missing: {err}");
}
assert_eq!(tree(dir), before, "a refused collision writes nothing");
assert!(registry(dir).contains("capacity = 40"), "{}", registry(dir));
stdout(
dir,
&[
"capacity", "set", "--scope", "team", "--id", "Platform", "--value", "41", "--unit",
"d",
],
);
assert!(registry(dir).contains("capacity = 41"), "{}", registry(dir));
assert_eq!(stdout(dir, &["capacity", "list"]).lines().count(), 2);
}
#[test]
fn a_hand_written_entry_of_another_kind_on_one_handle_is_not_replaced_either() {
let raw = format!(
"{SCOPES}\n[capacity.cycle-release-a]\nassignee = \"ada\"\ncapacity = 2\nunit = \"d\"\n"
);
let tmp = project(&raw);
let dir = tmp.path();
let before = tree(dir);
let (code, _, err) = refused(
dir,
&[
"capacity",
"set",
"--scope",
"cycle",
"--id",
"Release A",
"--value",
"9",
"--unit",
"d",
],
);
assert_eq!(code, 1, "{err}");
assert!(err.contains("already configures assignee `ada`"), "{err}");
assert!(err.contains("would replace that ceiling"), "{err}");
assert_eq!(tree(dir), before, "a refused collision writes nothing");
let list = stdout(dir, &["capacity", "list"]);
assert!(list.contains("assignee\tada"), "{list}");
assert!(stdout(dir, &["capacity", "show", "ada"]).contains("capacity: 2"));
}
#[test]
fn show_refuses_a_reference_that_names_nothing_or_two_things() {
let tmp = project(SCOPES);
let dir = tmp.path();
let before = tree(dir);
let (code, _, err) = refused(dir, &["capacity", "show", "Release A"]);
assert_eq!(code, 2, "an unknown reference is not-found: {err}");
assert!(err.contains("no capacity entry names `Release A`"), "{err}");
stdout(
dir,
&[
"capacity", "set", "--scope", "team", "--id", "Platform", "--value", "8", "--unit", "d",
],
);
stdout(
dir,
&[
"capacity", "set", "--scope", "assignee", "--id", "Platform", "--value", "3", "--unit",
"d",
],
);
let after = tree(dir);
let (code, _, err) = refused(dir, &["capacity", "show", "Platform"]);
assert_eq!(code, 1, "{err}");
assert!(err.contains("matches 2 capacity entries"), "{err}");
assert!(
err.contains("team `Platform`") && err.contains("assignee `Platform`"),
"{err}"
);
assert_eq!(tree(dir), after, "a read writes nothing");
assert!(
stdout(dir, &["capacity", "show", "Platform", "--scope", "team"])
.contains("reference: Platform")
);
assert_ne!(tree(dir), before, "the two sets were written");
}
#[test]
fn calculate_reports_the_shared_values_and_mutates_no_issue() {
let tmp = project(SCOPES);
let dir = tmp.path();
stdout(
dir,
&[
"capacity",
"set",
"--scope",
"cycle",
"--id",
"Release A",
"--value",
"6",
"--unit",
"d",
],
);
stdout(
dir,
&[
"create",
"--title",
"one",
"--estimate",
"5d",
"--cycles",
"Release A",
"--assignee",
"ada",
],
);
stdout(
dir,
&[
"create",
"--title",
"two",
"--estimate",
"3d",
"--cycles",
"Release A",
"--assignee",
"ada",
"--status",
"done",
],
);
stdout(
dir,
&[
"create",
"--title",
"three",
"--cycles",
"Release A",
"--assignee",
"ada",
],
);
let before = tree(dir);
let calculation = stdout(
dir,
&[
"capacity",
"calculate",
"--scope",
"cycle",
"--id",
"Release A",
],
);
for line in [
"scope: cycle",
"reference: Release A",
"display: Release A",
"state: configured",
"capacity: 6",
"unit: d",
"committed: 8",
"completed: 3",
"remaining: 5",
"overflow: 2",
"utilization: 133%",
"counted_issues: 2",
"uncounted_issues: 0",
"unestimated_issues: 1",
"source_units: d, w",
"diagnostics: 1",
] {
assert!(
calculation.contains(line),
"`{line}` missing: {calculation}"
);
}
let out = stdout(
dir,
&[
"capacity",
"calculate",
"--scope",
"cycle",
"--id",
"Release A",
"--warnings",
],
);
assert!(out.contains("capacity: 6") && out.contains("committed: 8"));
assert_eq!(
tree(dir),
before,
"a calculation writes nothing: no issue status, assignment or date moved"
);
let (code, _, warnings) = run(
dir,
&[
"capacity",
"calculate",
"--scope",
"cycle",
"--id",
"Release A",
"--warnings",
],
);
assert_eq!(code, 0, "{warnings}");
for line in [
"E-tix-capacity",
"capacity_overflow",
"cycle `Release A`",
"current: 8",
"limit: 6",
"overflow: 2",
"unit: d",
"issues: #1, #2",
"= help:",
] {
assert!(warnings.contains(line), "`{line}` missing: {warnings}");
}
let (_, out, quiet) = run(
dir,
&[
"capacity",
"calculate",
"--scope",
"cycle",
"--id",
"Release A",
],
);
assert_eq!(
out.lines().find(|l| l.starts_with("overflow:")),
Some("overflow: 2")
);
assert!(quiet.is_empty(), "{quiet}");
}
#[test]
fn a_missing_conversion_is_actionable_and_never_a_silent_zero() {
let tmp = project(SCOPES);
let dir = tmp.path();
stdout(
dir,
&[
"capacity",
"set",
"--scope",
"cycle",
"--id",
"Release A",
"--value",
"10",
"--unit",
"d",
],
);
stdout(
dir,
&[
"create",
"--title",
"one",
"--estimate",
"3q",
"--cycles",
"Release A",
"--assignee",
"ada",
],
);
let before = tree(dir);
let (code, out, err) = run(
dir,
&[
"capacity",
"calculate",
"--scope",
"cycle",
"--id",
"Release A",
],
);
assert_eq!(code, 0, "{err}");
assert!(out.contains("committed: 0"), "{out}");
assert!(out.contains("uncounted_issues: 1"), "{out}");
assert!(out.contains("diagnostics: 1"), "{out}");
for line in [
"missing_conversion",
"issue #1",
"`3q`",
"unit: d",
"issues: #1",
"= help: declare the unit under `[kanban.<board>.wip.units]`",
] {
assert!(err.contains(line), "`{line}` missing: {err}");
}
assert_eq!(
tree(dir),
before,
"the estimate is reported, never rewritten, zeroed or moved"
);
}
#[test]
fn an_unconfigured_or_unresolved_scope_is_not_a_zero() {
let tmp = project(SCOPES);
let dir = tmp.path();
stdout(
dir,
&[
"create",
"--title",
"one",
"--estimate",
"5d",
"--cycles",
"Release A",
"--assignee",
"ada",
],
);
let (code, out, err) = run(
dir,
&[
"capacity",
"calculate",
"--scope",
"cycle",
"--id",
"Release A",
],
);
assert_eq!(code, 0, "{err}");
assert!(out.contains("state: unconfigured"), "{out}");
for absent in [
"capacity: -",
"unit: -",
"committed: -",
"completed: -",
"remaining: -",
"overflow: -",
"utilization: -",
"counted_issues: -",
] {
assert!(out.contains(absent), "`{absent}` missing: {out}");
}
assert!(err.is_empty(), "{err}");
let (code, out, err) = run(
dir,
&["capacity", "calculate", "--scope", "cycle", "--id", "GTD"],
);
assert_eq!(code, 0, "{err}");
assert!(out.contains("state: unconfigured"), "{out}");
assert!(out.contains("diagnostics: 1"), "{out}");
assert!(
err.contains("unknown_scope") && err.contains("`GTD`"),
"{err}"
);
}
#[test]
fn a_real_close_captures_the_capacity_payload_and_the_readback_reports_it() {
let raw = format!("{SCOPES}{SCHEDULE}");
let tmp = project(&raw);
let dir = tmp.path();
let id = generated_instance_with_capacity(dir, 10, "d");
create_issue(dir, "one", "6d", "todo");
create_issue(dir, "two", "5d", "done");
let closed = stdout(
dir,
&[
"cycle",
"schedule",
"close",
"sprint",
"--cycle",
&id,
"--reason",
"iteration complete",
"--confirm",
],
);
assert!(closed.contains("applied"), "{closed}");
assert!(
closed.contains("docs/cycle_snapshots/sprint.000001.md"),
"{closed}"
);
let snapshot_path = dir.join("docs/cycle_snapshots/sprint.000001.md");
assert!(
snapshot_path.exists(),
"the close captured a snapshot: {closed}"
);
let snapshot = read(&snapshot_path);
let keys = payload_keys(&snapshot);
let keys: Vec<&str> = keys.iter().map(String::as_str).collect();
assert_eq!(
keys, PAYLOAD_KEYS,
"the close writes the mission's payload, not an empty map: {snapshot}"
);
for line in [
"state: configured",
"capacity: 10",
"unit: d",
"committed: 11",
"completed: 5",
"remaining: 6",
"overflow: 1",
"utilization: 110",
"counted_issues: 2",
"unestimated_issues: 0",
] {
assert!(
line_is_a_snapshot_value(&snapshot, line),
"`{line}` missing: {snapshot}"
);
}
let readback = stdout(dir, &["capacity", "snapshot", &id]);
for line in [
"cycle: sprint:000001",
"snapshot: docs/cycle_snapshots/sprint.000001.md",
"evidence: present",
"state: configured",
"capacity: 10",
"committed: 11",
"completed: 5",
"remaining: 6",
"overflow: 1",
"utilization: 110%",
"source_units: d, w",
] {
assert!(readback.contains(line), "`{line}` missing: {readback}");
}
assert!(stdout(dir, &["capacity", "snapshot", INSTANCE_NAME]).contains("evidence: present"));
}
#[test]
fn a_snapshots_stored_findings_render_under_the_same_warning_policy_as_a_live_one() {
let raw = format!("{SCOPES}{SCHEDULE}");
let tmp = project(&raw);
let dir = tmp.path();
let id = generated_instance_with_capacity(dir, 4, "d");
create_issue(dir, "one", "5d", "todo");
create_issue(dir, "two", "3q", "todo");
stdout(
dir,
&[
"cycle",
"schedule",
"close",
"sprint",
"--cycle",
&id,
"--reason",
"iteration complete",
"--confirm",
],
);
let snapshot_path = dir.join("docs/cycle_snapshots/sprint.000001.md");
let recorded = fs::read(&snapshot_path).unwrap();
let snapshot = String::from_utf8(recorded.clone()).unwrap();
assert!(snapshot.contains("diagnostics:"), "{snapshot}");
assert!(snapshot.contains("kind: missing_conversion"), "{snapshot}");
assert!(snapshot.contains("kind: capacity_overflow"), "{snapshot}");
let (code, out, err) = run(dir, &["capacity", "snapshot", &id]);
assert_eq!(code, 0, "{err}");
assert!(out.contains("state: configured"), "{out}");
assert!(out.contains("capacity: 4"), "{out}");
assert!(out.contains("committed: 5"), "{out}");
assert!(out.contains("overflow: 1"), "{out}");
assert!(out.contains("diagnostics: 2"), "{out}");
assert!(err.contains("missing_conversion"), "{err}");
assert!(err.contains("`3q`"), "{err}");
assert!(!err.contains("capacity_overflow"), "{err}");
let (code, _, err) = run(dir, &["capacity", "snapshot", &id, "--warnings"]);
assert_eq!(code, 0, "{err}");
for line in [
"capacity_overflow",
"committed 5d exceeds the configured capacity of 4d by 1d",
"current: 5",
"limit: 4",
"overflow: 1",
"unit: d",
"issues: #1",
"= help:",
] {
assert!(err.contains(line), "`{line}` missing: {err}");
}
assert_eq!(
fs::read(&snapshot_path).unwrap(),
recorded,
"reading the evidence back never rewrites it"
);
}
fn line_is_a_snapshot_value(snapshot: &str, line: &str) -> bool {
let (key, value) = line.split_once(": ").expect("a `key: value` line");
snapshot
.lines()
.any(|candidate| candidate.trim() == format!("{key}: {value}"))
}
#[test]
fn a_legacy_snapshot_without_a_payload_reads_as_none_and_a_missing_one_is_not_found() {
let raw = format!("{SCOPES}{SCHEDULE}");
let tmp = project(&raw);
let dir = tmp.path();
let id = generated_instance_with_capacity(dir, 10, "d");
fs::create_dir_all(dir.join("docs/cycle_snapshots")).unwrap();
fs::write(
dir.join("docs/cycle_snapshots/sprint.000001.md"),
"---\ncycle_id: sprint:000001\nschedule_id: sprint\nsequence: 1\n\
start_date: 2026-01-05\ndue_date: 2026-01-12\n\
closed_at: 2026-01-13T00:00:00Z\nclosed_by: ada\nstate_summary: closed\n---\n",
)
.unwrap();
let (code, out, err) = run(dir, &["capacity", "snapshot", &id]);
assert_eq!(
code, 0,
"no capacity evidence is a value, never an error: {err}"
);
assert!(out.contains("evidence: none"), "{out}");
assert!(
out.contains("snapshot: docs/cycle_snapshots/sprint.000001.md"),
"{out}"
);
assert!(err.contains("carries no capacity payload"), "{err}");
let (code, _, err) = run(dir, &["capacity", "snapshot", "Release A"]);
assert_eq!(code, 2, "{err}");
assert!(err.contains("hand-written cycle"), "{err}");
let (code, _, err) = run(dir, &["capacity", "snapshot", "nope"]);
assert_eq!(code, 2, "{err}");
assert!(err.contains("no cycle `nope` resolves"), "{err}");
stdout(
dir,
&[
"cycle",
"schedule",
"generate",
"sprint",
"--confirm",
"--through",
"2026-01-19",
],
);
let (code, _, err) = run(dir, &["capacity", "snapshot", "sprint:000002"]);
assert_eq!(code, 2, "{err}");
assert!(err.contains("has no snapshot"), "{err}");
}
#[test]
fn reopen_appends_exactly_one_event_and_leaves_the_prior_snapshot_byte_identical() {
let raw = format!("{SCOPES}{SCHEDULE}");
let tmp = project(&raw);
let dir = tmp.path();
let id = generated_instance_with_capacity(dir, 10, "d");
create_issue(dir, "one", "6d", "todo");
stdout(
dir,
&[
"cycle",
"schedule",
"close",
"sprint",
"--cycle",
&id,
"--reason",
"iteration complete",
"--confirm",
],
);
let snapshot_path = dir.join("docs/cycle_snapshots/sprint.000001.md");
let snapshot_before = fs::read(&snapshot_path).unwrap();
let record_path = dir.join("docs/cycles").join(INSTANCE_FILE);
let record = read(&record_path);
assert!(record.contains("state: closed"), "{record}");
assert_eq!(record.matches("cycle_closed").count(), 1, "{record}");
let tree_before = tree(dir);
let (code, _, err) = refused(
dir,
&[
"cycle",
"schedule",
"reopen",
"sprint",
"--cycle",
&id,
"--reason",
"correction",
],
);
assert_eq!(code, 1, "{err}");
assert!(err.contains("--confirm"), "{err}");
assert_eq!(
tree(dir),
tree_before,
"an unconfirmed reopen writes nothing"
);
let (code, _, err) = refused(
dir,
&[
"cycle",
"schedule",
"reopen",
"sprint",
"--cycle",
&id,
"--confirm",
],
);
assert_eq!(code, 1, "{err}");
assert!(err.contains("missing_reason"), "{err}");
assert_eq!(
tree(dir),
tree_before,
"an un-reasoned reopen writes nothing"
);
let reopened = stdout(
dir,
&[
"cycle",
"schedule",
"reopen",
"sprint",
"--cycle",
&id,
"--reason",
"correction",
"--confirm",
],
);
assert!(reopened.contains("applied"), "{reopened}");
assert!(reopened.contains("reopened `sprint:000001`"), "{reopened}");
assert!(
reopened.contains("is unchanged and stays readable"),
"{reopened}"
);
assert_eq!(
fs::read(&snapshot_path).unwrap(),
snapshot_before,
"the prior snapshot is byte-identical after a reopen"
);
let record = read(&record_path);
assert!(record.contains("state: active"), "{record}");
assert_eq!(record.matches("cycle_closed").count(), 1, "{record}");
assert_eq!(record.matches("cycle_reopened").count(), 1, "{record}");
assert!(record.contains("reason: correction"), "{record}");
assert!(record.contains("snapshot: sprint:000001@"), "{record}");
let after = tree(dir);
let repeat = stdout(
dir,
&[
"cycle",
"schedule",
"reopen",
"sprint",
"--cycle",
&id,
"--reason",
"again",
"--confirm",
],
);
assert!(repeat.contains("no-op"), "{repeat}");
assert_eq!(tree(dir), after, "a no-op reopen writes nothing");
assert_eq!(read(&record_path).matches("cycle_reopened").count(), 1);
stdout(
dir,
&[
"capacity", "set", "--scope", "cycle", "--id", &id, "--value", "20", "--unit", "d",
],
);
assert!(registry(dir).contains("capacity = 20"), "{}", registry(dir));
}
#[test]
fn a_closed_cycles_capacity_cannot_be_configured_or_deleted() {
let raw = format!("{SCOPES}{SCHEDULE}");
let tmp = project(&raw);
let dir = tmp.path();
let id = generated_instance_with_capacity(dir, 10, "d");
stdout(
dir,
&[
"cycle",
"schedule",
"close",
"sprint",
"--cycle",
&id,
"--reason",
"done",
"--confirm",
],
);
let before = tree(dir);
for args in [
vec![
"capacity", "set", "--scope", "cycle", "--id", &id, "--value", "20", "--unit", "d",
],
vec!["capacity", "delete", "--scope", "cycle", "--id", &id],
] {
let (code, _, err) = refused(dir, &args);
assert_eq!(code, 1, "{args:?}: {err}");
assert!(err.contains("is closed"), "{args:?}: {err}");
assert!(
err.contains("reopen it through the explicit `History` event"),
"{args:?}: {err}"
);
assert_eq!(tree(dir), before, "{args:?} must write nothing");
}
}
#[test]
fn a_close_with_no_capacity_configured_still_records_an_explicit_unconfigured_state() {
let raw = format!("{SCOPES}{SCHEDULE}");
let tmp = project(&raw);
let dir = tmp.path();
stdout(
dir,
&["cycle", "schedule", "generate", "sprint", "--confirm"],
);
let closed = stdout(
dir,
&[
"cycle",
"schedule",
"close",
"sprint",
"--cycle",
"sprint:000001",
"--reason",
"iteration complete",
"--confirm",
],
);
assert!(closed.contains("applied"), "{closed}");
let snapshot = read(&dir.join("docs/cycle_snapshots/sprint.000001.md"));
assert!(snapshot.contains("state: unconfigured"), "{snapshot}");
assert!(
snapshot.contains("snapshot_id: sprint:000001@"),
"{snapshot}"
);
for omitted in ["capacity:", "committed:", "overflow:", "utilization:"] {
assert!(
!snapshot.contains(omitted),
"an unconfigured scope omits `{omitted}` rather than writing a zero: {snapshot}"
);
}
let readback = stdout(dir, &["capacity", "snapshot", "sprint:000001"]);
assert!(readback.contains("evidence: present"), "{readback}");
assert!(readback.contains("state: unconfigured"), "{readback}");
assert!(readback.contains("committed: -"), "{readback}");
}
#[test]
fn a_refused_close_or_reopen_leaves_the_tree_byte_identical() {
let raw = format!("{SCOPES}{SCHEDULE}");
let tmp = project(&raw);
let dir = tmp.path();
stdout(
dir,
&["cycle", "schedule", "generate", "sprint", "--confirm"],
);
let before = tree(dir);
for args in [
vec![
"cycle",
"schedule",
"close",
"sprint",
"--cycle",
"sprint:000001",
"--reason",
"x",
],
vec![
"cycle",
"schedule",
"close",
"sprint",
"--cycle",
"sprint:000001",
"--confirm",
],
vec![
"cycle",
"schedule",
"reopen",
"sprint",
"--cycle",
"sprint:000001",
"--reason",
"x",
],
vec![
"cycle",
"schedule",
"reopen",
"sprint",
"--cycle",
"sprint:000001",
"--confirm",
],
] {
let (code, _, err) = refused(dir, &args);
assert_eq!(code, 1, "{args:?}: {err}");
assert_eq!(tree(dir), before, "{args:?} must write nothing");
}
let (code, _, err) = refused(
dir,
&[
"cycle",
"schedule",
"reopen",
"sprint",
"--cycle",
"sprint:000009",
"--reason",
"correction",
"--confirm",
],
);
assert_eq!(code, 1, "{err}");
assert!(err.contains("sprint:000009"), "{err}");
assert_eq!(tree(dir), before);
}
#[test]
fn the_legacy_cycle_commands_and_detection_are_unaffected() {
let raw = format!("{SCOPES}{SCHEDULE}");
let tmp = project(&raw);
let dir = tmp.path();
stdout(
dir,
&[
"capacity",
"set",
"--scope",
"cycle",
"--id",
"Release A",
"--value",
"6",
"--unit",
"d",
],
);
assert_eq!(
stdout(
dir,
&[
"cycle",
"create",
"--name",
"Release B",
"--start-date",
"2026-02-01"
]
),
"Release B\n"
);
assert!(stdout(dir, &["cycle", "list"]).contains("Release B"));
stdout(
dir,
&[
"cycle",
"edit",
"--name",
"Release B",
"--description",
"the second",
],
);
assert!(stdout(dir, &["cycle", "list"]).contains("the second"));
assert_eq!(
stdout(dir, &["cycle", "delete", "--name", "Release B", "--yes"]),
"deleted cycle `Release B`\n"
);
let before = tree(dir);
let first = stdout(
dir,
&["cycle", "schedule", "detect", "--date", "2026-01-19"],
);
assert_eq!(
first,
stdout(
dir,
&["cycle", "schedule", "detect", "--date", "2026-01-19"]
)
);
assert_eq!(tree(dir), before, "detection writes nothing");
stdout(
dir,
&[
"create",
"--title",
"one",
"--estimate",
"5d",
"--cycles",
"Release A",
"--assignee",
"ada",
],
);
let calculated = tree(dir);
stdout(
dir,
&[
"capacity",
"calculate",
"--scope",
"cycle",
"--id",
"Release A",
"--warnings",
],
);
assert_eq!(tree(dir), calculated, "a report moves nothing");
stdout(dir, &["move", "1", "doing"]);
assert_ne!(
tree(dir),
calculated,
"the move is the only thing that writes"
);
}