"""step 重跑幂等:gate 派生行必须随 step 清理,否则崩溃重跑累积重复。""" from __future__ import annotations import sqlite3 from typing import TYPE_CHECKING from app.harness.runner import _clear_step_rows if TYPE_CHECKING: from pathlib import Path def _mk_db(tmp_path: Path) -> Path: """构造含 rollout 行、gate 派生行、他 step 行与前缀陷阱行的最小 harness.db。 参数: tmp_path: pytest 临时目录。 返回: harness.db 路径。 """ db = tmp_path / "harness.db" conn = sqlite3.connect(db) conn.execute("CREATE TABLE predictions (run_id TEXT, question_id TEXT)") conn.execute("CREATE TABLE traces (run_id TEXT, question_id TEXT)") conn.execute("CREATE TABLE gate_evidence (run_id TEXT, epoch INTEGER, step INTEGER)") conn.execute("CREATE TABLE quadrant_pair (run_id TEXT, epoch INTEGER, step INTEGER)") rows = [ ("infer_adhoc_e1_s0", "q1"), # rollout 行 ("infer_adhoc_e1_s0_gate_action-reasoning_base", "q2"), # gate base 臂 ("infer_adhoc_e1_s0_gate_action-reasoning_cand", "q3"), # gate cand 臂 ("infer_adhoc_e1_s1", "q4"), # 其他 step,不许误删 ("infer_adhoc_e1_s10_gate_x_base", "q5"), # s10 前缀陷阱,不许误删 ] conn.executemany("INSERT INTO predictions VALUES (?, ?)", rows) conn.executemany("INSERT INTO traces VALUES (?, ?)", rows) conn.execute("INSERT INTO gate_evidence VALUES ('infer_adhoc', 1, 0)") conn.execute("INSERT INTO gate_evidence VALUES ('infer_adhoc', 1, 1)") conn.execute("INSERT INTO quadrant_pair VALUES ('infer_adhoc', 1, 0)") conn.commit() conn.close() return db def test_clear_step_rows_removes_rollout_and_gate_rows(tmp_path) -> None: """rollout 行 + 本 step 全部 gate 派生行被清;他 step 与 s10 前缀陷阱不动。""" db = _mk_db(tmp_path) _clear_step_rows(str(db), baseline_run_id="infer_adhoc", epoch=1, step=0) conn = sqlite3.connect(db) left = {r[0] for r in conn.execute("SELECT run_id FROM predictions")} assert left == {"infer_adhoc_e1_s1", "infer_adhoc_e1_s10_gate_x_base"} left_t = {r[0] for r in conn.execute("SELECT run_id FROM traces")} assert left_t == left ge = list(conn.execute("SELECT step FROM gate_evidence")) assert ge == [(1,)] # 只剩 step=1 的行 assert list(conn.execute("SELECT COUNT(*) FROM quadrant_pair"))[0][0] == 0 conn.close() def test_clear_step_rows_missing_tables_is_noop(tmp_path) -> None: """gate_evidence/quadrant_pair 表尚未建(首个 step)时不报错。""" db = tmp_path / "harness.db" conn = sqlite3.connect(db) conn.execute("CREATE TABLE predictions (run_id TEXT)") conn.execute("CREATE TABLE traces (run_id TEXT)") conn.commit() conn.close() _clear_step_rows(str(db), baseline_run_id="infer_adhoc", epoch=1, step=0) def test_clear_step_rows_like_specials_in_run_id(tmp_path) -> None: """run_id 含 % 与反斜杠时不通配误删他 run 行(LIKE 全特殊字符转义回归锁)。""" db = tmp_path / "harness.db" conn = sqlite3.connect(db) conn.execute("CREATE TABLE predictions (run_id TEXT, question_id TEXT)") conn.execute("CREATE TABLE traces (run_id TEXT, question_id TEXT)") rows = [ (r"we%ird\run_e1_s0", "q1"), # 本 step rollout (r"we%ird\run_e1_s0_gate_x_base", "q2"), # 本 step gate 行 (r"weXird\run_e1_s0_gate_x_base", "q3"), # % 若未转义会误匹配此行 (r"we%irdXrun_e1_s0_gate_x_base", "q4"), # \ 若未转义会误匹配此行 ] conn.executemany("INSERT INTO predictions VALUES (?, ?)", rows) conn.executemany("INSERT INTO traces VALUES (?, ?)", rows) conn.commit() conn.close() _clear_step_rows(str(db), baseline_run_id=r"we%ird\run", epoch=1, step=0) conn = sqlite3.connect(db) left = {r[0] for r in conn.execute("SELECT run_id FROM predictions")} conn.close() assert left == {r"weXird\run_e1_s0_gate_x_base", r"we%irdXrun_e1_s0_gate_x_base"}