#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod tests {
use super::*;
use crate::cli::handlers::work_contract::{
EvidenceType, FalsificationMethod, FalsificationResult,
};
fn make_report(all_passed: bool) -> FalsificationReport {
let mut results = vec![ClaimResult {
index: 1,
hypothesis: "All baseline files still exist".to_string(),
method: FalsificationMethod::ManifestIntegrity,
result: FalsificationResult::passed("All 10 files present"),
is_blocking: true,
}];
if !all_passed {
results.push(ClaimResult {
index: 2,
hypothesis: "Total coverage >= 95%".to_string(),
method: FalsificationMethod::AbsoluteCoverage,
result: FalsificationResult::failed(
"80.0% < 95.0%",
EvidenceType::NumericComparison {
actual: 80.0,
threshold: 95.0,
},
),
is_blocking: true,
});
}
let passed = results.iter().filter(|r| !r.result.falsified).count();
let failed = results
.iter()
.filter(|r| r.result.falsified && r.is_blocking)
.count();
FalsificationReport {
total_claims: results.len(),
passed,
failed,
warnings: 0,
all_passed,
claim_results: results,
}
}
#[test]
fn receipt_from_report_all_passed() {
let report = make_report(true);
let receipt = FalsificationReceipt::from_report(
&report,
"abc123".to_string(),
"PMAT-100".to_string(),
FalsificationTrigger::WorkComplete,
None,
None,
);
assert!(receipt.summary.allows_completion);
assert_eq!(receipt.summary.passed, 1);
assert_eq!(receipt.summary.failed, 0);
assert_eq!(receipt.summary.overridden, 0);
assert_eq!(receipt.git_sha, "abc123");
assert_eq!(receipt.work_item_id, "PMAT-100");
assert!(!receipt.content_hash.is_empty());
}
#[test]
fn receipt_from_report_with_failures() {
let report = make_report(false);
let receipt = FalsificationReceipt::from_report(
&report,
"def456".to_string(),
"PMAT-101".to_string(),
FalsificationTrigger::WorkComplete,
None,
None,
);
assert!(!receipt.summary.allows_completion);
assert_eq!(receipt.summary.passed, 1);
assert_eq!(receipt.summary.failed, 1);
assert_eq!(receipt.verdicts.len(), 2);
}
#[test]
fn receipt_from_report_with_overrides() {
let report = make_report(false);
let overrides = vec!["coverage".to_string()];
let ticket = "DEBT-001".to_string();
let receipt = FalsificationReceipt::from_report(
&report,
"ghi789".to_string(),
"PMAT-102".to_string(),
FalsificationTrigger::WorkComplete,
Some(&overrides),
Some(&ticket),
);
assert!(receipt.summary.allows_completion);
assert_eq!(receipt.summary.overridden, 1);
assert_eq!(receipt.overrides[0].claim_id, "coverage");
assert_eq!(receipt.overrides[0].ticket, "DEBT-001");
}
#[test]
fn content_hash_stable() {
let report = make_report(true);
let r1 = FalsificationReceipt::from_report(
&report,
"abc".to_string(),
"X-1".to_string(),
FalsificationTrigger::ManualCli,
None,
None,
);
assert!(!r1.content_hash.is_empty());
assert!(r1.content_hash.chars().all(|c| c.is_ascii_hexdigit()));
assert!(r1.verify_integrity());
}
#[test]
fn content_hash_detects_tampering() {
let report = make_report(true);
let mut receipt = FalsificationReceipt::from_report(
&report,
"abc".to_string(),
"X-1".to_string(),
FalsificationTrigger::CiPipeline,
None,
None,
);
assert!(receipt.verify_integrity());
receipt.git_sha = "tampered".to_string();
assert!(!receipt.verify_integrity());
}
#[test]
fn freshness_matches_sha() {
let report = make_report(true);
let receipt = FalsificationReceipt::from_report(
&report,
"abc123".to_string(),
"X-1".to_string(),
FalsificationTrigger::WorkComplete,
None,
None,
);
assert!(receipt.is_fresh("abc123", MAX_RECEIPT_AGE_SECS));
}
#[test]
fn freshness_stale_sha() {
let report = make_report(true);
let receipt = FalsificationReceipt::from_report(
&report,
"abc123".to_string(),
"X-1".to_string(),
FalsificationTrigger::WorkComplete,
None,
None,
);
assert!(!receipt.is_fresh("different_sha", MAX_RECEIPT_AGE_SECS));
}
#[test]
fn freshness_expired() {
let report = make_report(true);
let mut receipt = FalsificationReceipt::from_report(
&report,
"abc123".to_string(),
"X-1".to_string(),
FalsificationTrigger::WorkComplete,
None,
None,
);
let old_time = chrono::Utc::now() - chrono::Duration::hours(48);
receipt.timestamp = old_time.to_rfc3339();
assert!(!receipt.is_fresh("abc123", MAX_RECEIPT_AGE_SECS));
}
#[test]
fn ledger_persist_and_load() {
let temp_dir = tempfile::TempDir::new().unwrap();
let ledger = FalsificationLedger::new(temp_dir.path());
let report = make_report(true);
let receipt = FalsificationReceipt::from_report(
&report,
"abc123".to_string(),
"PMAT-200".to_string(),
FalsificationTrigger::WorkComplete,
None,
None,
);
let path = ledger.persist_receipt(&receipt).unwrap();
assert!(path.exists());
let loaded = ledger.latest_receipt("PMAT-200").unwrap();
assert!(loaded.is_some());
let loaded = loaded.unwrap();
assert_eq!(loaded.id, receipt.id);
assert_eq!(loaded.git_sha, "abc123");
assert!(loaded.verify_integrity());
}
#[test]
fn ledger_append_only() {
let temp_dir = tempfile::TempDir::new().unwrap();
let ledger = FalsificationLedger::new(temp_dir.path());
let report = make_report(true);
let r1 = FalsificationReceipt::from_report(
&report,
"sha1".to_string(),
"ITEM-1".to_string(),
FalsificationTrigger::WorkComplete,
None,
None,
);
let r2 = FalsificationReceipt::from_report(
&report,
"sha2".to_string(),
"ITEM-2".to_string(),
FalsificationTrigger::ManualCli,
None,
None,
);
ledger.append_to_ledger(&r1).unwrap();
ledger.append_to_ledger(&r2).unwrap();
let ledger_path = temp_dir.path().join(".pmat-work/ledger.jsonl");
let content = std::fs::read_to_string(&ledger_path).unwrap();
let lines: Vec<&str> = content.lines().collect();
assert_eq!(lines.len(), 2);
let entry1: LedgerEntry = serde_json::from_str(lines[0]).unwrap();
let entry2: LedgerEntry = serde_json::from_str(lines[1]).unwrap();
assert_eq!(entry1.work_item_id, "ITEM-1");
assert_eq!(entry2.work_item_id, "ITEM-2");
}
#[test]
fn latest_receipt_returns_most_recent() {
let temp_dir = tempfile::TempDir::new().unwrap();
let ledger = FalsificationLedger::new(temp_dir.path());
let report = make_report(true);
let r1 = FalsificationReceipt::from_report(
&report,
"sha_old".to_string(),
"PMAT-300".to_string(),
FalsificationTrigger::WorkComplete,
None,
None,
);
ledger.persist_receipt(&r1).unwrap();
let r2 = FalsificationReceipt::from_report(
&report,
"sha_new".to_string(),
"PMAT-300".to_string(),
FalsificationTrigger::WorkComplete,
None,
None,
);
ledger.persist_receipt(&r2).unwrap();
let latest = ledger.latest_receipt("PMAT-300").unwrap().unwrap();
assert_eq!(latest.git_sha, "sha_new");
}
#[test]
fn has_fresh_receipt_true() {
let temp_dir = tempfile::TempDir::new().unwrap();
let ledger = FalsificationLedger::new(temp_dir.path());
let report = make_report(true);
let receipt = FalsificationReceipt::from_report(
&report,
"current_head".to_string(),
"PMAT-400".to_string(),
FalsificationTrigger::WorkComplete,
None,
None,
);
ledger.persist_receipt(&receipt).unwrap();
assert!(ledger
.has_fresh_receipt("PMAT-400", "current_head")
.unwrap());
}
#[test]
fn has_fresh_receipt_false_stale_sha() {
let temp_dir = tempfile::TempDir::new().unwrap();
let ledger = FalsificationLedger::new(temp_dir.path());
let report = make_report(true);
let receipt = FalsificationReceipt::from_report(
&report,
"old_sha".to_string(),
"PMAT-401".to_string(),
FalsificationTrigger::WorkComplete,
None,
None,
);
ledger.persist_receipt(&receipt).unwrap();
assert!(!ledger.has_fresh_receipt("PMAT-401", "new_sha").unwrap());
}
#[test]
fn has_fresh_receipt_false_no_receipts() {
let temp_dir = tempfile::TempDir::new().unwrap();
let ledger = FalsificationLedger::new(temp_dir.path());
assert!(!ledger.has_fresh_receipt("PMAT-999", "any_sha").unwrap());
}
#[test]
fn verify_integrity_report() {
let temp_dir = tempfile::TempDir::new().unwrap();
let ledger = FalsificationLedger::new(temp_dir.path());
let report = make_report(true);
let receipt = FalsificationReceipt::from_report(
&report,
"abc".to_string(),
"PMAT-500".to_string(),
FalsificationTrigger::WorkComplete,
None,
None,
);
ledger.persist_receipt(&receipt).unwrap();
let integrity = ledger.verify_integrity("PMAT-500").unwrap();
assert_eq!(integrity.total, 1);
assert_eq!(integrity.valid, 1);
assert_eq!(integrity.tampered, 0);
assert_eq!(integrity.missing, 0);
}
#[test]
fn verify_integrity_detects_tampering() {
let temp_dir = tempfile::TempDir::new().unwrap();
let ledger = FalsificationLedger::new(temp_dir.path());
let report = make_report(true);
let receipt = FalsificationReceipt::from_report(
&report,
"abc".to_string(),
"PMAT-501".to_string(),
FalsificationTrigger::WorkComplete,
None,
None,
);
let path = ledger.persist_receipt(&receipt).unwrap();
let mut content: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
content["git_sha"] = serde_json::Value::String("tampered".to_string());
std::fs::write(&path, serde_json::to_string_pretty(&content).unwrap()).unwrap();
let integrity = ledger.verify_integrity("PMAT-501").unwrap();
assert_eq!(integrity.total, 1);
assert_eq!(integrity.tampered, 1);
assert_eq!(integrity.valid, 0);
}
#[test]
fn hypothesis_to_claim_id_mapping() {
assert_eq!(
hypothesis_to_claim_id("All baseline files still exist"),
"manifest"
);
assert_eq!(hypothesis_to_claim_id("Total coverage >= 95%"), "coverage");
assert_eq!(hypothesis_to_claim_id("TDG score >= baseline"), "tdg");
assert_eq!(
hypothesis_to_claim_id("No coverage exclusion gaming"),
"coverage-gaming"
);
assert_eq!(hypothesis_to_claim_id("make lint passes"), "lint");
assert_eq!(
hypothesis_to_claim_id("No dead code introduced"),
"dead-code"
);
assert_eq!(
hypothesis_to_claim_id("All match arm variants have test coverage"),
"variant-coverage"
);
assert_eq!(
hypothesis_to_claim_id("No fix-after-fix chains exceed limit"),
"fix-chain"
);
assert_eq!(
hypothesis_to_claim_id("Cross-crate integration tests pass"),
"cross-crate"
);
assert_eq!(
hypothesis_to_claim_id("No performance regressions detected"),
"regression-gate"
);
}
#[test]
fn ledger_entry_from_receipt() {
let report = make_report(true);
let receipt = FalsificationReceipt::from_report(
&report,
"abc".to_string(),
"X-1".to_string(),
FalsificationTrigger::PreCommit,
None,
None,
);
let entry = LedgerEntry::from_receipt(&receipt);
assert_eq!(entry.receipt_id, receipt.id);
assert_eq!(entry.work_item_id, "X-1");
assert_eq!(entry.trigger, FalsificationTrigger::PreCommit);
assert!(entry.allows_completion);
}
#[test]
fn receipt_trigger_variants() {
let triggers = vec![
FalsificationTrigger::WorkComplete,
FalsificationTrigger::ManualCli,
FalsificationTrigger::CiPipeline,
FalsificationTrigger::McpTool,
FalsificationTrigger::PreCommit,
];
for trigger in triggers {
let json = serde_json::to_string(&trigger).unwrap();
let deserialized: FalsificationTrigger = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized, trigger);
}
}
fn make_provenance() -> AgentProvenance {
AgentProvenance {
model: "claude-fable-5".to_string(),
effort: "high".to_string(),
harness: AgentHarness::ClaudeCode,
workflow_id: None,
parent: None,
source: ProvenanceSource::Declared,
}
}
fn make_v2_receipt() -> FalsificationReceipt {
let report = make_report(true);
let mut receipt = FalsificationReceipt::from_report(
&report,
"abc123".to_string(),
"MACS-TEST".to_string(),
FalsificationTrigger::WorkComplete,
None,
None,
);
receipt.agent = Some(make_provenance());
receipt.agent_events = vec![AgentEvent::ModelSwitch {
at: "2026-07-02T12:00:00Z".to_string(),
from: "claude-fable-5".to_string(),
to: "claude-opus-4-8".to_string(),
}];
receipt.content_hash = receipt.compute_content_hash();
receipt
}
#[test]
fn new_receipts_are_schema_version_2() {
let report = make_report(true);
let receipt = FalsificationReceipt::from_report(
&report,
"abc123".to_string(),
"MACS-TEST".to_string(),
FalsificationTrigger::WorkComplete,
None,
None,
);
assert_eq!(receipt.schema_version, 2);
assert!(receipt.agent.is_none());
assert!(receipt.agent_events.is_empty());
assert!(receipt.verify_integrity());
}
#[test]
fn v2_receipt_with_agent_roundtrips_and_verifies() {
let receipt = make_v2_receipt();
assert!(receipt.verify_integrity());
let json = serde_json::to_string(&receipt).expect("receipt serializes");
let parsed: FalsificationReceipt = serde_json::from_str(&json).expect("receipt parses");
assert!(parsed.verify_integrity());
assert_eq!(parsed.agent, receipt.agent);
assert_eq!(parsed.agent_events, receipt.agent_events);
assert_eq!(parsed.schema_version, 2);
}
#[test]
fn nine_field_v1_json_defaults_to_schema_version_1() {
let report = make_report(true);
let mut receipt = FalsificationReceipt::from_report(
&report,
"abc123".to_string(),
"LEGACY-1".to_string(),
FalsificationTrigger::WorkComplete,
None,
None,
);
receipt.schema_version = 1;
receipt.content_hash = receipt.compute_content_hash();
let mut value = serde_json::to_value(&receipt).expect("to_value");
let obj = value.as_object_mut().expect("object");
obj.remove("schema_version");
obj.remove("agent");
obj.remove("agent_events");
let parsed: FalsificationReceipt =
serde_json::from_value(value).expect("v1 receipt parses");
assert_eq!(parsed.schema_version, 1);
assert!(parsed.agent.is_none());
assert!(parsed.agent_events.is_empty());
assert!(parsed.verify_integrity(), "v1 rules must verify v1 receipts");
}
#[test]
fn v1_receipts_still_verify() {
let root = env!("CARGO_MANIFEST_DIR");
let dir = std::path::Path::new(root).join("fixtures/receipts/v1");
let mut checked = 0usize;
for entry in std::fs::read_dir(&dir).expect("fixtures/receipts/v1 exists") {
let path = entry.expect("dir entry").path();
if path.extension().is_none_or(|e| e != "json") {
continue;
}
let text = std::fs::read_to_string(&path).expect("fixture readable");
let receipt: FalsificationReceipt =
serde_json::from_str(&text).expect("v1 fixture parses");
assert_eq!(receipt.schema_version, 1, "{}", path.display());
assert!(receipt.verify_integrity(), "{} must verify", path.display());
checked += 1;
}
assert!(checked >= 1, "at least one v1 fixture committed");
}
#[test]
fn hash_stability_golden_v2() {
let root = env!("CARGO_MANIFEST_DIR");
let dir = std::path::Path::new(root).join("fixtures/receipts/v2");
let mut checked = 0usize;
for entry in std::fs::read_dir(&dir).expect("fixtures/receipts/v2 exists") {
let path = entry.expect("dir entry").path();
if path.extension().is_none_or(|e| e != "json") {
continue;
}
let text = std::fs::read_to_string(&path).expect("fixture readable");
let receipt: FalsificationReceipt =
serde_json::from_str(&text).expect("v2 fixture parses");
assert_eq!(receipt.schema_version, 2, "{}", path.display());
assert!(
receipt.verify_integrity(),
"{} content_hash must re-derive under v2 canonical rules",
path.display()
);
checked += 1;
}
assert!(checked >= 2, "at least two v2 fixtures committed");
}
#[test]
fn canonical_json_sorted_keys() {
let a: serde_json::Value =
serde_json::from_str(r#"{"zeta":1,"alpha":{"nested_z":true,"nested_a":null},"mid":[2,1]}"#)
.expect("parse a");
let b: serde_json::Value =
serde_json::from_str(r#"{"alpha":{"nested_a":null,"nested_z":true},"mid":[2,1],"zeta":1}"#)
.expect("parse b");
assert_eq!(canonical_json(&a), canonical_json(&b));
assert_eq!(
canonical_json(&a),
r#"{"alpha":{"nested_a":null,"nested_z":true},"mid":[2,1],"zeta":1}"#
);
}
#[test]
fn v2_hash_ignores_field_insertion_order() {
let receipt = make_v2_receipt();
let json = serde_json::to_string(&receipt).expect("serializes");
let parsed: FalsificationReceipt = serde_json::from_str(&json).expect("parses");
assert_eq!(parsed.content_hash, receipt.content_hash);
assert_eq!(parsed.compute_content_hash(), receipt.compute_content_hash());
}
#[test]
fn v2_hash_covers_agent_fields() {
let mut receipt = make_v2_receipt();
assert!(receipt.verify_integrity());
if let Some(agent) = receipt.agent.as_mut() {
agent.model = "tampered-model".to_string();
}
assert!(!receipt.verify_integrity(), "agent tamper must be detected");
}
#[test]
fn legacy_jsonl_parse() {
let line = r#"{"receipt_id":"0197-x","work_item_id":"PMAT-521","timestamp":"2026-04-16T19:42:51Z","git_sha":"abc","trigger":"WorkComplete","passed":17,"failed":5,"overridden":5,"allows_completion":true,"content_hash":"deadbeef"}"#;
let entry: LedgerEntry = serde_json::from_str(line).expect("legacy ledger line parses");
assert_eq!(entry.work_item_id, "PMAT-521");
assert!(entry.allows_completion);
}
proptest::proptest! {
#![proptest_config(proptest::prelude::ProptestConfig::with_cases(16))]
#[test]
fn provenance_roundtrip_prop(
model in "[a-z0-9-]{1,24}",
effort in proptest::sample::select(vec!["low", "medium", "high", "xhigh", "max"]),
harness_pick in 0usize..6,
other in "[a-z-]{1,12}",
workflow_id in proptest::option::of("[a-z0-9]{4,12}"),
parent in proptest::option::of("[a-z0-9]{4,12}"),
source_pick in 0usize..3,
note in proptest::option::of("[ -~]{0,40}"),
subagents in 0u32..1000,
) {
let harness = match harness_pick {
0 => AgentHarness::ClaudeCode,
1 => AgentHarness::ClaudeAgentSdk,
2 => AgentHarness::UltracodeWorkflow,
3 => AgentHarness::CiPipeline,
4 => AgentHarness::Human,
_ => AgentHarness::Other(other.clone()),
};
let source = match source_pick {
0 => ProvenanceSource::Declared,
1 => ProvenanceSource::Detected,
_ => ProvenanceSource::Mixed,
};
let provenance = AgentProvenance {
model: model.clone(),
effort: effort.to_string(),
harness,
workflow_id,
parent,
source,
};
let events = vec![
AgentEvent::Refusal { at: "2026-07-02T00:00:00Z".to_string(), note },
AgentEvent::WorkflowSpawn {
at: "2026-07-02T00:00:01Z".to_string(),
workflow_id: "wf-prop".to_string(),
subagents,
},
];
let pj = serde_json::to_string(&provenance).expect("provenance serializes");
let p2: AgentProvenance = serde_json::from_str(&pj).expect("provenance parses");
proptest::prop_assert_eq!(&p2, &provenance);
let ej = serde_json::to_string(&events).expect("events serialize");
let e2: Vec<AgentEvent> = serde_json::from_str(&ej).expect("events parse");
proptest::prop_assert_eq!(&e2, &events);
let report = make_report(true);
let mut receipt = FalsificationReceipt::from_report(
&report,
"sha-prop".to_string(),
"MACS-PROP".to_string(),
FalsificationTrigger::WorkComplete,
None,
None,
);
receipt.agent = Some(provenance);
receipt.agent_events = events;
receipt.content_hash = receipt.compute_content_hash();
let rj = serde_json::to_string(&receipt).expect("receipt serializes");
let r2: FalsificationReceipt = serde_json::from_str(&rj).expect("receipt parses");
proptest::prop_assert!(r2.verify_integrity());
proptest::prop_assert_eq!(r2.compute_content_hash(), receipt.content_hash);
}
}
#[test]
#[ignore = "fixture generator, not a test"]
fn generate_v2_receipt_fixtures() {
let root = env!("CARGO_MANIFEST_DIR");
let dir = std::path::Path::new(root).join("fixtures/receipts/v2");
std::fs::create_dir_all(&dir).expect("create fixtures dir");
let mut a = make_v2_receipt();
a.id = "0197f000-0000-7000-8000-00000000000a".to_string();
a.git_sha = "bab0f9af9000000000000000000000000000000a".to_string();
a.timestamp = "2026-07-02T12:00:00+00:00".to_string();
a.content_hash = a.compute_content_hash();
std::fs::write(
dir.join("receipt-golden-a.json"),
serde_json::to_string_pretty(&a).expect("serialize a"),
)
.expect("write a");
let mut b = make_v2_receipt();
b.id = "0197f000-0000-7000-8000-00000000000b".to_string();
b.git_sha = "bab0f9af9000000000000000000000000000000b".to_string();
b.timestamp = "2026-07-02T12:00:01+00:00".to_string();
b.agent = Some(AgentProvenance {
model: "claude-opus-4-8".to_string(),
effort: "xhigh".to_string(),
harness: AgentHarness::UltracodeWorkflow,
workflow_id: Some("wf_golden".to_string()),
parent: Some("session-root".to_string()),
source: ProvenanceSource::Mixed,
});
b.agent_events = vec![
AgentEvent::Refusal {
at: "2026-07-02T11:59:00Z".to_string(),
note: Some("flagged request".to_string()),
},
AgentEvent::SessionRestart { at: "2026-07-02T11:59:30Z".to_string() },
AgentEvent::WorkflowSpawn {
at: "2026-07-02T11:59:45Z".to_string(),
workflow_id: "wf_golden".to_string(),
subagents: 10,
},
];
b.content_hash = b.compute_content_hash();
std::fs::write(
dir.join("receipt-golden-b.json"),
serde_json::to_string_pretty(&b).expect("serialize b"),
)
.expect("write b");
}
#[test]
fn declared_flags_win_over_env() {
let declared = DeclaredAgent {
model: Some("claude-fable-5".to_string()),
effort: Some("high".to_string()),
harness: Some("claude-code".to_string()),
workflow_id: None,
parent: None,
};
let env = |k: &str| {
if k == "CLAUDE_CODE_EFFORT_LEVEL" {
Some("low".to_string())
} else {
None
}
};
let p = resolve_agent_provenance_with_env(&declared, &env).expect("provenance");
assert_eq!(p.effort, "high", "declared effort beats detected env");
assert_eq!(p.source, ProvenanceSource::Declared);
assert_eq!(p.harness, AgentHarness::ClaudeCode);
}
#[test]
fn env_detected_marked_advisory() {
let env = |k: &str| {
if k == "CLAUDE_CODE_EFFORT_LEVEL" {
Some("xhigh".to_string())
} else {
None
}
};
let p = resolve_agent_provenance_with_env(&DeclaredAgent::default(), &env)
.expect("detected provenance");
assert_eq!(p.effort, "xhigh");
assert_eq!(p.source, ProvenanceSource::Detected);
assert_eq!(p.harness, AgentHarness::ClaudeCode);
assert_eq!(p.model, "unknown");
let declared = DeclaredAgent {
model: Some("claude-fable-5".to_string()),
..Default::default()
};
let p = resolve_agent_provenance_with_env(&declared, &env).expect("mixed provenance");
assert_eq!(p.source, ProvenanceSource::Mixed);
assert_eq!(p.effort, "xhigh");
assert_eq!(p.model, "claude-fable-5");
}
#[test]
fn missing_provenance_yields_none_v2() {
let env = |_: &str| None;
assert!(
resolve_agent_provenance_with_env(&DeclaredAgent::default(), &env).is_none(),
"no flags + no env => no provenance"
);
let receipt = FalsificationReceipt::from_report(
&make_report(true),
"sha".to_string(),
"T-NONE".to_string(),
FalsificationTrigger::WorkComplete,
None,
None,
);
assert_eq!(receipt.schema_version, 2);
assert!(receipt.agent.is_none());
assert!(receipt.verify_integrity());
}
#[test]
fn ledger_entry_carries_agent() {
let receipt = make_v2_receipt();
let entry = LedgerEntry::from_receipt(&receipt);
let agent = entry.agent.expect("agent summary on ledger entry");
assert_eq!(agent.model, "claude-fable-5");
assert_eq!(agent.effort, "high");
assert_eq!(agent.harness, AgentHarness::ClaudeCode);
let mut bare = make_v2_receipt();
bare.agent = None;
assert!(LedgerEntry::from_receipt(&bare).agent.is_none());
}
#[test]
fn with_agent_reseals_content_hash() {
let base = FalsificationReceipt::from_report(
&make_report(true),
"sha".to_string(),
"T-SEAL".to_string(),
FalsificationTrigger::WorkComplete,
None,
None,
);
let sealed = base.clone().with_agent(Some(make_provenance()), Vec::new());
assert!(sealed.verify_integrity(), "hash must be recomputed");
assert_ne!(sealed.content_hash, base.content_hash);
assert!(sealed.agent.is_some());
}
#[test]
fn event_appends_to_active_ticket() {
let temp_dir = tempfile::TempDir::new().unwrap();
let ledger = FalsificationLedger::new(temp_dir.path());
let id = ledger
.append_event(
"MACS-EV",
AgentEvent::Refusal {
at: "2026-07-02T00:00:00Z".to_string(),
note: Some("turn ended flagged".to_string()),
},
)
.expect("append event");
assert!(id.starts_with("ev-"));
let events = ledger.load_events("MACS-EV").expect("load events");
assert_eq!(events.len(), 1);
assert_eq!(events[0].id, id);
assert!(matches!(events[0].event, AgentEvent::Refusal { .. }));
}
#[test]
fn event_refusal_blocks_complete() {
let temp_dir = tempfile::TempDir::new().unwrap();
let ledger = FalsificationLedger::new(temp_dir.path());
ledger
.append_event(
"MACS-EV",
AgentEvent::Refusal {
at: "2026-07-02T00:00:00Z".to_string(),
note: None,
},
)
.expect("append refusal");
let unacked = ledger.unacked_refusals("MACS-EV").expect("query unacked");
assert_eq!(unacked.len(), 1, "refusal must block until acknowledged");
}
#[test]
fn ack_event_unblocks_with_reason() {
let temp_dir = tempfile::TempDir::new().unwrap();
let ledger = FalsificationLedger::new(temp_dir.path());
let refusal_id = ledger
.append_event(
"MACS-EV",
AgentEvent::Refusal {
at: "2026-07-02T00:00:00Z".to_string(),
note: None,
},
)
.expect("append refusal");
ledger
.append_event(
"MACS-EV",
AgentEvent::Ack {
at: "2026-07-02T00:01:00Z".to_string(),
ack_of: refusal_id.clone(),
reason: "root cause: flagged phrasing; rephrased and continued".to_string(),
},
)
.expect("append ack");
assert!(
ledger.unacked_refusals("MACS-EV").expect("query").is_empty(),
"acked refusal must no longer block"
);
let events = ledger.load_events("MACS-EV").expect("load");
assert_eq!(events.len(), 2);
match &events[1].event {
AgentEvent::Ack { ack_of, reason, .. } => {
assert_eq!(ack_of, &refusal_id);
assert!(!reason.is_empty());
}
other => panic!("expected Ack, got {other:?}"),
}
}
#[test]
fn model_switch_recorded_from_to() {
let temp_dir = tempfile::TempDir::new().unwrap();
let ledger = FalsificationLedger::new(temp_dir.path());
ledger
.append_event(
"MACS-EV",
AgentEvent::ModelSwitch {
at: "2026-07-02T00:00:00Z".to_string(),
from: "claude-fable-5".to_string(),
to: "claude-opus-4-8".to_string(),
},
)
.expect("append switch");
let events = ledger.load_events("MACS-EV").expect("load");
match &events[0].event {
AgentEvent::ModelSwitch { from, to, .. } => {
assert_eq!(from, "claude-fable-5");
assert_eq!(to, "claude-opus-4-8");
}
other => panic!("expected ModelSwitch, got {other:?}"),
}
assert!(ledger.unacked_refusals("MACS-EV").expect("query").is_empty());
}
#[test]
fn events_append_only() {
let temp_dir = tempfile::TempDir::new().unwrap();
let ledger = FalsificationLedger::new(temp_dir.path());
for i in 0..3 {
ledger
.append_event(
"MACS-EV",
AgentEvent::SessionRestart {
at: format!("2026-07-02T00:00:0{i}Z"),
},
)
.expect("append");
let events = ledger.load_events("MACS-EV").expect("load");
assert_eq!(events.len(), i + 1, "every append adds exactly one line");
}
let events = ledger.load_events("MACS-EV").expect("load");
let ids: Vec<&str> = events.iter().map(|r| r.id.as_str()).collect();
let mut sorted = ids.clone();
sorted.sort_unstable();
assert_eq!(ids, sorted, "uuid7 record ids are time-ordered");
}
fn persist_v2(ledger: &FalsificationLedger, ticket: &str, model: &str) -> FalsificationReceipt {
let report = make_report(true);
let mut receipt = FalsificationReceipt::from_report(
&report,
"sha".to_string(),
ticket.to_string(),
FalsificationTrigger::WorkComplete,
None,
None,
);
receipt = receipt.with_agent(
Some(AgentProvenance {
model: model.to_string(),
effort: "xhigh".to_string(),
harness: AgentHarness::ClaudeCode,
workflow_id: None,
parent: None,
source: ProvenanceSource::Declared,
}),
Vec::new(),
);
ledger.persist_receipt(&receipt).unwrap();
ledger.append_to_ledger(&receipt).unwrap();
receipt
}
#[test]
fn verify_recomputes_all_hashes_v1_and_v2() {
let temp = tempfile::TempDir::new().unwrap();
let ledger = FalsificationLedger::new(temp.path());
persist_v2(&ledger, "MACS-001", "claude-fable-5");
let mut v1 = FalsificationReceipt::from_report(
&make_report(true),
"sha".to_string(),
"LEGACY-1".to_string(),
FalsificationTrigger::WorkComplete,
None,
None,
);
v1.schema_version = 1;
v1.content_hash = v1.compute_content_hash();
ledger.persist_receipt(&v1).unwrap();
let v = ledger.verify_all().unwrap();
assert_eq!(v.total_receipts, 2);
assert_eq!(v.verified, 2, "both v1 and v2 verify under their own rules");
assert!(v.ok());
}
#[test]
fn verify_red_on_tampered_entry() {
let temp = tempfile::TempDir::new().unwrap();
let ledger = FalsificationLedger::new(temp.path());
let receipt = persist_v2(&ledger, "MACS-002", "claude-fable-5");
let dir = temp.path().join(".pmat-work/MACS-002/falsification");
let file = std::fs::read_dir(&dir).unwrap().next().unwrap().unwrap().path();
let mut value: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(&file).unwrap()).unwrap();
value["agent"]["model"] = serde_json::Value::String("tampered".into());
std::fs::write(&file, serde_json::to_string_pretty(&value).unwrap()).unwrap();
let v = ledger.verify_all().unwrap();
assert!(!v.ok());
assert_eq!(v.tampered.len(), 1);
assert!(v.tampered[0].contains(&receipt.id));
}
#[test]
fn report_groups_by_model_effort_harness() {
let temp = tempfile::TempDir::new().unwrap();
let ledger = FalsificationLedger::new(temp.path());
persist_v2(&ledger, "MACS-001", "claude-fable-5");
persist_v2(&ledger, "MACS-002", "claude-fable-5");
persist_v2(&ledger, "MACS-003", "claude-opus-4-8");
let v = ledger.verify_all().unwrap();
let fable = v
.by_provenance
.iter()
.find(|(k, _)| k.contains("claude-fable-5"))
.map(|(_, n)| *n);
assert_eq!(fable, Some(2));
let opus = v
.by_provenance
.iter()
.find(|(k, _)| k.contains("claude-opus-4-8"))
.map(|(_, n)| *n);
assert_eq!(opus, Some(1));
}
#[test]
fn rule_r1_order_violation_detected() {
let temp = tempfile::TempDir::new().unwrap();
let ledger = FalsificationLedger::new(temp.path());
persist_v2(&ledger, "MACS-005", "claude-fable-5");
persist_v2(&ledger, "MACS-003", "claude-fable-5");
let v = ledger.verify_all().unwrap();
assert_eq!(v.r1_violations.len(), 1, "{:?}", v.r1_violations);
assert!(v.r1_violations[0].contains("MACS-003"));
assert!(!v.ok());
}
#[test]
fn verify_readonly() {
let temp = tempfile::TempDir::new().unwrap();
let ledger = FalsificationLedger::new(temp.path());
persist_v2(&ledger, "MACS-001", "claude-fable-5");
let before = std::fs::read_dir(temp.path().join(".pmat-work/MACS-001/falsification"))
.unwrap()
.filter_map(|e| e.ok())
.map(|e| std::fs::read(e.path()).unwrap())
.collect::<Vec<_>>();
let _ = ledger.verify_all().unwrap();
let after = std::fs::read_dir(temp.path().join(".pmat-work/MACS-001/falsification"))
.unwrap()
.filter_map(|e| e.ok())
.map(|e| std::fs::read(e.path()).unwrap())
.collect::<Vec<_>>();
assert_eq!(before, after, "verify must not mutate receipts");
}
}