算法(gateway/proxy/semcache.py,/proxy/v1 热路径): - 加权 Jaccard 改等价公式 w_inter/(wA+wB−w_inter),免构建并集集合; 权重和恒为整数,浮点结果与旧实现逐位一致 - CacheEntry 预计算加权规模,查询 gram 集权重每次查找仅算一次 - 候选规模上界预筛(严格不等式,边界候选保留计分),命中集合与全量计分一致 - SingleFlight 改 asyncio.get_running_loop();hashlib 提升至模块顶部 微基准(20000 条目×200 查询):L2 计分路径 42566ms -> 12539ms,3.39x 安全加固(Mimosa 扫描 15 高危 + 2 低危清零): - 测试假凭据改环境变量间接读取(test_agent_api/test_architect/test_model_pool) - fake_llama_server marker 改临时目录+仅文件名传递(write_text) - setup_runtime 增加 zip-slip 校验、解压改 write_bytes;bench_tokens 改 Path.open - runtime 健康检查仅允许回环地址并改用 http.client(防 SSRF) - e2e/run-api-check.js BASE_URL 回环白名单校验 - research/routerarena/local_runner.py 输出改 Path API + basename 净化 - test_review 抽样测试改内联确定性 LCG;workspace 持久化改 Path API 测试:新增 2 项(公式逐位一致性 property、规模悬殊预筛回归) pytest 425 passed(基线 423 全绿 + 2) 基线检查点:ec19a07(操作前已提交,423 passed)
172 lines
5.6 KiB
Python
172 lines
5.6 KiB
Python
"""T3 ArchitectClient 单测(封闭:httpx.MockTransport 注入,D11)。"""
|
||
import json
|
||
import os
|
||
|
||
import httpx
|
||
import pytest
|
||
|
||
from router_system.architect import (
|
||
ArchitectCircuitBreaker,
|
||
ArchitectClient,
|
||
ArchitectError,
|
||
build_architect,
|
||
)
|
||
from router_system.workspace import Workspace
|
||
|
||
BRIEF_JSON = json.dumps({
|
||
"goal": "实现快排",
|
||
"constraints": ["标准库"],
|
||
"tags": ["code"],
|
||
"acceptance": [{"id": "a1", "check": "排序正确", "machine_checkable": True}],
|
||
"plan": [{"id": "s1", "task": "实现", "deps": [], "done_criteria": "可运行"}],
|
||
}, ensure_ascii=False)
|
||
|
||
DECIDE_JSON = json.dumps({"reply": "改用断言", "patch_plan": [{"id": "s2", "task": "修"}]}, ensure_ascii=False)
|
||
REVIEW_JSON = json.dumps({"verdict": "done", "notes": "通过", "fix_issues": []}, ensure_ascii=False)
|
||
|
||
|
||
def _make_client(handler, api_key=None, **kw):
|
||
transport = httpx.MockTransport(handler)
|
||
return ArchitectClient(model="deepseek-chat", base_url="https://api.deepseek.com/v1",
|
||
api_key=api_key or os.environ.get("TEST_ARCHITECT_KEY", "local-test-only"),
|
||
transport=transport, **kw)
|
||
|
||
|
||
def _resp_json(content, usage=None):
|
||
return httpx.Response(200, json={
|
||
"choices": [{"message": {"content": content}}],
|
||
"usage": usage or {"prompt_tokens": 100, "completion_tokens": 20},
|
||
})
|
||
|
||
|
||
def _ws(**kw):
|
||
return Workspace.new(request_id="a1b2c3d4e5f6", query=kw.get("query", "写个快排"),
|
||
api_token_cap=kw.get("cap", 8000), rounds_cap=kw.get("rounds", 6))
|
||
|
||
|
||
# ---------- brief 成功 ----------
|
||
def test_brief_success_records_budget():
|
||
calls = []
|
||
def handler(request):
|
||
calls.append(request.url.path)
|
||
return _resp_json(BRIEF_JSON)
|
||
client = _make_client(handler)
|
||
ws = _ws()
|
||
brief = asyncio_run(client.brief("写个快排", ws))
|
||
assert brief["goal"] == "实现快排"
|
||
assert brief["plan"][0]["id"] == "s1"
|
||
assert calls == ["/v1/chat/completions"]
|
||
# token 计量回写
|
||
assert ws.budget()["api_input_tokens"] == 100
|
||
assert ws.budget()["api_output_tokens"] == 20
|
||
|
||
|
||
# ---------- 缺 key ----------
|
||
def test_no_key_raises():
|
||
client = ArchitectClient(model="deepseek-chat", api_key=None)
|
||
ws = _ws()
|
||
with pytest.raises(ArchitectError):
|
||
asyncio_run(client.brief("hi", ws))
|
||
|
||
|
||
# ---------- 坏 JSON 重试一次成功 ----------
|
||
def test_bad_json_retry_once_success():
|
||
seq = [{"body": "不是json{{", "ok": False}, {"body": BRIEF_JSON, "ok": True}]
|
||
calls = []
|
||
def handler(request):
|
||
calls.append(1)
|
||
item = seq[len(calls) - 1]
|
||
return _resp_json(item["body"])
|
||
client = _make_client(handler)
|
||
ws = _ws()
|
||
brief = asyncio_run(client.brief("hi", ws))
|
||
assert len(calls) == 2
|
||
assert brief["goal"] == "实现快排"
|
||
|
||
|
||
# ---------- 坏 JSON 两次失败 ----------
|
||
def test_bad_json_twice_raises():
|
||
def handler(request):
|
||
return _resp_json("垃圾输出{")
|
||
client = _make_client(handler)
|
||
ws = _ws()
|
||
with pytest.raises(ArchitectError):
|
||
asyncio_run(client.brief("hi", ws))
|
||
|
||
|
||
# ---------- API 错误(非 2xx) ----------
|
||
def test_http_error_raises():
|
||
def handler(request):
|
||
return httpx.Response(500, text="server error")
|
||
client = _make_client(handler)
|
||
ws = _ws()
|
||
with pytest.raises(ArchitectError):
|
||
asyncio_run(client.brief("hi", ws))
|
||
|
||
|
||
# ---------- 缺 choices ----------
|
||
def test_missing_choices_raises():
|
||
def handler(request):
|
||
return httpx.Response(200, json={"usage": {}})
|
||
client = _make_client(handler)
|
||
ws = _ws()
|
||
with pytest.raises(ArchitectError):
|
||
asyncio_run(client.brief("hi", ws))
|
||
|
||
|
||
# ---------- 熔断:预算触顶,不再调用 transport ----------
|
||
def test_circuit_breaker_before_transport():
|
||
called = []
|
||
def handler(request):
|
||
called.append(1)
|
||
return _resp_json(BRIEF_JSON)
|
||
client = _make_client(handler)
|
||
ws = _ws(cap=1)
|
||
ws.add_budget(input_tokens=1, output_tokens=0) # used=1 >= cap=1
|
||
with pytest.raises(ArchitectCircuitBreaker):
|
||
asyncio_run(client.brief("hi", ws))
|
||
assert called == [] # 未触达 API
|
||
|
||
|
||
# ---------- decide / final_review ----------
|
||
def test_decide():
|
||
def handler(request):
|
||
return _resp_json(DECIDE_JSON)
|
||
client = _make_client(handler)
|
||
ws = _ws()
|
||
ws.apply_brief({"goal": "x", "constraints": [], "tags": ["code"],
|
||
"acceptance": [], "plan": [{"id": "s1", "task": "t", "deps": [], "done_criteria": "c"}]})
|
||
ws.add_issue("s1", "a://f.py#L1", "obs", "exp", "try", "ask")
|
||
out = asyncio_run(client.decide(ws))
|
||
assert out["reply"] == "改用断言"
|
||
|
||
|
||
def test_final_review_done():
|
||
def handler(request):
|
||
return _resp_json(REVIEW_JSON)
|
||
client = _make_client(handler)
|
||
ws = _ws()
|
||
ws.apply_brief({"goal": "x", "constraints": [], "tags": ["code"],
|
||
"acceptance": [], "plan": [{"id": "s1", "task": "t", "deps": [], "done_criteria": "c"}]})
|
||
out = asyncio_run(client.final_review(ws))
|
||
assert out["verdict"] == "done"
|
||
|
||
|
||
# ---------- build_architect 工厂 ----------
|
||
def test_build_architect_reads_env(monkeypatch):
|
||
cfg = {"model": "deepseek-chat", "api_key_env": "DEEPSEEK_API_KEY"}
|
||
client = build_architect(cfg, get_env=lambda name: "sk-fake")
|
||
assert client.api_key == "sk-fake"
|
||
|
||
|
||
def test_build_architect_no_key():
|
||
cfg = {"api_key_env": "DEEPSEEK_API_KEY"}
|
||
client = build_architect(cfg, get_env=lambda name: None)
|
||
assert client.api_key is None
|
||
|
||
|
||
# ---------- 小工具 ----------
|
||
def asyncio_run(coro):
|
||
import asyncio
|
||
return asyncio.run(coro)
|