PMFlow/backend/tests/test_sstc_tasks_ai_breakdow...

198 lines
7.3 KiB
Python

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"] == "需求澄清与范围确认"