"""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()) # 前两次失败(占位)-> issue;decide 后重跑成功 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")