Files
codex-app-server/cloud/tests/test_agent_def_v2.py
T
host 1672ab9ff2 feat: agent definition v2 (registry, persistence, developerInstructions channel)
Registry: POST /tasks accepts agent_name, loading <name>.json
from repo-root agents/ (traversal-guarded, skill-pack re-validated);
agent and agent_name together is a 400. Persistence: tasks gains
agent_name/agent_version/agent_instructions (migrate + PG schema),
returned by GET /tasks; stored prompt stays clean. Channel: local
and docker dispatch pass --developer-instructions to host.py
dispatch -> create_thread(developerInstructions=...); run/fork do
not re-send. Cloud executor has no such channel, so the worker
falls back to the v1 marked header in the in-memory dispatch
prompt only (DB prompt stays clean). Goal/coordinator tasks
still reject instructions.

Tests: test_agent_def_v2.py (7 tests) + updated v1 suite.
2026-11-04 18:01:44 +00:00

185 lines
7.0 KiB
Python

"""声明式 agent 定义 v2 单测:注册表、落库、developerInstructions 通道。
运行(在仓库根目录下):
python3 -m unittest discover -s cloud/tests -p "test_agent_def_v2.py" -v
覆盖:agent_name 查注册表、agent 与 agent_name 同给 400、
未知 agent_name 400、本地/docker 派发命令含
--developer-instructions、无指令时命令不变、host dispatch 把
指令传给 create_thread 的 instructions(即 developerInstructions)。
不跑真实模型、不联网;host 部分用假 server,只断言传参。
"""
import http.client
import importlib.util
import json
import os
import sys
import tempfile
import threading
import unittest
from pathlib import Path
try:
from cloud.agent_def import default_agents_dir
from cloud.auth import AuthStore
from cloud.sandbox import DockerExecutor, LocalExecutor
from cloud.server import create_server
except ImportError:
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from cloud.agent_def import default_agents_dir
from cloud.auth import AuthStore
from cloud.sandbox import DockerExecutor, LocalExecutor
from cloud.server import create_server
def _load_host():
spec = importlib.util.spec_from_file_location(
"hostmod_v2", str(Path(__file__).resolve().parents[2] / "host.py"))
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
class ServerAgentNameTestCase(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 _list_tasks(self):
status, data = self._req(
"GET", "/tasks?project_id=" + self.project["id"],
key=self.owner_key)
self.assertEqual(status, 200)
return data["tasks"]
def test_agent_name_from_registry(self):
# 注册表里有 agents/research-brief.json(仓库自带示例)。
self.assertTrue((default_agents_dir()
/ "research-brief.json").is_file())
status, task = self._req("POST", "/tasks", {
"project_id": self.project["id"], "prompt": "do research",
"agent_name": "research-brief"}, self.owner_key)
self.assertEqual(status, 201, task)
self.assertEqual(task["agent_name"], "research-brief")
self.assertEqual(task["agent_version"], "v1")
self.assertEqual(task["effort"], "medium")
self.assertEqual(task["executor"], "docker")
self.assertEqual(task["prompt"], "do research")
stored = self._list_tasks()[0]
self.assertEqual(stored["agent_instructions"][:20],
"Confirm the question")
def test_agent_and_agent_name_conflict(self):
status, _data = self._req("POST", "/tasks", {
"project_id": self.project["id"], "prompt": "do research",
"agent": {"name": "research-brief"},
"agent_name": "research-brief"}, self.owner_key)
self.assertEqual(status, 400)
self.assertEqual(self._list_tasks(), [])
def test_unknown_agent_name(self):
status, _data = self._req("POST", "/tasks", {
"project_id": self.project["id"], "prompt": "do research",
"agent_name": "no-such-agent"}, self.owner_key)
self.assertEqual(status, 400)
self.assertEqual(self._list_tasks(), [])
class InstructionsChannelTestCase(unittest.TestCase):
def test_local_dispatch_includes_instructions(self):
captured = []
def runner(cmd, env=None):
captured.append(cmd)
return {"returncode": 0, "stdout": "ok", "stderr": ""}
ex = LocalExecutor(host_py_path="host.py", runner=runner,
repo="/tmp/repo")
ex.dispatch_task({"id": "t1", "prompt": "hi",
"agent_instructions": "be careful"})
self.assertIn("--developer-instructions", captured[0])
idx = captured[0].index("--developer-instructions")
self.assertEqual(captured[0][idx + 1], "be careful")
self.assertLess(idx, captured[0].index("dispatch"))
def test_local_dispatch_without_instructions_unchanged(self):
captured = []
def runner(cmd, env=None):
captured.append(cmd)
return {"returncode": 0, "stdout": "ok", "stderr": ""}
ex = LocalExecutor(host_py_path="host.py", runner=runner,
repo="/tmp/repo")
ex.dispatch_task({"id": "t1", "prompt": "hi"})
self.assertNotIn("--developer-instructions", captured[0])
def test_docker_inner_command_includes_instructions(self):
ex = DockerExecutor.__new__(DockerExecutor)
ex.config_path = None
ex.model = None
ex.ignore_host_budget = False
cmd = ex._inner_host_command(
"dispatch", model="m",
developer_instructions="be careful")
self.assertIn("--developer-instructions", cmd)
self.assertEqual(cmd[cmd.index("--developer-instructions") + 1],
"be careful")
def test_host_dispatch_passes_instructions(self):
host = _load_host()
class FakeServer:
def __init__(self):
self.calls = []
def create_thread(self, cwd, model=None, sandbox=None,
approval_policy=None, instructions=None):
self.calls.append(instructions)
return "thr1"
srv = FakeServer()
state = {"tasks": {}}
with tempfile.TemporaryDirectory() as tmpdir:
host.dispatch(srv, state, "t1", "hi", tmpdir, "m", "low",
None, None, use_worktree=False,
developer_instructions="be careful")
self.assertEqual(srv.calls, ["be careful"])