rehearsal: union task_attempts schema (main old DDL + gap-3 ledger cols), relax 2 hook no-table asserts to no-rows, gateway-env fake tolerates main routing reads, extend single-writer fixture list by name (test-only inserts)
This commit is contained in:
1 parent
f76a566aa8
commit
1eefe17474
4 files changed
+90
-3
No files matched your search
+42
-1
@@ -168,11 +168,52 @@ OLD_TABLE_ALTER = (
|
||||
)
|
||||
|
||||
|
||||
def _existing_columns(conn, table):
|
||||
"""已有列集合(automations.py 同款口径:SQLite 走 PRAGMA,
|
||||
其它后端异常时回空集、后续 ALTER 逐条 try/except 兜底)。"""
|
||||
try:
|
||||
return {row[1] for row in conn.execute(
|
||||
"PRAGMA table_info(%s)" % table).fetchall()}
|
||||
except Exception:
|
||||
return set()
|
||||
|
||||
|
||||
def ensure_task_attempts_table(conn):
|
||||
"""Create the ledger table if absent. Idempotent; does not
|
||||
touch existing rows. The caller owns the commit (``record_attempt``
|
||||
commits; a bare ensure inside a larger transaction does not)."""
|
||||
commits; a bare ensure inside a larger transaction does not).
|
||||
|
||||
演练注 FLAG(合入语义取舍点,主结论见演练报告):
|
||||
main 侧 init_db/worker 用 cloud.attempts.py 的旧 DDL 建同名
|
||||
task_attempts 表(kind/interrupted_reason/checkpoint_round/
|
||||
result_text/error_text,无 outcome 系列);本模块的 CREATE
|
||||
TABLE IF NOT EXISTS 在旧表已存在时是空操作,之后按台账
|
||||
列写行即报 no column named outcome。本解法把表升级为两
|
||||
组列的并集:旧表先建时补台账 4 列,台账表先建时补旧 5
|
||||
列(新增列一律可空/带默认值,绝不对已有行加 NOT NULL,
|
||||
否则旧行会挡住 ALTER)。两边读路径均按列名取数,互不
|
||||
干扰;但这只是演练期保守解法——真合前须用户定:并集表、
|
||||
台账改名、或统一 schema 三选一,不可悄悄定为并集表。
|
||||
"""
|
||||
conn.execute(SQLITE_DDL)
|
||||
existing = _existing_columns(conn, "task_attempts")
|
||||
for col, ddl in (
|
||||
("outcome", "TEXT"),
|
||||
("is_defect", "INTEGER DEFAULT 0"),
|
||||
("defect_category", "TEXT"),
|
||||
("corrects_attempt_no", "INTEGER"),
|
||||
("kind", "TEXT"),
|
||||
("interrupted_reason", "TEXT"),
|
||||
("checkpoint_round", "INTEGER"),
|
||||
("result_text", "TEXT"),
|
||||
("error_text", "TEXT")):
|
||||
if col not in existing:
|
||||
try:
|
||||
conn.execute(
|
||||
"ALTER TABLE task_attempts ADD COLUMN %s %s"
|
||||
% (col, ddl))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def record_attempt(conn, task_id, outcome, is_defect=False,
|
||||
|
||||
@@ -149,6 +149,19 @@ def _table_exists(db_path, table):
|
||||
return row[0] == 1
|
||||
|
||||
|
||||
def _task_attempts_row_count(db_path):
|
||||
"""task_attempts 行数(表不存在时计 0;COUNT(*) 与列集无关,
|
||||
旧 DDL 表与并集表均可查)。"""
|
||||
conn = sqlite3.connect(db_path)
|
||||
try:
|
||||
if not _table_exists(db_path, "task_attempts"):
|
||||
return 0
|
||||
return conn.execute(
|
||||
"SELECT COUNT(*) FROM task_attempts").fetchone()[0]
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def _completed_runner(cmd):
|
||||
"""Fake CLI: dispatch/run always succeed, host.py-style JSON."""
|
||||
if "dispatch" in cmd:
|
||||
@@ -466,7 +479,11 @@ class TestWorkerHook(unittest.TestCase):
|
||||
self.assertFalse(worker.attempt_ledger_hook)
|
||||
result = worker.run_one()
|
||||
self.assertEqual(result["status"], "completed")
|
||||
self.assertFalse(_table_exists(self.db_path, "task_attempts"))
|
||||
# 演练注:main 侧 init_db 已先建同名旧 DDL 的
|
||||
# task_attempts 表(见 attempt_ledger 并集表说明),
|
||||
# “表不存在”在合入后不再是可观测口径;保留的意图是
|
||||
# “关时不写行”,故此处断言零行而非表不存在。
|
||||
self.assertEqual(_task_attempts_row_count(self.db_path), 0)
|
||||
self.assertFalse(
|
||||
_table_exists(self.db_path, "task_attempt_origin_markers"))
|
||||
|
||||
@@ -559,7 +576,9 @@ class TestWorkerHook(unittest.TestCase):
|
||||
worker = self._worker(attempt_ledger_hook=True, env={})
|
||||
result = worker.run_one()
|
||||
self.assertEqual(result["status"], "failed")
|
||||
self.assertFalse(_table_exists(self.db_path, "task_attempts"))
|
||||
# 演练注:同上,门失败不调钩子、故零行;表本身由
|
||||
# main 侧 init_db 预建,不再断言表不存在。
|
||||
self.assertEqual(_task_attempts_row_count(self.db_path), 0)
|
||||
|
||||
def test_hook_on_cancelled_dispatch_writes_interrupted_row(self):
|
||||
# Established pattern (test_worker
|
||||
|
||||
@@ -43,10 +43,25 @@ from pathlib import Path
|
||||
_PATTERN = re.compile(r"INSERT\s+INTO\s+audit_log", re.IGNORECASE)
|
||||
# Fixture files that deliberately insert audit_log rows into
|
||||
# throwaway test databases (see module docstring).
|
||||
# 演练注:合入时 main 侧 9 个审计测试文件同样只往临时库插
|
||||
# audit_log 行(TemporaryDirectory/control.db,逐文件核过),
|
||||
# 与已列 3 个属同类 fixture,按名补入——生产侧口径不变,
|
||||
# 仍只允许 cloud/auth.py。若用户要更严口径(测试也一律走
|
||||
# AuthStore.audit),那是另起一批改 9 个文件的活,本演练
|
||||
# 不悄悄替用户定(见演练报告 FLAG)。
|
||||
_FIXTURE_FILES = {
|
||||
"cloud/tests/test_tamper_ledger.py",
|
||||
"cloud/tests/test_chat_history.py",
|
||||
"cloud/tests/test_automation_run_due.py",
|
||||
"cloud/tests/test_95_code.py",
|
||||
"cloud/tests/test_audit_batch4.py",
|
||||
"cloud/tests/test_audit_batch5.py",
|
||||
"cloud/tests/test_audit_batch6.py",
|
||||
"cloud/tests/test_audit_fixes4.py",
|
||||
"cloud/tests/test_audit_fixes5.py",
|
||||
"cloud/tests/test_audit_loop_coord.py",
|
||||
"cloud/tests/test_audit_loop_coord_fix.py",
|
||||
"cloud/tests/test_routing_shadow.py",
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -374,6 +374,18 @@ class GatewayTestCase(_Base):
|
||||
# 2564-2565
|
||||
class _BadEnv:
|
||||
def get(self, *a, **k):
|
||||
# 演练注:main 侧 Worker.__init__ 新增路由策略
|
||||
# 环境读取(CLOUD_ROUTING_POLICY_FILE/JSON,
|
||||
# 未包在 try 内);假 env 若全键抛错,构造即
|
||||
# 炸、测不到 key_available 本身。故仅对路由
|
||||
# 策略键回 None(= 未配策略),其余键仍抛错,
|
||||
# key_available 的失败回 False 断言意图不变。
|
||||
# 另:__init__ 两处裸 env.get 是否应包 try,
|
||||
# 属 main 侧跟进项,本演练不改生产 __init__。
|
||||
key = a[0] if a else k.get("key")
|
||||
if key in ("CLOUD_ROUTING_POLICY_FILE",
|
||||
"CLOUD_ROUTING_POLICY_JSON"):
|
||||
return None
|
||||
raise RuntimeError("env down")
|
||||
|
||||
w = Worker(self.db_path, "/fake/host.py", env=_BadEnv())
|
||||
|
||||
Reference in new issue
Block a user