use std::path::Path;
use std::process::Command;
use octl_core::{append_and_apply_event, ensure_root, new_run_id, NodeId, RunPaths};
use serde_json::{json, Value};
use tempfile::TempDir;
fn bin(home: &TempDir) -> Command {
let mut c = Command::new(env!("CARGO_BIN_EXE_orchestratectl"));
c.env("ORCHESTRATECTL_HOME", home.path());
c
}
fn node_id() -> NodeId {
NodeId::parse_str("n-0001").unwrap()
}
fn git(repo: &Path, args: &[&str]) -> String {
let out = Command::new("git")
.arg("-C")
.arg(repo)
.args(args)
.output()
.expect("git runs");
assert!(
out.status.success(),
"git {args:?} failed: {}",
String::from_utf8_lossy(&out.stderr)
);
String::from_utf8_lossy(&out.stdout).trim().to_string()
}
fn repo_with_raw_selfmerge(repo: &Path) -> (String, String) {
git(repo, &["init", "-q", "-b", "main"]);
git(repo, &["config", "user.email", "t@t"]);
git(repo, &["config", "user.name", "t"]);
std::fs::write(repo.join("f"), "base\n").unwrap();
git(repo, &["add", "f"]);
git(repo, &["commit", "-qm", "base"]);
let base = git(repo, &["rev-parse", "HEAD"]);
git(repo, &["checkout", "-q", "-b", "wt/raw"]);
std::fs::write(repo.join("f"), "base\nwork\n").unwrap();
git(repo, &["commit", "-qam", "worker change"]);
git(repo, &["checkout", "-q", "main"]);
git(repo, &["merge", "-q", "--ff-only", "wt/raw"]);
(base, "wt/raw".to_string())
}
fn seed_raw_selfmerge_failed(
home: &Path,
run_id: &str,
repo: &Path,
base: Option<&str>,
branch: &str,
) -> RunPaths {
ensure_root(home).unwrap();
let dir = home.join("runs").join(run_id);
std::fs::create_dir_all(&dir).unwrap();
let paths = RunPaths::new(dir, run_id).unwrap();
append_and_apply_event(
&paths,
"run.created",
None,
None,
json!({
"kind": "spinoff",
"lifecycle": "autonomous",
"title": "raw-selfmerge-death",
"source_repo": repo.display().to_string(),
"source_branch": "main",
}),
)
.unwrap();
let mut node_data = json!({
"kind": "spinoff",
"worktree_path": repo.display().to_string(),
"branch": branch,
});
if let Some(base) = base {
node_data
.as_object_mut()
.unwrap()
.insert("base_sha".into(), json!(base));
}
append_and_apply_event(&paths, "node.created", Some(&node_id()), None, node_data).unwrap();
append_and_apply_event(
&paths,
"worker.exited",
Some(&node_id()),
None,
json!({ "signal": 9 }),
)
.unwrap();
let mut report = json!({ "success": false, "reason": "agent-died" });
octl_core::ReportOrigin::Supervisor.stamp(&mut report);
append_and_apply_event(&paths, "node.report", Some(&node_id()), None, report).unwrap();
append_and_apply_event(
&paths,
"run.status",
None,
None,
json!({ "status": "failed" }),
)
.unwrap();
paths
}
fn run_show_json(home: &TempDir, run_id: &str) -> Value {
let out = bin(home)
.args(["--output", "json", "run", "show", run_id])
.output()
.expect("spawn run show");
assert!(
out.status.success(),
"run show failed: {}",
String::from_utf8_lossy(&out.stderr)
);
serde_json::from_slice(&out.stdout).expect("run show json")
}
#[test]
fn run_show_flags_raw_selfmerge_death_as_false_failed_without_auto_success() {
let home = TempDir::new().unwrap();
let repo = TempDir::new().unwrap();
let (base, branch) = repo_with_raw_selfmerge(repo.path());
let run_id = new_run_id();
seed_raw_selfmerge_failed(home.path(), &run_id, repo.path(), Some(&base), &branch);
let v = run_show_json(&home, &run_id);
let d = &v["data"];
assert_eq!(
d["status"], "failed",
"a raw-selfmerge death must never be auto-promoted to done: {d}"
);
assert_eq!(d["landed"], true, "content is git-verified in source: {d}");
assert_eq!(
d["landed_method"], "git-verified",
"the landing must rest on git ground truth, not a report marker: {d}"
);
let ff = &d["false_failed"];
assert!(ff.is_object(), "false_failed block must be present: {d}");
assert_eq!(
ff["reason"],
"branch content is git-verified in source but no `run merge` recorded it (raw-git self-merge?)",
"false_failed reason: {ff}"
);
let hint = ff["resume_hint"].as_str().expect("resume_hint str");
assert!(
hint.contains("run salvage") && hint.contains(&run_id),
"hint must steer to `run salvage <id>`: {hint}"
);
assert!(
hint.contains("run merge"),
"hint must reinforce finishing through run merge: {hint}"
);
assert!(
Path::new(&repo.path().join(".git")).exists(),
"worktree/repo must be preserved (run show never tears down)"
);
let branch_ref = git(repo.path(), &["rev-parse", "--verify", &branch]);
assert!(
!branch_ref.is_empty(),
"the worker branch must be preserved, not deleted"
);
let out = bin(&home)
.args(["--output", "text", "run", "show", &run_id])
.output()
.expect("spawn");
assert!(out.status.success());
let text = String::from_utf8(out.stdout).expect("utf8");
assert!(
text.lines()
.any(|l| l.starts_with("false-failed:") && l.contains("run salvage")),
"text show must carry a false-failed line: {text}"
);
}
#[test]
fn run_show_does_not_flag_genuinely_unlanded_failed_run() {
let home = TempDir::new().unwrap();
let repo = TempDir::new().unwrap();
let repo_path = repo.path();
git(repo_path, &["init", "-q", "-b", "main"]);
git(repo_path, &["config", "user.email", "t@t"]);
git(repo_path, &["config", "user.name", "t"]);
std::fs::write(repo_path.join("f"), "base\n").unwrap();
git(repo_path, &["add", "f"]);
git(repo_path, &["commit", "-qm", "base"]);
let base = git(repo_path, &["rev-parse", "HEAD"]);
git(repo_path, &["checkout", "-q", "-b", "wt/raw"]);
std::fs::write(repo_path.join("f"), "base\nwork\n").unwrap();
git(repo_path, &["commit", "-qam", "unmerged work"]);
git(repo_path, &["checkout", "-q", "main"]);
let run_id = new_run_id();
seed_raw_selfmerge_failed(home.path(), &run_id, repo_path, Some(&base), "wt/raw");
let v = run_show_json(&home, &run_id);
let d = &v["data"];
assert_eq!(d["status"], "failed");
assert_eq!(d["landed"], false, "an unmerged branch is not landed: {d}");
assert!(
d.get("false_failed").is_none() || d["false_failed"].is_null(),
"an honestly-failed unlanded run must not be flagged false-failed: {d}"
);
}
#[test]
fn run_show_does_not_flag_never_advanced_branch_without_base_sha() {
let home = TempDir::new().unwrap();
let repo = TempDir::new().unwrap();
let repo_path = repo.path();
git(repo_path, &["init", "-q", "-b", "main"]);
git(repo_path, &["config", "user.email", "t@t"]);
git(repo_path, &["config", "user.name", "t"]);
std::fs::write(repo_path.join("f"), "base\n").unwrap();
git(repo_path, &["add", "f"]);
git(repo_path, &["commit", "-qm", "base"]);
git(repo_path, &["branch", "wt/raw"]);
std::fs::write(repo_path.join("g"), "more\n").unwrap();
git(repo_path, &["add", "g"]);
git(repo_path, &["commit", "-qm", "main advances"]);
let run_id = new_run_id();
seed_raw_selfmerge_failed(home.path(), &run_id, repo_path, None, "wt/raw");
let v = run_show_json(&home, &run_id);
let d = &v["data"];
assert_eq!(d["status"], "failed");
assert!(
d.get("false_failed").is_none() || d["false_failed"].is_null(),
"a never-advanced branch with no fork point must not be flagged false-failed: {d}"
);
}