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