- run_dual 编排:规划者两阶段 JSON(plan/review,失败回喂重试一次再降级),
redo 时裁决意见回喂执行者,交接上限 agent.max_handoffs(默认 2)
- 交接文档 agent_runs/{id}/handoff.json(智能体版交流文本:instructions/acceptance/exchanges)
- /agent 新增 executor_pool_id;规划者==执行者条目拒绝;整体 token_cap 覆盖两级调用
- ToolLoop 增 emit_final 开关(内层循环不发终态,防前端 SSE 提前收口)
- 前端:执行者选择器 + phase/message 事件渲染(阶段徽标 + 双色消息卡)
- 测试 +3(done/redo/执行者故障),全量 277 passed
- fix(tests): test_config_get_put_reset 增加设置备份/恢复隔离,防止清掉用户真实配置
136 lines
4.1 KiB
Python
136 lines
4.1 KiB
Python
"""FastAPI 网关测试:v1 legacy 端点保持 + v2 端点(封闭,注入 mock 管线)。"""
|
||
import pytest
|
||
|
||
pytest.importorskip("fastapi")
|
||
pytest.importorskip("httpx")
|
||
|
||
from fastapi.testclient import TestClient
|
||
|
||
import gateway.api as ga
|
||
from gateway.api import app
|
||
|
||
|
||
@pytest.fixture()
|
||
def client():
|
||
return TestClient(app)
|
||
|
||
|
||
@pytest.fixture()
|
||
def v2_client(client):
|
||
# 用 mock worker 构建真实 v2 管线并注入(无需 API key / 真实模型)
|
||
pipe = ga.build_v2_pipeline(worker_cfg_override={"backend": "mock"})
|
||
ga.set_pipeline(pipe)
|
||
return client
|
||
|
||
|
||
def test_health(client):
|
||
resp = client.get("/health")
|
||
assert resp.status_code == 200
|
||
data = resp.json()
|
||
assert data["status"] == "ok"
|
||
assert "code" in data["domains"]
|
||
|
||
|
||
def test_chat_legacy(client):
|
||
resp = client.post("/chat/legacy", json={"query": "用 Python 写一个快速排序函数"})
|
||
assert resp.status_code == 200
|
||
data = resp.json()
|
||
assert data["response"]
|
||
assert data["domain"] == "code"
|
||
assert "route" in data
|
||
|
||
|
||
def test_chat_v2(v2_client):
|
||
# POST /chat 立即返回 request_id(异步协议)
|
||
resp = v2_client.post("/chat", json={"query": "请介绍快速排序算法"})
|
||
assert resp.status_code == 200
|
||
data = resp.json()
|
||
assert "request_id" in data
|
||
assert data["status"] == "pending"
|
||
|
||
# 轮询 /runs/{id}/status 直到完成
|
||
import time
|
||
for _ in range(50): # 最多 5s
|
||
time.sleep(0.1)
|
||
status_resp = v2_client.get(f"/runs/{data['request_id']}/status")
|
||
assert status_resp.status_code == 200
|
||
s = status_resp.json()
|
||
if s["status"] in ("done", "failed"):
|
||
break
|
||
|
||
assert s["status"] == "done", f"期望 done,实际 {s['status']},error={s.get('error')}"
|
||
assert s["response"]
|
||
assert "pipeline_status" in s
|
||
assert s["pipeline_status"] in ("fast_path", "done", "escalated")
|
||
# fast_path 不写 workspace.json,所以 workspace_path 可能为 None
|
||
if s["pipeline_status"] != "fast_path":
|
||
assert s["workspace_path"] is not None
|
||
|
||
|
||
def test_chat_empty_query(client):
|
||
resp = client.post("/chat", json={"query": ""})
|
||
assert resp.status_code == 422
|
||
|
||
|
||
def test_metrics(client):
|
||
resp = client.get("/metrics")
|
||
assert resp.status_code == 200
|
||
data = resp.json()
|
||
assert "router" in data
|
||
assert "cache" in data
|
||
assert "v2" in data
|
||
assert "review" in data
|
||
|
||
|
||
|
||
|
||
|
||
def test_config_get_put_reset(client):
|
||
"""/config 端到端。settings.json 是活文件(用户真实配置),
|
||
测试前后必须备份/恢复,禁止把用户配置清掉。"""
|
||
import json
|
||
store = ga.settings_store()
|
||
snapshot = json.loads(json.dumps(store._data, ensure_ascii=False))
|
||
try:
|
||
# GET 默认
|
||
r = client.get("/config")
|
||
assert r.status_code == 200
|
||
assert r.json()["worker"]["backend"] in ("llama_server", "openai", "mock")
|
||
# PUT 更新 worker
|
||
r2 = client.put("/config", json={"worker": {"backend": "mock", "temperature": 0.5}})
|
||
assert r2.status_code == 200
|
||
assert r2.json()["worker"]["temperature"] == 0.5
|
||
# 重置
|
||
r3 = client.post("/config/reset")
|
||
assert r3.status_code == 200
|
||
assert r3.json()["worker"]["backend"] == "llama_server"
|
||
finally:
|
||
store._data = snapshot
|
||
store.save()
|
||
ga.rebuild_pipeline()
|
||
|
||
|
||
def test_workspace_not_found(client):
|
||
resp = client.get("/runs/nonexistent/workspace")
|
||
assert resp.status_code == 404
|
||
|
||
|
||
def test_web_ui_served(client):
|
||
resp = client.get("/")
|
||
assert resp.status_code == 200
|
||
assert resp.headers["content-type"].startswith("text/html")
|
||
# Vue SPA:由 Vite 生成,特征是 <div id="app"> 和 /static/assets/ 引用
|
||
assert '<div id="app">' in resp.text
|
||
assert '/static/assets/' in resp.text
|
||
|
||
|
||
def test_review_flow(client):
|
||
q = ga.get_review()
|
||
rid = q.enqueue("req-x", "q", "ans", tags=["safety"], reason="test")
|
||
assert q.count() >= 1
|
||
resp = client.get("/review/queue")
|
||
assert resp.status_code == 200
|
||
resp2 = client.post(f"/review/{rid}", params={"verdict": "approve"})
|
||
assert resp2.status_code == 200
|
||
assert resp2.json()["ok"] is True
|