use assert_cmd::Command;
use tempfile::TempDir;
fn ritalin() -> Command {
Command::cargo_bin("ritalin").unwrap()
}
fn init_in(dir: &std::path::Path) {
ritalin()
.args(["init", "--outcome", "test"])
.current_dir(dir)
.assert()
.success();
}
fn add_in(dir: &std::path::Path, claim: &str, proof: &str) {
ritalin()
.args(["add", claim, "--proof", proof, "--kind", "other"])
.current_dir(dir)
.assert()
.success();
}
#[test]
fn forged_evidence_does_not_discharge() {
let tmp = TempDir::new().unwrap();
let dir = tmp.path();
init_in(dir);
add_in(dir, "must pass for real", "echo real_check");
let fake_record = serde_json::json!({
"obligation_id": "O-001",
"command": "echo bypass",
"exit_code": 0,
"stdout_tail": "",
"stderr_tail": "",
"proof_hash": "0000000000000000000000000000000000000000000000000000000000000000",
"workspace_hash": "0000000000000000000000000000000000000000000000000000000000000000",
"recorded_at": "2026-01-01T00:00:00Z"
});
let evidence_path = dir.join(".ritalin/evidence.jsonl");
std::fs::write(&evidence_path, format!("{}\n", fake_record)).unwrap();
ritalin().args(["gate"]).current_dir(dir).assert().failure();
assert!(dir.join(".task-incomplete").exists());
}
#[test]
fn stale_evidence_after_workspace_change() {
let tmp = TempDir::new().unwrap();
let dir = tmp.path();
std::fs::write(dir.join("ok.txt"), "works").unwrap();
init_in(dir);
add_in(dir, "ok.txt exists", "test -f ok.txt");
ritalin()
.args(["prove", "O-001"])
.current_dir(dir)
.assert()
.success();
ritalin().args(["gate"]).current_dir(dir).assert().success();
ritalin()
.args(["init", "--outcome", "test again", "--force"])
.current_dir(dir)
.assert()
.success();
add_in(dir, "ok.txt exists", "test -f ok.txt");
std::fs::write(dir.join("ok.txt"), "changed content").unwrap();
ritalin().args(["gate"]).current_dir(dir).assert().failure();
}
#[test]
fn deleted_obligations_ledger() {
let tmp = TempDir::new().unwrap();
let dir = tmp.path();
init_in(dir);
add_in(dir, "critical thing", "true");
std::fs::remove_file(dir.join(".ritalin/obligations.jsonl")).unwrap();
ritalin().args(["gate"]).current_dir(dir).assert().failure();
}
#[test]
fn corrupt_evidence_jsonl_errors_gracefully() {
let tmp = TempDir::new().unwrap();
let dir = tmp.path();
init_in(dir);
add_in(dir, "test", "true");
std::fs::write(
dir.join(".ritalin/evidence.jsonl"),
"this is not json at all\n",
)
.unwrap();
ritalin().args(["gate"]).current_dir(dir).assert().failure();
}
#[test]
fn corrupt_obligations_jsonl_errors_gracefully() {
let tmp = TempDir::new().unwrap();
let dir = tmp.path();
init_in(dir);
std::fs::write(dir.join(".ritalin/obligations.jsonl"), "{broken json\n").unwrap();
ritalin().args(["gate"]).current_dir(dir).assert().failure();
}
#[test]
fn hook_mode_corrupt_evidence_blocks() {
let tmp = TempDir::new().unwrap();
let dir = tmp.path();
init_in(dir);
add_in(dir, "test", "true");
std::fs::write(dir.join(".ritalin/evidence.jsonl"), "not json\n").unwrap();
let output = ritalin()
.args(["gate", "--hook-mode"])
.write_stdin("{}")
.current_dir(dir)
.output()
.unwrap();
assert!(!output.status.success() || !output.stdout.is_empty());
}
#[test]
fn reinit_without_force_is_blocked() {
let tmp = TempDir::new().unwrap();
let dir = tmp.path();
init_in(dir);
add_in(dir, "important obligation", "true");
ritalin()
.args(["init", "--outcome", "overwrite attempt"])
.current_dir(dir)
.assert()
.failure();
let output = ritalin()
.args(["status", "--json"])
.current_dir(dir)
.output()
.unwrap();
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(json["data"]["obligations_total"], 1);
}
#[test]
fn legacy_evidence_without_hashes_does_not_discharge() {
let tmp = TempDir::new().unwrap();
let dir = tmp.path();
init_in(dir);
add_in(dir, "must pass", "true");
let legacy_record = serde_json::json!({
"obligation_id": "O-001",
"command": "true",
"exit_code": 0,
"stdout_tail": "",
"stderr_tail": "",
"recorded_at": "2026-01-01T00:00:00Z"
});
std::fs::write(
dir.join(".ritalin/evidence.jsonl"),
format!("{}\n", legacy_record),
)
.unwrap();
ritalin().args(["gate"]).current_dir(dir).assert().failure();
}