fix: 9.5-code reachable items (parse unification, goal branch, fail-open logs, M7 test, fingerprint)
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.
This commit is contained in:
1 parent
d95d678379
commit
1b8b6e525d
7 files changed
+498
-57
No files matched your search
@@ -0,0 +1,321 @@
|
||||
"""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)
|
||||
Reference in new issue
Block a user