use std::fs;
use std::path::{Path, PathBuf};
use chrono::{DateTime, Local};
use serde_json::{Value, json};
use super::*;
use crate::logs::reading::{Entry, Snapshot};
const FOLDER: &str = "acme/widgets/commands/issue";
const STAMP: &str = "20261019T110000-0400";
fn now() -> DateTime<Local> {
DateTime::parse_from_rfc3339("2026-10-19T12:00:00-04:00")
.unwrap()
.with_timezone(&Local)
}
fn at(minute: u32) -> String {
format!("2026-10-19T11:{minute:02}:00-04:00")
}
fn record(kind: &str, number: Option<u64>, minute: u32) -> Value {
json!({
"version": 1, "kind": kind, "repository": "acme/widgets",
"name": number.map_or_else(|| format!("pickup-{STAMP}"), |number| format!("acme/widgets#{number}")),
"issue": number.map(|number| json!({"number": number, "url": format!("https://github.com/acme/widgets/issues/{number}"), "title": "Widgets"})),
"hostname": "factory", "process_id": 0, "process_start": "gone",
"harness": "claude", "model": null, "effort": null,
"started_at": at(minute), "ended_at": at(59),
"stages": [], "session_log": null, "outcome": "merged", "cause": null,
"issue_branch": null, "pull_request": null
})
}
fn stage(name: &str, minute: u32, session: Option<&Path>) -> Value {
json!({"name": name, "began_at": at(minute), "session_log": session})
}
fn write(root: &Path, name: &str, value: &Value) -> PathBuf {
let path = root.join(FOLDER).join(name);
fs::create_dir_all(path.parent().unwrap()).unwrap();
fs::write(&path, serde_json::to_vec(value).unwrap()).unwrap();
path
}
fn session(root: &Path, name: &str, second: u64) -> PathBuf {
let path = root.join(FOLDER).join(name);
fs::create_dir_all(path.parent().unwrap()).unwrap();
fs::write(&path, "").unwrap();
fs::File::open(&path)
.unwrap()
.set_modified(std::time::UNIX_EPOCH + std::time::Duration::from_secs(second))
.unwrap();
path
}
fn commands(snapshot: &Snapshot) -> Vec<&Command> {
snapshot
.entries()
.iter()
.filter_map(|entry| match entry {
Entry::Record(command) => Some(command.as_ref()),
Entry::History(_) => None,
})
.collect()
}
fn outline(nodes: &[Node]) -> Vec<String> {
nodes
.iter()
.map(|node| match &node.fact {
Fact::Command => format!("cmd {}", node.command.record.name),
Fact::Run => format!("run {}", node.command.record.name),
Fact::Stage(time) => format!("stage {}", time.stage.name),
Fact::Session(attachment) => format!(
"sess {}",
attachment.path.file_name().unwrap().to_string_lossy()
),
})
.collect()
}
fn find<'n, 'a>(nodes: &'n [Node<'a>], line: &str) -> &'n Node<'a> {
let index = outline(nodes)
.iter()
.position(|candidate| candidate == line)
.unwrap_or_else(|| panic!("{line} not in {:?}", outline(nodes)));
&nodes[index]
}
fn nth<'n, 'a>(nodes: &'n [Node<'a>], line: &str, n: usize) -> &'n Node<'a> {
let outline = outline(nodes);
let index = outline
.iter()
.enumerate()
.filter(|(_, candidate)| *candidate == line)
.nth(n)
.unwrap_or_else(|| panic!("{line} #{n} not in {outline:?}"))
.0;
&nodes[index]
}
#[test]
fn dispatched_and_child_runs_descend_from_the_stage_that_started_them() {
let dir = tempfile::tempdir().unwrap();
let mut pickup = record("Pickup run", None, 0);
pickup["stages"] = json!([
stage("Architecture review", 0, None),
stage("dispatched Spec run", 1, None)
]);
let mut spec = record("Spec run", Some(20), 1);
spec["stages"] = json!([stage("Spec review", 2, None), stage("Tickets", 5, None)]);
pickup["dispatched"] = json!([spec]);
let path = write(dir.path(), &format!("{STAMP}~pickup.command.json"), &pickup);
for (number, minute) in [(21, 6), (22, 1)] {
let mut ticket = record("Run", Some(number), minute);
ticket["parent_record"] = json!(path);
ticket["parent_dispatch"] = json!("https://github.com/acme/widgets/issues/20");
ticket["stages"] = json!([stage("implement", minute, None)]);
write(
dir.path(),
&format!("{STAMP}~pickup.command.dispatch-20-issue-{number}.child.json"),
&ticket,
);
}
let snapshot = Snapshot::load(dir.path(), now()).unwrap();
let owner = commands(&snapshot)[0];
let whole = of(owner, owner).unwrap();
assert_eq!(
outline(&whole),
[
"cmd pickup-20261019T110000-0400",
"run pickup-20261019T110000-0400",
"stage Architecture review",
"stage dispatched Spec run",
"run acme/widgets#20",
"stage Spec review",
"stage Tickets",
"run acme/widgets#21",
"stage implement",
"run acme/widgets#22",
"stage implement",
]
);
let id = |line| find(&whole, line).id();
assert_eq!(
find(&whole, "run pickup-20261019T110000-0400").parent,
id("cmd pickup-20261019T110000-0400")
);
assert_eq!(
find(&whole, "run acme/widgets#20").parent,
id("stage dispatched Spec run")
);
assert_eq!(
find(&whole, "run acme/widgets#21").parent,
id("stage Tickets")
);
assert_eq!(
find(&whole, "run acme/widgets#22").parent,
id("run acme/widgets#20")
);
assert_eq!(
nth(&whole, "stage implement", 1).parent,
id("run acme/widgets#22")
);
let ticket = &owner.dispatched[0].children[0];
let selected = of(ticket, owner).unwrap();
assert_eq!(
outline(&selected),
[
"cmd acme/widgets#21",
"run acme/widgets#21",
"stage implement"
]
);
assert_eq!(selected[0].id(), whole[0].id());
assert_eq!(selected[0].context.trace, whole[0].context.trace);
assert_eq!(selected[1].id(), id("run acme/widgets#21"));
assert_eq!(selected[1].parent, selected[0].id());
assert_eq!(selected[2].id(), find(&whole, "stage implement").id());
}
#[test]
fn each_session_attaches_once_to_its_descendant_or_first_naming_stage() {
let dir = tempfile::tempdir().unwrap();
let folder = dir.path().join(FOLDER);
let current = session(
dir.path(),
&format!("20-{STAMP}~spec-implement.logs.jsonl"),
1,
);
let former = folder.join(format!("20-{STAMP}~spec-implement.jsonl"));
let ticket_session = session(
dir.path(),
&format!("21-{STAMP}~spec-implement.logs.jsonl"),
2,
);
let inventory_only = session(
dir.path(),
&format!("20-{STAMP}~spec-repair-1.logs.jsonl"),
3,
);
let mut spec = record("Spec run", Some(20), 0);
spec["stages"] = json!([
stage("implement", 0, Some(&former)),
stage("Repair", 20, Some(&ticket_session)),
stage("pull request", 30, Some(¤t))
]);
spec["session_log"] = json!(current);
let path = write(dir.path(), &format!("20-{STAMP}~spec.command.json"), &spec);
let mut ticket = record("Run", Some(21), 10);
ticket["parent_record"] = json!(path);
ticket["stages"] = json!([stage("implement", 10, None)]);
write(
dir.path(),
&format!("20-{STAMP}~spec.command.issue-21.child.json"),
&ticket,
);
let snapshot = Snapshot::load(dir.path(), now()).unwrap();
let owner = commands(&snapshot)[0];
let nodes = of(owner, owner).unwrap();
assert_eq!(
outline(&nodes),
[
"cmd acme/widgets#20",
"run acme/widgets#20",
"stage implement",
"stage Repair",
"stage pull request",
"run acme/widgets#21",
"stage implement",
"sess 21-20261019T110000-0400~spec-implement.logs.jsonl",
"sess 20-20261019T110000-0400~spec-implement.logs.jsonl",
"sess 20-20261019T110000-0400~spec-repair-1.logs.jsonl",
]
);
let id = |line| find(&nodes, line).id();
assert_eq!(
find(&nodes, "run acme/widgets#21").parent,
id("stage implement")
);
let attachment = |node: &Node| match &node.fact {
Fact::Session(attachment) => (
attachment.path.clone(),
attachment.original.clone(),
attachment
.stage
.as_ref()
.map(|time| time.stage.name.clone()),
),
_ => panic!("not a Session"),
};
let claimed = find(
&nodes,
"sess 21-20261019T110000-0400~spec-implement.logs.jsonl",
);
assert_eq!(claimed.parent, id("run acme/widgets#21"));
assert_eq!(claimed.context.run, id("run acme/widgets#21").unwrap());
assert_eq!(
attachment(claimed),
(
ticket_session.clone(),
folder.join(format!("21-{STAMP}~spec-implement.jsonl")),
None
)
);
let reused = find(
&nodes,
"sess 20-20261019T110000-0400~spec-implement.logs.jsonl",
);
assert_eq!(reused.parent, id("stage implement"));
assert_eq!(
attachment(reused),
(current.clone(), former.clone(), Some("implement".into()))
);
let fallback = find(
&nodes,
"sess 20-20261019T110000-0400~spec-repair-1.logs.jsonl",
);
assert_eq!(fallback.parent, id("run acme/widgets#20"));
assert_eq!(attachment(fallback).0, inventory_only);
assert_eq!(attachment(fallback).2, None);
let selected = of(&owner.children[0], owner).unwrap();
assert_eq!(
outline(&selected),
[
"cmd acme/widgets#21",
"run acme/widgets#21",
"stage implement",
"sess 21-20261019T110000-0400~spec-implement.logs.jsonl",
]
);
assert_eq!(selected[3].parent, claimed.parent);
}
#[test]
fn distinct_commands_with_equal_start_stamps_have_distinct_identities() {
let dir = tempfile::tempdir().unwrap();
let mut spec = record("Spec run", Some(20), 0);
spec["stages"] = json!([stage("implement", 0, None)]);
for name in ["first", "second"] {
write(
dir.path(),
&format!("20-{STAMP}~{name}.command.json"),
&spec,
);
}
let snapshot = Snapshot::load(dir.path(), now()).unwrap();
let commands = commands(&snapshot);
assert_eq!(commands.len(), 2);
let [first, second] = [commands[0], commands[1]].map(|command| of(command, command).unwrap());
assert_eq!(outline(&first), outline(&second));
assert_ne!(first[0].context.trace, second[0].context.trace);
for (first, second) in first.iter().zip(&second) {
assert_ne!(first.id(), second.id());
}
}