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()
}
fn repo() -> TempDir {
let tmp = TempDir::new().unwrap();
tix().current_dir(tmp.path()).arg("init").assert().success();
tmp
}
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 failure(dir: &Path, args: &[&str]) -> (i32, String) {
let out = tix()
.current_dir(dir)
.args(args)
.assert()
.failure()
.get_output()
.clone();
(
out.status.code().unwrap_or(-1),
String::from_utf8(out.stderr).unwrap(),
)
}
fn snapshot(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 registry(dir: &Path) -> Vec<u8> {
fs::read(dir.join("tix.toml")).unwrap()
}
fn create_weekly(dir: &Path, extra: &[&str]) {
let mut args = vec![
"cycle",
"schedule",
"create",
"--id",
"sprint",
"--name",
"Engineering sprint",
"--cadence",
"weekly",
"--duration",
"1",
"--duration-unit",
"weeks",
"--start-date",
"2026-01-05",
"--timezone",
"UTC",
"--confirm",
];
args.extend_from_slice(extra);
let out = stdout(dir, &args);
assert!(out.contains("created schedule `sprint`"), "{out}");
}
fn cycle_count(dir: &Path) -> usize {
let cycles = dir.join("docs").join("cycles");
fs::read_dir(cycles)
.map(|entries| {
entries
.flatten()
.filter(|entry| entry.path().extension().is_some_and(|ext| ext == "md"))
.count()
})
.unwrap_or(0)
}
fn snapshot_count(dir: &Path) -> usize {
let snapshots = dir.join("docs").join("cycle_snapshots");
fs::read_dir(snapshots)
.map(|entries| {
entries
.flatten()
.filter(|entry| entry.path().extension().is_some_and(|ext| ext == "md"))
.count()
})
.unwrap_or(0)
}
fn remove_markdown(dir: &Path) {
let Ok(entries) = fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
let path = entry.path();
if path.extension().is_some_and(|ext| ext == "md") {
fs::remove_file(path).unwrap();
}
}
}
#[test]
fn a_schedule_round_trips_create_show_list_edit_and_delete() {
let tmp = repo();
let dir = tmp.path();
create_weekly(dir, &["--look-ahead", "2", "--interval", "2"]);
let list = stdout(dir, &["cycle", "schedule", "list"]);
assert!(
list.starts_with(
"id\tname\tstate\tcadence\tduration\tstart_date\tlook_ahead\tnext_sequence\tgenerated"
),
"{list}"
);
let row = list.lines().nth(1).expect("one schedule row");
let columns: Vec<&str> = row.split('\t').collect();
assert_eq!(columns[0], "sprint");
assert_eq!(columns[1], "Engineering sprint");
assert_eq!(columns[2], "active");
assert_eq!(columns[5], "2026-01-05");
assert_eq!(columns[6], "2");
assert_eq!(columns[7], "1", "a fresh schedule's cursor is sequence 1");
assert_eq!(columns[8], "0");
let show = stdout(dir, &["cycle", "schedule", "show", "sprint"]);
assert!(show.contains("id: sprint"), "{show}");
assert!(show.contains("name: Engineering sprint"), "{show}");
assert!(show.contains("state: active"), "{show}");
assert!(show.contains("start_date: 2026-01-05"), "{show}");
assert!(show.contains("look_ahead: 2"), "{show}");
assert!(show.contains("next_sequence: 1"), "{show}");
assert!(show.contains("next_start: 2026-01-05"), "{show}");
assert!(show.contains("timezone: +00:00"), "{show}");
let by_name = stdout(dir, &["cycle", "schedule", "show", "Engineering sprint"]);
assert!(by_name.contains("id: sprint"), "{by_name}");
let edited = stdout(
dir,
&[
"cycle",
"schedule",
"edit",
"sprint",
"--name",
"Platform sprint",
"--look-ahead",
"4",
"--interval",
"1",
"--confirm",
],
);
assert!(edited.contains("updated schedule `sprint`"), "{edited}");
let show = stdout(dir, &["cycle", "schedule", "show", "sprint"]);
assert!(show.contains("id: sprint"), "{show}");
assert!(show.contains("name: Platform sprint"), "{show}");
assert!(show.contains("look_ahead: 4"), "{show}");
let deleted = stdout(dir, &["cycle", "schedule", "delete", "sprint", "--confirm"]);
assert!(deleted.contains("deleted schedule `sprint`"), "{deleted}");
let list = stdout(dir, &["cycle", "schedule", "list"]);
assert_eq!(list.lines().count(), 1, "header only: {list}");
}
#[test]
fn create_is_validated_before_persistence_and_writes_nothing_on_refusal() {
let tmp = repo();
let dir = tmp.path();
let before = registry(dir);
for timezone in ["Europe/Berlin", "+1:00"] {
let (code, err) = failure(
dir,
&[
"cycle",
"schedule",
"create",
"--id",
"sprint",
"--name",
"S",
"--cadence",
"weekly",
"--duration",
"1",
"--duration-unit",
"weeks",
"--start-date",
"2026-01-05",
"--timezone",
timezone,
"--confirm",
],
);
assert_eq!(code, 1, "{err}");
assert!(err.contains("timezone"), "{err}");
assert_eq!(registry(dir), before, "a refused create writes nothing");
}
let (code, err) = failure(
dir,
&[
"cycle",
"schedule",
"create",
"--id",
"sprint",
"--name",
"S",
"--cadence",
"yearly",
"--duration",
"1",
"--duration-unit",
"weeks",
"--start-date",
"2026-01-05",
"--timezone",
"UTC",
"--confirm",
],
);
assert_eq!(code, 1, "{err}");
assert!(err.contains("not a cadence family"), "{err}");
assert_eq!(registry(dir), before);
let (_, err) = failure(
dir,
&[
"cycle",
"schedule",
"create",
"--id",
"sprint",
"--name",
"S",
"--cadence",
"weekly",
"--unit",
"days",
"--duration",
"1",
"--duration-unit",
"weeks",
"--start-date",
"2026-01-05",
"--timezone",
"UTC",
"--confirm",
],
);
assert!(err.contains("cadence.unit"), "{err}");
assert_eq!(registry(dir), before);
let base = |duration: &'static str, extra: &[&'static str]| -> Vec<&'static str> {
let mut args = vec![
"cycle",
"schedule",
"create",
"--id",
"sprint",
"--name",
"S",
"--cadence",
"weekly",
"--duration",
duration,
];
args.extend_from_slice(extra);
args.push("--timezone");
args.push("UTC");
args.push("--confirm");
args
};
for (args, needle) in [
(
base(
"1",
&["--duration-unit", "weeks", "--start-date", "2026-02-30"],
),
"start_date",
),
(
base(
"0",
&["--duration-unit", "weeks", "--start-date", "2026-01-05"],
),
"duration.count",
),
(
base(
"1",
&[
"--duration-unit",
"fortnights",
"--start-date",
"2026-01-05",
],
),
"is not a unit",
),
] {
let (_, err) = failure(dir, &args);
assert!(err.contains(needle), "expected `{needle}` in: {err}");
assert_eq!(registry(dir), before, "a refused create writes nothing");
}
}
#[test]
fn a_mutation_without_confirm_is_refused_and_writes_nothing() {
let tmp = repo();
let dir = tmp.path();
let before = snapshot(dir);
let (code, err) = failure(
dir,
&[
"cycle",
"schedule",
"create",
"--id",
"sprint",
"--name",
"S",
"--cadence",
"weekly",
"--duration",
"1",
"--duration-unit",
"weeks",
"--start-date",
"2026-01-05",
"--timezone",
"UTC",
],
);
assert_eq!(code, 1, "{err}");
assert!(err.contains("--confirm"), "{err}");
assert_eq!(
snapshot(dir),
before,
"an unconfirmed create writes nothing"
);
create_weekly(dir, &[]);
let before = snapshot(dir);
for args in [
vec!["cycle", "schedule", "delete", "sprint"],
vec!["cycle", "schedule", "edit", "sprint", "--name", "X"],
vec!["cycle", "schedule", "generate", "sprint"],
vec!["cycle", "schedule", "pause", "sprint"],
vec![
"cycle",
"schedule",
"close",
"sprint",
"--cycle",
"sprint:000001",
],
] {
let (code, err) = failure(dir, &args);
assert_eq!(code, 1, "{args:?}: {err}");
assert!(err.contains("--confirm"), "{args:?}: {err}");
assert_eq!(snapshot(dir), before, "{args:?} must write nothing");
}
}
#[test]
fn detection_is_read_only_and_byte_stable() {
let tmp = repo();
let dir = tmp.path();
create_weekly(dir, &["--look-ahead", "2"]);
stdout(
dir,
&["cycle", "schedule", "generate", "sprint", "--confirm"],
);
let before = snapshot(dir);
let first = stdout(
dir,
&["cycle", "schedule", "detect", "--date", "2026-01-19"],
);
let second = stdout(
dir,
&["cycle", "schedule", "detect", "--date", "2026-01-19"],
);
assert_eq!(first, second, "detection is byte-stable (C-004)");
assert_eq!(snapshot(dir), before, "detection writes nothing (C-004)");
assert!(
first.starts_with(
"schedule_id\tkind\tcycle_id\tstart\tdue\tnext_start\tnext_id\tactions\tdetail"
),
"{first}"
);
let ended = first
.lines()
.find(|line| line.split('\t').nth(1) == Some("ended"))
.unwrap_or_else(|| panic!("an `ended` warning: {first}"));
let columns: Vec<&str> = ended.split('\t').collect();
assert_eq!(columns[2], "sprint:000001");
assert_eq!(columns[3], "2026-01-05");
assert_eq!(columns[4], "2026-01-12");
assert_eq!(columns[7], "close,extend,ignore");
let today = stdout(dir, &["cycle", "schedule", "detect"]);
let today_again = stdout(dir, &["cycle", "schedule", "detect"]);
assert_eq!(today, today_again);
assert_eq!(snapshot(dir), before);
}
#[test]
fn generation_is_confirmed_idempotent_for_a_fixed_window() {
let tmp = repo();
let dir = tmp.path();
create_weekly(dir, &["--look-ahead", "2"]);
let before = snapshot(dir);
let (code, err) = failure(
dir,
&[
"cycle",
"schedule",
"generate",
"sprint",
"--through",
"2026-02-16",
],
);
assert_eq!(code, 1, "{err}");
assert_eq!(snapshot(dir), before);
let created = stdout(
dir,
&[
"cycle",
"schedule",
"generate",
"sprint",
"--through",
"2026-02-16",
"--confirm",
],
);
assert_eq!(created.matches("created `sprint:").count(), 7, "{created}");
let count = cycle_count(dir);
assert_eq!(count, 7);
let snapshot_after = snapshot(dir);
let repeat = stdout(
dir,
&[
"cycle",
"schedule",
"generate",
"sprint",
"--through",
"2026-02-16",
"--confirm",
],
);
assert!(repeat.contains("nothing to generate"), "{repeat}");
assert_eq!(cycle_count(dir), count, "a repeat creates no duplicate key");
assert_eq!(
snapshot(dir),
snapshot_after,
"a repeat writes nothing at all"
);
let ahead = stdout(
dir,
&["cycle", "schedule", "generate", "sprint", "--confirm"],
);
assert_eq!(ahead.matches("created `sprint:").count(), 2, "{ahead}");
}
#[test]
fn a_paused_schedule_refuses_generation_until_resumed() {
let tmp = repo();
let dir = tmp.path();
create_weekly(dir, &["--look-ahead", "1"]);
stdout(dir, &["cycle", "schedule", "pause", "sprint", "--confirm"]);
let before = snapshot(dir);
let (code, err) = failure(
dir,
&["cycle", "schedule", "generate", "sprint", "--confirm"],
);
assert_eq!(code, 1, "{err}");
assert!(err.contains("paused"), "{err}");
assert_eq!(snapshot(dir), before, "a refused generation writes nothing");
stdout(dir, &["cycle", "schedule", "resume", "sprint", "--confirm"]);
let out = stdout(
dir,
&["cycle", "schedule", "generate", "sprint", "--confirm"],
);
assert!(out.contains("created `sprint:000001`"), "{out}");
}
#[test]
fn every_lifecycle_action_applies_and_each_refusal_writes_nothing() {
let tmp = repo();
let dir = tmp.path();
create_weekly(dir, &["--look-ahead", "4"]);
stdout(
dir,
&["cycle", "schedule", "generate", "sprint", "--confirm"],
);
let before = snapshot(dir);
let (code, err) = failure(
dir,
&[
"cycle",
"schedule",
"close",
"sprint",
"--cycle",
"sprint:000001",
"--confirm",
],
);
assert_eq!(code, 1, "{err}");
assert!(err.contains("missing_reason"), "{err}");
assert_eq!(
snapshot(dir),
before,
"a close without a reason writes nothing"
);
let closed = stdout(
dir,
&[
"cycle",
"schedule",
"close",
"sprint",
"--cycle",
"sprint:000001",
"--reason",
"iteration complete",
"--confirm",
],
);
assert!(closed.contains("applied"), "{closed}");
assert!(
dir.join("docs/cycle_snapshots/sprint.000001.md").exists(),
"close captured an immutable snapshot: {closed}"
);
let after_close = snapshot(dir);
let repeat = stdout(
dir,
&[
"cycle",
"schedule",
"close",
"sprint",
"--cycle",
"sprint:000001",
"--reason",
"again",
"--confirm",
],
);
assert!(repeat.contains("no-op"), "{repeat}");
assert_eq!(snapshot(dir), after_close, "a no-op close writes nothing");
let extended = stdout(
dir,
&[
"cycle",
"schedule",
"extend",
"sprint",
"--cycle",
"sprint:000002",
"--reason",
"scope change",
"--confirm",
],
);
assert!(extended.contains("applied"), "{extended}");
let (code, err) = failure(
dir,
&[
"cycle",
"schedule",
"extend",
"sprint",
"--cycle",
"sprint:000002",
"--reason",
"scope change",
"--by",
"2",
"--confirm",
],
);
assert_eq!(code, 1, "{err}");
assert!(err.contains("--by 2"), "{err}");
let skipped = stdout(
dir,
&[
"cycle",
"schedule",
"skip",
"sprint",
"--cycle",
"sprint:000003",
"--reason",
"holiday",
"--confirm",
],
);
assert!(skipped.contains("applied"), "{skipped}");
assert!(
stdout(dir, &["cycle", "schedule", "pause", "sprint", "--confirm"]).contains("applied")
);
assert!(
stdout(dir, &["cycle", "schedule", "resume", "sprint", "--confirm"]).contains("applied")
);
let adjusted = stdout(
dir,
&[
"cycle",
"schedule",
"adjust-future",
"sprint",
"--from",
"2026-01-05",
"--reason",
"planning change",
"--confirm",
],
);
assert!(adjusted.contains("applied"), "{adjusted}");
let before = snapshot(dir);
let ignored = stdout(
dir,
&[
"cycle",
"schedule",
"ignore",
"sprint",
"--cycle",
"sprint:000004",
"--confirm",
],
);
assert!(ignored.contains("no-op"), "{ignored}");
assert_eq!(snapshot(dir), before, "ignore writes nothing");
let before = snapshot(dir);
let (code, err) = failure(
dir,
&[
"cycle",
"schedule",
"ignore",
"sprint",
"--cycle",
"sprint:000009",
"--confirm",
],
);
assert_eq!(code, 1, "{err}");
assert!(err.contains("does not exist"), "{err}");
let (_, err) = failure(
dir,
&[
"cycle",
"schedule",
"adjust-future",
"sprint",
"--from",
"2026-12-31",
"--reason",
"none such",
"--confirm",
],
);
assert!(err.contains("no instance"), "{err}");
assert_eq!(snapshot(dir), before, "refused actions write nothing");
}
#[test]
fn an_unknown_schedule_selector_is_not_found() {
let tmp = repo();
let dir = tmp.path();
let (code, err) = failure(dir, &["cycle", "schedule", "show", "nope"]);
assert_eq!(code, 2, "target-not-found is exit 2: {err}");
assert!(err.contains("nope"), "{err}");
}
#[test]
fn legacy_cycle_verbs_and_issue_references_are_unchanged() {
let tmp = repo();
let dir = tmp.path();
let created = stdout(
dir,
&[
"cycle",
"create",
"--name",
"Legacy",
"--start-date",
"2026-01-01",
],
);
assert!(created.contains("Legacy"), "{created}");
let list = stdout(dir, &["cycle", "list"]);
assert!(
list.lines().any(|line| line.starts_with("Legacy\t")),
"{list}"
);
let renamed = stdout(
dir,
&["cycle", "edit", "--name", "Legacy", "--new-name", "Legacy2"],
);
assert!(
renamed.contains("renamed cycle `Legacy` to `Legacy2`"),
"{renamed}"
);
let list = stdout(dir, &["cycle", "list"]);
assert!(
list.lines().any(|line| line.starts_with("Legacy2\t")),
"{list}"
);
stdout(
dir,
&["create", "--title", "An issue", "--cycles", "Legacy2"],
);
let moved = stdout(dir, &["move", "1", "in progress"]);
assert!(!moved.trim().is_empty(), "{moved}");
let shown = stdout(dir, &["show", "1"]);
assert!(shown.contains("Legacy2"), "{shown}");
assert!(shown.contains("in progress"), "{shown}");
let deleted = stdout(dir, &["cycle", "delete", "--name", "Legacy2", "--yes"]);
assert!(deleted.contains("deleted cycle `Legacy2`"), "{deleted}");
let list = stdout(dir, &["cycle", "list"]);
assert!(
!list.lines().any(|line| line.starts_with("Legacy2\t")),
"{list}"
);
}
#[test]
fn a_schedule_namespace_does_not_disturb_a_legacy_cycle_registry() {
let tmp = repo();
let dir = tmp.path();
stdout(
dir,
&[
"cycle",
"create",
"--name",
"Legacy",
"--start-date",
"2026-01-01",
],
);
create_weekly(dir, &[]);
let list = stdout(dir, &["cycle", "list"]);
assert!(
list.lines().any(|line| line.starts_with("Legacy\t")),
"the legacy registry still lists its own cycle: {list}"
);
let schedules = stdout(dir, &["cycle", "schedule", "list"]);
assert!(
schedules.lines().any(|line| line.starts_with("sprint\t")),
"{schedules}"
);
}
#[test]
fn a_deleted_schedule_id_is_spent_and_never_rebinds_its_cycles() {
let tmp = repo();
let dir = tmp.path();
create_weekly(dir, &["--look-ahead", "1"]);
stdout(
dir,
&["cycle", "schedule", "generate", "sprint", "--confirm"],
);
assert_eq!(cycle_count(dir), 1, "one generated instance");
stdout(dir, &["cycle", "schedule", "delete", "sprint", "--confirm"]);
assert_eq!(
cycle_count(dir),
1,
"deleting the definition leaves its generated cycle on disk"
);
let before = registry(dir);
let (code, err) = failure(
dir,
&[
"cycle",
"schedule",
"create",
"--id",
"sprint",
"--name",
"Other",
"--cadence",
"daily",
"--duration",
"1",
"--duration-unit",
"days",
"--start-date",
"2030-01-01",
"--timezone",
"UTC",
"--confirm",
],
);
assert_eq!(code, 1, "{err}");
assert!(err.contains("still claimed"), "{err}");
assert_eq!(registry(dir), before, "a refused create writes nothing");
stdout(
dir,
&[
"cycle",
"schedule",
"create",
"--id",
"fresh",
"--name",
"Fresh",
"--cadence",
"weekly",
"--duration",
"1",
"--duration-unit",
"weeks",
"--start-date",
"2030-01-07",
"--timezone",
"UTC",
"--confirm",
],
);
let show = stdout(dir, &["cycle", "schedule", "show", "fresh"]);
assert!(show.contains("generated: 0"), "{show}");
assert!(show.contains("next_sequence: 1"), "{show}");
let list = stdout(dir, &["cycle", "schedule", "list"]);
let row = list
.lines()
.find(|line| line.starts_with("fresh\t"))
.expect("fresh schedule row");
assert_eq!(row.split('\t').nth(8), Some("0"), "{list}");
}
#[test]
fn a_schedule_id_with_only_a_snapshot_left_is_still_spent() {
let tmp = repo();
let dir = tmp.path();
stdout(
dir,
&[
"cycle",
"schedule",
"create",
"--id",
"cs-a",
"--name",
"A",
"--cadence",
"weekly",
"--duration",
"1",
"--duration-unit",
"weeks",
"--start-date",
"2026-01-05",
"--timezone",
"UTC",
"--look-ahead",
"1",
"--confirm",
],
);
stdout(dir, &["cycle", "schedule", "generate", "cs-a", "--confirm"]);
stdout(
dir,
&[
"cycle",
"schedule",
"close",
"cs-a",
"--cycle",
"cs-a:000001",
"--reason",
"done",
"--confirm",
],
);
assert_eq!(snapshot_count(dir), 1, "close captured the snapshot");
stdout(dir, &["cycle", "schedule", "delete", "cs-a", "--confirm"]);
remove_markdown(&dir.join("docs").join("cycles"));
assert_eq!(cycle_count(dir), 0, "the record is gone");
let before = registry(dir);
let (code, err) = failure(
dir,
&[
"cycle",
"schedule",
"create",
"--id",
"cs-a",
"--name",
"B",
"--cadence",
"daily",
"--duration",
"1",
"--duration-unit",
"days",
"--start-date",
"2030-01-01",
"--timezone",
"UTC",
"--confirm",
],
);
assert_eq!(code, 1, "{err}");
assert!(err.contains("closed-cycle snapshot"), "{err}");
assert!(
!err.contains("generated cycle record"),
"names only the real holder: {err}"
);
assert_eq!(registry(dir), before, "a refused create writes nothing");
remove_markdown(&dir.join("docs").join("cycle_snapshots"));
assert_eq!(snapshot_count(dir), 0);
assert_eq!(cycle_count(dir), 0);
stdout(
dir,
&[
"cycle",
"schedule",
"create",
"--id",
"cs-a",
"--name",
"B",
"--cadence",
"daily",
"--duration",
"1",
"--duration-unit",
"days",
"--start-date",
"2030-01-01",
"--timezone",
"UTC",
"--confirm",
],
);
let show = stdout(dir, &["cycle", "schedule", "show", "cs-a"]);
assert!(show.contains("generated: 0"), "{show}");
assert!(show.contains("name: B"), "{show}");
}
#[test]
fn a_name_resolves_case_insensitively_for_non_ascii_letters() {
let tmp = repo();
let dir = tmp.path();
stdout(
dir,
&[
"cycle",
"schedule",
"create",
"--id",
"cs-umlaut",
"--name",
"Ärger",
"--cadence",
"weekly",
"--duration",
"1",
"--duration-unit",
"weeks",
"--start-date",
"2026-01-05",
"--timezone",
"UTC",
"--confirm",
],
);
let show = stdout(dir, &["cycle", "schedule", "show", "ärger"]);
assert!(show.contains("id: cs-umlaut"), "{show}");
let edited = stdout(
dir,
&[
"cycle",
"schedule",
"edit",
"ÄRGER",
"--look-ahead",
"3",
"--confirm",
],
);
assert!(edited.contains("updated schedule `cs-umlaut`"), "{edited}");
}
#[test]
fn the_legacy_rollover_advisory_still_fires_for_a_cycle_command() {
let tmp = repo();
let dir = tmp.path();
stdout(
dir,
&[
"cycle",
"create",
"--name",
"Old",
"--start-date",
"2020-01-01",
"--due-date",
"2020-02-01",
],
);
stdout(
dir,
&[
"cycle",
"create",
"--name",
"New",
"--start-date",
"2030-01-01",
],
);
stdout(dir, &["create", "--title", "Open", "--cycles", "Old"]);
let out = tix()
.current_dir(dir)
.args(["cycle", "list"])
.assert()
.success()
.get_output()
.clone();
let stderr = String::from_utf8(out.stderr).unwrap();
assert!(stderr.contains("hint: rollover"), "{stderr}");
assert!(stderr.contains(r#"from "Old" to "New""#), "{stderr}");
}