Files
codex-app-server/cloud/tests/test_audit_loop_coord_fix.py
T

284 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""协调员审计循环复审修复测试:B1 + I1 + I2 + I3。
只用 init_db 的本地库与注入的假 runner,不跑真实模型回合、
不联网。覆盖(按任务书):
- B1:旧门拒绝审计行(早于本轮 started_at)不再误判新失败,
子任务执行失败应推回而非升级;本轮门拒绝审计行仍升级;
started_at 未知时保守保留门拒绝(fail-closed 向升级)。
- I1:merged 计完成——evaluate_outcome / build_report /
合流 pass 三处对齐;sync 不把 merged 子任务重排。
- I2:合流推回事务里父 CAS 失败时,子任务不被孤儿重排、
包列不写、且有明确审计;推回成功时父任务已由合流
CAS 回 queued(hook 不再另做父 CAS)。
- I3:推回存库分支异常时走升级而非回正常终局,error
写明存库失败、记 escalated 审计;已消耗的轮次不恢复。
"""
import json
import os
import sqlite3
import sys
import tempfile
import time
import unittest
from pathlib import Path
from types import SimpleNamespace
try:
from cloud.coordinator import build_report, evaluate_outcome
from cloud.server import init_db
from cloud.worker import Worker
except ImportError:
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from cloud.coordinator import build_report, evaluate_outcome
from cloud.server import init_db
from cloud.worker import Worker
def _insert(db_path, task_id, status="queued", parent=None, part=None,
result=None, error=None, audit_loop=0, parts=None):
now = time.time()
conn = sqlite3.connect(db_path)
try:
conn.execute(
"INSERT INTO tasks(id, project_id, prompt, status,"
" idempotency_key, depends_on, priority, created_by,"
" created_at, updated_at, last_heartbeat, audit_loop,"
" parent_task_id, coordinator_part, result_text, error_text,"
" coordinator_parts)"
" VALUES(?, 'p1', 'do', ?, NULL, '[]', 0, 'tester',"
" ?, ?, NULL, ?, ?, ?, ?, ?, ?)",
(task_id, status, now, now, audit_loop, parent, part,
result, error,
json.dumps(parts) if parts is not None else None))
conn.commit()
finally:
conn.close()
def _row(db_path, task_id):
conn = sqlite3.connect(db_path)
try:
return conn.execute(
"SELECT status, result_text, error_text, audit_loop_packet"
" FROM tasks WHERE id = ?", (task_id,)).fetchone()
finally:
conn.close()
def _packet(db_path, task_id):
row = _row(db_path, task_id)
if row is None or row[3] is None:
return None
return json.loads(row[3])
def _set_started(db_path, task_id, started_at):
conn = sqlite3.connect(db_path)
try:
conn.execute("UPDATE tasks SET started_at = ? WHERE id = ?",
(started_at, task_id))
conn.commit()
finally:
conn.close()
def _gate_audit(db_path, detail, created_at):
conn = sqlite3.connect(db_path)
try:
conn.execute(
"CREATE TABLE IF NOT EXISTS audit_log(user_id TEXT,"
" project_id TEXT, action TEXT, detail TEXT,"
" created_at DOUBLE PRECISION)")
conn.execute(
"INSERT INTO audit_log(user_id, project_id, action,"
" detail, created_at) VALUES('system', 'p1',"
" 'worker_capability_gate_rejected', ?, ?)",
(detail, created_at))
conn.commit()
finally:
conn.close()
def _completed_runner(cmd):
return SimpleNamespace(returncode=0, stdout="{}", stderr="")
class CoordFixTestCase(unittest.TestCase):
def setUp(self):
self._old = os.environ.get("CODEX_PUSH_ALLOW_PRIVATE_TARGETS")
os.environ["CODEX_PUSH_ALLOW_PRIVATE_TARGETS"] = "1"
self.tmp = tempfile.TemporaryDirectory()
self.db_path = os.path.join(self.tmp.name, "control.db")
init_db(self.db_path)
self.env = {"XCM_API_KEY": "test-placeholder"}
self.audits = []
self.worker = Worker(self.db_path, "/fake/host.py",
runner=_completed_runner, env=self.env,
max_retries=0, retry_backoff_seconds=0)
self.worker._audit = lambda p, a, d="": self.audits.append((p, a, d))
conn = sqlite3.connect(self.db_path)
try:
self.worker._ensure_result_columns(conn)
conn.commit()
finally:
conn.close()
def tearDown(self):
if self._old is None:
os.environ.pop("CODEX_PUSH_ALLOW_PRIVATE_TARGETS", None)
else:
os.environ["CODEX_PUSH_ALLOW_PRIVATE_TARGETS"] = self._old
self.tmp.cleanup()
def _parent(self, task_id="P1"):
return {"id": task_id, "project_id": "p1", "audit_loop": 1}
def _parts(self, *names):
return [{"name": n, "on_failure": "fail"} for n in names]
# ---- B1 ----
def test_b1_old_gate_audit_does_not_block_pushback(self):
# 门拒绝 → 修复 → 执行失败:旧审计行早于本轮 started_at,
# 必须被作用域过滤掉,子任务按执行失败推回而非升级。
_insert(self.db_path, "P1", status="running", audit_loop=1)
_insert(self.db_path, "C1", status="failed", parent="P1",
part="a", error="boom")
now = time.time()
_set_started(self.db_path, "C1", now)
_gate_audit(self.db_path, "C1: old gate rejection", now - 100)
out = self.worker._coord_audit_loop_merge(
self._parent(), self._parts("a"),
[{"name": "a", "id": "C1"}])
self.assertEqual(out["decision"], "pushback")
self.assertEqual(_row(self.db_path, "C1")[0], "queued")
def test_b1_current_gate_audit_still_escalates(self):
# 本轮门拒绝审计行晚于 started_at,仍应升级、不耗轮次。
_insert(self.db_path, "P1", status="running", audit_loop=1)
_insert(self.db_path, "C1", status="failed", parent="P1",
part="a", error="gateway exploded")
now = time.time()
_set_started(self.db_path, "C1", now)
_gate_audit(self.db_path, "C1: new gate rejection", now + 10)
out = self.worker._coord_audit_loop_merge(
self._parent(), self._parts("a"),
[{"name": "a", "id": "C1"}])
self.assertEqual(out["decision"], "escalate")
self.assertEqual(_row(self.db_path, "C1")[0], "failed")
def test_b1_unknown_started_at_is_conservative(self):
# started_at 未知(老行/缺列)时保守保留门拒绝,向升级
# fail-closed,绝不因起始未知就把门拒绝任务推回。
_insert(self.db_path, "P1", status="running", audit_loop=1)
_insert(self.db_path, "C1", status="failed", parent="P1",
part="a", error="gateway exploded")
_gate_audit(self.db_path, "C1: gate rejection",
time.time())
out = self.worker._coord_audit_loop_merge(
self._parent(), self._parts("a"),
[{"name": "a", "id": "C1"}])
self.assertEqual(out["decision"], "escalate")
# ---- I1 ----
def test_i1_merged_counts_as_completed_everywhere(self):
overall, failed, _tol = evaluate_outcome(
[{"name": "a", "status": "merged"},
{"name": "b", "status": "completed"}],
[{"name": "a"}, {"name": "b"}])
self.assertEqual(overall, "completed")
self.assertEqual(failed, [])
_report, overall2 = build_report(
[{"name": "a", "id": "C1", "status": "merged",
"result_text": "done", "error_text": ""},
{"name": "b", "id": "C2", "status": "completed",
"result_text": "done", "error_text": ""}],
[{"name": "a"}, {"name": "b"}])
self.assertEqual(overall2, "completed")
# 合流:merged 子任务按完成计,审计 pass。
_insert(self.db_path, "P1", status="running", audit_loop=1)
_insert(self.db_path, "C1", status="merged", parent="P1",
part="a", result="done")
out = self.worker._coord_audit_loop_merge(
self._parent(), self._parts("a"),
[{"name": "a", "id": "C1"}])
self.assertEqual(out["decision"], "pass")
def test_i1_sync_does_not_requeue_merged_child(self):
# sync 把 merged 当完成直接复用,绝不回 queued 清结果。
_insert(self.db_path, "P1", status="running", audit_loop=1)
_insert(self.db_path, "C1", status="merged", parent="P1",
part="a", result="done")
self.worker._quota_exceeded = lambda *a: None
children = self.worker._coord_sync_children(
{"id": "P1", "project_id": "p1", "audit_loop": 1,
"created_by": "tester", "priority": 0},
[{"name": "a", "prompt": "do a"}])
self.assertEqual(children[0]["id"], "C1")
row = _row(self.db_path, "C1")
self.assertEqual(row[0], "merged")
self.assertEqual(row[1], "done")
# ---- I2 ----
def test_i2_parent_cas_failure_rolls_back_children(self):
# 父任务状态已被改动(模拟并发取消):合流事务回滚,
# 子任务不被孤儿重排、包列不写、且有明确审计。
_insert(self.db_path, "P1", status="interrupted",
audit_loop=1)
_insert(self.db_path, "C1", status="failed", parent="P1",
part="a", error="boom")
out = self.worker._coord_audit_loop_merge(
self._parent(), self._parts("a"),
[{"name": "a", "id": "C1"}])
self.assertEqual(out["decision"], "pushback_parent_cas_failed")
self.assertEqual(_row(self.db_path, "C1")[0], "failed")
self.assertIsNone(_packet(self.db_path, "C1"))
self.assertIsNone(_packet(self.db_path, "P1"))
self.assertTrue(any(
a == "worker_coord_audit_loop_pushback_parent_cas_failed"
for _, a, _ in self.audits))
def test_i2_pushback_parent_already_queued_by_merge(self):
# 推回成功时父任务已由合流事务 CAS 回 queued(hook
# 不再另做父 CAS);子任务重排、包可读。
_insert(self.db_path, "P1", status="running", audit_loop=1)
_insert(self.db_path, "C1", status="failed", parent="P1",
part="a", error="boom")
out = self.worker._coord_audit_loop_merge(
self._parent(), self._parts("a"),
[{"name": "a", "id": "C1"}])
self.assertEqual(out["decision"], "pushback")
self.assertEqual(_row(self.db_path, "P1")[0], "queued")
self.assertEqual(_row(self.db_path, "C1")[0], "queued")
self.assertIsNotNone(_packet(self.db_path, "C1"))
# ---- I3 ----
def test_i3_pushback_store_failure_escalates(self):
# 推回存库分支内任何异常都走升级而非回正常终局:
# 用拆分包抛异常模拟存库分支异常(该异常位于同一
# try 块内,与真实 DB 写失败走同一条 except 路径)。
_insert(self.db_path, "P1", status="running", audit_loop=1)
_insert(self.db_path, "C1", status="failed", parent="P1",
part="a", error="boom")
orig = Worker._coord_split_packet
try:
Worker._coord_split_packet = staticmethod(
lambda *a: (_ for _ in ()).throw(
RuntimeError("db write failed")))
out = self.worker._coord_audit_loop_merge(
self._parent(), self._parts("a"),
[{"name": "a", "id": "C1"}])
finally:
Worker._coord_split_packet = orig
self.assertEqual(out["decision"], "escalate")
self.assertIn("推回存库失败", out["error"])
self.assertTrue(any(a == "worker_coord_audit_loop_escalated"
for _, a, _ in self.audits))
# 升级清包:父包与子包都不应残留。
self.assertIsNone(_packet(self.db_path, "P1"))
self.assertIsNone(_packet(self.db_path, "C1"))
if __name__ == "__main__":
unittest.main()