Files
projectAIpopular/tests/test_pipeline.py
T

162 lines
6.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""T6 CollaborativePipeline 编排单测(封闭:MockTransport + 假 generate)。"""
import json
import httpx
import pytest
from router_system.architect import ArchitectClient
from router_system.pipeline import CollaborativePipeline
from router_system.verifier import Verifier
from router_system.worker import WorkerLoop
BRIEF_JSON = json.dumps({
"goal": "用 Python 实现一个函数",
"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": "s1", "task": "简化实现"}]},
ensure_ascii=False)
REVIEW_DONE = json.dumps({"verdict": "done", "notes": "通过", "fix_issues": []}, ensure_ascii=False)
def _architect_handler(brief=BRIEF_JSON, decide=DECIDE_JSON, review=REVIEW_DONE):
def handler(request):
body = json.loads(request.content)
msg = (body.get("messages") or [{}])[-1].get("content", "")
if "任务 brief" in msg or "任务分析" in msg:
return _resp(brief)
if "裁决" in msg:
return _resp(decide)
if "终审" in msg:
return _resp(review)
# 兜底(breach:architect_do
return _resp("(兜底答案)")
return handler
def _resp(content):
return httpx.Response(200, json={
"choices": [{"message": {"content": content}}],
"usage": {"prompt_tokens": 100, "completion_tokens": 20},
})
def _make_architect(handler):
return ArchitectClient(model="deepseek-chat", base_url="https://api.deepseek.com/v1",
api_key="sk-test", transport=httpx.MockTransport(handler))
def _fake_generate(texts):
it = iter(texts)
async def _g(prompt):
try:
return next(it)
except StopIteration:
return ""
return _g
def _make_worker(generate):
return WorkerLoop(generate=generate,
verifier=Verifier(sandbox_runner=lambda c, e, t: (0, "", "")),
kb=None, max_fix_attempts=2)
def _pipeline(architect, worker, **kw):
return CollaborativePipeline(architect=architect, worker=worker,
fast_path=kw.get("fast_path", False),
rounds_cap=kw.get("rounds_cap", 6),
api_token_cap=kw.get("api_token_cap", 8000),
breach_policy=kw.get("breach_policy", "architect_do"),
run_dir=kw.get("run_dir", "runs"))
def asyncio_run(coro):
import asyncio
return asyncio.run(coro)
# ---------- 快路径 ----------
def test_fast_path_returns_direct(tmp_path):
architect = _make_architect(_architect_handler())
worker = _make_worker(_fake_generate(["这是一段足够长的、非占位的直接回答内容,用于快路径。"]))
pipe = _pipeline(architect, worker, fast_path=True, run_dir=str(tmp_path / "runs"))
res = asyncio_run(pipe.run("讲一下排序"))
assert res.status == "fast_path"
assert res.fast_path is True
assert res.api_input_tokens == 0 # 未调用 Architect
assert "快路径" in res.response or "直接" in res.response
# ---------- 协作:一次通过 ----------
def test_collab_one_pass(tmp_path):
architect = _make_architect(_architect_handler())
worker = _make_worker(_fake_generate(["def my_func():\n return 42"]))
pipe = _pipeline(architect, worker, fast_path=False, run_dir=str(tmp_path / "runs"))
res = asyncio_run(pipe.run("写个函数"))
assert res.status == "done"
assert res.fast_path is False
assert res.api_input_tokens >= 100 # brief + 终审 用量已计量
assert res.workspace_path is not None
# 交流文本已落盘
import os
assert os.path.exists(res.workspace_path)
assert "done" in res.route
# ---------- 协作:带 issue 修复 ----------
def test_collab_issue_fix(tmp_path):
architect = _make_architect(_architect_handler())
# 前两次失败(占位)-> issuedecide 后重跑成功
worker = _make_worker(_fake_generate([
"待实现", "待实现", "def ok():\n return 1",
]))
pipe = _pipeline(architect, worker, fast_path=False, run_dir=str(tmp_path / "runs"))
res = asyncio_run(pipe.run("写个函数"))
assert res.status == "done"
assert any("issue" in r for r in res.route)
assert any("decide" in r for r in res.route)
# 至少有 1 条 decision
import json as _json
ws = json.load(open(res.workspace_path, encoding="utf-8"))
assert len(ws["decisions"]) >= 1
# ---------- 熔断兜底:本地降级 ----------
def test_breach_local_degrade(tmp_path):
architect = _make_architect(_architect_handler())
worker = _make_worker(_fake_generate(["不通过"]))
pipe = _pipeline(architect, worker, fast_path=False, api_token_cap=1,
breach_policy="local_only", run_dir=str(tmp_path / "runs"))
res = asyncio_run(pipe.run("写个函数"))
assert res.status == "failed"
assert "本地降级" in res.response
assert any("breach" in r for r in res.route)
# ---------- 熔断兜底:Architect 代做 ----------
def test_breach_architect_do(tmp_path):
# review 一直打回 fix(制造压力),最终走 breach:architect_do
def handler(request):
body = json.loads(request.content)
msg = (body.get("messages") or [{}])[-1].get("content", "")
if "任务 brief" in msg or "任务分析" in msg:
return _resp(BRIEF_JSON)
if "自动升级" in msg:
return _resp("Architect 兜底答案)")
return _resp(DECIDE_JSON)
architect = _make_architect(handler)
worker = _make_worker(_fake_generate(["待实现"]))
pipe = _pipeline(architect, worker, fast_path=False, rounds_cap=1,
api_token_cap=8000, breach_policy="architect_do",
run_dir=str(tmp_path / "runs"))
res = asyncio_run(pipe.run("写个函数"))
# rounds_cap=1:一轮后预算未满但回合触顶 -> breach -> architect_do
assert "breach" in " ".join(res.route)
assert res.status in ("escalated", "failed")