mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-09-11 14:38:38 +00:00
* 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.
165 lines
7.2 KiB
Python
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
|