191 lines
9.3 KiB
Python
191 lines
9.3 KiB
Python
#!/usr/bin/env python3
|
||
"""技能 4 修订离线测试:授权、仲裁、快照降级、硬门、复扫、盲评与落库合同。"""
|
||
import json
|
||
import pathlib
|
||
import sys
|
||
import tempfile
|
||
import unittest
|
||
from unittest.mock import patch
|
||
|
||
PROJECT_ROOT = pathlib.Path(__file__).resolve().parents[3]
|
||
SCRIPT_DIR = PROJECT_ROOT / "muse" / "lifecycle" / "quality" / "humanization" / "skills" / "修正正文机器味" / "scripts"
|
||
DIAGNOSE_SCRIPT_DIR = PROJECT_ROOT / "muse" / "lifecycle" / "quality" / "humanization" / "skills" / "诊断机器味" / "scripts"
|
||
sys.path.insert(0, str(SCRIPT_DIR))
|
||
sys.path.insert(0, str(DIAGNOSE_SCRIPT_DIR))
|
||
|
||
import diagnose_ai_flavor as diag # noqa: E402
|
||
import revise_ai_flavor as rev # noqa: E402
|
||
from deai.pipeline import DowngradedToAudit # noqa: E402
|
||
|
||
CLEAN_TEXT = "值得注意的是,门外已经下起了雨。"
|
||
NUMBER_TEXT = "值得注意的是,他付了三百两。"
|
||
TASK = {"mode": "Patch", "allowed_scope": "全文", "author_approved_revision": True}
|
||
PAIRWISE = {"choice": "candidate", "rationale": "候选保真且删除了无功能套语", "selection_model": "gpt-5.6-sol"}
|
||
|
||
|
||
def _artifact(text: str) -> dict:
|
||
artifact = diag.run_diagnosis(text, work_ref="synthetic:demo")
|
||
for finding in artifact["findings"]:
|
||
finding["decision_proposal"] = "repair"
|
||
finding["arbitration_note"] = "测试仲裁:确认该命中无当前功能"
|
||
return artifact
|
||
|
||
|
||
def _delete_patch(artifact: dict, span: str, exact: str) -> dict:
|
||
finding = next(f for f in artifact["findings"] if span in f["spans"])
|
||
return {
|
||
"finding_id": finding["id"], "action": "delete", "original_exact": exact,
|
||
"replacement": "", "rationale": "空元话语,删除后信息不变",
|
||
"protected_invariants": [],
|
||
}
|
||
|
||
|
||
class RevisionContractTest(unittest.TestCase):
|
||
def test_full_patch_chain_passes_gates(self):
|
||
artifact = _artifact(CLEAN_TEXT)
|
||
patches = [_delete_patch(artifact, "值得注意的是", "值得注意的是,")]
|
||
record, audit = rev.run_revision(
|
||
CLEAN_TEXT, artifact=artifact, patches=patches,
|
||
task_contract=TASK, fact_snapshot={"entities": []}, pairwise=PAIRWISE,
|
||
)
|
||
self.assertTrue(record["hard_gate"]["pass"], record["hard_gate"])
|
||
self.assertTrue(record["regression_gate"]["pass"], record["regression_gate"])
|
||
self.assertEqual(record["candidate_text"], "门外已经下起了雨。")
|
||
self.assertEqual(record["pairwise_choice"]["choice"], "candidate")
|
||
self.assertIn("summary", audit)
|
||
|
||
def test_no_artifact_refuses_to_start(self):
|
||
with self.assertRaisesRegex(rev.ReviseContractError, "没有诊断产物"):
|
||
rev.run_revision(CLEAN_TEXT, artifact={}, patches=[{"x": 1}], task_contract=TASK)
|
||
|
||
def test_no_patches_refuses_to_start(self):
|
||
artifact = _artifact(CLEAN_TEXT)
|
||
with self.assertRaisesRegex(rev.ReviseContractError, "无事可做"):
|
||
rev.run_revision(CLEAN_TEXT, artifact=artifact, patches=[], task_contract=TASK)
|
||
|
||
def test_missing_snapshot_downgrades_to_audit(self):
|
||
artifact = _artifact(CLEAN_TEXT)
|
||
patches = [_delete_patch(artifact, "值得注意的是", "值得注意的是,")]
|
||
with self.assertRaises(DowngradedToAudit):
|
||
rev.run_revision(
|
||
CLEAN_TEXT, artifact=artifact, patches=patches,
|
||
task_contract=TASK, fact_snapshot=None, pairwise=PAIRWISE,
|
||
)
|
||
|
||
def test_patch_outside_author_scope_is_rejected(self):
|
||
artifact = _artifact(CLEAN_TEXT)
|
||
patches = [_delete_patch(artifact, "值得注意的是", "值得注意的是,")]
|
||
with self.assertRaisesRegex(ValueError, "allowed_scope"):
|
||
rev.run_revision(
|
||
CLEAN_TEXT, artifact=artifact, patches=patches,
|
||
task_contract={**TASK, "allowed_scope": ["f-other"]},
|
||
fact_snapshot={"entities": []}, pairwise=PAIRWISE,
|
||
)
|
||
|
||
def test_forged_deterministic_finding_is_rejected(self):
|
||
artifact = _artifact(CLEAN_TEXT)
|
||
patches = [_delete_patch(artifact, "值得注意的是", "值得注意的是,")]
|
||
artifact["findings"][0]["rule_id"] = "l999"
|
||
with self.assertRaisesRegex(ValueError, "active 规则"):
|
||
rev.run_revision(
|
||
CLEAN_TEXT, artifact=artifact, patches=patches,
|
||
task_contract=TASK, fact_snapshot={"entities": []}, pairwise=PAIRWISE,
|
||
)
|
||
|
||
def test_unarbitrated_finding_is_rejected(self):
|
||
artifact = diag.run_diagnosis(CLEAN_TEXT, work_ref="synthetic:demo")
|
||
patches = [_delete_patch(artifact, "值得注意的是", "值得注意的是,")]
|
||
with self.assertRaisesRegex(ValueError, "未经过 repair 仲裁"):
|
||
rev.run_revision(
|
||
CLEAN_TEXT, artifact=artifact, patches=patches,
|
||
task_contract=TASK, fact_snapshot={"entities": []}, pairwise=PAIRWISE,
|
||
)
|
||
|
||
def test_fact_delta_blocks_new_number(self):
|
||
artifact = _artifact(NUMBER_TEXT)
|
||
finding = next(f for f in artifact["findings"] if "值得注意的是" in f["spans"])
|
||
patches = [{
|
||
"finding_id": finding["id"], "action": "local_rewrite",
|
||
"original_exact": "值得注意的是,他付了三百两",
|
||
"replacement": "他付了三百五十两",
|
||
"rationale": "故意引入新数字,验证硬门拒收",
|
||
"protected_invariants": [],
|
||
}]
|
||
record, _ = rev.run_revision(
|
||
NUMBER_TEXT, artifact=artifact, patches=patches,
|
||
task_contract=TASK, fact_snapshot={"entities": []}, pairwise=PAIRWISE,
|
||
)
|
||
self.assertFalse(record["hard_gate"]["pass"])
|
||
self.assertTrue(record["hard_gate"]["checks"]["fact_delta"]["failures"])
|
||
|
||
def _run_cli(self, *, offline: bool, persist_return=None):
|
||
# 默认(非 --offline)模式生产读数据库规则库;离线测试补丁文件库接缝,不触真实连接。
|
||
from deai import load
|
||
|
||
def _file_library(from_db):
|
||
samples = load.load_samples()
|
||
rules = load.load_rules(samples=samples)
|
||
return samples, rules, load.rule_library_version(rules)
|
||
|
||
artifact = _artifact(CLEAN_TEXT)
|
||
patches = [_delete_patch(artifact, "值得注意的是", "值得注意的是,")]
|
||
with tempfile.TemporaryDirectory() as tmp:
|
||
root = pathlib.Path(tmp)
|
||
text_path = root / "text.txt"
|
||
text_path.write_text(CLEAN_TEXT, encoding="utf-8")
|
||
artifact_path = root / "artifact.json"
|
||
artifact_path.write_text(json.dumps(artifact, ensure_ascii=False), encoding="utf-8")
|
||
patches_path = root / "patches.json"
|
||
patches_path.write_text(json.dumps(patches, ensure_ascii=False), encoding="utf-8")
|
||
contract_path = root / "contract.json"
|
||
contract_path.write_text(json.dumps(TASK), encoding="utf-8")
|
||
snapshot_path = root / "snapshot.json"
|
||
snapshot_path.write_text(json.dumps({"entities": []}), encoding="utf-8")
|
||
pairwise_path = root / "pairwise.json"
|
||
pairwise_path.write_text(json.dumps(PAIRWISE, ensure_ascii=False), encoding="utf-8")
|
||
output = root / "report.json"
|
||
with patch.object(rev, "persist_revision", return_value=persist_return) as persist, \
|
||
patch.object(rev, "load_current_baseline", return_value=None), \
|
||
patch.object(rev, "load_revision_library", side_effect=_file_library):
|
||
argv = [
|
||
"--text-file", str(text_path), "--artifact", str(artifact_path),
|
||
"--patches", str(patches_path), "--task-contract", str(contract_path),
|
||
"--snapshot", str(snapshot_path), "--pairwise", str(pairwise_path),
|
||
"--work-ref", "synthetic:demo", "--output", str(output),
|
||
]
|
||
if offline:
|
||
argv.append("--offline")
|
||
code = rev.main(argv)
|
||
return code, persist.call_count, json.loads(output.read_text(encoding="utf-8"))
|
||
|
||
def test_cli_requires_artifact_file(self):
|
||
with tempfile.TemporaryDirectory() as tmp:
|
||
root = pathlib.Path(tmp)
|
||
text_path = root / "text.txt"
|
||
text_path.write_text(CLEAN_TEXT, encoding="utf-8")
|
||
patches_path = root / "patches.json"
|
||
patches_path.write_text("[]", encoding="utf-8")
|
||
contract_path = root / "contract.json"
|
||
contract_path.write_text(json.dumps(TASK), encoding="utf-8")
|
||
code = rev.main([
|
||
"--text-file", str(text_path), "--artifact", str(root / "missing.json"),
|
||
"--patches", str(patches_path), "--task-contract", str(contract_path),
|
||
"--work-ref", "synthetic:demo", "--output", str(root / "r.json"), "--offline",
|
||
])
|
||
self.assertEqual(code, 2)
|
||
|
||
def test_cli_offline_patch_chain_writes_report_without_db(self):
|
||
code, calls, report = self._run_cli(offline=True)
|
||
self.assertEqual(code, 0)
|
||
self.assertEqual(calls, 0)
|
||
self.assertEqual(report["final"]["candidate_text"], "门外已经下起了雨。")
|
||
|
||
def test_cli_default_persists_revision(self):
|
||
code, calls, _ = self._run_cli(offline=False, persist_return={"run_id": "rev-x"})
|
||
self.assertEqual(code, 0)
|
||
self.assertEqual(calls, 1)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
unittest.main()
|