use std::fs;
use assert_cmd::Command;
use tempfile::tempdir;
const VALID: &str = r#"{
"schema_version": 1,
"nodes": [
{ "kind": "agent", "payload": { "id": "research", "agent_hash": "sha256:1111111111111111111111111111111111111111111111111111111111111111" } },
{ "kind": "gate", "payload": { "id": "approve", "approval_schema": { "type": "object" } } }
],
"edges": [ { "from": "research", "to": "approve" } ]
}"#;
fn salvor() -> Command {
let mut command = Command::cargo_bin("salvor").expect("salvor binary builds");
command.env("RUST_LOG", "warn");
command
}
#[test]
fn validate_accepts_a_valid_document_and_prints_a_summary() {
let dir = tempdir().expect("tempdir");
let path = dir.path().join("valid.json");
fs::write(&path, VALID).expect("write");
let output = salvor()
.args(["graph", "validate"])
.arg(&path)
.output()
.expect("runs");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
output.status.success(),
"valid document exits 0: {output:?}"
);
assert!(
stdout.contains("graph ok: 2 node(s), 1 edge(s)"),
"{stdout}"
);
assert!(stdout.contains("entry: research"), "{stdout}");
assert!(stdout.contains("terminal: approve"), "{stdout}");
}
#[test]
fn validate_rejects_a_dangling_edge_with_a_precise_error() {
let dir = tempdir().expect("tempdir");
let path = dir.path().join("dangling.json");
let doc = VALID.replace(r#""to": "approve""#, r#""to": "aprove""#);
fs::write(&path, doc).expect("write");
let output = salvor()
.args(["graph", "validate"])
.arg(&path)
.output()
.expect("runs");
let stderr = String::from_utf8_lossy(&output.stderr);
assert_eq!(output.status.code(), Some(1), "invalid exits 1: {output:?}");
assert!(
stderr.contains("references unknown node id `aprove`"),
"names the missing id: {stderr}"
);
assert!(
stderr.contains("did you mean `approve`?"),
"suggests the near miss: {stderr}"
);
}
#[test]
fn validate_reports_a_cycle() {
let dir = tempdir().expect("tempdir");
let path = dir.path().join("cycle.json");
let doc = r#"{
"schema_version": 1,
"nodes": [
{ "kind": "agent", "payload": { "id": "a", "agent_hash": "sha256:1111111111111111111111111111111111111111111111111111111111111111" } },
{ "kind": "agent", "payload": { "id": "b", "agent_hash": "sha256:2222222222222222222222222222222222222222222222222222222222222222" } }
],
"edges": [ { "from": "a", "to": "b" }, { "from": "b", "to": "a" } ]
}"#;
fs::write(&path, doc).expect("write");
let output = salvor()
.args(["graph", "validate"])
.arg(&path)
.output()
.expect("runs");
let stderr = String::from_utf8_lossy(&output.stderr);
assert_eq!(output.status.code(), Some(1), "invalid exits 1");
assert!(
stderr.contains("cycle detected"),
"reports the cycle: {stderr}"
);
}
#[test]
fn validate_rejects_an_unknown_field() {
let dir = tempdir().expect("tempdir");
let path = dir.path().join("stray.json");
let doc = VALID.replace(
r#""edges": [ { "from": "research", "to": "approve" } ]"#,
r#""edges": [ { "from": "research", "to": "approve" } ], "surprise": true"#,
);
fs::write(&path, doc).expect("write");
let output = salvor()
.args(["graph", "validate"])
.arg(&path)
.output()
.expect("runs");
let stderr = String::from_utf8_lossy(&output.stderr);
assert_eq!(output.status.code(), Some(1), "strict parse exits 1");
assert!(
stderr.contains("not a valid graph document"),
"reports a parse failure: {stderr}"
);
assert!(
stderr.contains("surprise"),
"names the stray field: {stderr}"
);
}
#[test]
fn validate_errors_cleanly_on_a_missing_file() {
let dir = tempdir().expect("tempdir");
let path = dir.path().join("does-not-exist.json");
let output = salvor()
.args(["graph", "validate"])
.arg(&path)
.output()
.expect("runs");
let stderr = String::from_utf8_lossy(&output.stderr);
assert_eq!(
output.status.code(),
Some(1),
"missing file exits 1, no panic"
);
assert!(
stderr.contains("cannot read graph file"),
"clear read error: {stderr}"
);
assert!(!stderr.contains("panicked"), "must not panic: {stderr}");
}
#[test]
fn validate_errors_cleanly_on_garbage() {
let dir = tempdir().expect("tempdir");
let path = dir.path().join("garbage.json");
fs::write(&path, "this is not json {").expect("write");
let output = salvor()
.args(["graph", "validate"])
.arg(&path)
.output()
.expect("runs");
let stderr = String::from_utf8_lossy(&output.stderr);
assert_eq!(output.status.code(), Some(1), "garbage exits 1, no panic");
assert!(
stderr.contains("not a valid graph document"),
"reports the parse failure: {stderr}"
);
assert!(!stderr.contains("panicked"), "must not panic: {stderr}");
}
#[test]
fn schema_prints_the_json_schema_and_exits_zero() {
let output = salvor().args(["graph", "schema"]).output().expect("runs");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(output.status.success(), "schema exits 0: {output:?}");
for expected in [
"schema_version",
"nodes",
"edges",
"AgentNode",
"approval_schema",
] {
assert!(stdout.contains(expected), "schema mentions {expected}");
}
serde_json::from_str::<serde_json::Value>(&stdout).expect("schema is valid JSON");
}