Parse: agent_tools/tool_approvals column parsing unified into agent_def shared helpers (parse None = undeclared; server keeps None, worker maps to []/{} at _task_from_row, dispatch behaviour unchanged). Goal: _apply_resume_preamble goal branch proven reachable via _goal_convert_queued on a queued auto-resumed task, kept with a test and corrected docs (not dead code). Fail-open: audit write failure and preamble lookup failure now log warnings (best-effort unchanged); partial policy failure and shadow parse drops documented as known gaps. M7: shadow endpoint explicit_capability_failures_truncated test. Fingerprint: RoutingPolicy.content_fingerprint (canonical JSON sha256, version excluded) surfaced only in dry-run output, never in audit rows. M1 wording: legacy gap stated as fixed stock persisting per-task until completion, not converging.
322 lines
13 KiB
Python
322 lines
13 KiB
Python
"""9.5-code track: code-reachable items only.
|
|
|
|
- Parse unification: agent_tools / tool_approvals column parsing
|
|
lives only in agent_def.py; server keeps None (undeclared),
|
|
worker maps None to [] / {} at the _task_from_row boundary.
|
|
- Goal branch of _apply_resume_preamble is reachable (via
|
|
_goal_convert_queued on a queued auto-resumed task), so it is
|
|
kept and tested, not deleted as dead code.
|
|
- Fail-open inventory: audit write failure and preamble lookup
|
|
failure log a warning without breaking dispatch.
|
|
- M7: shadow endpoint explicit_capability_failures_truncated flag.
|
|
- Policy content fingerprint: stable for the same document,
|
|
changes when requires change, never touches audit rows.
|
|
|
|
Run: python3 -m unittest discover -s cloud/tests -p "test_95_code.py" -v
|
|
No real models, no network, no real gateway, no real PG.
|
|
"""
|
|
|
|
import http.client
|
|
import json
|
|
import os
|
|
import sqlite3
|
|
import sys
|
|
import tempfile
|
|
import threading
|
|
import time
|
|
import unittest
|
|
from types import SimpleNamespace
|
|
from unittest import mock
|
|
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", ".."))
|
|
|
|
import cloud.worker as worker_module
|
|
from cloud.agent_def import (
|
|
parse_agent_tool_approvals_column,
|
|
parse_agent_tools_column,
|
|
)
|
|
from cloud.attempts import ensure_attempts_table, record_attempt
|
|
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 (
|
|
_agent_tool_approvals_from_row,
|
|
_agent_tools_from_row,
|
|
create_server,
|
|
init_db,
|
|
)
|
|
from cloud.worker import (
|
|
Worker,
|
|
_agent_tool_approvals_from_col,
|
|
_agent_tools_from_col,
|
|
)
|
|
|
|
|
|
class ParseUnificationTestCase(unittest.TestCase):
|
|
def test_shared_parser_is_single_source(self):
|
|
# Missing / dirty / wrong-type all parse to None.
|
|
for raw in (None, "not json", '{"a": 1}', '["a", 1]', 42):
|
|
if raw == '["a", 1]':
|
|
# A JSON list parses even if items are not strings;
|
|
# the parser only guarantees list shape, not items.
|
|
self.assertEqual(parse_agent_tools_column(raw),
|
|
["a", 1])
|
|
continue
|
|
self.assertIsNone(parse_agent_tools_column(raw))
|
|
for raw in (None, "not json", '["a"]', '{"a": 1}', 42):
|
|
if raw == '{"a": 1}':
|
|
self.assertEqual(
|
|
parse_agent_tool_approvals_column(raw), {"a": 1})
|
|
continue
|
|
self.assertIsNone(parse_agent_tool_approvals_column(raw))
|
|
# Valid values are copied, not aliased.
|
|
src = ["websearch"]
|
|
self.assertEqual(parse_agent_tools_column(src), src)
|
|
self.assertIsNot(parse_agent_tools_column(src), src)
|
|
src = {"websearch": "ask"}
|
|
self.assertEqual(parse_agent_tool_approvals_column(src), src)
|
|
self.assertIsNot(parse_agent_tool_approvals_column(src), src)
|
|
|
|
def test_server_keeps_none_worker_maps_to_empty(self):
|
|
# Server serialization: missing/dirty stays None (undeclared).
|
|
self.assertIsNone(_agent_tools_from_row({}))
|
|
self.assertIsNone(_agent_tools_from_row({"agent_tools": "junk"}))
|
|
self.assertEqual(
|
|
_agent_tools_from_row({"agent_tools": '["websearch"]'}),
|
|
["websearch"])
|
|
self.assertIsNone(_agent_tool_approvals_from_row({}))
|
|
self.assertIsNone(
|
|
_agent_tool_approvals_from_row(
|
|
{"agent_tool_approvals": "junk"}))
|
|
# Worker dispatch: same parse, None mapped to [] / {}.
|
|
self.assertEqual(_agent_tools_from_col(None), [])
|
|
self.assertEqual(_agent_tools_from_col("junk"), [])
|
|
self.assertEqual(_agent_tools_from_col('["websearch"]'),
|
|
["websearch"])
|
|
self.assertEqual(_agent_tool_approvals_from_col(None), {})
|
|
self.assertEqual(_agent_tool_approvals_from_col("junk"), {})
|
|
self.assertEqual(
|
|
_agent_tool_approvals_from_col('{"websearch": "ask"}'),
|
|
{"websearch": "ask"})
|
|
|
|
|
|
def _insert_task(db_path, task_id, prompt="做活", status="queued",
|
|
project_id="p1", **cols):
|
|
now = time.time()
|
|
conn = sqlite3.connect(db_path)
|
|
try:
|
|
base = {"id": task_id, "project_id": project_id,
|
|
"prompt": prompt, "status": status,
|
|
"idempotency_key": None, "depends_on": "[]",
|
|
"priority": 0, "created_by": "tester",
|
|
"created_at": now, "updated_at": now,
|
|
"last_heartbeat": None}
|
|
base.update(cols)
|
|
keys = list(base.keys())
|
|
conn.execute(
|
|
"INSERT INTO tasks(%s) VALUES(%s)"
|
|
% (", ".join(keys), ", ".join("?" * len(keys))),
|
|
[base[k] for k in keys])
|
|
conn.commit()
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
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.env = {"XCM_API_KEY": "test-placeholder"}
|
|
|
|
def tearDown(self):
|
|
self.tmp.cleanup()
|
|
|
|
def _worker(self):
|
|
def _runner(cmd):
|
|
return SimpleNamespace(returncode=0, stdout="{}",
|
|
stderr="")
|
|
return Worker(self.db_path, "/fake/host.py",
|
|
runner=_runner, env=self.env)
|
|
|
|
|
|
class GoalBranchTestCase(WorkerCase):
|
|
def test_goal_branch_reached_for_converted_goal_task(self):
|
|
# Reachability proof (9.5-code): a non-goal task auto-resumed
|
|
# as platform_fresh (count=1, queued) that is then converted
|
|
# to a goal while still queued reaches the goal branch:
|
|
# no _resume (resume_message was cleared by auto_resume),
|
|
# no _goal_resume_breakpoint, count>0, latest attempt fresh.
|
|
_insert_task(self.db_path, "t1", prompt="做活",
|
|
goal="做目标", goal_mode=1, auto_resume_count=1)
|
|
conn = sqlite3.connect(self.db_path)
|
|
try:
|
|
ensure_attempts_table(conn)
|
|
record_attempt(conn, "t1", 1, "platform_fresh",
|
|
"worker_shutdown", -1,
|
|
"做到一半:已建 a.py", "被平台杀掉")
|
|
conn.commit()
|
|
finally:
|
|
conn.close()
|
|
worker = self._worker()
|
|
task = worker.get_task("t1")
|
|
injected = worker._apply_resume_preamble(task)
|
|
self.assertIn("上次尝试说明", injected["goal"])
|
|
self.assertTrue(injected["goal"].endswith("做目标"))
|
|
self.assertIn("上次尝试说明", injected["prompt"])
|
|
self.assertTrue(injected["prompt"].endswith("做活"))
|
|
# Memory only: the stored goal is untouched.
|
|
conn = sqlite3.connect(self.db_path)
|
|
try:
|
|
row = conn.execute("SELECT goal FROM tasks WHERE id = ?",
|
|
("t1",)).fetchone()
|
|
finally:
|
|
conn.close()
|
|
self.assertEqual(row[0], "做目标")
|
|
|
|
|
|
class FailOpenLogTestCase(WorkerCase):
|
|
def test_audit_failure_logs_without_breaking_dispatch(self):
|
|
worker = self._worker()
|
|
calls = []
|
|
with mock.patch.object(
|
|
worker_module, "AuthStore",
|
|
side_effect=RuntimeError("db down")), \
|
|
mock.patch.object(
|
|
worker_module, "log_event",
|
|
side_effect=lambda *a, **k: calls.append(a[1])):
|
|
# Must not raise: audit is best-effort by design.
|
|
worker._audit("p1", "worker_claim", "t1")
|
|
self.assertEqual(calls, ["worker_audit_failed"])
|
|
|
|
def test_preamble_lookup_failure_logs_and_returns_task(self):
|
|
worker = self._worker()
|
|
task = {"id": "t1", "project_id": "p1", "prompt": "做活",
|
|
"auto_resume_count": 1}
|
|
calls = []
|
|
with mock.patch.object(
|
|
worker_module, "_db_connect",
|
|
side_effect=RuntimeError("db down")), \
|
|
mock.patch.object(
|
|
worker_module, "log_event",
|
|
side_effect=lambda *a, **k: calls.append(a[1])):
|
|
out = worker._apply_resume_preamble(task)
|
|
self.assertIs(out["prompt"], "做活")
|
|
self.assertEqual(
|
|
calls, ["worker_resume_preamble_lookup_failed"])
|
|
|
|
|
|
class FingerprintTestCase(unittest.TestCase):
|
|
def test_stable_and_sensitive_to_requires(self):
|
|
doc = {"mode": "shadow",
|
|
"tiers": {"default": "m1"},
|
|
"model_capabilities": {"m1": {"tool_calling": True}}}
|
|
a = parse_routing_policy(doc)
|
|
b = parse_routing_policy(
|
|
{"model_capabilities": {"m1": {"tool_calling": True}},
|
|
"tiers": {"default": "m1"}, "mode": "shadow"})
|
|
self.assertEqual(a.content_fingerprint(),
|
|
b.content_fingerprint())
|
|
c = parse_routing_policy({
|
|
"mode": "shadow",
|
|
"tiers": {"default": {"model": "m1",
|
|
"requires": {"tool_calling": True}}},
|
|
"model_capabilities": {"m1": {"tool_calling": True}}})
|
|
self.assertNotEqual(a.content_fingerprint(),
|
|
c.content_fingerprint())
|
|
# Operator version string does not affect the fingerprint.
|
|
d = parse_routing_policy(dict(doc, version="v99"))
|
|
self.assertEqual(a.content_fingerprint(),
|
|
d.content_fingerprint())
|
|
# Dry-run surfaces it; audit rows never carry it.
|
|
report = dry_run_policy_document(doc)
|
|
self.assertEqual(report["content_fingerprint"],
|
|
a.content_fingerprint())
|
|
|
|
|
|
class _HttpServerCase(unittest.TestCase):
|
|
def setUp(self):
|
|
self._old = {k: os.environ.get(k) for k in (
|
|
"CLOUD_ROUTING_POLICY_JSON", "CLOUD_ROUTING_POLICY_FILE")}
|
|
os.environ.pop("CLOUD_ROUTING_POLICY_JSON", None)
|
|
os.environ.pop("CLOUD_ROUTING_POLICY_FILE", None)
|
|
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)
|
|
for k, v in self._old.items():
|
|
if v is None:
|
|
os.environ.pop(k, None)
|
|
else:
|
|
os.environ[k] = v
|
|
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 _project(self, name="p"):
|
|
status, project = self._req(
|
|
"POST", "/projects", {"name": name, "auto_pr": 1},
|
|
self.owner_key)
|
|
self.assertEqual(status, 201, project)
|
|
return project["id"]
|
|
|
|
|
|
class ExplicitTruncationTestCase(_HttpServerCase):
|
|
def test_explicit_failures_truncated_flag(self):
|
|
pid = self._project()
|
|
conn = sqlite3.connect(self.db_path)
|
|
try:
|
|
conn.executemany(
|
|
"INSERT INTO audit_log(user_id, project_id, action,"
|
|
" detail, created_at) VALUES(?, ?, ?, ?, ?)",
|
|
[("tester", pid,
|
|
"worker_capability_gateway_explicit",
|
|
"E1 model=m1 tier=default mode=active"
|
|
" missing=tool_calling unknown=False blocked=True",
|
|
time.time())] * 10001)
|
|
conn.commit()
|
|
finally:
|
|
conn.close()
|
|
status, report = self._req(
|
|
"GET", "/routing-shadow?project_id=" + pid,
|
|
key=self.owner_key)
|
|
self.assertEqual(status, 200, report)
|
|
self.assertTrue(
|
|
report["explicit_capability_failures_truncated"])
|
|
self.assertEqual(
|
|
len(report["explicit_capability_failures"]), 10000)
|