zizi 091b66a9bb 重构: 收敛 Agent/Skill 运行时与创作质量闭环
将角色与 Skill 从 .claude 迁入 .agent,移除 Claude CLI 运行时并接入固定 Opus 角色 profile、完整 schema、预算 deadline、raw 与回执证据链。

同步拆分 Skill 职责、复利 lesson、Gate 回放、Dashboard 人审入口、数据库登记和机械门禁;候选设计正文不包含在本提交中。
2026-08-22 02:12:32 +08:00

82 lines
3.7 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
"""build_semantic_input_v3 生产投影的离线测试。
验证:WriterContext + 候选能被投影成通过闭集校验的 semantic-detector-input-v3;
sourceRef 多余字段被清洗;身份字段严格绑定;哈希自洽。
跑法:.venv/bin/python tests/skills/check-content-consistency/test_build_semantic_input.py
"""
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"
TEST_DIR = PROJECT_ROOT / "tests" / "skills" / "check-content-consistency"
SCRIPT_DIR = SKILLS_DIR / "check-content-consistency" / "scripts"
CONTINUATION_DIR = SKILLS_DIR / "write-next-chapter" / "scripts"
READ_CONTEXT_DIR = SKILLS_DIR / "assemble-context" / "scripts"
for path in (TEST_DIR, SCRIPT_DIR, CONTINUATION_DIR, READ_CONTEXT_DIR):
if str(path) not in sys.path:
sys.path.insert(0, str(path))
from run_writer_semantic_detector import ( # noqa: E402
build_semantic_input_v3, validate_semantic_detector_input,
)
from test_check_writer_candidate import _valid_pair # noqa: E402
class BuildSemanticInputV3Test(unittest.TestCase):
def test_projects_valid_input_that_passes_closed_validation(self) -> None:
context, candidate = _valid_pair()
payload = build_semantic_input_v3(
run_id=context["runId"], sample_id="writer-ch2", opaque_arm_id="production",
writer_context=context, candidate=candidate)
# 返回值本身必须能再过一遍闭集校验(幂等自洽)
validated = validate_semantic_detector_input(payload)
self.assertEqual(validated["runId"], context["runId"])
self.assertEqual(validated["candidateSha256"], candidate["candidateSha256"])
self.assertEqual(validated["candidateVersion"], candidate["candidateVersion"])
self.assertEqual(
validated["contextSnapshotSha256"], context["contextSnapshot"]["contextSha256"])
self.assertEqual(validated["asOf"], context["asOf"])
self.assertNotIn("_expectedAssertionIds", payload)
def test_cleans_source_ref_extra_fields(self) -> None:
context, candidate = _valid_pair()
# _bound_context 的 factEvidence.sourceRef 带 sourceType 多余字段,必须被清洗掉
payload = build_semantic_input_v3(
run_id=context["runId"], sample_id="writer-ch2", opaque_arm_id="production",
writer_context=context, candidate=candidate)
for item in payload["factEvidence"]:
self.assertNotIn("sourceType", item["sourceRef"])
# 原上下文不被改动
self.assertIn("sourceType", context["factEvidence"][0]["sourceRef"])
def test_outline_constraints_get_stable_ids(self) -> None:
context, candidate = _valid_pair()
payload = build_semantic_input_v3(
run_id=context["runId"], sample_id="writer-ch2", opaque_arm_id="production",
writer_context=context, candidate=candidate)
ids = [item["constraintId"] for item in payload["hardConstraints"]]
self.assertEqual(ids, [f"constraint-{i + 1}" for i in range(len(ids))])
self.assertEqual(
[item["constraintId"] for item in payload["fineOutline"]["hardConstraints"]], ids)
def test_candidate_body_hash_mismatch_fails_closed(self) -> None:
context, candidate = _valid_pair()
bad = copy.deepcopy(candidate)
bad["candidateSha256"] = "sha256:" + "f" * 64
with self.assertRaises(Exception):
build_semantic_input_v3(
run_id=context["runId"], sample_id="writer-ch2", opaque_arm_id="production",
writer_context=context, candidate=bad)
if __name__ == "__main__":
unittest.main()