371 lines
15 KiB
Python
371 lines
15 KiB
Python
"""审计循环调用方接线测试:run_one 终局块 + 包投递 + 升级投递。
|
|
|
|
只用注入的假 runner 与 init_db 的本地库,不跑真实模型回合、
|
|
不联网、不登录任何服务;env 里的值只是占位用的假名。
|
|
|
|
覆盖(按任务书):开关关闭时完成路径不变;确定性门 fail →
|
|
推回(任务重排、packet 可读、下一轮 prompt 含 packet);修好
|
|
后 pass(packet 清理、任务完成);3 轮仍 fail → 升级(任务终态、
|
|
审计在、不再重排);主观 needs_review → 任务结局不变、不重排、
|
|
只记审计;计数损坏 → 直接升级、不循环。
|
|
"""
|
|
|
|
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
|
|
|
|
try:
|
|
from cloud.auth import AuthStore
|
|
from cloud.server import create_server, init_db
|
|
from cloud.worker import Worker
|
|
except ImportError: # discover -s cloud/tests 时把项目根目录补进 sys.path
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
|
|
from cloud.auth import AuthStore
|
|
from cloud.server import create_server, init_db
|
|
from cloud.worker import Worker
|
|
|
|
|
|
def _insert_task(db_path, task_id, prompt="do work", status="queued",
|
|
project_id="p1", audit_loop=0, goal_mode=0):
|
|
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)"
|
|
" VALUES(?, ?, ?, ?, NULL, '[]', 0, 'tester', ?, ?, NULL,"
|
|
" ?, ?)",
|
|
(task_id, project_id, prompt, status, now, now,
|
|
audit_loop, goal_mode),
|
|
)
|
|
conn.commit()
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
def _row(db_path, task_id):
|
|
conn = sqlite3.connect(db_path)
|
|
try:
|
|
row = conn.execute(
|
|
"SELECT status, error_text, audit_loop, audit_loop_packet"
|
|
" FROM tasks WHERE id = ?", (task_id,)).fetchone()
|
|
finally:
|
|
conn.close()
|
|
return row
|
|
|
|
|
|
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 _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 _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="")
|
|
|
|
|
|
class WireTestCase(unittest.TestCase):
|
|
def setUp(self):
|
|
self._old_push_allow = 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.calls = []
|
|
self.audits = []
|
|
|
|
def tearDown(self):
|
|
if getattr(self, "_old_push_allow", None) is None:
|
|
os.environ.pop("CODEX_PUSH_ALLOW_PRIVATE_TARGETS", None)
|
|
else:
|
|
os.environ["CODEX_PUSH_ALLOW_PRIVATE_TARGETS"] = (
|
|
self._old_push_allow)
|
|
self.tmp.cleanup()
|
|
|
|
def _worker(self, runner=None, **kwargs):
|
|
kwargs.setdefault("env", self.env)
|
|
kwargs.setdefault("max_retries", 0)
|
|
kwargs.setdefault("retry_backoff_seconds", 0)
|
|
worker = Worker(self.db_path, "/fake/host.py",
|
|
runner=runner or _completed_runner, **kwargs)
|
|
worker._audit = lambda p, a, d="": self.audits.append((p, a, d))
|
|
return worker
|
|
|
|
def _recording(self, inner):
|
|
def fake(cmd):
|
|
self.calls.append(list(cmd))
|
|
return inner(cmd)
|
|
return fake
|
|
|
|
# -- 开关关闭:完成路径不变 --------------------------------------
|
|
def test_off_task_completes_unchanged(self):
|
|
_insert_task(self.db_path, "T1", audit_loop=0)
|
|
out = self._worker().run_one()
|
|
self.assertEqual(out["status"], "completed")
|
|
self.assertEqual(_row(self.db_path, "T1")[0], "completed")
|
|
self.assertIsNone(_packet(self.db_path, "T1"))
|
|
|
|
# -- fail -> 推回 -> 修好 -> pass(完整 run_one 路径) ------------
|
|
def test_fail_pushback_then_pass_via_run_one(self):
|
|
_insert_task(self.db_path, "T2", audit_loop=1)
|
|
failing = self._worker(runner=self._recording(_failing_runner))
|
|
out = failing.run_one()
|
|
self.assertEqual(out["status"], "queued")
|
|
self.assertEqual(_row(self.db_path, "T2")[0], "queued")
|
|
packet = _packet(self.db_path, "T2")
|
|
self.assertIsNotNone(packet)
|
|
self.assertEqual(packet["round"], 1)
|
|
self.assertEqual(packet["findings"][0]["id"], "execution_failed")
|
|
self.assertTrue(any(a == "worker_audit_loop_pushback"
|
|
for _, a, _ in self.audits))
|
|
# 下一轮:包以前言贴进 prompt(库 prompt 本身不动),修好后 pass。
|
|
self.calls.clear()
|
|
self.audits.clear()
|
|
passing = self._worker(runner=self._recording(_completed_runner))
|
|
out = passing.run_one()
|
|
self.assertEqual(out["status"], "completed")
|
|
self.assertEqual(_row(self.db_path, "T2")[0], "completed")
|
|
self.assertIsNone(_packet(self.db_path, "T2"))
|
|
prompted = " ".join(" ".join(c) for c in self.calls)
|
|
self.assertIn("execution_failed", prompted)
|
|
conn = sqlite3.connect(self.db_path)
|
|
try:
|
|
prompt = conn.execute(
|
|
"SELECT prompt FROM tasks WHERE id = 'T2'").fetchone()[0]
|
|
finally:
|
|
conn.close()
|
|
self.assertEqual(prompt, "do work")
|
|
|
|
# -- 3 轮仍 fail -> 升级 -------------------------------------------
|
|
def test_three_rounds_then_escalate_via_run_one(self):
|
|
_insert_task(self.db_path, "T3", audit_loop=1)
|
|
worker = self._worker(runner=_failing_runner)
|
|
for _ in range(3):
|
|
out = worker.run_one()
|
|
self.assertEqual(out["status"], "queued")
|
|
out = worker.run_one()
|
|
self.assertEqual(out["status"], "failed")
|
|
self.assertEqual(_row(self.db_path, "T3")[0], "failed")
|
|
self.assertIsNone(_packet(self.db_path, "T3"))
|
|
self.assertIn("审计循环升级", _row(self.db_path, "T3")[1])
|
|
self.assertTrue(any(a == "worker_audit_loop_escalated"
|
|
for _, a, _ in self.audits))
|
|
# 升级后不再自动重排:下一轮认领不到该任务。
|
|
self.assertEqual(worker.run_one()["dispatched"], False)
|
|
|
|
# -- 计数损坏 -> 直接升级、不循环 ----------------------------------
|
|
def test_corrupted_count_escalates_via_wire(self):
|
|
_insert_task(self.db_path, "T4", status="failed", audit_loop=1)
|
|
worker = self._worker()
|
|
worker.audit_loop_evaluate(
|
|
{"id": "T4", "project_id": "p1", "audit_loop": True},
|
|
{"execution": False},
|
|
findings=[])
|
|
# 上面那行若因空 finding 报错也不影响:直接写坏状态行。
|
|
conn = sqlite3.connect(self.db_path)
|
|
try:
|
|
conn.execute(
|
|
"INSERT OR REPLACE INTO task_audit_loop_state"
|
|
"(task_id, round, passed_gates, criteria, last_decision,"
|
|
" updated_at) VALUES('T4', 'not-a-number', '[]', '{}',"
|
|
" 'pushback', ?)", (time.time(),))
|
|
conn.commit()
|
|
finally:
|
|
conn.close()
|
|
task = worker.get_task("T4")
|
|
final, error, decision = worker._audit_loop_wire(
|
|
task, "failed", {"stdout": ""}, "boom")
|
|
self.assertEqual(decision, "escalate")
|
|
self.assertEqual(final, "failed")
|
|
self.assertIn("审计循环升级", error)
|
|
self.assertIsNone(_packet(self.db_path, "T4"))
|
|
|
|
# -- goal 门:goal_met False 推回,None 不列入门 -------------------
|
|
def test_goal_met_false_pushbacks_and_none_is_not_a_gate(self):
|
|
worker = self._worker()
|
|
# 终局已写回的前提:库状态须与传入的 final 一致(completed),
|
|
# 否则接线的 CAS 会正确地拒绝改状态——那是测试搭错,不是 bug。
|
|
_insert_task(self.db_path, "T5", status="completed", audit_loop=1,
|
|
goal_mode=1)
|
|
task = worker.get_task("T5")
|
|
gates, findings = worker._audit_loop_gate_results(
|
|
task, "completed", {"goal_met": False, "goal_error": "nope"})
|
|
self.assertFalse(gates["goal_verify"])
|
|
self.assertEqual(findings[0].id, "goal_unmet")
|
|
final, _, decision = worker._audit_loop_wire(
|
|
task, "completed", {"goal_met": False, "goal_error": "nope"},
|
|
None)
|
|
self.assertEqual(decision, "pushback")
|
|
self.assertEqual(final, "queued")
|
|
gates, _ = worker._audit_loop_gate_results(
|
|
task, "completed", {"goal_met": None})
|
|
self.assertNotIn("goal_verify", gates)
|
|
|
|
# -- 主观 needs_review:结局不变、不重排、只记审计 -----------------
|
|
def test_needs_review_leaves_outcome_untouched(self):
|
|
_insert_task(self.db_path, "T6", status="failed", audit_loop=1)
|
|
worker = self._worker()
|
|
# 审计批 5:接线改走 _audit_loop_evaluate_on_conn(同连接
|
|
# 单事务),桩点随之移到新内核;意图不变——防御分支仍在,
|
|
# needs_review 时终局、包列一律不动、只记审计。
|
|
worker._audit_loop_evaluate_on_conn = (
|
|
lambda conn, t, g, findings=None, fixed=None: {
|
|
"enabled": True, "decision": "needs_review", "packet": None,
|
|
"new_state": None, "reason": "subjective"})
|
|
task = worker.get_task("T6")
|
|
final, _, decision = worker._audit_loop_wire(
|
|
task, "failed", {"stdout": ""}, "boom")
|
|
self.assertEqual(decision, "needs_review")
|
|
self.assertEqual(final, "failed")
|
|
self.assertEqual(_row(self.db_path, "T6")[0], "failed")
|
|
self.assertTrue(any(a == "worker_audit_loop_needs_review"
|
|
for _, a, _ in self.audits))
|
|
|
|
# -- 包注入:有效包贴前言、无效包不动、重复调用不叠加 --------------
|
|
def test_packet_injection_and_invalid_packet(self):
|
|
worker = self._worker()
|
|
packet = json.dumps({
|
|
"task_id": "T7", "round": 1,
|
|
"findings": [{"id": "execution_failed", "severity": "high",
|
|
"evidence": "boom", "reproduction": "re-run",
|
|
"acceptance": "task completes",
|
|
"kind": "deterministic", "gate": "execution"}],
|
|
"criteria": {"execution_failed": "execution"}})
|
|
task = {"id": "T7", "project_id": "p1", "prompt": "do work",
|
|
"audit_loop_packet": packet}
|
|
out = worker._apply_audit_loop_packet(task)
|
|
self.assertIn("execution_failed", out["prompt"])
|
|
self.assertTrue(out["prompt"].startswith("[审计推回包"))
|
|
again = worker._apply_audit_loop_packet(out)
|
|
self.assertEqual(again["prompt"].count("execution_failed"), 1)
|
|
bad = worker._apply_audit_loop_packet(
|
|
{"id": "T7", "project_id": "p1", "prompt": "do work",
|
|
"audit_loop_packet": "{not json"})
|
|
self.assertEqual(bad["prompt"], "do work")
|
|
|
|
# -- 列与回读:init_db 建列、get_task 读到开关 ----------------------
|
|
def test_columns_and_get_task_flag(self):
|
|
_insert_task(self.db_path, "T8", audit_loop=1)
|
|
worker = self._worker()
|
|
task = worker.get_task("T8")
|
|
self.assertEqual(task["audit_loop"], 1)
|
|
self.assertIsNone(task["audit_loop_packet"])
|
|
|
|
|
|
class ServerCreateTests(unittest.TestCase):
|
|
"""POST /tasks 的 audit_loop 字段:接受、拒绝、协调员拒绝。
|
|
|
|
复用 test_server_goal_notify 的 create_server + 真实 HTTP 骨架。
|
|
"""
|
|
|
|
def setUp(self):
|
|
self._old_push_allow = 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")
|
|
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()
|
|
|
|
def tearDown(self):
|
|
self.server.shutdown()
|
|
self.server.server_close()
|
|
self.thread.join(timeout=5)
|
|
if getattr(self, "_old_push_allow", None) is None:
|
|
os.environ.pop("CODEX_PUSH_ALLOW_PRIVATE_TARGETS", None)
|
|
else:
|
|
os.environ["CODEX_PUSH_ALLOW_PRIVATE_TARGETS"] = (
|
|
self._old_push_allow)
|
|
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)
|
|
hdrs = {}
|
|
payload = None
|
|
if body is not None:
|
|
payload = json.dumps(body).encode("utf-8")
|
|
hdrs["Content-Type"] = "application/json"
|
|
if key is not None:
|
|
hdrs["X-API-Key"] = key
|
|
try:
|
|
conn.request(method, path, body=payload, headers=hdrs)
|
|
resp = conn.getresponse()
|
|
data = resp.read()
|
|
finally:
|
|
conn.close()
|
|
try:
|
|
parsed = json.loads(data.decode("utf-8")) if data else None
|
|
except ValueError:
|
|
parsed = None
|
|
return resp.status, parsed
|
|
|
|
def test_create_task_audit_loop_flag(self):
|
|
_, project = self._req(
|
|
"POST", "/projects", {"name": "p"}, self.owner_key)
|
|
pid = project["id"]
|
|
status, task = self._req(
|
|
"POST", "/tasks",
|
|
{"project_id": pid, "prompt": "do it", "audit_loop": True},
|
|
self.owner_key)
|
|
self.assertEqual(status, 201, task)
|
|
self.assertEqual(task["audit_loop"], 1)
|
|
status, _ = self._req(
|
|
"POST", "/tasks",
|
|
{"project_id": pid, "prompt": "bad", "audit_loop": "yes"},
|
|
self.owner_key)
|
|
self.assertEqual(status, 400)
|
|
# 协调员接线轨:父任务带 audit_loop 不再 400——此前拒绝
|
|
# 的理由是“父任务接线覆盖不到、接受即静默丢弃”,合流
|
|
# 接线(worker._coord_audit_loop_merge)落地后该理由已
|
|
# 消除;子任务仍不直接接受外部开关(worker 内部 INSERT
|
|
# 不写该列)。非法值仍 400。
|
|
status, task = self._req(
|
|
"POST", "/tasks",
|
|
{"project_id": pid, "prompt": "coord",
|
|
"coordinator_parts": [{"name": "a", "prompt": "do a"}],
|
|
"audit_loop": True},
|
|
self.owner_key)
|
|
self.assertEqual(status, 201, task)
|
|
self.assertEqual(task["audit_loop"], 1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|