muse-agent-example/tests/skills/write-next-chapter/test_production_evidence_reassemble.py

223 lines
8.4 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
"""生产补证重组装离线测试:缺口 → 新 attempt / 新快照 / 创作输入变化。"""
from __future__ import annotations
import sys
import unittest
from pathlib import Path
from unittest.mock import patch
PROJECT_ROOT = Path(__file__).resolve().parents[3]
SKILLS_DIR = PROJECT_ROOT / ".agent" / "skills"
TEST_DIR = PROJECT_ROOT / "tests" / "skills" / "write-next-chapter"
CHECK_TEST_DIR = PROJECT_ROOT / "tests" / "skills" / "check-content-consistency"
SCRIPT_DIR = SKILLS_DIR / "write-next-chapter" / "scripts"
READ_CONTEXT_DIR = SKILLS_DIR / "assemble-context" / "scripts"
for path in (READ_CONTEXT_DIR, SCRIPT_DIR, TEST_DIR, CHECK_TEST_DIR):
sys.path.insert(0, str(path))
from production_evidence_reassemble import ( # noqa: E402
EvidenceReassembleError,
reassemble_writer_context_for_gaps,
)
from run_writer_pipeline import InMemoryCasStateStore, PipelineError, run_writer_pipeline # noqa: E402
from writer_contract import build_writer_creative_input, retrieval_identity # noqa: E402
from test_check_writer_candidate import _requirements, _valid_pair # noqa: E402
from test_run_writer_pipeline import _semantic_report, _writer_output # noqa: E402
class ProductionEvidenceReassembleTests(unittest.TestCase):
def test_zero_hit_still_changes_creative_input(self) -> None:
context, _ = _valid_pair()
gaps = [
{
"gapId": "gap-ch3-1",
"query": "污染链路深度数值含义",
"reason": "无正典依据",
"priority": "low",
}
]
with patch(
"production_evidence_reassemble._search_canonical_snippets",
return_value=[],
):
advanced = reassemble_writer_context_for_gaps(context, gaps, 2, work_id=12)
self.assertEqual(advanced["runId"], context["runId"])
self.assertEqual(advanced["attempt"], 2)
self.assertNotEqual(
advanced["contextSnapshot"]["contextSha256"],
context["contextSnapshot"]["contextSha256"],
)
self.assertEqual(
advanced["contextSnapshot"]["contextSha256"],
retrieval_identity(advanced),
)
self.assertNotEqual(
build_writer_creative_input(advanced),
build_writer_creative_input(context),
)
styles = " ".join(advanced.get("styleConstraints") or [])
self.assertNotIn("禁止写入", styles)
self.assertIn("补证轮元数据", styles)
self.assertIn("未解决", styles)
def test_prose_hit_appends_fact_evidence(self) -> None:
context, _ = _valid_pair()
gaps = [{"gapId": "gap-a", "query": "徽章", "reason": "需补证", "priority": "high"}]
hits = [
{
"kind": "prose",
"term": "徽章",
"chapter": 1,
"blockId": 99,
"revision": 3,
"start": 0,
"end": 12,
"text": "旧徽章属于林澈",
}
]
with patch(
"production_evidence_reassemble._search_canonical_snippets",
return_value=hits,
):
advanced = reassemble_writer_context_for_gaps(context, gaps, 2, work_id=12)
ids = {item["evidenceId"] for item in advanced["factEvidence"]}
self.assertIn("gap-gap-a-prose-2-0", ids)
styles = " ".join(advanced.get("styleConstraints") or [])
self.assertIn("已追加", styles)
def test_invalid_attempt_fails_closed(self) -> None:
context, _ = _valid_pair()
with self.assertRaises(EvidenceReassembleError):
reassemble_writer_context_for_gaps(
context,
[{"gapId": "g1", "query": "x", "reason": "y"}],
1,
work_id=12,
)
def test_pipeline_evidence_hit_stops_at_authorization(self) -> None:
"""needs_evidence + 检索命中 -> 授权终态(重写需人授权,以新运行继续)。"""
context, _ = _valid_pair()
calls: list[int] = []
hits = [
{
"kind": "prose",
"term": "徽章",
"chapter": 1,
"blockId": 99,
"revision": 3,
"start": 0,
"end": 12,
"text": "旧徽章属于林澈",
}
]
def writer(current: dict, version: int) -> dict:
calls.append(int(current["attempt"]))
return _writer_output(current, version)
def detector(current: dict, candidate: dict, _mechanical: dict) -> dict:
return _semantic_report(
current,
candidate,
status="needs_evidence",
gaps=[
{
"gapId": "gap-1",
"query": "旧徽章归属",
"reason": "缺口",
"priority": "high",
"candidateSha256": candidate["candidateSha256"],
"candidateQuote": "旧徽章",
"startCodePoint": 11,
"endCodePoint": 14,
}
],
)
def provider(current, gaps, attempt):
with patch(
"production_evidence_reassemble._search_canonical_snippets",
return_value=hits,
):
return reassemble_writer_context_for_gaps(
current, gaps, attempt, work_id=int(current["workId"])
)
with self.assertRaises(PipelineError) as caught:
run_writer_pipeline(
context=context,
requirements=_requirements(),
writer=writer,
evidence_provider=provider,
semantic_detector=detector,
state_store=InMemoryCasStateStore(),
)
self.assertEqual(caught.exception.code, "AUTHORIZATION_REQUIRED")
# 只跑单次收敛:写手只被调用一次,重写等人授权。
self.assertEqual(calls, [1])
self.assertEqual(caught.exception.details["nextAttempt"], 2)
self.assertTrue(caught.exception.details["reassembledContextSha256"].startswith("sha256:"))
def test_unfillable_gaps_pass_without_rewrite(self) -> None:
"""正典零命中的缺口是新设定提案:不重写,报告升格 passed,进人闸。"""
context, _ = _valid_pair()
calls: list[int] = []
def writer(current: dict, version: int) -> dict:
calls.append(int(current["attempt"]))
return _writer_output(current, version)
def detector(current: dict, candidate: dict, _mechanical: dict) -> dict:
return _semantic_report(
current,
candidate,
status="needs_evidence",
gaps=[
{
"gapId": "gap-ch3-01",
"query": "深渊洋流完整规则",
"reason": "尚无设定文档",
"priority": "medium",
"candidateSha256": candidate["candidateSha256"],
"candidateQuote": "旧徽章",
"startCodePoint": 11,
"endCodePoint": 14,
}
],
)
def provider(current, gaps, attempt):
with patch(
"production_evidence_reassemble._search_canonical_snippets",
return_value=[],
):
return reassemble_writer_context_for_gaps(
current, gaps, attempt, work_id=int(current["workId"])
)
result = run_writer_pipeline(
context=context,
requirements=_requirements(),
writer=writer,
evidence_provider=provider,
semantic_detector=detector,
state_store=InMemoryCasStateStore(),
)
self.assertEqual(result["status"], "PASSED")
self.assertEqual(calls, [1])
self.assertEqual(result.get("rewriteCount"), 0)
final_report = result["trace"][-1]["semanticReport"]
self.assertEqual(final_report["status"], "passed")
self.assertEqual(final_report["evidenceGaps"], [])
self.assertTrue(final_report["newSettingCandidates"])
if __name__ == "__main__":
unittest.main()