#!/usr/bin/env python3 """投影登记与恢复对 muse-example 真实库的集成测试。 覆盖: - 提交即登记:accept 同事务登记 pending 投影,绑正文 revision 与文本哈希;重放不重复登记; - 换版即失效:新 revision 提交后旧投影全部 stale; - 失败不冒充完成:stale/failed 不得直接置 completed(触发器兜底),failed 显式 retry 才回 pending; - 恢复巡检:refresh_staleness 按当前正文对账,漂移投影标 stale。 测试数据 unittest-proj- 前缀隔离,结束物理清理(本表可变,直接 DELETE)。 跑法(需 Tailscale 内网可达 muse-example): .venv/bin/python .claude/skills/decide-candidate/scripts/test_projection_db.py """ from __future__ import annotations import hashlib import pathlib import sys import uuid 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 from projection_registry import ( # noqa: E402 ProjectionError, finish_projection, refresh_staleness, retry_projection, ) from write_canonical import accept # noqa: E402 SUFFIX = uuid.uuid4().hex[:10] WORK_TITLE = f"unittest-proj-{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: 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 example_projection_run WHERE work_id=%s", (_work_id,)) 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() -> 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) -> int: 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,'production',%s,%s,%s,'writer-production-v1','continuation','writer'," "'passed',TRUE,'passed',%s,'unittest') RETURNING id", (work_id, f"unittest-proj-run-{SUFFIX}-{version}", version, _sha(body), body, _sha("sem:" + body))).fetchone()[0] conn.commit() _candidate_ids.append(candidate_id) return candidate_id def _command_id(tag: str) -> str: cid = f"unittest-proj-{tag}-{SUFFIX}" _command_ids.append(cid) return cid def _projections(work_id: int) -> list[tuple]: with connect(readonly=True) as conn: return conn.execute( "SELECT id, kind, source_revision, source_text_hash, status, attempt " "FROM example_projection_run WHERE work_id=%s ORDER BY id", (work_id,)).fetchall() def test_commit_registers_and_replay_is_noop(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, projection_kinds=("extraction", "summary")) assert result["status"] == "accepted" rows = _projections(work_id) assert len(rows) == 2 and result["projection_ids"] == [row[0] for row in rows] for row in rows: assert row[2] == result["revision"], "投影必须绑本次正文 revision" assert row[3] == _sha(body), "投影必须绑本次正文哈希" assert row[4] == "pending" replay = accept(candidate_id, rationale="unittest", expected_revision=1, command_id=command_id, projection_kinds=("extraction", "summary")) assert replay["status"] == "already_applied" assert len(_projections(work_id)) == 2, "重放不得重复登记投影" def test_new_revision_stales_old_projections(work_id: int) -> None: body = f"投影测试正文第二版 {SUFFIX}" candidate_id = _insert_candidate(work_id, body=body, version="2") result = accept(candidate_id, rationale="unittest", expected_revision=1, command_id=_command_id("accept-2"), projection_kinds=("extraction",)) assert result["status"] == "accepted" and result["revision"] == 2 rows = _projections(work_id) stale_rows = [row for row in rows if row[4] == "stale"] pending_rows = [row for row in rows if row[4] == "pending"] # rev1 的 extraction+summary 全部 stale;rev2 的 extraction 是唯一 pending assert len(stale_rows) == 2 and all(row[2] == 1 for row in stale_rows) assert len(pending_rows) == 1 and pending_rows[0][2] == 2 assert pending_rows[0][1] == "extraction" def test_failed_never_masquerades_completed(work_id: int) -> None: rows = _projections(work_id) pending = next(row for row in rows if row[4] == "pending") # worker 报告失败 finish_projection(pending[0], "failed", detail={"reason": "extractor_unbuilt"}) # 失败不得直接洗白成 completed(应用层条件 UPDATE 不命中) try: finish_projection(pending[0], "completed") raise AssertionError("failed 投影不得报告 completed") except AssertionError: raise except ProjectionError: pass # 绕过应用层直接 UPDATE 也被触发器拒绝 try: with connect() as conn: conn.execute("UPDATE example_projection_run SET status='completed' WHERE id=%s", (pending[0],)) conn.commit() raise AssertionError("触发器必须拒绝 failed→completed") except AssertionError: raise except Exception: pass # 显式 retry 才能回 pending,且 attempt+1 retried = retry_projection(pending[0]) assert retried["status"] == "pending" and retried["attempt"] == 2 ok = finish_projection(pending[0], "completed") assert ok["status"] == "completed" # completed 不得重试 try: retry_projection(pending[0]) raise AssertionError("completed 投影不得重试") except AssertionError: raise except ProjectionError: pass def test_stale_projections_cannot_report_outcomes(work_id: int) -> None: stale_row = next(row for row in _projections(work_id) if row[4] == "stale") try: with connect() as conn: conn.execute("UPDATE example_projection_run SET status='completed' WHERE id=%s", (stale_row[0],)) conn.commit() raise AssertionError("stale→completed 必须被拒绝") except AssertionError: raise except Exception: pass try: with connect() as conn: conn.execute("UPDATE example_projection_run SET status='failed' WHERE id=%s", (stale_row[0],)) conn.commit() raise AssertionError("stale→failed 必须被拒绝") except AssertionError: raise except Exception: pass # stale 走 retry 恢复:attempt+1 回 pending,随后可以正常完成 retried = retry_projection(stale_row[0]) assert retried["status"] == "pending" finish_projection(stale_row[0], "completed") def test_refresh_staleness_detects_hash_drift(work_id: int) -> None: # 手工造一条"漂移"投影:revision 与当前正文一致但哈希是旧的(模拟派生后正文被改) with connect(readonly=True) as conn: current = 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()[0] with connect() as conn: drifted_id = conn.execute( "INSERT INTO example_projection_run(work_id, target_chapter, kind, source_revision, " "source_text_hash, status, idempotency_key, creator) " "VALUES (%s,1,'dashboard',%s,%s,'completed',%s,'unittest') RETURNING id", (work_id, current, "f" * 64, f"unittest-drift-{SUFFIX}")).fetchone()[0] conn.commit() stale_ids = refresh_staleness(work_id=work_id) assert drifted_id in stale_ids, "哈希漂移的投影必须被标 stale" with connect(readonly=True) as conn: status = conn.execute( "SELECT status FROM example_projection_run WHERE id=%s", (drifted_id,)).fetchone()[0] assert status == "stale" def main() -> None: work_id = _setup() tests = ( ("提交即登记且重放幂等", test_commit_registers_and_replay_is_noop), ("换版即失效旧投影", test_new_revision_stales_old_projections), ("失败不冒充完成与重试", test_failed_never_masquerades_completed), ("stale 不得报告结果", test_stale_projections_cannot_report_outcomes), ("恢复巡检发现哈希漂移", test_refresh_staleness_detects_hash_drift), ) try: for name, test in tests: test(work_id) print(f"PASS: {name}") print("PASS:投影登记与恢复真实库集成测试全部通过") finally: _cleanup() if __name__ == "__main__": main()