将角色与 Skill 从 .claude 迁入 .agent,移除 Claude CLI 运行时并接入固定 Opus 角色 profile、完整 schema、预算 deadline、raw 与回执证据链。 同步拆分 Skill 职责、复利 lesson、Gate 回放、Dashboard 人审入口、数据库登记和机械门禁;候选设计正文不包含在本提交中。
106 lines
5.0 KiB
Python
106 lines
5.0 KiB
Python
#!/usr/bin/env python3
|
||
"""技能 3 诊断离线测试:检测只查不改、产物头完整、检测完成即落库(--offline 除外)。"""
|
||
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 / ".agent" / "skills" / "diagnose-ai-flavor" / "scripts"
|
||
sys.path.insert(0, str(SCRIPT_DIR))
|
||
|
||
import diagnose_ai_flavor as diag # noqa: E402
|
||
|
||
# 合成 AI 味文本:命中 l001/l002/l003(语料只用合成文本,版权边界见 humanization/README)
|
||
AI_FLAVOR_TEXT = (
|
||
"研究表明,能进这种地方的修士都不简单。"
|
||
"值得注意的是,门外已经下起了雨。"
|
||
"他嘴角微微上扬,没有说话。"
|
||
)
|
||
|
||
|
||
class DiagnosisContractTest(unittest.TestCase):
|
||
def test_run_diagnosis_produces_complete_artifact(self):
|
||
artifact = diag.run_diagnosis(AI_FLAVOR_TEXT, work_ref="synthetic:demo")
|
||
for key in ("text_hash", "rule_library_version", "mode", "findings"):
|
||
self.assertIn(key, artifact)
|
||
rule_ids = {f["rule_id"] for f in artifact["findings"]}
|
||
self.assertTrue({"l001", "l002", "l003"} <= rule_ids, rule_ids)
|
||
for f in artifact["findings"]:
|
||
self.assertIn(f["decision_proposal"], {"repair", "ask"})
|
||
self.assertTrue(f["spans"][0] in AI_FLAVOR_TEXT)
|
||
|
||
def test_work_ref_is_required(self):
|
||
with self.assertRaisesRegex(diag.DiagnoseContractError, "work_ref"):
|
||
diag.run_diagnosis(AI_FLAVOR_TEXT, work_ref="")
|
||
|
||
def test_empty_text_is_rejected(self):
|
||
with self.assertRaisesRegex(diag.DiagnoseContractError, "为空"):
|
||
diag.run_diagnosis("", work_ref="synthetic:demo")
|
||
|
||
def test_rule_library_version_is_stable_fingerprint(self):
|
||
rules1, v1 = diag.load_active_library()
|
||
_, v2 = diag.load_active_library()
|
||
self.assertEqual(v1, v2)
|
||
self.assertTrue(v1.startswith("v-"))
|
||
|
||
def test_cli_offline_writes_artifact_without_db(self):
|
||
with tempfile.TemporaryDirectory() as tmp:
|
||
text_path = pathlib.Path(tmp) / "text.txt"
|
||
text_path.write_text(AI_FLAVOR_TEXT, encoding="utf-8")
|
||
output = pathlib.Path(tmp) / "artifact.json"
|
||
with patch.object(diag, "persist_diagnosis") as persist:
|
||
code = diag.main(["run", "--text-file", str(text_path), "--work-ref", "synthetic:demo",
|
||
"--output", str(output), "--offline"])
|
||
self.assertEqual(code, 0)
|
||
persist.assert_not_called()
|
||
artifact = json.loads(output.read_text(encoding="utf-8"))
|
||
self.assertIn("findings", artifact)
|
||
|
||
def test_persist_rejects_text_hash_mismatch_before_db(self):
|
||
artifact = diag.run_diagnosis(AI_FLAVOR_TEXT, work_ref="synthetic:demo")
|
||
artifact["text_hash"] = "sha256:deadbeef"
|
||
with self.assertRaises(diag.DiagnoseContractError):
|
||
diag.persist_diagnosis(artifact, text=AI_FLAVOR_TEXT)
|
||
|
||
def test_cli_default_persists_detection(self):
|
||
# 默认(非 --offline)模式生产读数据库规则库;离线测试补丁文件库接缝,不触真实连接。
|
||
from deai import load
|
||
|
||
def _file_library(from_db=False):
|
||
samples = load.load_samples()
|
||
rules = load.load_rules(samples=samples)
|
||
return rules, diag.rule_library_version(rules)
|
||
|
||
with tempfile.TemporaryDirectory() as tmp:
|
||
text_path = pathlib.Path(tmp) / "text.txt"
|
||
text_path.write_text(AI_FLAVOR_TEXT, encoding="utf-8")
|
||
output = pathlib.Path(tmp) / "artifact.json"
|
||
with patch.object(diag, "persist_diagnosis", return_value={"run_id": "diag-x"}) as persist, \
|
||
patch.object(diag, "load_active_library", side_effect=_file_library):
|
||
code = diag.main(["run", "--text-file", str(text_path), "--work-ref", "synthetic:demo",
|
||
"--output", str(output)])
|
||
self.assertEqual(code, 0)
|
||
persist.assert_called_once()
|
||
|
||
def test_cli_external_findings_hash_mismatch_rejected(self):
|
||
with tempfile.TemporaryDirectory() as tmp:
|
||
text_path = pathlib.Path(tmp) / "text.txt"
|
||
text_path.write_text(AI_FLAVOR_TEXT, encoding="utf-8")
|
||
findings_path = pathlib.Path(tmp) / "external.json"
|
||
findings_path.write_text(json.dumps([{
|
||
"id": "x1", "text_hash": "sha256:deadbeef", "rule_id": "sem001", "rule_version": 1,
|
||
"spans": ["他"], "context_window": "他", "layer": "semantic", "evidence": "外部判定证据",
|
||
"possible_function": "none", "confidence": "medium", "decision_proposal": "pending",
|
||
}]), encoding="utf-8")
|
||
code = diag.main(["run", "--text-file", str(text_path), "--work-ref", "synthetic:demo",
|
||
"--external-findings", str(findings_path),
|
||
"--output", str(pathlib.Path(tmp) / "a.json"), "--offline"])
|
||
self.assertEqual(code, 2)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
unittest.main()
|