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):
|
def ensure_task_attempts_table(conn):
|
||||||
"""Create the ledger table if absent. Idempotent; does not
|
"""Create the ledger table if absent. Idempotent; does not
|
||||||
touch existing rows. The caller owns the commit (``record_attempt``
|
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)
|
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,
|
def record_attempt(conn, task_id, outcome, is_defect=False,
|
||||||
|
|||||||
@@ -149,6 +149,19 @@ def _table_exists(db_path, table):
|
|||||||
return row[0] == 1
|
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):
|
def _completed_runner(cmd):
|
||||||
"""Fake CLI: dispatch/run always succeed, host.py-style JSON."""
|
"""Fake CLI: dispatch/run always succeed, host.py-style JSON."""
|
||||||
if "dispatch" in cmd:
|
if "dispatch" in cmd:
|
||||||
@@ -466,7 +479,11 @@ class TestWorkerHook(unittest.TestCase):
|
|||||||
self.assertFalse(worker.attempt_ledger_hook)
|
self.assertFalse(worker.attempt_ledger_hook)
|
||||||
result = worker.run_one()
|
result = worker.run_one()
|
||||||
self.assertEqual(result["status"], "completed")
|
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(
|
self.assertFalse(
|
||||||
_table_exists(self.db_path, "task_attempt_origin_markers"))
|
_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={})
|
worker = self._worker(attempt_ledger_hook=True, env={})
|
||||||
result = worker.run_one()
|
result = worker.run_one()
|
||||||
self.assertEqual(result["status"], "failed")
|
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):
|
def test_hook_on_cancelled_dispatch_writes_interrupted_row(self):
|
||||||
# Established pattern (test_worker
|
# Established pattern (test_worker
|
||||||
|
|||||||
@@ -43,10 +43,25 @@ from pathlib import Path
|
|||||||
_PATTERN = re.compile(r"INSERT\s+INTO\s+audit_log", re.IGNORECASE)
|
_PATTERN = re.compile(r"INSERT\s+INTO\s+audit_log", re.IGNORECASE)
|
||||||
# Fixture files that deliberately insert audit_log rows into
|
# Fixture files that deliberately insert audit_log rows into
|
||||||
# throwaway test databases (see module docstring).
|
# throwaway test databases (see module docstring).
|
||||||
|
# 演练注:合入时 main 侧 9 个审计测试文件同样只往临时库插
|
||||||
|
# audit_log 行(TemporaryDirectory/control.db,逐文件核过),
|
||||||
|
# 与已列 3 个属同类 fixture,按名补入——生产侧口径不变,
|
||||||
|
# 仍只允许 cloud/auth.py。若用户要更严口径(测试也一律走
|
||||||
|
# AuthStore.audit),那是另起一批改 9 个文件的活,本演练
|
||||||
|
# 不悄悄替用户定(见演练报告 FLAG)。
|
||||||
_FIXTURE_FILES = {
|
_FIXTURE_FILES = {
|
||||||
"cloud/tests/test_tamper_ledger.py",
|
"cloud/tests/test_tamper_ledger.py",
|
||||||
"cloud/tests/test_chat_history.py",
|
"cloud/tests/test_chat_history.py",
|
||||||
"cloud/tests/test_automation_run_due.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
|
# 2564-2565
|
||||||
class _BadEnv:
|
class _BadEnv:
|
||||||
def get(self, *a, **k):
|
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")
|
raise RuntimeError("env down")
|
||||||
|
|
||||||
w = Worker(self.db_path, "/fake/host.py", env=_BadEnv())
|
w = Worker(self.db_path, "/fake/host.py", env=_BadEnv())
|
||||||
|
|||||||
Reference in new issue
Block a user