Files

537 lines
22 KiB
Python
Raw Permalink 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.
"""审计修复批 6 测试:B1、I1、I2、M1、M2、M5、M6、M7。
只用注入的假 runner 与 init_db 的本地库,不跑真实模型回合、
不联网。按任务书:M3(报告措辞)、M4(观察项)、M8/M9
(已知边界)不需要代码,本批只在报告里写明不做;N-12 的
goal 包前言续跑缺口不在本批范围。
行为变更锁定(本批有意改语义,测试即契约):
- B1:run_one 入口初始化 _gates_checked=[],零门任务两轮后
passed_gates 不再含硬编码三门;政策任务标记不受影响。
- I1:_GATE_ERROR_MARKERS 补全长措辞
"known to lack a required capability",无结构化行时显式拦
与复核拒绝仍归 gate_rejected 且有回落审计行。
- I2:接线 error 时返回行实际状态(不再是输入终局),
run_one 终局块在行已不是终态时抑制终态审计与通知;
批 5 的 test_escalate_cas_miss_audits_skipped 断言意图
随之改写(final == 行状态 queued,不再是输入终局 failed)。
- M1:轮次耗尽 + 无 finding 时 reason 为 max_rounds_exceeded
(不再被 gate_failed_without_finding 掩盖),决策不变。
- M2:两处 worker_routing 写点均以 chosen 结尾(行尾约定)。
- M5:升级结果写回有界重试一次(仍带 only_if_status 守卫)。
- M6:_coord_clear_packets 在清包前即失败时包列原样不动。
- M7:pass 清包 CAS 未命中按 I2 新行为断言。
"""
import json
import os
import sqlite3
import sys
import tempfile
import time
import unittest
from pathlib import Path
from types import SimpleNamespace
from unittest import mock
try:
from cloud.audit_loop import LoopState, evaluate
from cloud.auth import AuthStore
from cloud.routing_shadow import parse_worker_routing_detail
from cloud.server import init_db
from cloud.worker import Worker, _GATE_ERROR_MARKERS
import cloud.worker as worker_module
except ImportError: # discover -s cloud/tests 时把项目根目录补进 sys.path
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from cloud.audit_loop import LoopState, evaluate
from cloud.auth import AuthStore
from cloud.routing_shadow import parse_worker_routing_detail
from cloud.server import init_db
from cloud.worker import Worker, _GATE_ERROR_MARKERS
import cloud.worker as worker_module
def _insert_task(db_path, task_id, model=None, status="queued",
project_id="p1", audit_loop=0, parent=None, part=None,
error=None, result=None, packet=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,"
" model, audit_loop_packet)"
" VALUES(?, ?, 'do work', ?, NULL, '[]', 0, 'tester',"
" ?, ?, NULL, ?, ?, ?, ?, ?, ?, ?)",
(task_id, project_id, status, now, now, audit_loop,
parent, part, result, error, model, packet))
conn.commit()
finally:
conn.close()
def _set_started_at(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 _insert_audit(db_path, project_id, action, detail, created_at=None):
conn = sqlite3.connect(db_path)
try:
conn.execute(
"INSERT INTO audit_log(user_id, project_id, action, detail,"
" created_at) VALUES(NULL, ?, ?, ?, ?)",
(project_id, action, detail,
created_at if created_at is not None else time.time()))
conn.commit()
finally:
conn.close()
def _task_row(db_path, task_id):
conn = sqlite3.connect(db_path)
try:
return conn.execute(
"SELECT status, audit_loop_packet FROM tasks WHERE id = ?",
(task_id,)).fetchone()
finally:
conn.close()
def _state_row(db_path, task_id):
conn = sqlite3.connect(db_path)
try:
return conn.execute(
"SELECT round, passed_gates, last_decision"
" FROM task_audit_loop_state WHERE task_id = ?",
(task_id,)).fetchone()
finally:
conn.close()
def _seed_state(db_path, task_id, round_no, last_decision,
passed='["execution"]'):
conn = sqlite3.connect(db_path)
try:
conn.execute(
"CREATE TABLE IF NOT EXISTS task_audit_loop_state("
"task_id TEXT PRIMARY KEY, round INTEGER,"
" passed_gates TEXT, criteria TEXT, last_decision TEXT,"
" updated_at DOUBLE PRECISION)")
conn.execute(
"INSERT OR REPLACE INTO task_audit_loop_state"
"(task_id, round, passed_gates, criteria, last_decision,"
" updated_at) VALUES(?, ?, ?, ?, ?, ?)",
(task_id, round_no, passed, '{}', last_decision,
time.time()))
conn.commit()
finally:
conn.close()
def _routing_details(db_path):
conn = sqlite3.connect(db_path)
try:
return [r[0] for r in conn.execute(
"SELECT detail FROM audit_log WHERE action = 'worker_routing'"
" ORDER BY id").fetchall()]
finally:
conn.close()
def _completed_runner(cmd):
name = cmd[cmd.index("--name") + 1] if "dispatch" in cmd else "t"
return SimpleNamespace(
returncode=0,
stdout=json.dumps({"task": name, "status": "completed",
"text": "done"}),
stderr="")
def _failing_runner(cmd):
name = cmd[cmd.index("--name") + 1] if "dispatch" in cmd else "t"
return SimpleNamespace(
returncode=0,
stdout=json.dumps({"task": name, "status": "failed",
"text": "boom"}),
stderr="")
def _worker(db_path, policy_doc=None, env=None, gateway=None,
runner=None):
calls = []
def recording(cmd):
calls.append(list(cmd))
return (runner or _completed_runner)(cmd)
worker = Worker(db_path, "/fake/host.py", runner=recording,
env=env or {"XCM_API_KEY": "test-placeholder"},
routing_policy=policy_doc)
if gateway is not None:
worker._ensure_gateway = lambda task: gateway
worker._sub2api_default_model = lambda e: "gateway-model"
worker._calls = calls
return worker
class _RunOneCase(unittest.TestCase):
"""run_one 级夹具(批 4 GateResultsCheckedTestCase 同款口径)。"""
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)
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()
class B1ZeroGateTestCase(_RunOneCase):
"""B1:run_one 入口初始化 _gates_checked,零门不再硬编码三门。"""
def test_explicit_zero_gate_two_rounds_passed_gates_empty(self):
# 显式任务、无网关、无策略、无工具、开审计循环:
# 旧代码两轮后 passed_gates 是三门硬编码(漏网的直接
# 原因正是本例缺 passed_gates 断言);新代码应为空集。
_insert_task(self.db_path, "T3", model="explicit-model",
audit_loop=1)
worker = _worker(self.db_path, gateway=(None, {}),
runner=_failing_runner)
for _ in range(2):
out = worker.run_one()
self.assertEqual(out["status"], "queued")
row = _state_row(self.db_path, "T3")
self.assertIsNotNone(row)
self.assertEqual(row[0], 2)
self.assertEqual(json.loads(row[1]), [])
self.assertEqual(row[2], "pushback")
def test_policy_two_rounds_marks_survive_init(self):
# 对照:政策任务本轮实际跑过的 capability 标记不能被
# 入口初始化冲掉,两轮后 passed_gates 仍为 ["capability"]。
_insert_task(self.db_path, "T2", audit_loop=1)
worker = _worker(self.db_path, {
"mode": "active",
"tiers": {"default": {"model": "policy-model",
"requires": {"tool_calling": True}}},
"model_capabilities": {
"policy-model": {"tool_calling": True},
},
}, gateway=(None, {}), runner=_failing_runner)
for _ in range(2):
out = worker.run_one()
self.assertEqual(out["status"], "queued")
row = _state_row(self.db_path, "T2")
self.assertIsNotNone(row)
self.assertEqual(row[0], 2)
self.assertEqual(json.loads(row[1]), ["capability"])
self.assertEqual(row[2], "pushback")
class I1MarkerTestCase(unittest.TestCase):
"""I1:无结构化行时显式拦与复核拒绝仍归 gate_rejected。"""
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)
store = AuthStore(self.db_path)
store.conn.close()
self.audits = []
self.worker = Worker(self.db_path, "/fake/host.py",
runner=_completed_runner,
env={"XCM_API_KEY": "test-placeholder"},
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 _merge(self, child_error, audit_rows=()):
now = time.time()
_insert_task(self.db_path, "P1", status="running", audit_loop=1,
parent=None)
_insert_task(self.db_path, "C1", status="failed", parent="P1",
part="a", error=child_error)
_set_started_at(self.db_path, "C1", now - 100)
for action, detail in audit_rows:
_insert_audit(self.db_path, "p1", action, detail,
created_at=now)
return self.worker._coord_audit_loop_merge(
{"id": "P1", "project_id": "p1", "audit_loop": 1},
[{"name": "a", "prompt": "do a"}],
[{"id": "C1", "name": "a"}])
def test_explicit_block_error_only_is_gate_rejected(self):
# 显式网关拦的生产方 error 文案(worker.py 网关块),
# 无 gate_rejected 行、无 gateway_explicit 行时仍须
# 走子串回落归 gate_rejected(旧代码误归 execution
# 而推回)。回落审计行须点名新补的 marker。
out = self._merge(
"default tier explicit model m lacks capability:"
" tool_calling; refusing to dispatch a model known to"
" lack a required capability")
self.assertEqual(out["decision"], "escalate")
self.assertIn("a", out.get("gate_rejected", []))
self.assertEqual(_task_row(self.db_path, "C1")[0], "failed")
marker_rows = [d for _p, a, d in self.audits
if a == "worker_coord_gate_classified_by_marker"]
self.assertTrue(marker_rows)
self.assertIn("C1", marker_rows[0])
self.assertIn("marker=known to lack a required capability",
marker_rows[0])
def test_recheck_reject_error_only_is_gate_rejected(self):
# 写回复核拒绝的生产方 error 文案(worker.py 复核块),
# 同款无结构化行情形。
out = self._merge(
"default tier persisted model m lacks capability:"
" tool_calling; refusing to dispatch a model known to"
" lack a required capability")
self.assertEqual(out["decision"], "escalate")
self.assertIn("a", out.get("gate_rejected", []))
self.assertEqual(_task_row(self.db_path, "C1")[0], "failed")
marker_rows = [d for _p, a, d in self.audits
if a == "worker_coord_gate_classified_by_marker"]
self.assertTrue(marker_rows)
self.assertIn("C1", marker_rows[0])
self.assertIn("marker=known to lack a required capability",
marker_rows[0])
class _WireCase(unittest.TestCase):
"""接线级夹具(批 5 _WorkerCase 同款口径,另加 run_one 级
测试所需的 CODEX_PUSH_ALLOW_PRIVATE_TARGETS 环境)。"""
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.audits = []
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 _worker(self, **kwargs):
kwargs.setdefault("env", {"XCM_API_KEY": "test-placeholder"})
kwargs.setdefault("max_retries", 0)
kwargs.setdefault("retry_backoff_seconds", 0)
kwargs.setdefault("runner", _completed_runner)
worker = Worker(self.db_path, "/fake/host.py", **kwargs)
worker._audit = lambda p, a, d="": self.audits.append((p, a, d))
return worker
def _actions(self):
return [a for _, a, _ in self.audits]
class I2WireTestCase(_WireCase):
"""I2 + M7:接线 error 返回行实际状态;pass CAS 未命中同款。"""
def test_pass_cas_miss_returns_row_status_and_keeps_packet(self):
# M7:pass 清包 CAS 未命中——行已被并发重排回 queued,
# 接线返回 error + 行状态 queued(不再是输入终局
# completed),旧包原样不动(事务回滚),状态行不动。
_insert_task(self.db_path, "TP2", status="queued", audit_loop=1,
packet='{"round": 1}')
_seed_state(self.db_path, "TP2", 1, "pushback")
worker = self._worker()
task = worker.get_task("TP2")
final, _, decision = worker._audit_loop_wire(
task, "completed", {"stdout": ""}, "boom")
self.assertEqual(decision, "error")
self.assertEqual(final, "queued")
row = _task_row(self.db_path, "TP2")
self.assertEqual(row[0], "queued")
self.assertEqual(row[1], '{"round": 1}')
state = _state_row(self.db_path, "TP2")
self.assertEqual(state[0], 1)
self.assertEqual(state[2], "pushback")
self.assertNotIn("worker_audit_loop_passed", self._actions())
def test_run_one_suppresses_terminal_on_error_nonterminal(self):
# I2 终局块:接线 error 且行已不是终态时,抑制终态审计
# 与 failed 通知,返回体状态与行一致(queued)。
_insert_task(self.db_path, "TS", status="queued", audit_loop=1)
worker = self._worker(runner=_failing_runner)
notifies = []
worker._notify_task_finished = lambda *a, **k: notifies.append(
(a, k))
worker._audit_loop_wire = lambda t, f, r, e: (
"queued", "boom", "error")
out = worker.run_one()
self.assertEqual(out["status"], "queued")
self.assertNotIn("worker_failed", self._actions())
self.assertNotIn("worker_completed", self._actions())
self.assertFalse([n for n in notifies if n[0][2] == "failed"])
def test_run_one_terminal_still_runs_on_error_terminal(self):
# 对照:接线 error 但行仍是终态 failed 时,终态审计与
# failed 通知照常跑——抑制只针对“行已不是终态”情形,
# 不是见 error 就全抑。
_insert_task(self.db_path, "TF", status="queued", audit_loop=1)
worker = self._worker(runner=_failing_runner)
notifies = []
worker._notify_task_finished = lambda *a, **k: notifies.append(
(a, k))
worker._audit_loop_wire = lambda t, f, r, e: (
"failed", "boom", "error")
out = worker.run_one()
self.assertEqual(out["status"], "failed")
self.assertIn("worker_failed", self._actions())
self.assertTrue([n for n in notifies if n[0][2] == "failed"])
class M1MaxRoundsLabelTestCase(unittest.TestCase):
"""M1:轮次耗尽 + 无 finding 时 reason 为 max_rounds_exceeded。"""
def test_max_rounds_label_wins_over_no_finding(self):
# 上一轮未通过过的门本轮失败、无 finding、无回归、无缺门:
# 旧代码 reason 是 gate_failed_without_finding;新代码
# max_rounds 分支在前,reason 为 max_rounds_exceeded,
# 决策同为 escalate、轮次不变。
state = LoopState("T1", round=3, passed_gates=[],
criteria={}, last_decision="pushback")
out = evaluate(state, "T1", {"execution": False}, findings=[])
self.assertEqual(out["decision"], "escalate")
self.assertEqual(out["reason"], "max_rounds_exceeded")
self.assertEqual(out["new_state"].round, 3)
self.assertEqual(out["new_state"].last_decision, "escalate")
class M2ChosenLastTestCase(_RunOneCase):
"""M2:两处 worker_routing 写点均以 chosen 结尾(行尾约定)。"""
def _assert_chosen_last(self, details):
self.assertTrue(details)
for detail in details:
parsed = parse_worker_routing_detail(detail)
self.assertIsNotNone(parsed, detail)
last_token = detail.strip().rsplit(" ", 1)[-1]
self.assertTrue(last_token.startswith("chosen="), detail)
def test_policy_routing_row_ends_with_chosen(self):
# 策略/shadow 决策路径的 worker_routing 行。
_insert_task(self.db_path, "M1", audit_loop=0)
worker = _worker(self.db_path, {
"mode": "shadow",
"tiers": {"default": "policy-model"},
}, gateway=(None, {}), runner=_failing_runner)
worker.run_one()
details = _routing_details(self.db_path)
self.assertTrue([d for d in details if "source=shadow" in d])
self._assert_chosen_last(
[d for d in details if "source=shadow" in d])
def test_explicit_routing_row_ends_with_chosen(self):
# 显式任务路径的 worker_routing 行(source=explicit)。
_insert_task(self.db_path, "M2", model="explicit-model",
audit_loop=0)
worker = _worker(self.db_path, gateway=(None, {}),
runner=_failing_runner)
worker.run_one()
details = _routing_details(self.db_path)
self.assertTrue([d for d in details if "source=explicit" in d])
self._assert_chosen_last(
[d for d in details if "source=explicit" in d])
class M5EscalateRetryTestCase(_WireCase):
"""M5:升级结果写回有界重试一次(仍带 failed 守卫)。"""
def _escalating_worker(self):
_insert_task(self.db_path, "TE", status="failed", audit_loop=1)
_seed_state(self.db_path, "TE", 3, "pushback")
return self._worker()
def test_escalate_result_write_retried_once(self):
# 第一次写失败、第二次成功:接线仍返回 escalate,
# 写回恰好调两次(重试生效,且不是无界重试)。
worker = self._escalating_worker()
calls = []
real = worker._write_execution_result
def flaky(*a, **k):
calls.append(1)
if len(calls) == 1:
raise RuntimeError("transient write failure")
return real(*a, **k)
worker._write_execution_result = flaky
task = worker.get_task("TE")
final, _, decision = worker._audit_loop_wire(
task, "failed", {"stdout": ""}, "boom")
self.assertEqual(decision, "escalate")
self.assertEqual(final, "failed")
self.assertEqual(len(calls), 2)
def test_escalate_result_write_bounded_on_repeated_failure(self):
# 两次都失败:仍返回 escalate(不谎报成 error),
# 写回恰好调两次,不会无界重试。
worker = self._escalating_worker()
worker._write_execution_result = mock.Mock(
side_effect=RuntimeError("persistent write failure"))
task = worker.get_task("TE")
final, _, decision = worker._audit_loop_wire(
task, "failed", {"stdout": ""}, "boom")
self.assertEqual(decision, "escalate")
self.assertEqual(final, "failed")
self.assertEqual(worker._write_execution_result.call_count, 2)
class M6ClearPacketsEarlyFailureTestCase(_WireCase):
"""M6:清包前即失败时包列原样不动(旧测试只在清干净后注错)。"""
def test_clear_packets_failure_before_clear_keeps_packets(self):
_insert_task(self.db_path, "PP", status="failed", audit_loop=1,
packet='{"round": 1}')
_insert_task(self.db_path, "CC", status="failed", audit_loop=0,
packet='{"round": 1}')
worker = self._worker()
def _boom(path):
raise RuntimeError("injected connect failure")
with mock.patch.object(worker_module, "_db_connect", _boom):
# 不抛错,且包列原样不动——失败不改已定结局。
worker._coord_clear_packets(
{"id": "PP", "project_id": "p1"}, ["CC"])
self.assertEqual(_task_row(self.db_path, "PP")[1],
'{"round": 1}')
self.assertEqual(_task_row(self.db_path, "CC")[1],
'{"round": 1}')