import json from types import SimpleNamespace import pytest from app.api import tasks as tasks_api from app.services.sstc.charter_data import ( _build_task_breakdown_context, _build_task_breakdown_prompt, ) from app.schemas import TaskBreakdownRequest def test_task_breakdown_context_flattens_charter_modules() -> None: project = SimpleNamespace( id=6, title="内部知识库升级", name="内部知识库升级", description="围绕内部知识库问答能力升级", status="active", assigned_role="backend", ) charter = SimpleNamespace( id=55, title="内部知识库升级宪章", version=2, status="aligning", content={ "modules": { "module_1": { "problem_statement": "减少重复检索", "goal_statement": "提升命中率 / 降低重复提问", "kpis": "首响 < 3s", }, "module_2": { "in_scope": "问答检索,知识补充", "out_of_scope": "权限模型重构", }, "module_4": { "milestones": "需求冻结\n开发完成", }, "module_5": { "risk_register": "需求膨胀; 依赖不清", }, "module_6": { "governance_rule": "附件仅作证据引用", }, } }, ) context = _build_task_breakdown_context( description="支持导出知识库内容", project=project, charter=charter, ) assert context["project"]["title"] == "内部知识库升级" assert context["project"]["status"] == "active" assert context["charter"]["summary"] == "减少重复检索" assert context["charter"]["objectives"] == ["提升命中率", "降低重复提问"] assert context["charter"]["in_scope"] == ["问答检索", "知识补充"] assert context["charter"]["out_of_scope"] == ["权限模型重构"] assert context["charter"]["milestones"] == ["需求冻结", "开发完成"] assert "附件仅作证据引用" in context["charter"]["constraints"][0] def test_task_breakdown_prompt_embeds_charter_context() -> None: prompt = _build_task_breakdown_prompt( project_id=6, context={ "project": { "id": 6, "title": "内部知识库升级", "description": "围绕内部知识库问答能力升级", "status": "active", "assigned_role": "backend", }, "charter": { "id": 55, "title": "内部知识库升级宪章", "version": 2, "status": "aligning", "summary": "减少重复检索", "objectives": ["提升命中率", "降低重复提问"], "scope": "知识库检索与问答", "in_scope": ["问答检索", "知识补充"], "out_of_scope": ["权限模型重构"], "success_criteria": ["首响 < 3s"], "constraints": ["附件仅作证据引用"], "milestones": ["需求冻结", "开发完成"], "risks": ["需求膨胀"], "signal_ids": [12], }, }, available_skills="FastAPI, React, LangChain", ) assert "需求宪章与项目上下文" in prompt assert "内部知识库升级宪章" in prompt assert "FastAPI, React, LangChain" in prompt assert "objectives" in prompt assert "constraints" in prompt @pytest.mark.asyncio async def test_general_task_breakdown_route_uses_project_and_charter_context(monkeypatch) -> None: captured: dict[str, object] = {} async def _fake_load_context(db, *, tenant_id: int, description: str, project_id=None, charter_id=None): _ = (db, tenant_id, project_id, charter_id) captured["description"] = description return { "request": {"description": description}, "project": { "id": 6, "title": "内部知识库升级", "description": "围绕内部知识库问答能力升级", "status": "active", "assigned_role": "backend", }, "charter": { "id": 55, "title": "内部知识库升级宪章", "version": 2, "status": "aligning", "summary": "减少重复检索", "objectives": ["提升命中率", "降低重复提问"], "scope": "知识库检索与问答", "in_scope": ["问答检索", "知识补充"], "out_of_scope": ["权限模型重构"], "success_criteria": ["首响 < 3s"], "constraints": ["附件仅作证据引用"], "milestones": ["需求冻结", "开发完成"], "risks": ["需求膨胀"], "signal_ids": [12], }, } async def _fake_load_skills(db, *, tenant_id: int): _ = (db, tenant_id) return "FastAPI, React, LangChain" class _FakeLLM: async def generate(self, prompt: str, **kwargs): captured["prompt"] = prompt captured["llm_kwargs"] = kwargs return json.dumps( [ { "task_name": "需求澄清与范围确认", "type": "ai", "estimated_hours": 4, "priority": "high", "required_skill_names": ["FastAPI"], }, { "task_name": "核心方案落地", "type": "backend", "estimated_hours": 12, "priority": "medium", "required_skill_names": ["React", "LangChain"], }, ], ensure_ascii=False, ) @staticmethod def _parse_json_array(text: str): parsed = json.loads(text) return [item for item in parsed if isinstance(item, dict)] class _FakePlane: async def sync_tasks(self, tasks, is_urgent: bool = False): _ = is_urgent return {"synced_count": len(tasks)} monkeypatch.setattr(tasks_api, "_load_task_breakdown_context", _fake_load_context) monkeypatch.setattr(tasks_api, "_load_available_skill_names", _fake_load_skills) monkeypatch.setattr(tasks_api, "llm_service", _FakeLLM()) monkeypatch.setattr(tasks_api, "plane_service", _FakePlane()) result = await tasks_api.breakdown_tasks( TaskBreakdownRequest( description="支持导出知识库内容", project_id=6, charter_id=55, sync_to_plane=True, ), db=SimpleNamespace(), current_user=SimpleNamespace(tenant_id=1, id=1), ) assert "内部知识库升级宪章" in str(captured["prompt"]) assert "FastAPI, React, LangChain" in str(captured["prompt"]) assert result["sync_result"]["synced_count"] == 2 assert result["tasks"][0]["task_name"] == "需求澄清与范围确认"