200 lines
7.7 KiB
Python
200 lines
7.7 KiB
Python
#!/usr/bin/env python3
|
|
"""正文候选机械硬门的细纲、证据和 Unicode 绑定测试。"""
|
|
|
|
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 / "continuation" / "scripts"
|
|
READ_CONTEXT_DIR = SKILLS_DIR / "read-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, _bound_output # noqa: E402
|
|
from check_writer_candidate import check_writer_candidate # noqa: E402
|
|
from writer_contract import han_count # noqa: E402
|
|
|
|
|
|
def _candidate_body() -> str:
|
|
"""构造同时满足四类细纲机械锚点和动态篇幅的候选正文。"""
|
|
|
|
prefix = "林澈完成围攻突围,又把旧徽章压回掌心。"
|
|
hook = "城门忽然打开"
|
|
filler_count = 4000 - han_count(prefix) - han_count(hook)
|
|
return prefix + "文" * filler_count + hook
|
|
|
|
|
|
def _requirements() -> dict:
|
|
"""构造可信上下文层派生的机械硬约束。"""
|
|
|
|
return {
|
|
"requiredEvents": [
|
|
{"requirementId": "event-1", "anchors": ["完成围攻突围"]},
|
|
],
|
|
"requiredCharacters": ["林澈"],
|
|
"foreshadowingActions": [
|
|
{"requirementId": "foreshadow-1", "anchors": ["旧徽章"]},
|
|
],
|
|
"chapterEndHook": {
|
|
"requirementId": "hook-1",
|
|
"anchors": ["城门忽然打开"],
|
|
"maxDistanceFromEnd": 20,
|
|
},
|
|
"detectedNewSettings": [],
|
|
"frozenConflicts": [],
|
|
}
|
|
|
|
|
|
def _rehash(output: dict) -> None:
|
|
"""正文变更后同步候选和 claim 哈希,供单项硬门测试使用。"""
|
|
|
|
digest = "sha256:" + hashlib.sha256(output["candidateBody"].encode("utf-8")).hexdigest()
|
|
output["candidateSha256"] = digest
|
|
for claim in output["claimLedger"]:
|
|
claim["candidateSha256"] = digest
|
|
|
|
|
|
def _valid_pair() -> tuple[dict, dict]:
|
|
"""构造一组能通过严格合同和机械硬门的上下文与候选。"""
|
|
|
|
context = _bound_context()
|
|
output = _bound_output(context, body=_candidate_body())
|
|
return context, output
|
|
|
|
|
|
class CheckWriterCandidateTest(unittest.TestCase):
|
|
def test_missing_event_character_foreshadow_or_hook_blocks(self):
|
|
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_candidate_claim_hash_unicode_offset_and_evidence_reference_block(self):
|
|
context, output = _valid_pair()
|
|
cases = {}
|
|
|
|
bad_candidate_hash = copy.deepcopy(output)
|
|
bad_candidate_hash["candidateSha256"] = "sha256:" + "f" * 64
|
|
cases["CANDIDATE_HASH_MISMATCH"] = bad_candidate_hash
|
|
|
|
bad_claim_hash = copy.deepcopy(output)
|
|
bad_claim_hash["claimLedger"][0]["candidateSha256"] = "sha256:" + "e" * 64
|
|
cases["CLAIM_HASH_MISMATCH"] = bad_claim_hash
|
|
|
|
bad_unicode_offset = copy.deepcopy(output)
|
|
bad_unicode_offset["candidateBody"] = "林澈🙂" + bad_unicode_offset["candidateBody"][2:]
|
|
_rehash(bad_unicode_offset)
|
|
bad_unicode_offset["claimLedger"][0]["endCodePoint"] = len(bad_unicode_offset["candidateBody"]) + 1
|
|
cases["CLAIM_UNICODE_OFFSET_INVALID"] = bad_unicode_offset
|
|
|
|
bad_evidence = copy.deepcopy(output)
|
|
bad_evidence["claimLedger"][0]["factEvidenceId"] = "fact-missing"
|
|
cases["CLAIM_EVIDENCE_MISSING"] = bad_evidence
|
|
|
|
for expected_code, candidate in cases.items():
|
|
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_candidate_body_outside_dynamic_length_contract_blocks(self):
|
|
"""机械门必须独立复核动态篇幅,不能只信 writer adapter。"""
|
|
|
|
context, output = _valid_pair()
|
|
output["candidateBody"] = "文" * 1000
|
|
_rehash(output)
|
|
|
|
report = check_writer_candidate(context, output, _requirements())
|
|
|
|
self.assertFalse(report["passed"])
|
|
self.assertIn(
|
|
"CANDIDATE_LENGTH_OUT_OF_RANGE",
|
|
{item["code"] for item in report["blockingFailures"]},
|
|
)
|
|
|
|
def test_new_setting_must_have_overlapping_declaration(self):
|
|
context, output = _valid_pair()
|
|
marker = "赤霄城"
|
|
insertion_start = len("林澈完成围攻突围,又把旧徽章压回掌心。")
|
|
output["candidateBody"] = (
|
|
output["candidateBody"][:insertion_start]
|
|
+ marker
|
|
+ output["candidateBody"][insertion_start + len(marker):]
|
|
)
|
|
_rehash(output)
|
|
requirements = _requirements()
|
|
requirements["detectedNewSettings"] = [
|
|
{
|
|
"settingId": "setting-1",
|
|
"factType": "location",
|
|
"startCodePoint": insertion_start,
|
|
"endCodePoint": insertion_start + len(marker),
|
|
}
|
|
]
|
|
|
|
blocked = check_writer_candidate(context, output, requirements)
|
|
self.assertIn("NEW_SETTING_UNDECLARED", {item["code"] for item in blocked["blockingFailures"]})
|
|
|
|
output["newSettingDeclarations"] = [
|
|
{
|
|
"declarationId": "declaration-1",
|
|
"factType": "location",
|
|
"text": marker,
|
|
"startCodePoint": insertion_start,
|
|
"endCodePoint": insertion_start + len(marker),
|
|
}
|
|
]
|
|
passed = check_writer_candidate(context, output, requirements)
|
|
self.assertTrue(passed["passed"])
|
|
|
|
def test_frozen_fact_conflict_blocks_with_traceable_source(self):
|
|
context, output = _valid_pair()
|
|
requirements = _requirements()
|
|
requirements["frozenConflicts"] = [
|
|
{
|
|
"conflictId": "conflict-1",
|
|
"message": "候选把已毁地点写成完好",
|
|
"sourceRef": "state:8@state-v1",
|
|
}
|
|
]
|
|
|
|
report = check_writer_candidate(context, output, requirements)
|
|
|
|
self.assertFalse(report["passed"])
|
|
failure = next(item for item in report["blockingFailures"] if item["code"] == "FROZEN_FACT_CONFLICT")
|
|
self.assertEqual(failure["sourceRef"], "state:8@state-v1")
|
|
|
|
def test_valid_candidate_passes_mechanical_gate_and_exposes_semantic_interface(self):
|
|
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(
|
|
set(report["semanticReview"]["checks"]),
|
|
{"hard_event_semantics", "claim_truth_alignment", "knowledge_scope", "ability_cost_consistency"},
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|