#!/usr/bin/env python3 """write_canonical 接受通道对 muse-example 真实库的故障注入测试。 覆盖先审后入主链的机械不变量: - 接受原子性:事务中断不留半提交(正文/归因/决策要么全有要么全无); - command_id 重放幂等:同一命令第二次执行不重复写正文、决策; - revision CAS:旧 expected_revision 拒绝,正文不被旧候选覆盖; - 先审后入兜底:semantic_status 非 passed 的候选一律不得接受; - 机械隔离:评测/诊断候选 DB 级拒绝。 测试数据用 unittest-commit- 前缀隔离;example_user_decision 是 append-only 表, 清理时短暂禁用其防删触发器(try/finally 保证恢复)。 跑法(需 Tailscale 内网可达 muse-example): .venv/bin/python .claude/skills/decide-candidate/scripts/test_write_canonical_db.py """ from __future__ import annotations import hashlib import pathlib import sys import uuid from typing import Any from unittest import mock SCRIPT_DIR = pathlib.Path(__file__).resolve().parent SKILLS_DIR = SCRIPT_DIR.parents[1] for path in (SCRIPT_DIR, SKILLS_DIR / "access-database" / "scripts"): if str(path) not in sys.path: sys.path.insert(0, str(path)) from db import connect # noqa: E402 import write_canonical # noqa: E402 from write_canonical import ConflictError, accept, discard # noqa: E402 SUFFIX = uuid.uuid4().hex[:10] WORK_TITLE = f"unittest-commit-{SUFFIX}" _command_ids: list[str] = [] _candidate_ids: list[int] = [] _work_id: int | None = None def _sha(text: str) -> str: return hashlib.sha256(text.encode("utf-8")).hexdigest() def _cleanup() -> None: """物理清理测试行;append-only 决策表短暂禁用防删触发器,finally 恢复。""" if _work_id is None: return with connect() as conn: try: conn.execute( "ALTER TABLE example_user_decision DISABLE TRIGGER trg_example_user_decision_append_only") try: if _candidate_ids: conn.execute( "DELETE FROM example_user_decision WHERE candidate_id = ANY(%s)", (_candidate_ids,)) if _command_ids: conn.execute( "DELETE FROM muse_content_command_log WHERE command_id = ANY(%s)", (_command_ids,)) conn.execute( "DELETE FROM muse_content_block_source_attribution WHERE work_id=%s", (_work_id,)) conn.execute("DELETE FROM muse_content_block WHERE work_id=%s", (_work_id,)) conn.execute("DELETE FROM muse_content_chapter WHERE work_id=%s", (_work_id,)) if _candidate_ids: conn.execute( "DELETE FROM example_candidate WHERE id = ANY(%s)", (_candidate_ids,)) conn.execute("DELETE FROM muse_content_work WHERE id=%s", (_work_id,)) conn.commit() finally: conn.execute( "ALTER TABLE example_user_decision ENABLE TRIGGER trg_example_user_decision_append_only") conn.commit() except Exception: conn.rollback() raise def _setup_work_and_chapter() -> int: global _work_id with connect() as conn: work_id = conn.execute( "INSERT INTO muse_content_work(owner_user_id, title, status, creator) " "VALUES (1,%s,'writing','unittest') RETURNING id", (WORK_TITLE,)).fetchone()[0] conn.execute( "INSERT INTO muse_content_chapter(work_id, title, order_no, creator) " "VALUES (%s,'测试章',1,'unittest')", (work_id,)) conn.commit() _work_id = work_id return work_id def _insert_candidate(work_id: int, *, body: str, version: str, run_type: str = "production", state: str = "passed", semantic_status: str | None = "passed", semantic_sha: str | None = None) -> int: """直接造一个 Shadow 候选行(测试夹具,不经 persist_writer_execution)。""" sha = _sha(body) if semantic_status == "passed" and semantic_sha is None: semantic_sha = _sha("semantic-report:" + sha) with connect() as conn: candidate_id = conn.execute( "INSERT INTO example_candidate(work_id, target_chapter, run_id, attempt, run_type, " "candidate_version, candidate_sha256, candidate_body, quality_policy_version, mode, " "source_role, state, acceptance_eligible, semantic_status, semantic_report_sha256, creator) " "VALUES (%s,1,%s,1,%s,%s,%s,%s,'writer-production-v1','continuation','writer',%s," "TRUE,%s,%s,'unittest') RETURNING id", (work_id, f"unittest-commit-run-{SUFFIX}-{version}", run_type, version, sha, body, state, semantic_status, semantic_sha)).fetchone()[0] conn.commit() _candidate_ids.append(candidate_id) return candidate_id def _command_id(tag: str) -> str: cid = f"unittest-commit-{tag}-{SUFFIX}" _command_ids.append(cid) return cid def _block_revision(work_id: int) -> int: with connect(readonly=True) as conn: row = conn.execute( "SELECT COALESCE(MAX(b.revision),0) FROM muse_content_block b " "JOIN muse_content_chapter c ON b.chapter_id=c.id AND c.deleted=false " "WHERE c.work_id=%s AND c.order_no=1 AND b.deleted=false", (work_id,)).fetchone() return int(row[0]) def _candidate_state(candidate_id: int) -> str: with connect(readonly=True) as conn: return conn.execute( "SELECT state FROM example_candidate WHERE id=%s", (candidate_id,)).fetchone()[0] def _decision_count(candidate_id: int) -> int: with connect(readonly=True) as conn: return int(conn.execute( "SELECT COUNT(*) FROM example_user_decision WHERE candidate_id=%s", (candidate_id,)).fetchone()[0]) def test_accept_atomic_and_replay_idempotent(work_id: int) -> None: body = f"测试正文第一版 {SUFFIX}" candidate_id = _insert_candidate(work_id, body=body, version="1") command_id = _command_id("accept-1") result = accept(candidate_id, rationale="unittest", expected_revision=0, command_id=command_id) assert result["status"] == "accepted", result assert result["revision"] == 1 assert _block_revision(work_id) == 1 assert _candidate_state(candidate_id) == "accepted" assert _decision_count(candidate_id) == 1 # 同一 command_id 重放:不重复写正文、不新增决策 replay = accept(candidate_id, rationale="unittest", expected_revision=1, command_id=command_id) assert replay["status"] == "already_applied", replay assert _block_revision(work_id) == 1 assert _decision_count(candidate_id) == 1 with connect(readonly=True) as conn: block_rows = int(conn.execute( "SELECT COUNT(*) FROM muse_content_block WHERE work_id=%s AND deleted=false", (work_id,)).fetchone()[0]) attribution_rows = int(conn.execute( "SELECT COUNT(*) FROM muse_content_block_source_attribution WHERE work_id=%s AND deleted=false", (work_id,)).fetchone()[0]) assert block_rows == 1 and attribution_rows == 1, (block_rows, attribution_rows) def test_stale_expected_revision_rejected(work_id: int) -> None: body = f"测试正文第二版 {SUFFIX}" candidate_id = _insert_candidate(work_id, body=body, version="2") # 当前正文块已在 revision=1;拿旧期望 0 接受必须拒绝,且正文不变 try: accept(candidate_id, rationale="unittest", expected_revision=0, command_id=_command_id("accept-stale")) raise AssertionError("旧 expected_revision 必须被拒绝") except AssertionError: raise except ConflictError as exc: assert "REVISION_CONFLICT" in str(exc) assert _block_revision(work_id) == 1, "拒绝后正文不得变化" assert _candidate_state(candidate_id) == "passed", "拒绝后候选保持 passed 待人工处理" # 正确期望 1 → 接受成 revision 2(旧候选不能覆盖新正文的顺序保证) ok = accept(candidate_id, rationale="unittest", expected_revision=1, command_id=_command_id("accept-v2")) assert ok["status"] == "accepted" and ok["revision"] == 2 assert _block_revision(work_id) == 2 def test_semantic_backstop(work_id: int) -> None: # 机械 passed 但语义缺失/未过:DB 兜底拒绝 no_semantic = _insert_candidate(work_id, body=f"无语义证据 {SUFFIX}", version="3", semantic_status=None, semantic_sha=None) failed_semantic = _insert_candidate(work_id, body=f"语义未过 {SUFFIX}", version="4", semantic_status="failed") for cid in (no_semantic, failed_semantic): try: accept(cid, rationale="unittest", expected_revision=_block_revision(work_id), command_id=_command_id(f"accept-nosem-{cid}")) raise AssertionError("未过语义审查的候选不得接受") except AssertionError: raise except ConflictError as exc: assert "语义审查" in str(exc), str(exc) assert _candidate_state(no_semantic) == "passed" assert _candidate_state(failed_semantic) == "passed" # discard 不受语义兜底限制(丢弃是安全操作) dropped = discard(failed_semantic, rationale="unittest", command_id=_command_id("discard-failed-semantic")) assert dropped["status"] == "discarded" assert _candidate_state(failed_semantic) == "discarded" def test_non_production_rejected(work_id: int) -> None: eval_candidate = _insert_candidate(work_id, body=f"评测候选 {SUFFIX}", version="5", run_type="eval") try: accept(eval_candidate, rationale="unittest", expected_revision=_block_revision(work_id), command_id=_command_id("accept-eval")) raise AssertionError("评测候选不得接受") except AssertionError: raise except ConflictError as exc: assert "eval" in str(exc) assert _candidate_state(eval_candidate) == "passed" def test_interrupted_accept_leaves_no_half_commit(work_id: int) -> None: """事务后半段注入故障:正文/归因/命令/决策全部回滚,候选保持 passed。""" body = f"中断测试正文 {SUFFIX}" candidate_id = _insert_candidate(work_id, body=body, version="6") revision_before = _block_revision(work_id) def _explode(*_args, **_kwargs): raise RuntimeError("injected-fault: 决策归档前崩溃") command_id = _command_id("accept-interrupted") with mock.patch.object(write_canonical, "_refresh_work_metrics", side_effect=_explode): try: accept(candidate_id, rationale="unittest", expected_revision=revision_before, command_id=command_id) raise AssertionError("注入故障必须中止接受") except AssertionError: raise except RuntimeError as exc: assert "injected-fault" in str(exc) # 半提交检查:正文 revision 不变、无归因新增、命令日志未落(整事务回滚)、决策为 0 assert _block_revision(work_id) == revision_before, "中断不得改变正文" assert _decision_count(candidate_id) == 0, "中断不得留下决策" assert _candidate_state(candidate_id) == "passed", "中断后候选必须保持 passed" with connect(readonly=True) as conn: logged = conn.execute( "SELECT COUNT(*) FROM muse_content_command_log WHERE command_id=%s", (command_id,)).fetchone()[0] assert logged == 0, "命令日志随事务回滚,重放仍可执行" # 中断后用同一 command_id 重试仍可成功(幂等键未被污染) retried = accept(candidate_id, rationale="unittest", expected_revision=revision_before, command_id=command_id) assert retried["status"] == "accepted" assert _block_revision(work_id) == revision_before + 1 def test_dry_run_writes_nothing(work_id: int) -> None: body = f"试跑正文 {SUFFIX}" candidate_id = _insert_candidate(work_id, body=body, version="7") revision_before = _block_revision(work_id) result = accept(candidate_id, rationale="unittest", expected_revision=revision_before, command_id=_command_id("accept-dry"), dry_run=True) assert result["status"] == "dry_run_ok" assert _block_revision(work_id) == revision_before assert _candidate_state(candidate_id) == "passed" assert _decision_count(candidate_id) == 0 def main() -> None: work_id = _setup_work_and_chapter() tests = ( ("accept 原子性与重放幂等", test_accept_atomic_and_replay_idempotent), ("旧 expected_revision 拒绝", test_stale_expected_revision_rejected), ("语义兜底拒绝", test_semantic_backstop), ("非生产候选拒绝", test_non_production_rejected), ("中断不留半提交", test_interrupted_accept_leaves_no_half_commit), ("dry-run 不落库", test_dry_run_writes_nothing), ) try: for name, test in tests: test(work_id) print(f"PASS: {name}") print("PASS:write_canonical 真实库故障注入测试全部通过") finally: _cleanup() if __name__ == "__main__": main()