#!/usr/bin/env python3 """CandidateEnvelope v2 机械硬门的离线测试。""" from __future__ import annotations import copy import hashlib import pathlib import sys import unittest SCRIPT_DIR = pathlib.Path(__file__).resolve().parent SKILLS_DIR = SCRIPT_DIR.parents[1] CONTINUATION_DIR = SKILLS_DIR / "write-next-chapter" / "scripts" READ_CONTEXT_DIR = SKILLS_DIR / "assemble-context" / "scripts" for path in (SCRIPT_DIR, CONTINUATION_DIR, READ_CONTEXT_DIR): sys.path.insert(0, str(path)) from test_run_writer import _bound_context # noqa: E402 from check_writer_candidate import check_writer_candidate # noqa: E402 from writer_contract import build_candidate_envelope, han_count # noqa: E402 def _candidate_body() -> str: prefix = "林澈完成围攻突围,又把旧徽章压回掌心。" hook = "城门忽然打开" return prefix + "文" * (4000 - han_count(prefix) - han_count(hook)) + hook def _requirements() -> dict: return { "requiredEvents": [{"requirementId": "event-1", "anchors": ["完成围攻突围"]}], "requiredCharacters": ["林澈"], "foreshadowingActions": [{"requirementId": "foreshadow-1", "anchors": ["旧徽章"]}], "chapterEndHook": {"requirementId": "hook-1", "anchors": ["城门忽然打开"], "maxDistanceFromEnd": 20}, } def _rehash(output: dict) -> None: output["candidateSha256"] = "sha256:" + hashlib.sha256(output["candidateBody"].encode("utf-8")).hexdigest() def _valid_pair() -> tuple[dict, dict]: context = _bound_context() return context, build_candidate_envelope(context, {"candidateBody": _candidate_body()}) class CheckWriterCandidateTest(unittest.TestCase): def test_missing_outline_anchors_block(self) -> None: context, output = _valid_pair() mutations = { "HARD_EVENT_MISSING": ("完成围攻突围", "完成撤离"), "REQUIRED_CHARACTER_MISSING": ("林澈", "无名人"), "FORESHADOWING_ACTION_MISSING": ("旧徽章", "旧物件"), "CHAPTER_END_HOOK_MISSING": ("城门忽然打开", "风声渐渐平息"), } for expected_code, (old, new) in mutations.items(): candidate = copy.deepcopy(output) candidate["candidateBody"] = candidate["candidateBody"].replace(old, new) _rehash(candidate) report = check_writer_candidate(context, candidate, _requirements()) with self.subTest(expected_code=expected_code): self.assertFalse(report["passed"]) self.assertIn(expected_code, {item["code"] for item in report["blockingFailures"]}) def test_hash_context_and_length_are_mechanical(self) -> None: context, output = _valid_pair() bad_hash = copy.deepcopy(output) bad_hash["candidateSha256"] = "sha256:" + "f" * 64 self.assertIn("CANDIDATE_HASH_MISMATCH", {item["code"] for item in check_writer_candidate(context, bad_hash, _requirements())["blockingFailures"]}) bad_context = copy.deepcopy(output) bad_context["contextSnapshotSha256"] = "sha256:" + "e" * 64 self.assertIn("CONTEXT_BINDING_MISMATCH", {item["code"] for item in check_writer_candidate(context, bad_context, _requirements())["blockingFailures"]}) short = copy.deepcopy(output) short["candidateBody"] = "文" * 1000 _rehash(short) self.assertIn("CANDIDATE_LENGTH_OUT_OF_RANGE", {item["code"] for item in check_writer_candidate(context, short, _requirements())["blockingFailures"]}) def test_writer_owned_semantic_fields_are_rejected_not_consumed(self) -> None: context, output = _valid_pair() for field, value in ( ("claimLedger", []), ("evidenceRequests", []), ("newSettingDeclarations", []), ): candidate = copy.deepcopy(output) candidate[field] = value report = check_writer_candidate(context, candidate, _requirements()) with self.subTest(field=field): self.assertFalse(report["passed"]) self.assertIn("OUTPUT_CONTRACT_INVALID", {item["code"] for item in report["blockingFailures"]}) def test_valid_candidate_passes_and_requests_independent_semantic_detection(self) -> None: context, output = _valid_pair() report = check_writer_candidate(context, output, _requirements()) self.assertTrue(report["passed"]) self.assertEqual(report["candidateSha256"], output["candidateSha256"]) self.assertEqual(report["semanticReview"]["status"], "pending") self.assertEqual(report["semanticReview"]["inputContract"], "WriterContext v1 + CandidateEnvelope v2 -> SemanticDetection v3") if __name__ == "__main__": unittest.main()