use assert_cmd::Command;
use predicates::prelude::*;
use truth_mirror::{
cli::{Agent, ReviewerHarness},
ledger::{LedgerEntry, LedgerStore, ReviewerConfig, Verdict},
resolve::set_stdin_tty_override_for_testing,
};
fn seed_rejection(state_dir: &std::path::Path, sha: &str) {
let store = LedgerStore::new(state_dir);
store
.append_entry(&LedgerEntry::new_at(
sha,
Verdict::Reject,
"CLAIM: rejected | verified: cargo test | evidence: tests:cargo-test",
vec!["tests:cargo-test".to_owned()],
ReviewerConfig::new("claude", "claude-opus-4-1", false),
vec!["unsupported claim".to_owned()],
100,
))
.unwrap();
}
fn head_sha() -> String {
let out = std::process::Command::new("git")
.args(["rev-parse", "HEAD"])
.output()
.expect("git rev-parse HEAD");
String::from_utf8(out.stdout).unwrap().trim().to_owned()
}
fn seed_rejection_with_attempts(state_dir: &std::path::Path, sha: &str, attempts: u32) {
let store = LedgerStore::new(state_dir);
store.append_entry(&rejected_entry_at(sha, 100)).unwrap();
if attempts > 0 {
store
.append_petition_transition(
sha,
truth_mirror::ledger::Disposition::Open,
truth_mirror::ledger::ResolutionKind::Resolved,
"seeded prior attempt",
attempts,
)
.unwrap();
}
}
fn rejected_entry_at(sha: &str, timestamp: u64) -> LedgerEntry {
LedgerEntry::new_at(
sha,
Verdict::Reject,
"CLAIM: rejected | verified: cargo test | evidence: tests:cargo-test",
vec!["tests:cargo-test".to_owned()],
ReviewerConfig::new("claude", "claude-opus-4-1", false),
vec!["unsupported claim".to_owned()],
timestamp,
)
}
#[test]
fn resolve_petition_enqueues_record_and_increments_attempts() {
let temp = tempfile::tempdir().unwrap();
seed_rejection(temp.path(), "abc123");
let fix = head_sha();
Command::cargo_bin("truth-mirror")
.unwrap()
.args([
"--state-dir",
temp.path().to_str().unwrap(),
"resolve",
"abc123",
"--fixed-by",
&fix,
])
.assert()
.success()
.stdout(predicate::str::contains("enqueued petition review"))
.stdout(predicate::str::contains("attempt 1/2"));
let queue_contents = std::fs::read_to_string(temp.path().join("review-queue.jsonl")).unwrap();
assert!(queue_contents.contains(&format!("\"commit_sha\":\"{fix}\"")));
assert!(queue_contents.contains("\"petition_for\":\"abc123\""));
Command::cargo_bin("truth-mirror")
.unwrap()
.args([
"--state-dir",
temp.path().to_str().unwrap(),
"ledger",
"history",
"abc123",
])
.assert()
.success()
.stdout(predicate::str::contains("attempts=1"));
}
#[test]
fn resolve_petition_refuses_unknown_sha() {
let temp = tempfile::tempdir().unwrap();
Command::cargo_bin("truth-mirror")
.unwrap()
.args([
"--state-dir",
temp.path().to_str().unwrap(),
"resolve",
"missing123",
"--fixed-by",
&head_sha(),
])
.assert()
.failure()
.stderr(predicate::str::contains("missing123"));
}
#[test]
fn resolve_petition_refuses_when_already_resolved() {
let temp = tempfile::tempdir().unwrap();
let store = LedgerStore::new(temp.path());
store
.append_entry(&rejected_entry_at("abc123", 100))
.unwrap();
store.resolve("abc123").unwrap();
Command::cargo_bin("truth-mirror")
.unwrap()
.args([
"--state-dir",
temp.path().to_str().unwrap(),
"resolve",
"abc123",
"--fixed-by",
&head_sha(),
])
.assert()
.failure()
.stderr(predicate::str::contains("not open"));
}
#[test]
fn resolve_petition_escalates_to_needs_human_on_third_attempt() {
let temp = tempfile::tempdir().unwrap();
seed_rejection_with_attempts(temp.path(), "abc123", 2);
Command::cargo_bin("truth-mirror")
.unwrap()
.args([
"--state-dir",
temp.path().to_str().unwrap(),
"resolve",
"abc123",
"--fixed-by",
&head_sha(),
])
.assert()
.success()
.stdout(predicate::str::contains("refused 3rd petition attempt"))
.stdout(predicate::str::contains("needs-human"));
let ledger = LedgerStore::new(temp.path());
let entry = ledger.show("abc123").unwrap();
assert_eq!(
entry.disposition,
truth_mirror::ledger::Disposition::NeedsHuman
);
assert_eq!(
entry.resolution.as_ref().unwrap().kind,
truth_mirror::ledger::ResolutionKind::NeedsHuman
);
}
#[test]
fn resolve_waive_refuses_when_stdin_is_not_a_tty() {
let temp = tempfile::tempdir().unwrap();
seed_rejection(temp.path(), "abc123");
let assertion = Command::cargo_bin("truth-mirror")
.unwrap()
.args([
"--state-dir",
temp.path().to_str().unwrap(),
"resolve",
"abc123",
"--waive",
"--reason",
"approved exception",
])
.assert()
.failure();
assertion.stderr(predicate::str::contains("TTY"));
let ledger = LedgerStore::new(temp.path());
let entry = ledger.show("abc123").unwrap();
assert_eq!(entry.disposition, truth_mirror::ledger::Disposition::Open);
assert!(entry.resolution.is_none());
}
#[test]
fn resolve_waive_happy_path_under_simulated_tty() {
let temp = tempfile::tempdir().unwrap();
seed_rejection(temp.path(), "abc123");
set_stdin_tty_override_for_testing(Some(true));
let store = truth_mirror::ledger::LedgerStore::new(temp.path());
let config = truth_mirror::config::TruthMirrorConfig::default();
let args = truth_mirror::cli::ResolveArgs {
rejection_sha: "abc123".to_owned(),
fixed_by: None,
waive: true,
reason: Some("approved exception".to_owned()),
};
let exit = truth_mirror::resolve::run(args, temp.path(), &config).unwrap();
assert_eq!(exit, std::process::ExitCode::SUCCESS);
set_stdin_tty_override_for_testing(None);
let entry = store.show("abc123").unwrap();
assert_eq!(entry.disposition, truth_mirror::ledger::Disposition::Waived);
let markdown = std::fs::read_to_string(temp.path().join("ledger.md")).unwrap();
assert!(markdown.contains("approved exception"));
}
#[test]
fn resolve_waive_refuses_with_empty_reason() {
let temp = tempfile::tempdir().unwrap();
seed_rejection(temp.path(), "abc123");
Command::cargo_bin("truth-mirror")
.unwrap()
.args([
"--state-dir",
temp.path().to_str().unwrap(),
"resolve",
"abc123",
"--waive",
"--reason",
" ",
])
.assert()
.failure()
.stderr(predicate::str::contains("non-empty"));
}
#[test]
fn resolve_waive_without_reason_is_a_clap_error() {
let temp = tempfile::tempdir().unwrap();
seed_rejection(temp.path(), "abc123");
Command::cargo_bin("truth-mirror")
.unwrap()
.args([
"--state-dir",
temp.path().to_str().unwrap(),
"resolve",
"abc123",
"--waive",
])
.assert()
.failure()
.stderr(predicate::str::contains("--reason"));
}
#[test]
fn resolve_petition_then_accept_transitions_to_resolved() {
use truth_mirror::claim::{Claim, EvidenceRef};
use truth_mirror::config::TruthMirrorConfig;
use truth_mirror::ledger::{Disposition, LedgerStore, ResolutionKind};
use truth_mirror::reviewer::{
PetitionContext, ReviewJob, ReviewPlan, ReviewRequest, execute_review_job,
};
let temp = tempfile::tempdir().unwrap();
let store = LedgerStore::new(temp.path());
store
.append_entry(&rejected_entry_at("abc123", 100))
.unwrap();
store
.append_petition_transition(
"abc123",
truth_mirror::ledger::Disposition::Open,
truth_mirror::ledger::ResolutionKind::Resolved,
"petition review enqueued: fix=def456, attempts=1/2",
1,
)
.unwrap();
let claim = Claim::new(
"petitions to resolve abc123",
"cargo test",
vec![EvidenceRef::parse("tests:cargo-test").unwrap()],
)
.unwrap();
let original = store.show("abc123").unwrap();
let petition = PetitionContext {
original_sha: "abc123".to_owned(),
fix_sha: "def456".to_owned(),
original_claim: original.claim.clone(),
original_summary: original.summary.clone(),
original_findings: original.findings.clone(),
original_structured_findings: original.structured_findings.clone(),
original_reviewer_model: original.reviewer.model.clone(),
attempts_so_far: 1,
};
let plan_request = ReviewRequest::new(
Agent::Codex,
"gpt-5.4",
ReviewerHarness::Codex,
"gpt-5.5",
false,
"review fix".to_owned(),
);
let _ = ReviewPlan::build(plan_request.clone()).unwrap();
let job = ReviewJob {
commit_sha: "def456".to_owned(),
claim,
diff: "diff --git a/src/lib.rs b/src/lib.rs\n".to_owned(),
context: String::new(),
request: plan_request,
strict: None,
petition: Some(petition),
};
let pass_json = serde_json::json!({
"verdict": "PASS",
"summary": "Fix materially addresses each original finding.",
"findings": [],
"next_steps": [],
"memory_skill": {
"kind": "none",
"learning_source": "",
"reasoning": "No reusable procedural memory to propose."
}
})
.to_string();
struct ConstRunner(String);
impl truth_mirror::reviewer::ProcessRunner for ConstRunner {
fn run(
&self,
_invocation: &truth_mirror::reviewer::InvocationPlan,
_prompt: &str,
) -> Result<truth_mirror::reviewer::ProcessOutput, truth_mirror::reviewer::ReviewerError>
{
Ok(truth_mirror::reviewer::ProcessOutput {
status_code: Some(0),
stdout: self.0.clone(),
stderr: String::new(),
})
}
}
let config = TruthMirrorConfig::default();
let _ = config; let execution = execute_review_job(job, &ConstRunner(pass_json), &store).unwrap();
assert_eq!(execution.entries[0].verdict, Verdict::Pass);
let entry = store.show("abc123").unwrap();
assert_eq!(entry.disposition, Disposition::Resolved);
assert_eq!(
entry.resolution.as_ref().unwrap().kind,
ResolutionKind::Resolved
);
assert!(entry.petition_attempts > 0);
}
#[test]
fn resolve_petition_non_accept_increments_attempts_within_bound() {
use truth_mirror::claim::{Claim, EvidenceRef};
use truth_mirror::config::TruthMirrorConfig;
use truth_mirror::reviewer::{
PetitionContext, ProcessOutput, ProcessRunner, ReviewJob, ReviewPlan, ReviewRequest,
execute_review_job,
};
let temp = tempfile::tempdir().unwrap();
let store = LedgerStore::new(temp.path());
store
.append_entry(&rejected_entry_at("abc123", 100))
.unwrap();
store
.append_petition_transition(
"abc123",
truth_mirror::ledger::Disposition::Open,
truth_mirror::ledger::ResolutionKind::Resolved,
"petition review enqueued: fix=def456, attempts=1/2",
1,
)
.unwrap();
let original = store.show("abc123").unwrap();
let petition = PetitionContext {
original_sha: "abc123".to_owned(),
fix_sha: "def456".to_owned(),
original_claim: original.claim.clone(),
original_summary: original.summary.clone(),
original_findings: original.findings.clone(),
original_structured_findings: original.structured_findings.clone(),
original_reviewer_model: original.reviewer.model.clone(),
attempts_so_far: 1,
};
let claim = Claim::new(
"petitions to resolve abc123",
"cargo test",
vec![EvidenceRef::parse("tests:cargo-test").unwrap()],
)
.unwrap();
let plan_request = ReviewRequest::new(
Agent::Codex,
"gpt-5.4",
ReviewerHarness::Codex,
"gpt-5.5",
false,
"review fix".to_owned(),
);
let _ = ReviewPlan::build(plan_request.clone()).unwrap();
let job = ReviewJob {
commit_sha: "def456".to_owned(),
claim,
diff: "diff --git a/src/lib.rs b/src/lib.rs\n".to_owned(),
context: String::new(),
request: plan_request,
strict: None,
petition: Some(petition),
};
let reject_json = serde_json::json!({
"verdict": "REJECT",
"summary": "Fix does not address the finding.",
"findings": [{
"severity": "high",
"title": "still unaddressed",
"body": "Fix is incomplete.",
"file": "src/lib.rs",
"line_start": 1,
"line_end": 1,
"confidence": 90,
"recommendation": "Address the root cause."
}],
"next_steps": ["fix it"],
"memory_skill": {
"kind": "none",
"learning_source": "",
"reasoning": "no reusable memory"
}
})
.to_string();
struct ConstRunner(String);
impl ProcessRunner for ConstRunner {
fn run(
&self,
_invocation: &truth_mirror::reviewer::InvocationPlan,
_prompt: &str,
) -> Result<ProcessOutput, truth_mirror::reviewer::ReviewerError> {
Ok(ProcessOutput {
status_code: Some(0),
stdout: self.0.clone(),
stderr: String::new(),
})
}
}
let _config = TruthMirrorConfig::default();
let execution = execute_review_job(job, &ConstRunner(reject_json), &store).unwrap();
assert_eq!(execution.entries[0].verdict, Verdict::Reject);
let entry = store.show("abc123").unwrap();
assert_eq!(entry.disposition, truth_mirror::ledger::Disposition::Open);
assert_eq!(entry.petition_attempts, 1);
}
#[test]
fn resolve_petition_non_accept_at_bound_escalates_to_needs_human() {
use truth_mirror::claim::{Claim, EvidenceRef};
use truth_mirror::config::TruthMirrorConfig;
use truth_mirror::reviewer::{
PetitionContext, ProcessOutput, ProcessRunner, ReviewJob, ReviewPlan, ReviewRequest,
execute_review_job,
};
let temp = tempfile::tempdir().unwrap();
let store = LedgerStore::new(temp.path());
store
.append_entry(&rejected_entry_at("abc123", 100))
.unwrap();
store
.append_petition_transition(
"abc123",
truth_mirror::ledger::Disposition::Open,
truth_mirror::ledger::ResolutionKind::Resolved,
"petition review enqueued: fix=def456, attempts=2/2",
2,
)
.unwrap();
let original = store.show("abc123").unwrap();
let petition = PetitionContext {
original_sha: "abc123".to_owned(),
fix_sha: "def456".to_owned(),
original_claim: original.claim.clone(),
original_summary: original.summary.clone(),
original_findings: original.findings.clone(),
original_structured_findings: original.structured_findings.clone(),
original_reviewer_model: original.reviewer.model.clone(),
attempts_so_far: 2,
};
let claim = Claim::new(
"petitions to resolve abc123",
"cargo test",
vec![EvidenceRef::parse("tests:cargo-test").unwrap()],
)
.unwrap();
let plan_request = ReviewRequest::new(
Agent::Codex,
"gpt-5.4",
ReviewerHarness::Codex,
"gpt-5.5",
false,
"review fix".to_owned(),
);
let _ = ReviewPlan::build(plan_request.clone()).unwrap();
let job = ReviewJob {
commit_sha: "def456".to_owned(),
claim,
diff: "diff --git a/src/lib.rs b/src/lib.rs\n".to_owned(),
context: String::new(),
request: plan_request,
strict: None,
petition: Some(petition),
};
let reject_json = serde_json::json!({
"verdict": "REJECT",
"summary": "Fix still does not address the finding.",
"findings": [{
"severity": "high",
"title": "still unaddressed",
"body": "Fix is incomplete.",
"file": "src/lib.rs",
"line_start": 1,
"line_end": 1,
"confidence": 90,
"recommendation": "Address the root cause."
}],
"next_steps": ["fix it"],
"memory_skill": {
"kind": "none",
"learning_source": "",
"reasoning": "no reusable memory"
}
})
.to_string();
struct ConstRunner(String);
impl ProcessRunner for ConstRunner {
fn run(
&self,
_invocation: &truth_mirror::reviewer::InvocationPlan,
_prompt: &str,
) -> Result<ProcessOutput, truth_mirror::reviewer::ReviewerError> {
Ok(ProcessOutput {
status_code: Some(0),
stdout: self.0.clone(),
stderr: String::new(),
})
}
}
let _config = TruthMirrorConfig::default();
let _ = execute_review_job(job, &ConstRunner(reject_json), &store).unwrap();
let entry = store.show("abc123").unwrap();
assert_eq!(
entry.disposition,
truth_mirror::ledger::Disposition::NeedsHuman
);
assert_eq!(entry.petition_attempts, 2);
}