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

594 lines
24 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.
"""审计批 5(I-9、I-14、N-1~N-12、I-12、事务分离)测试。
只用注入的假 runner 与 init_db 的本地库,不跑真实模型回合、
不联网。按任务书:新测试 + 相关旧套件确认无回归;本文件
只覆盖本批的新行为与新契约,旧行为由旧套件锁定。
"""
import http.client
import json
import os
import sqlite3
import sys
import tempfile
import threading
import time
import unittest
from pathlib import Path
from types import SimpleNamespace
from unittest import mock
try:
from cloud.agent_def import (
missing_required_tools,
parse_agent_tools_column,
)
from cloud.audit_loop import Finding
from cloud.auth import AuthStore
from cloud.models import parse_routing_policy
from cloud.routing_dryrun import dry_run_policy_document
from cloud.server import CloudControlPlaneHandler, create_server, init_db
from cloud.worker import Worker
import cloud.worker as worker_module
import cloud.server as server_module
except ImportError:
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from cloud.agent_def import (
missing_required_tools,
parse_agent_tools_column,
)
from cloud.audit_loop import Finding
from cloud.auth import AuthStore
from cloud.models import parse_routing_policy
from cloud.routing_dryrun import dry_run_policy_document
from cloud.server import CloudControlPlaneHandler, create_server, init_db
from cloud.worker import Worker
import cloud.worker as worker_module
import cloud.server as server_module
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 _insert_task(db_path, task_id, status="queued", audit_loop=0,
goal_mode=0, goal=None, verify_command=None,
goal_max_rounds=None, model=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,"
" goal_mode, goal, verify_command, goal_max_rounds, model,"
" audit_loop_packet)"
" VALUES(?, 'p1', 'do work', ?, NULL, '[]', 0, 'tester',"
" ?, ?, NULL, ?, ?, ?, ?, ?, ?, ?)",
(task_id, status, now, now, audit_loop, goal_mode, goal,
verify_command, goal_max_rounds, model, packet))
conn.commit()
finally:
conn.close()
def _set_status(db_path, task_id, status):
conn = sqlite3.connect(db_path)
try:
conn.execute("UPDATE tasks SET status = ? WHERE id = ?",
(status, task_id))
conn.commit()
finally:
conn.close()
def _row(db_path, task_id):
conn = sqlite3.connect(db_path)
try:
return conn.execute(
"SELECT status, audit_loop_packet, goal, verify_command,"
" goal_max_rounds, model 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, 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):
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, '["execution"]', '{}', last_decision,
time.time()))
conn.commit()
finally:
conn.close()
class _FailingExecuteConn:
"""注入失败的连接包装:指定 SQL 片段一律抛错,其余透传。"""
def __init__(self, real, needle):
self._real = real
self._needle = needle
def execute(self, sql, params=()):
if self._needle in sql:
raise RuntimeError("injected store failure")
return self._real.execute(sql, params)
def __getattr__(self, name):
return getattr(self._real, name)
class _WorkerCase(unittest.TestCase):
def setUp(self):
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):
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 FingerprintTestCase(unittest.TestCase):
def test_fallback_models_change_fingerprint(self):
# I-14①:指纹宣称含 fallback_models,变化必须敏感。
base = {"mode": "shadow", "tiers": {"default": "m1"},
"model_capabilities": {"m1": {"tool_calling": True}}}
a = parse_routing_policy(base)
b = parse_routing_policy(
dict(base, fallback_models=["m2"]))
self.assertNotEqual(a.content_fingerprint(),
b.content_fingerprint())
c = parse_routing_policy(dict(base, fallback_models=["m2"]))
self.assertEqual(b.content_fingerprint(),
c.content_fingerprint())
class WireTxTestCase(_WorkerCase):
def _patched_worker(self, needle):
real_connect = worker_module._db_connect
def _connect(path):
return _FailingExecuteConn(real_connect(path), needle)
patcher = mock.patch.object(worker_module, "_db_connect",
_connect)
patcher.start()
self.addCleanup(patcher.stop)
return self._worker()
def test_pushback_store_failure_rolls_back_state(self):
# 事务分离:推回 UPDATE 失败时状态保存一并回滚,任务不
# 以“已推回状态 + failed 任务行”的半截态落地。
_insert_task(self.db_path, "TP", status="failed", audit_loop=1)
worker = self._patched_worker("SET status = 'queued'")
task = worker.get_task("TP")
final, _, decision = worker._audit_loop_wire(
task, "failed", {"stdout": ""}, "boom")
self.assertEqual(decision, "error")
self.assertEqual(final, "failed")
row = _row(self.db_path, "TP")
self.assertEqual(row[0], "failed")
self.assertIsNone(row[1])
self.assertIsNone(_state_row(self.db_path, "TP"))
self.assertNotIn("worker_audit_loop_pushback", self._actions())
def test_escalate_store_failure_claims_nothing(self):
# I-14②(改动后新行为):升级 UPDATE 失败时接线返回
# error、任务保持原终局、状态行不动,且不记“已升级”
# 审计——升级审计只在提交后记,绝不谎称已升级。
_insert_task(self.db_path, "TE", status="failed", audit_loop=1)
_seed_state(self.db_path, "TE", 3, "pushback")
worker = self._patched_worker("SET status = 'failed'")
task = worker.get_task("TE")
final, _, decision = worker._audit_loop_wire(
task, "failed", {"stdout": ""}, "boom")
self.assertEqual(decision, "error")
self.assertEqual(final, "failed")
self.assertEqual(_row(self.db_path, "TE")[0], "failed")
state = _state_row(self.db_path, "TE")
self.assertEqual(state[0], 3)
self.assertEqual(state[1], "pushback")
self.assertNotIn("worker_audit_loop_escalated", self._actions())
def test_escalate_cas_miss_audits_skipped(self):
# N-2:升级 CAS 未命中(状态已被改动)时记
# escalate_skipped 审计、不谎称已升级,任务与状态原样不动。
_insert_task(self.db_path, "TC", status="failed", audit_loop=1)
_seed_state(self.db_path, "TC", 3, "pushback")
_set_status(self.db_path, "TC", "queued")
worker = self._worker()
task = worker.get_task("TC")
final, _, decision = worker._audit_loop_wire(
task, "failed", {"stdout": ""}, "boom")
self.assertEqual(decision, "error")
self.assertEqual(final, "failed")
self.assertEqual(_row(self.db_path, "TC")[0], "queued")
self.assertEqual(_state_row(self.db_path, "TC")[0], 3)
self.assertIn("worker_audit_loop_escalate_skipped",
self._actions())
self.assertNotIn("worker_audit_loop_escalated", self._actions())
class CoordClearTestCase(_WorkerCase):
def test_clear_packets_failure_is_best_effort(self):
# I-14③:清包失败只记警告、不炸调用方、不改已定结局。
_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()
worker._coord_clear_packets(
{"id": "PP", "project_id": "p1"}, ["CC"])
self.assertIsNone(_row(self.db_path, "PP")[1])
self.assertIsNone(_row(self.db_path, "CC")[1])
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"])
class ClassifyAndSplitTestCase(_WorkerCase):
def test_merge_failed_is_gate_rejected(self):
# N-3:merge-failed 归 gate_rejected,不再是
# excluded/non_terminal;容忍声明仍优先。
worker = self._worker()
kind, reason = worker._coord_classify_child(
None, {"id": "C1", "status": "merge-failed"}, set())
self.assertEqual((kind, reason), ("gate_rejected", "merge_failed"))
kind, _ = worker._coord_classify_child(
{"on_failure": "tolerate"},
{"id": "C2", "status": "merge-failed"}, set())
self.assertEqual(kind, "excluded")
kind, _ = worker._coord_classify_child(
None, {"id": "C3", "status": "merged"}, set())
self.assertEqual(kind, "completed")
def test_wire_splits_subjective_before_evaluate(self):
# N-4:单任务接线先拆分再评估——确定性照常推回,主观
# 只记审计,不吞推回。
_insert_task(self.db_path, "TS", status="failed", audit_loop=1)
worker = self._worker()
det = Finding("execution_failed", "high", "boom", "re-run",
"task completes", kind="deterministic",
gate="execution")
subj = Finding("design_note", "low", "looks odd", "review",
"human ok", kind="subjective", gate=None)
worker._audit_loop_gate_results = (
lambda task, final, result, gates_checked=None: (
{"execution": False}, [det, subj]))
task = worker.get_task("TS")
final, _, decision = worker._audit_loop_wire(
task, "failed", {"stdout": ""}, "boom")
self.assertEqual(decision, "pushback")
self.assertEqual(final, "queued")
needs = [d for _, a, d in self.audits
if a == "worker_audit_loop_needs_review"]
self.assertTrue(any("design_note" in d for d in needs))
class TerminalSqlTestCase(unittest.TestCase):
def test_terminal_sql_matches_old_literal_set(self):
# N-6:生成的终态串集合与旧手写字面量逐字等价。
from cloud.worker import _terminal_status_sql
sql = _terminal_status_sql()
inner = sql.strip()[1:-1]
got = {part.strip().strip("'") for part in inner.split(",")}
self.assertEqual(
got,
{"completed", "failed", "interrupted", "merged",
"merge-failed"})
class GoalWireTestCase(_WorkerCase):
def test_goal_pushback_leaves_goal_columns_and_bounds_rounds(self):
# N-12 定性:推回不改 goal 列;包贴进内存 goal 与 prompt;
# 审计轮次 3 轮后必升级(无无界循环)。
_insert_task(self.db_path, "TG", status="failed", audit_loop=1,
goal_mode=1, goal="reach the goal",
verify_command="true", goal_max_rounds=3)
worker = self._worker()
task = worker.get_task("TG")
final, _, decision = worker._audit_loop_wire(
task, "failed", {"goal_met": False, "goal_error": "nope"},
"goal unmet")
self.assertEqual(decision, "pushback")
self.assertEqual(final, "queued")
row = _row(self.db_path, "TG")
self.assertEqual(row[2], "reach the goal")
self.assertEqual(row[3], "true")
self.assertEqual(row[4], 3)
packet = row[1]
self.assertIsNotNone(packet)
injected = worker._apply_audit_loop_packet({
"id": "TG", "project_id": "p1", "prompt": "do work",
"goal": "reach the goal", "goal_mode": 1,
"audit_loop_packet": packet})
self.assertTrue(injected["prompt"].startswith("[审计推回包"))
self.assertTrue(injected["goal"].startswith("[审计推回包"))
# 库里的原 prompt/goal 不动。
self.assertEqual(_row(self.db_path, "TG")[2], "reach the goal")
# 继续推回直至升级:轮次有界。
decisions = []
for _ in range(3):
_set_status(self.db_path, "TG", "failed")
task = worker.get_task("TG")
_, _, decision = worker._audit_loop_wire(
task, "failed",
{"goal_met": False, "goal_error": "nope"}, "goal unmet")
decisions.append(decision)
if decision == "escalate":
break
self.assertEqual(decisions[-1], "escalate")
self.assertEqual(_row(self.db_path, "TG")[0], "failed")
self.assertIsNone(_row(self.db_path, "TG")[1])
class ToolApprovalEffectiveTestCase(_WorkerCase):
def _insert_approval_task(self, task_id, approvals):
_insert_task(self.db_path, task_id, status="queued")
conn = sqlite3.connect(self.db_path)
try:
conn.execute(
"UPDATE tasks SET agent_tools = ?,"
" agent_tool_approvals = ? WHERE id = ?",
(json.dumps(["websearch"]), json.dumps(approvals),
task_id))
conn.commit()
finally:
conn.close()
def test_ask_is_audited_as_decline_under_host_deny(self):
# I-12:派发时真的调 effective,ask 在行里折算成
# decline,ask_honoured 恒为 False。
self._insert_approval_task("TA", {"websearch": "ask"})
worker = self._worker()
out = worker.run_one()
self.assertEqual(out["status"], "completed")
details = [d for _, a, d in self.audits
if a == "worker_tool_approvals_declared"]
self.assertTrue(details)
self.assertIn('"websearch"', details[0])
self.assertIn('"declared": "ask"', details[0])
self.assertIn('"effective": "decline"', details[0])
self.assertIn('"ask_honoured": false', details[0])
def test_auto_accepts_only_when_host_config_is_auto(self):
# host approval 取自传给 host 的 --config 文件:auto 时
# auto 声明 effective 为 accept;默认 deny 时为 decline。
self._insert_approval_task("TB", {"websearch": "auto"})
cfg = os.path.join(self.tmp.name, "host-config.json")
with open(cfg, "w", encoding="utf-8") as fh:
json.dump({"approval": "auto"}, fh)
worker = self._worker(config_path=cfg)
out = worker.run_one()
self.assertEqual(out["status"], "completed")
details = [d for _, a, d in self.audits
if a == "worker_tool_approvals_declared"]
self.assertIn('"effective": "accept"', details[0])
class GatewayExtractTestCase(_WorkerCase):
def _policy(self, capable):
return {"mode": "active",
"tiers": {"default": {
"model": "m1",
"requires": {"tool_calling": True}}},
"model_capabilities": {"m1": {"tool_calling": capable}}}
def test_blocked_and_pass_through(self):
# I-9①:抽取后行为逐字不变——被拦时早返回 dict +
# 门审计 + 标记点;满足时返回 None 继续派发。
_insert_task(self.db_path, "TG1", status="queued")
worker = self._worker(routing_policy=self._policy(False))
task = {"id": "TG1", "project_id": "p1", "model": "m1"}
blocked = worker._gateway_capability_check(
task, "sub2api", False, time.time())
self.assertIsNotNone(blocked)
self.assertEqual(blocked["status"], "failed")
self.assertIn("worker_capability_gate_rejected", self._actions())
self.assertIn("capability", task.get("_gates_checked", []))
self.audits.clear()
_insert_task(self.db_path, "TG2", status="queued")
worker2 = self._worker(routing_policy=self._policy(True))
task2 = {"id": "TG2", "project_id": "p1", "model": "m1"}
self.assertIsNone(worker2._gateway_capability_check(
task2, "sub2api", False, time.time()))
self.assertNotIn("worker_capability_gate_rejected",
self._actions())
class PersistExtractTestCase(_WorkerCase):
def test_writeback_only_for_applied_policy(self):
# I-9②:只持久化 source=policy 且 applied 的决策。
_insert_task(self.db_path, "TW", status="queued", model=None)
worker = self._worker()
task = {"id": "TW", "project_id": "p1"}
worker._persist_policy_model(
task, SimpleNamespace(chosen_model="m9", source="policy",
applied=True, tier="default"))
self.assertEqual(_row(self.db_path, "TW")[5], "m9")
self.assertIn("worker_model_persisted", self._actions())
self.audits.clear()
_insert_task(self.db_path, "TW2", status="queued", model=None)
worker._persist_policy_model(
{"id": "TW2", "project_id": "p1"},
SimpleNamespace(chosen_model="m9", source="shadow",
applied=False, tier="default"))
self.assertIsNone(_row(self.db_path, "TW2")[5])
self.assertNotIn("worker_model_persisted", self._actions())
class ServerHelperTestCase(unittest.TestCase):
def test_agent_definition_and_audit_loop_helpers(self):
# I-9③:两个小函数与原地行为一致(错误文案逐字)。
resolve = CloudControlPlaneHandler._resolve_create_agent_definition
_, err = resolve({"agent": {"a": 1}, "agent_name": "x"})
self.assertEqual(err, "agent and agent_name cannot both be used")
_, err = resolve({"agent": "not-a-dict"})
self.assertEqual(err, "agent must be an object")
validate = CloudControlPlaneHandler._validate_create_audit_loop
self.assertEqual(validate({}), (0, None))
self.assertEqual(validate({"audit_loop": True}), (1, None))
self.assertEqual(validate({"audit_loop": 1}), (1, None))
_, err = validate({"audit_loop": 2})
self.assertEqual(err, "audit_loop must be true or 1")
class ParseToleranceTestCase(unittest.TestCase):
def test_non_string_items_survive_and_are_tolerated(self):
# N-9(文档选项):解析层只保 list 形状,下游容忍非
# 字符串项——不过滤、不改旧脏行序列化。
self.assertEqual(parse_agent_tools_column('["a", 1]'),
["a", 1])
self.assertEqual(missing_required_tools(["a", 1], ""),
["a", 1])
class DryRunEmptyTestCase(unittest.TestCase):
def test_early_returns_share_shape(self):
# N-10:两处早期返回经 _empty_report 归一,键集合一致。
a = dry_run_policy_document("not-a-dict")
b = dry_run_policy_document({"mode": "bogus"})
self.assertFalse(a["valid"])
self.assertFalse(b["valid"])
self.assertEqual(set(a.keys()), set(b.keys()))
class ShadowEndpointTestCase(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.db_path = os.path.join(self.tmp.name, "control.db")
self.auth = AuthStore(self.db_path)
self.owner_id, self.owner_key = self.auth.create_user("owner")
self.server = create_server(self.db_path, "127.0.0.1", 0)
self.port = self.server.server_address[1]
self.thread = threading.Thread(
target=self.server.serve_forever, daemon=True)
self.thread.start()
status, self.project = self._req(
"POST", "/projects", {"name": "demo"}, self.owner_key)
self.assertEqual(status, 201)
def tearDown(self):
self.server.shutdown()
self.server.server_close()
self.thread.join(timeout=5)
try:
self.auth.conn.close()
except Exception:
pass
self.tmp.cleanup()
def _req(self, method, path, body=None, key=None):
conn = http.client.HTTPConnection("127.0.0.1", self.port,
timeout=10)
headers = {"Content-Type": "application/json"}
if key:
headers["X-API-Key"] = key
conn.request(method, path,
json.dumps(body) if body is not None else None,
headers)
resp = conn.getresponse()
raw = resp.read()
conn.close()
try:
data = json.loads(raw)
except ValueError:
data = None
return resp.status, data
def _shadow(self):
status, report = self._req(
"GET", "/routing-shadow?project_id=" + self.project["id"],
key=self.owner_key)
self.assertEqual(status, 200, report)
return report
def test_cost_query_failure_flagged_not_500(self):
# N-1 + I-14④:成本查询失败时报告仍 200、成本按空算、
# cost_unavailable 标明,不抛到 handler 层。
conn = sqlite3.connect(self.db_path)
try:
conn.execute("DROP TABLE IF EXISTS cost_records")
conn.commit()
finally:
conn.close()
class _BoomCostStore:
def __init__(self, *args, **kwargs):
raise RuntimeError("injected cost store failure")
with mock.patch.object(server_module, "CostStore",
_BoomCostStore):
report = self._shadow()
self.assertTrue(report["cost_unavailable"])
def test_sample_window_and_normal_cost_flag(self):
# N-5:截断样本(最早 10000 行)附带最早/最晚时间窗;
# 正常路径 cost_unavailable 为 False。
conn = sqlite3.connect(self.db_path)
try:
for task_id, created in (("t1", 10.0), ("t2", 20.0)):
conn.execute(
"INSERT INTO audit_log(user_id, project_id, action,"
" detail, created_at) VALUES(?, ?, ?, ?, ?)",
("system", self.project["id"], "worker_routing",
"%s tier=plan source=shadow applied=False"
" suggested=mx" % task_id, created))
conn.commit()
finally:
conn.close()
report = self._shadow()
self.assertFalse(report["cost_unavailable"])
self.assertEqual(report["sample_window"],
{"earliest_created_at": 10.0,
"latest_created_at": 20.0})
self.assertFalse(report["truncated"])
if __name__ == "__main__":
unittest.main()