use std::fs;
use std::path::{Path, PathBuf};
use chrono::{DateTime, Local};
use serde_json::{Value, json};
use super::*;
fn now() -> DateTime<Local> {
DateTime::parse_from_rfc3339("2026-10-19T12:00:00-04:00")
.unwrap()
.with_timezone(&Local)
}
fn record(number: u64) -> Value {
json!({
"version": 1, "kind": "Run", "repository": "acme/widgets",
"name": format!("acme/widgets#{number}"),
"issue": {"number": number, "url": format!("https://github.com/acme/widgets/issues/{number}"), "title": "Widgets"},
"hostname": "factory", "process_id": std::process::id(),
"process_start": crate::logs::process_identity::start(std::process::id()).unwrap(),
"harness": "claude", "model": null, "effort": null,
"started_at": "2026-10-19T11:00:00-04:00", "ended_at": null,
"stages": [], "session_log": null, "outcome": null, "cause": null,
"issue_branch": format!("issue-{number}"), "pull_request": null
})
}
fn write(root: &Path, name: &str, value: &Value) -> PathBuf {
let path = root.join("acme/widgets/commands/issue").join(name);
fs::create_dir_all(path.parent().unwrap()).unwrap();
fs::write(&path, serde_json::to_vec(value).unwrap()).unwrap();
path
}
#[test]
fn direct_run_selection_captures_its_owner_and_reloads_retained_work() {
let dir = tempfile::tempdir().unwrap();
let path = write(
dir.path(),
"7-20261019T110000-0400~direct.command.json",
&record(7),
);
let snapshot = Snapshot::load(dir.path(), now()).unwrap();
assert_eq!(snapshot.inbox(true, 7).len(), 1);
let selected = snapshot.resolve("WIDGETS#7").unwrap();
assert_eq!(selected.owner.unwrap().record_path, path);
assert!(selected.child_owner.is_none());
assert_eq!(selected.stop_target("WIDGETS#7").unwrap(), path);
assert!(!selected.process_exited());
let mut ended = record(7);
ended["issue_branch"] = "issue-7-preserved".into();
ended["pull_request"] = "https://github.com/acme/widgets/pull/9".into();
fs::write(&path, serde_json::to_vec(&ended).unwrap()).unwrap();
let retained = retained_work(&path, now()).unwrap();
assert_eq!(retained.branches, ["issue-7-preserved".to_string()].into());
assert_eq!(
retained.pull_requests,
["https://github.com/acme/widgets/pull/9".to_string()].into()
);
}
#[test]
fn pass_dispatches_and_equal_numbered_children_keep_exact_ownership() {
let dir = tempfile::tempdir().unwrap();
let stem = "20261019T110000-0400~security";
let mut pass = record(7);
pass["kind"] = "Security run".into();
pass["name"] = "secure-20261019T110000-0400".into();
pass["issue"] = Value::Null;
pass["issue_branch"] = "issue-20".into();
pass["pull_request"] = "https://github.com/acme/widgets/pull/20".into();
let path = write(dir.path(), &format!("{stem}.command.json"), &pass);
let mut dispatches = Vec::new();
for number in [20, 30] {
let mut dispatch = record(number);
if number == 20 {
dispatch["kind"] = "Spec run".into();
}
dispatches.push(dispatch);
let mut child = record(21);
child["parent_record"] = json!(path);
child["parent_dispatch"] =
format!("https://github.com/acme/widgets/issues/{number}").into();
child["issue_branch"] = format!("dispatch-{number}-ticket-21").into();
child["pull_request"] = format!("https://github.com/acme/widgets/pull/{number}").into();
child["outcome"] = "merged".into();
child["ended_at"] = "2026-10-19T11:30:00-04:00".into();
if number == 30 {
child["started_at"] = "2026-10-19T11:10:00-04:00".into();
}
write(
dir.path(),
&format!("{stem}.command.dispatch-{number}-issue-21.child.json"),
&child,
);
}
pass["dispatched"] = dispatches.into();
fs::write(&path, serde_json::to_vec(&pass).unwrap()).unwrap();
let separate = write(
dir.path(),
"20261019T110000-0400~separate.command.json",
&record(20),
);
let mut unrelated = record(99);
unrelated["parent_record"] = json!(separate);
write(dir.path(), "separate-99.child.json", &unrelated);
unrelated["parent_record"] = json!(path);
unrelated["parent_dispatch"] = "https://github.com/acme/widgets/issues/999".into();
write(dir.path(), "unmatched-99.child.json", &unrelated);
let snapshot = Snapshot::load(dir.path(), now()).unwrap();
let selected = snapshot.resolve("secure-20261019T110000-0400").unwrap();
let outer = selected.owner.unwrap();
assert_eq!(outer.record_path, path);
assert_eq!(outer.children.len(), 0);
assert_eq!(outer.dispatched.len(), 2);
for (index, number) in [20, 30].into_iter().enumerate() {
let dispatch = &outer.dispatched[index];
assert_eq!(dispatch.record.issue.as_ref().unwrap().number, number);
assert_eq!(dispatch.children.len(), 1);
assert_eq!(
dispatch.children[0].record.issue_branch.as_deref(),
Some(format!("dispatch-{number}-ticket-21").as_str())
);
}
let dispatched = snapshot.resolve("widgets#30").unwrap();
assert_eq!(dispatched.stop_target("widgets#30").unwrap(), path);
assert!(dispatched.child_owner.is_none());
let child = snapshot
.resolve("https://github.com/acme/widgets/issues/21")
.unwrap();
assert_eq!(child.owner.unwrap().record_path, path);
assert_eq!(child.child_owner.unwrap().record.name, "acme/widgets#30");
assert_eq!(
child.stop_target("widgets#21").unwrap_err().to_string(),
"cannot stop widgets#21: child Run; stop Run acme/widgets#30 instead"
);
assert!(!child.process_exited());
let retained = retained_work(&path, now()).unwrap();
assert_eq!(
retained.branches.into_iter().collect::<Vec<_>>(),
[
"dispatch-20-ticket-21",
"dispatch-30-ticket-21",
"issue-20",
"issue-30"
]
);
assert_eq!(
retained.pull_requests.into_iter().collect::<Vec<_>>(),
[
"https://github.com/acme/widgets/pull/20",
"https://github.com/acme/widgets/pull/30"
]
);
assert_eq!(
retained_work(&separate, now())
.unwrap()
.branches
.into_iter()
.collect::<Vec<_>>(),
["issue-20", "issue-99"]
);
assert_eq!(fs::read(&path).unwrap(), serde_json::to_vec(&pass).unwrap());
assert!(!path.with_extension("stop.json").exists());
}
#[test]
fn ticket_session_inventory_includes_stage_current_resume_and_numbered_repairs() {
let dir = tempfile::tempdir().unwrap();
let mut spec = record(20);
spec["kind"] = "Spec run".into();
let path = write(
dir.path(),
"20-20261019T110000-0400~spec.command.json",
&spec,
);
let folder = path.parent().unwrap();
let implement = folder.join("21-20261019T110000-0400~spec-implement.jsonl");
let resume = folder.join("21-20261019T110000-0400~spec-implement-resume.jsonl");
let repair = folder.join("21-20261019T110000-0400~spec-repair-2-resume.jsonl");
let previous = folder.join("21-20261019T110000-0400~spec-repair-1.jsonl");
let retained_reference = folder.join("retained-session.jsonl");
for (index, session) in [&retained_reference, &implement, &resume, &previous, &repair]
.into_iter()
.enumerate()
{
fs::write(session, "").unwrap();
let file = fs::File::open(session).unwrap();
file.set_modified(std::time::UNIX_EPOCH + std::time::Duration::from_secs(index as u64))
.unwrap();
}
fs::write(
folder.join("21-20261019T110000-0400~other-repair-9.jsonl"),
"unrelated",
)
.unwrap();
let mut ticket = record(21);
ticket["parent_record"] = json!(path);
ticket["stages"] = json!([
{"name": "implement", "began_at": "2026-10-19T11:00:00-04:00", "session_log": implement},
{"name": "Repair", "began_at": "2026-10-19T11:30:00-04:00", "session_log": retained_reference}
]);
ticket["session_log"] = json!(implement);
let child_path = write(
dir.path(),
"20-20261019T110000-0400~spec.command.issue-21.child.json",
&ticket,
);
let snapshot = Snapshot::load(dir.path(), now()).unwrap();
let selected = snapshot.resolve("ACME/WIDGETS#21").unwrap();
assert_eq!(selected.owner.unwrap().record_path, path);
assert_eq!(selected.child_owner.unwrap().record.name, "acme/widgets#20");
let Observation::Record(command) = selected.observation else {
panic!("record-backed Ticket")
};
assert_eq!(command.record_path, child_path);
assert_eq!(command.stage.as_ref().unwrap().age_seconds, 1800);
assert_eq!(
selected.observation.sessions().unwrap(),
[&retained_reference, &implement, &resume, &previous, &repair]
.into_iter()
.cloned()
.collect::<Vec<_>>()
);
assert_eq!(selected.observation.session("implement").unwrap(), resume);
assert_eq!(selected.observation.session("repair").unwrap(), repair);
assert_eq!(selected.observation.session("repair-1").unwrap(), previous);
assert_eq!(
selected.stop_target("widgets#21").unwrap_err().to_string(),
"cannot stop widgets#21: child Run; stop Spec run acme/widgets#20 instead"
);
}
#[test]
fn historical_selection_keeps_unknown_facts_and_has_no_process_or_stop_authority() {
let dir = tempfile::tempdir().unwrap();
let folder = dir.path().join("acme/widgets/commands/issue");
fs::create_dir_all(&folder).unwrap();
let session = folder.join("7-20261019T110000-0400~history-implement.jsonl");
fs::write(&session, "not interpreted as a live event").unwrap();
let snapshot = Snapshot::load(dir.path(), now()).unwrap();
assert!(snapshot.inbox(false, 7).is_empty());
assert_eq!(snapshot.inbox(true, 7).len(), 1);
let selected = snapshot.resolve("widgets#7").unwrap();
assert!(selected.owner.is_none());
assert!(selected.child_owner.is_none());
assert!(selected.process_exited());
assert_eq!(selected.observation.session("implement").unwrap(), session);
let details = serde_json::to_value(selected.observation.details().unwrap()).unwrap();
assert_eq!(details["state"], "unknown");
assert_eq!(details["stages"], json!([]));
assert_eq!(details["stage"], Value::Null);
assert_eq!(details["harness"], Value::Null);
assert_eq!(details["model"], Value::Null);
assert_eq!(details["source"], "session_log");
assert_eq!(
selected.stop_target("widgets#7").unwrap_err().to_string(),
"cannot stop widgets#7: Command unknown"
);
}
#[test]
fn outcome_ends_display_but_not_the_captured_process_for_following() {
let dir = tempfile::tempdir().unwrap();
let mut ended = record(7);
ended["outcome"] = "ready for review".into();
ended["ended_at"] = "2026-10-19T11:30:00-04:00".into();
let path = write(
dir.path(),
"7-20261019T110000-0400~ended.command.json",
&ended,
);
let snapshot = Snapshot::load(dir.path(), now()).unwrap();
let selected = snapshot.resolve("widgets#7").unwrap();
assert_eq!(selected.owner.unwrap().state, "ended");
assert!(!selected.process_exited());
assert_eq!(
selected.stop_target("widgets#7").unwrap_err().to_string(),
"cannot stop widgets#7: Command ended: ready for review"
);
ended["process_start"] = "reused-process-identity".into();
fs::write(&path, serde_json::to_vec(&ended).unwrap()).unwrap();
assert!(!selected.process_exited());
let refreshed = Snapshot::load(dir.path(), now()).unwrap();
assert!(refreshed.resolve("widgets#7").unwrap().process_exited());
}