deer-flow/backend/tests/test_receipt_verification.py
Zeren Wang 3b592c2053
feat(harness): subagent receipt citation verification (#5076)
* feat(harness): subagent receipt citation verification

- add receipt citation verification core
- harvest subagent tool receipts at terminal status
- transport subagent receipts and citation verdict via status contract
- verify subagent report citations at task write-back
- render citation verdicts in the delegation ledger

* fix(gateway): strip forged receipt verdicts from the delegations channel

normalize_input() and the checkpoint-state mutation sanitizer only
stripped server-owned metadata from message-shaped values, so an
external caller could submit a delegation entry carrying a forged
receipt_verdict that render_delegation_ledger would present as
runtime-owned execution evidence. Strip receipt_verdict from
caller-supplied delegation entries on both the run-creation and
thread-state mutation paths, with regression coverage for each.

* fix(harness): close silent-pass gaps in the zero-citation heuristic

The action-claim detector missed the most common completion verbs
(fixed/added/tested/changed/...) and had no CJK coverage at all, so
reports like 'I fixed the bug and added tests.' or '我已经创建了文件并运行了测试。'
were treated as claim-free: citation_resolved=True with no ledger
warning. Broaden the verb lists and add a language-independent safety
net: when the run harvested receipts and a nontrivial (>=240 char)
report cites none of them, flag it UNVERIFIED. Short claim-free
confirmations remain a vacuous pass.
2026-08-29 16:56:56 +08:00

165 lines
7.2 KiB
Python

"""Tests for parent-side receipt citation verification (RFC #4651 PR2)."""
from deerflow.agents.middlewares.receipt_verification import (
render_citation_verdict,
validate_receipt_verdict,
verify_receipt_citations,
)
from deerflow.agents.middlewares.tool_receipt import parse_citations
def _receipt(rid: str, tool: str = "write_file", status: str = "success") -> dict:
return {
"id": rid,
"tool_call_id": f"tc-{rid}",
"tool_name": tool,
"status": status,
"args_sha256": "a" * 16,
"output_sha256": "b" * 16,
"output_bytes": 10,
"created_at": "2026-08-24T00:00:00+00:00",
}
LEDGER = [_receipt("r1", "web_search"), _receipt("r2", "write_file"), _receipt("r3", "bash", status="error")]
class TestCitationExtraction:
def test_bare_and_anchored_citations(self):
verdict = verify_receipt_citations("wrote file [r2 write_file], searched [r1]", LEDGER)
assert verdict["cited"] == ["r2", "r1"]
assert verdict["resolved"] == ["r2", "r1"]
assert verdict["citation_resolved"] is True
def test_duplicate_citations_deduped_first_seen(self):
verdict = verify_receipt_citations("[r1] then [r2] then [r1] again", LEDGER)
assert verdict["cited"] == ["r1", "r2"]
def test_same_receipt_with_different_anchors_checks_every_pair(self):
verdict = verify_receipt_citations("[r2 write_file] then [r2 bash]", LEDGER)
assert verdict["cited"] == ["r2", "r2"]
assert verdict["resolved"] == ["r2"]
assert verdict["failed"] == [
{
"id": "r2",
"reason": "anchor mismatch: cited as bash, receipt r2 is write_file",
}
]
assert verdict["citation_resolved"] is False
def test_non_citation_brackets_ignored(self):
verdict = verify_receipt_citations("see [1] and [rX] and [note]", LEDGER)
assert verdict["cited"] == []
def test_parse_accepts_dotted_tool_names(self):
assert parse_citations("[r4 mcp.server-tool]") == [("r4", "mcp.server-tool")]
class TestClassification:
def test_error_status_receipt_is_failed(self):
verdict = verify_receipt_citations("tests passed [r3]", LEDGER)
assert verdict["resolved"] == []
assert verdict["failed"] == [{"id": "r3", "reason": "receipt status=error"}]
assert verdict["citation_resolved"] is False
def test_anchor_mismatch_is_failed(self):
verdict = verify_receipt_citations("saved the report [r1 write_file]", LEDGER)
assert verdict["failed"] == [{"id": "r1", "reason": "anchor mismatch: cited as write_file, receipt r1 is web_search"}]
assert verdict["citation_resolved"] is False
def test_anchor_match_resolves(self):
verdict = verify_receipt_citations("saved the report [r2 write_file]", LEDGER)
assert verdict["resolved"] == ["r2"]
assert verdict["citation_resolved"] is True
def test_unknown_id(self):
verdict = verify_receipt_citations("uploaded results [r9]", LEDGER)
assert verdict["unknown"] == ["r9"]
assert verdict["citation_resolved"] is False
class TestZeroCitationHeuristic:
def test_action_verb_without_citation_flagged(self):
verdict = verify_receipt_citations("I wrote the analysis and ran the tests.", LEDGER)
assert verdict["cited"] == []
assert verdict["no_citation_claims"] is True
assert verdict["citation_resolved"] is False
def test_file_path_without_citation_flagged(self):
verdict = verify_receipt_citations("Done, see /outputs/report.md for details", LEDGER)
assert verdict["no_citation_claims"] is True
def test_benign_report_is_vacuous_pass(self):
verdict = verify_receipt_citations("The answer is 42.", LEDGER)
assert verdict["no_citation_claims"] is False
assert verdict["citation_resolved"] is True
def test_common_completion_verbs_flagged(self):
"""The original list missed the most common completion phrasings."""
for text in ["I fixed the bug and added tests.", "I tested the change successfully."]:
verdict = verify_receipt_citations(text, LEDGER)
assert verdict["no_citation_claims"] is True, text
assert verdict["citation_resolved"] is False, text
def test_chinese_action_claims_flagged(self):
"""CJK reports have no word boundaries; common action verbs must match."""
verdict = verify_receipt_citations("我已经创建了文件并运行了测试。", LEDGER)
assert verdict["no_citation_claims"] is True
assert verdict["citation_resolved"] is False
def test_nontrivial_uncited_report_with_receipts_flagged(self):
"""Safety net in any language: the run demonstrably executed tools
(receipts harvested), yet a paragraph-length report cites none of them."""
report = "The analysis is complete. " * 12 # 300 chars, no verbs, no paths
verdict = verify_receipt_citations(report, LEDGER)
assert verdict["cited"] == []
assert verdict["no_citation_claims"] is True
def test_short_claim_free_report_with_receipts_stays_vacuous_pass(self):
verdict = verify_receipt_citations("The answer is 42.", LEDGER)
assert verdict["no_citation_claims"] is False
assert verdict["citation_resolved"] is True
def test_claim_free_report_without_receipts_stays_vacuous_pass(self):
"""No tools were stamped: nothing exists to cite against."""
report = "The analysis is complete. " * 12
verdict = verify_receipt_citations(report, [])
assert verdict["no_citation_claims"] is False
assert verdict["citation_resolved"] is True
def test_verdict_shape_vocabulary(self):
verdict = verify_receipt_citations("x [r1]", LEDGER)
assert verdict["source"] == "receipt_citations"
assert verdict["requirement"] == "cited_ids_in_execution_record"
assert "satisfied" not in verdict
class TestRender:
def test_render_counts_with_limitation_line(self):
verdict = verify_receipt_citations("[r1] [r3] [r9]", LEDGER)
rendered = render_citation_verdict(verdict)
assert rendered == ("citations: 1 resolved, 1 failed, 1 unknown — execution evidence only, does not validate claim correctness")
def test_render_unverified_for_no_citation_claims(self):
verdict = verify_receipt_citations("I wrote the file.", LEDGER)
assert render_citation_verdict(verdict) == ("citations: UNVERIFIED — action claims without receipt citations")
def test_render_empty_for_vacuous_pass(self):
verdict = verify_receipt_citations("The answer is 42.", LEDGER)
assert render_citation_verdict(verdict) == ""
class TestValidate:
def test_round_trip(self):
verdict = verify_receipt_citations("[r1] [r9]", LEDGER)
assert validate_receipt_verdict(dict(verdict)) == verdict
def test_rejects_malformed(self):
assert validate_receipt_verdict(None) is None
assert validate_receipt_verdict({"source": 1}) is None
assert validate_receipt_verdict({"citation_resolved": "yes"}) is None
bad = dict(verify_receipt_citations("[r3]", LEDGER))
bad["failed"] = [{"id": "r3"}] # missing reason
assert validate_receipt_verdict(bad) is None