muse-agent-example/tests/skills/revise-ai-flavor/test_revise_ai_flavor.py

191 lines
9.3 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
"""技能 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" / "revise-ai-flavor" / "scripts"
DIAGNOSE_SCRIPT_DIR = PROJECT_ROOT / "muse" / "lifecycle" / "quality" / "humanization" / "skills" / "diagnose-ai-flavor" / "scripts"
sys.path.insert(0, str(SCRIPT_DIR))
sys.path.insert(0, str(DIAGNOSE_SCRIPT_DIR))
import revise_ai_flavor as rev # noqa: E402
import diagnose_ai_flavor as diag # 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()