muse-agent-example/tests/skills/write-next-chapter/test_dispatch_writer_bridge.py
zizi 76d38f2dc7 修复: 派发链硬门失败关闭与测试账本隔离
- 模型锁定完整 ID 等值:role_policy 废弃子串匹配,前置校验+事后
  MODEL_POLICY_VIOLATION 熔断+回执 modelMatch,治理链角色放行 BUDGET_CHAIN
- 工具白名单只读机械强制:任务包 allowlist ⊆ 只读注册表,TOOL_NOT_READONLY
- 本地向量检索 truthful 化:aiContext 裁剪、指针行跳过、资格失败关闭
  (bindingStatus/productionRetrievalEligible 不再伪造 active)
- 角色提示词 name 回归英文系统 ID;planner 补 fine_outline 绑定;
  writer 数据契约对齐 writer-candidate-body-v1
- 测试账本隔离:sqlite_path 三层透传(bridge/two_phase),9 处派发测试
  改用临时库;清除 muse.db 测试残留 5 runs+26 events+reviews 5/6(有备份)
- test-inventory 补 3 条登记并机械重算 summary;评测场景补
  output_contract/fail_closed 与 stability 两类;死代码清理
  (project_paths.py、offline_only 死参数、harness/harness 残骸)
- 新增 metaphysical-diff-review 技能(红线 4.2 载体)并登记,共 59 技能
2026-08-30 23:06:19 +08:00

177 lines
7.0 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
"""生产写手框架派发桥的离线测试(不连真实框架、不连真实模型)。
固定合同:任务包装配(角色/冻结输入/探索白名单)、会话按章稳定、
成功路径绑定候选信封与证据引用、派发失败与证据缺失一律失败关闭。
"""
from __future__ import annotations
import json
import pathlib
import sys
import tempfile
import unittest
PROJECT_ROOT = pathlib.Path(__file__).resolve().parents[3]
SCRIPT_DIR = PROJECT_ROOT / "muse" / "content" / "work" / "skills" / "generate" / "write-next-chapter" / "scripts"
DISPATCH_TEST_DIR = PROJECT_ROOT / "tests" / "skills" / "dispatch-agent-task"
CHECK_TEST_DIR = PROJECT_ROOT / "tests" / "skills" / "check-content-consistency"
WNC_TEST_DIR = PROJECT_ROOT / "tests" / "skills" / "write-next-chapter"
for path in (SCRIPT_DIR, DISPATCH_TEST_DIR, CHECK_TEST_DIR, WNC_TEST_DIR):
if str(path) not in sys.path:
sys.path.insert(0, str(path))
import dispatch_writer_bridge as bridge # noqa: E402
from dispatch_agent_task import DEFAULT_RUN_DIR_ROOT # noqa: E402
from dispatch_writer_bridge import ( # noqa: E402
DispatchWriterError,
build_writer_dispatch_spec,
run_writer_via_dispatch,
writer_session_paths,
)
from read_tools import TOOL_REGISTRY # noqa: E402
from test_check_writer_candidate import _valid_pair # noqa: E402
from test_dispatch_agent_task import FIXED_MODEL, RecordingConnect, fake_launcher, pi_stream_lines # noqa: E402
class BridgeConnect(RecordingConnect):
"""派发桥专用假连接:补证证据引用查询返回 raw_row(可置 None 模拟缺证)。"""
def __init__(self):
super().__init__()
self.raw_row = (9001, 7777)
def __call__(self, *args, **kwargs):
outer = self
class _Cursor:
def execute(self, sql, params=None):
outer.log.append((sql, params))
return self
def fetchone(self):
sql = outer.log[-1][0]
if sql.startswith("SELECT run_id, work_id"):
return ("row", None, None, "running")
if sql.startswith("SELECT id, raw_content_id"):
return outer.raw_row
if sql.startswith("INSERT INTO example_run"):
return ("row",)
if sql.startswith("UPDATE example_run"):
state = "failed" if "failed" in (outer.log[-1][1] or ()) else "completed"
return ("row", state, None)
RecordingConnect.next_id += 1
return (RecordingConnect.next_id,)
def fetchall(self):
return []
def commit(self):
outer.log.append(("COMMIT", None))
def rollback(self):
outer.log.append(("ROLLBACK", None))
class _Ctx:
def __enter__(self):
return _Cursor()
def __exit__(self, *exc):
return False
return _Ctx()
def _candidate_body() -> str:
return "茧撕开舱门的一瞬,林深听见了深渊的回声。" * 8
class SpecBuildTest(unittest.TestCase):
def test_spec_freezes_creative_input_and_read_tools(self):
context, _ = _valid_pair()
spec = build_writer_dispatch_spec(
context, target_chapter=3, human_instruction="让恐惧落在身体上", candidate_version=2,
)
self.assertEqual(spec["role"], "writer")
self.assertEqual(spec["input"]["workId"], context["workId"])
self.assertEqual(spec["input"]["targetChapter"], 3)
self.assertEqual(spec["input"]["candidateVersion"], 2)
self.assertIn("让恐惧落在身体上", spec["taskPrompt"])
self.assertIn("creativeInput", spec["input"])
# 探索白名单 = 工具 server 登记表(单一事实源,防漂移)。
self.assertEqual(spec["toolAllowlist"], sorted(TOOL_REGISTRY))
self.assertEqual(
spec["outputSchema"]["required"], ["candidateBody"])
def test_session_paths_are_stable_per_chapter(self):
sid_a, dir_a = writer_session_paths(12, 3)
sid_b, dir_b = writer_session_paths(12, 3)
sid_c, dir_c = writer_session_paths(12, 4)
self.assertEqual((sid_a, dir_a), (sid_b, dir_b))
self.assertNotEqual(sid_a, sid_c)
self.assertNotEqual(dir_a, dir_c)
class DispatchRoundTripTest(unittest.TestCase):
def setUp(self):
self.tmp = pathlib.Path(tempfile.mkdtemp())
self.context, _ = _valid_pair()
# 运行目录按派发运行号固定;测试前清理,避免审计根冲突。
self.dispatch_run_id = f"{self.context['runId']}-writer-v1"
import shutil
shutil.rmtree(DEFAULT_RUN_DIR_ROOT / self.dispatch_run_id, ignore_errors=True)
def _run(self, lines, connect):
return run_writer_via_dispatch(
self.context,
candidate_version=1,
repo_root=PROJECT_ROOT,
provider="p",
model=FIXED_MODEL,
thinking="low",
human_instruction="",
spec_path=self.tmp / "task.json",
launcher=fake_launcher(lines),
connect_factory=connect,
sqlite_path=self.tmp / "ledger.db",
)
def test_success_binds_envelope_and_evidence_ref(self):
final_text = json.dumps({"candidateBody": _candidate_body()}, ensure_ascii=False)
connect = BridgeConnect()
envelope, receipt, raw_ref = self._run(
pi_stream_lines(final_text, model=FIXED_MODEL), connect
)
# 身份、哈希、版本由桥绑定,不来自模型。
self.assertEqual(envelope["runId"], self.context["runId"])
self.assertEqual(envelope["candidateVersion"], 1)
self.assertTrue(envelope["candidateSha256"].startswith("sha256:"))
self.assertEqual(envelope["candidateBody"], _candidate_body())
# 回执适配供生产账本消费;证据引用来自派发运行的调用账。
self.assertEqual(receipt.requested_model_id, f"p/{FIXED_MODEL}")
self.assertTrue(receipt.dispatch_run_id.startswith(self.context["runId"]))
self.assertEqual(raw_ref, (9001, 7777))
# 任务包落盘可审计。
saved_spec = json.loads((self.tmp / "task.json").read_text(encoding="utf-8"))
self.assertEqual(saved_spec["role"], "writer")
def test_dispatch_failure_fails_closed(self):
connect = BridgeConnect()
with self.assertRaises(DispatchWriterError) as caught:
# 空事件流 -> 框架层失败关闭。
self._run([], connect)
self.assertNotEqual(caught.exception.code, "")
def test_missing_raw_reference_fails_closed(self):
final_text = json.dumps({"candidateBody": _candidate_body()}, ensure_ascii=False)
connect = BridgeConnect()
connect.raw_row = None
with self.assertRaises(DispatchWriterError) as caught:
self._run(pi_stream_lines(final_text, model=FIXED_MODEL), connect)
self.assertEqual(caught.exception.code, "DISPATCH_EVIDENCE_MISSING")
if __name__ == "__main__":
unittest.main()