300 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""GateInputBuilder 的安全输入与同 ID 混淆项测试。"""
from __future__ import annotations
import copy
import pathlib
import sys
import unittest
PROJECT_ROOT = pathlib.Path(__file__).resolve().parents[3]
SKILLS_DIR = PROJECT_ROOT / ".agent" / "skills"
SCRIPT_DIR = PROJECT_ROOT / "muse" / "lifecycle" / "quality" / "skills" / "mechanical" / "adjudicate-quality-gate" / "scripts"
TEST_DIR = PROJECT_ROOT / "tests" / "skills" / "adjudicate-quality-gate"
QUALITY_GATE_DIR = PROJECT_ROOT / "muse" / "lifecycle" / "quality" / "skills" / "judge" / "score-content-quality" / "scripts"
for _import_dir in (SCRIPT_DIR, TEST_DIR, QUALITY_GATE_DIR):
if str(_import_dir) not in sys.path:
sys.path.insert(0, str(_import_dir))
from gate_input_builder import GateInputBuildError, GateInputBuilder, canonical_sha256
from test_writer_gate import build_input, source_bundle
from writer_rubric import RUBRIC_POLICY_VERSION
class GateInputBuilderTest(unittest.TestCase):
@staticmethod
def _rehash_wrapper(wrapper):
"""只重签 wrapper,故意不替攻击者重签生产内层回执。"""
wrapper["receiptSha256"] = canonical_sha256(
{key: value for key, value in wrapper.items() if key != "receiptSha256"}
)
def test_builder_emits_v3_policy_binding_and_complete_confounders(self):
gate_input = build_input(source_bundle())
self.assertEqual(gate_input["schemaVersion"], "writer-gate-input-v3")
self.assertEqual(gate_input["rubricPolicyVersion"], RUBRIC_POLICY_VERSION)
self.assertTrue(gate_input["builderReceiptSha256"].startswith("sha256:"))
self.assertEqual(
set(gate_input["samples"][0]["confounders"]),
{
"falseNegative",
"falsePositive",
"leakage",
"reviewerInstability",
"newCharactersWithoutCards",
},
)
def test_sensitive_manifest_field_is_rejected(self):
bundle = source_bundle()
bundle["manifest"]["samples"][0]["candidateBody"] = "正文不得进入安全 manifest"
with self.assertRaisesRegex(GateInputBuildError, "敏感字段"):
GateInputBuilder().build(**bundle)
def test_same_id_and_candidate_binding_are_mandatory(self):
bundle = source_bundle()
verdict = bundle["detector_reports"]["sample-1"]["A"]["assertionVerdicts"][0]
verdict["assertionId"] = "forged-id"
report = bundle["detector_reports"]["sample-1"]["A"]
from gate_input_builder import canonical_sha256
report["reportSha256"] = canonical_sha256(
{key: value for key, value in report.items() if key != "reportSha256"}
)
gate_input = build_input(bundle)
self.assertTrue(gate_input["samples"][0]["systemFailure"])
self.assertFalse(gate_input["samples"][0]["schemaValid"])
def test_detector_unknown_is_quality_signal_and_reduces_c_coverage(self):
"""合法 needs_evidence 可进入 Gate;unknown 不计入 C 臂硬约束覆盖率。"""
bundle = source_bundle(1, gate="A")
report = bundle["detector_reports"]["sample-1"]["C"]
report["status"] = "needs_evidence"
report["hardConstraintVerdicts"][0]["verdict"] = "unknown"
report["reportSha256"] = canonical_sha256(
{key: value for key, value in report.items() if key != "reportSha256"}
)
gate_input = build_input(bundle)
sample = gate_input["samples"][0]
self.assertTrue(sample["schemaValid"], sample)
self.assertFalse(sample["systemFailure"], sample)
self.assertEqual(sample["cArm"]["hardConstraintCoverage"], 0.0)
def test_empty_wrong_role_and_unbound_inner_execution_receipts_fail_closed(self):
"""wrapper 自哈希不能替代生产 runtime 原始 ExecutionReceipt 绑定。"""
mutations = {
"empty": lambda wrapper: wrapper.update(
{"executionReceipts": [], "executionReceiptSha256": []}
),
"wrong_role": lambda wrapper: wrapper["executionReceipts"][0].update(
{"adapterRole": "semantic_detector"}
),
"unbound": lambda wrapper: wrapper["executionReceipts"][0].update(
{"inputSha256": "sha256:" + "f" * 64}
),
}
expected_reason = {
"empty": "A_writer_inner_receipts_empty",
"wrong_role": "A_writer_inner_receipt_role_mismatch",
"unbound": "A_writer_inner_receipt_hash_mismatch",
}
for name, mutate in mutations.items():
bundle = source_bundle(1, gate="A")
wrapper = bundle["execution_receipts"]["sample-1"]["A"]["writer"]
mutate(wrapper)
self._rehash_wrapper(wrapper)
gate_input = build_input(bundle)
sample = gate_input["samples"][0]
with self.subTest(name=name):
self.assertTrue(sample["systemFailure"])
self.assertIn(expected_reason[name], sample["systemFailureReasons"])
def test_writer_and_detector_retry_chains_bind_to_final_receipt(self):
"""1..3 条重试回执均保留,semantic 报告必须绑定最后一条。"""
bundle = source_bundle(1, gate="A")
for arm in ("A", "B", "C"):
for role in ("writer", "semantic_detector"):
wrapper = bundle["execution_receipts"]["sample-1"][arm][role]
retry = copy.deepcopy(wrapper["executionReceipts"][0])
retry["invocationId"] = f"{role}-{arm}-retry-2"
wrapper["executionReceipts"].append(retry)
wrapper["executionReceiptSha256"].append(canonical_sha256(retry))
self._rehash_wrapper(wrapper)
detector = bundle["detector_reports"]["sample-1"][arm]
detector["modelReceiptSha256"] = bundle["execution_receipts"]["sample-1"][arm][
"semantic_detector"
]["executionReceiptSha256"][-1]
detector["reportSha256"] = canonical_sha256(
{key: value for key, value in detector.items() if key != "reportSha256"}
)
gate_input = build_input(bundle)
self.assertFalse(gate_input["samples"][0]["systemFailure"], gate_input["samples"][0])
def test_nonfinal_known_cost_api_error_receipt_can_precede_detector_success(self):
"""前置 API 失败必须保留审计;只有末笔成功且报告绑定末笔时可进入 Gate。"""
bundle = source_bundle(1, gate="A")
wrapper = bundle["execution_receipts"]["sample-1"]["A"]["semantic_detector"]
transient = copy.deepcopy(wrapper["executionReceipts"][0])
transient.update(
{
"invocationId": "semantic-A-transient-api-error",
"actualModelId": None,
"modelMatch": False,
"totalCostUsd": "0.000000",
"terminalReason": "api_error",
"isError": True,
"apiErrorStatus": None,
"exitCode": 1,
"structuredOutputSha256": None,
}
)
wrapper["executionReceipts"].insert(0, transient)
wrapper["executionReceiptSha256"].insert(0, canonical_sha256(transient))
self._rehash_wrapper(wrapper)
gate_input = build_input(bundle)
self.assertFalse(gate_input["samples"][0]["systemFailure"], gate_input["samples"][0])
def test_final_api_error_receipt_remains_system_failure(self):
"""API 错误只能出现在最终成功前;把它放在末笔不得借重试合同放行。"""
bundle = source_bundle(1, gate="A")
wrapper = bundle["execution_receipts"]["sample-1"]["A"]["semantic_detector"]
final_error = copy.deepcopy(wrapper["executionReceipts"][0])
final_error.update(
{
"actualModelId": None,
"modelMatch": False,
"totalCostUsd": "0.000000",
"terminalReason": "api_error",
"isError": True,
"apiErrorStatus": None,
"exitCode": 1,
"structuredOutputSha256": None,
}
)
wrapper["executionReceipts"] = [final_error]
wrapper["executionReceiptSha256"] = [canonical_sha256(final_error)]
self._rehash_wrapper(wrapper)
gate_input = build_input(bundle)
sample = gate_input["samples"][0]
self.assertTrue(sample["systemFailure"])
self.assertIn(
"A_semantic_detector_inner_receipt_not_successful",
sample["systemFailureReasons"],
)
def test_four_writer_receipts_fail_closed(self):
"""有界修订最多三次,第四条回执不得借审计链绕过。"""
bundle = source_bundle(1, gate="A")
wrapper = bundle["execution_receipts"]["sample-1"]["A"]["writer"]
for invocation in range(2, 6):
retry = copy.deepcopy(wrapper["executionReceipts"][0])
retry["invocationId"] = f"writer-A-retry-{invocation}"
wrapper["executionReceipts"].append(retry)
wrapper["executionReceiptSha256"].append(canonical_sha256(retry))
self._rehash_wrapper(wrapper)
gate_input = build_input(bundle)
self.assertTrue(gate_input["samples"][0]["systemFailure"])
self.assertIn(
"A_writer_inner_receipt_count_invalid",
gate_input["samples"][0]["systemFailureReasons"],
)
def test_judge_score_tampering_without_original_hash_resign_fails_closed(self):
"""篡改稳定报告分数但保留生产原始 hash 时不得进入 Gate 分数恢复。"""
bundle = source_bundle(1, gate="A")
report = bundle["judge_reports"]["sample-1"]
original_report_hash = report["reportSha256"]
report["candidateScores"][0]["scores"]["setting_entity_fidelity"] = 999
gate_input = build_input(bundle)
sample = gate_input["samples"][0]
self.assertEqual(report["reportSha256"], original_report_hash)
self.assertTrue(sample["systemFailure"])
self.assertFalse(sample["schemaValid"])
self.assertTrue(
any("judgeReports.sample-1.reportSha256 不一致" in reason for reason in sample["systemFailureReasons"])
)
def test_judge_report_resign_cannot_replace_original_panel_receipt_binding(self):
"""攻击者重签报告自哈希后,仍不能替换生产 panel 的原始模型回执集合。"""
bundle = source_bundle(1, gate="A")
report = bundle["judge_reports"]["sample-1"]
report["modelReceiptSha256"] = "sha256:" + "e" * 64
report["reportSha256"] = canonical_sha256(
{key: value for key, value in report.items() if key != "reportSha256"}
)
gate_input = build_input(bundle)
sample = gate_input["samples"][0]
self.assertTrue(sample["systemFailure"])
self.assertFalse(sample["schemaValid"])
self.assertTrue(
any("未绑定原始 panel 回执" in reason for reason in sample["systemFailureReasons"])
)
def test_judge_report_cannot_swap_preregistered_scenario_policy(self):
bundle = source_bundle(1, gate="A")
panel = bundle["judge_reports"]["sample-1"]
panel["reviewerReports"][0]["scenario"] = "character_dialogue"
panel["reportSha256"] = canonical_sha256(
{key: value for key, value in panel.items() if key != "reportSha256"}
)
gate_input = build_input(bundle)
sample = gate_input["samples"][0]
self.assertTrue(sample["systemFailure"])
self.assertFalse(sample["schemaValid"])
self.assertTrue(
any(
"未绑定当前样本场景评分策略" in reason
for reason in sample["systemFailureReasons"]
)
)
def test_legal_score_tampering_and_report_resign_still_fails_receipt_binding(self):
"""合法 9.5 分即使重签 panel 自哈希,也必须与模型原始 structured output 冲突。"""
bundle = source_bundle(1, gate="A")
report = bundle["judge_reports"]["sample-1"]
report["candidateScores"][0]["scores"]["setting_entity_fidelity"] = 9.5
report["reportSha256"] = canonical_sha256(
{key: value for key, value in report.items() if key != "reportSha256"}
)
gate_input = build_input(bundle)
sample = gate_input["samples"][0]
self.assertTrue(sample["systemFailure"])
self.assertFalse(sample["schemaValid"])
self.assertTrue(
any("模型原始 structured output" in reason for reason in sample["systemFailureReasons"])
)
if __name__ == "__main__":
unittest.main()