from __future__ import annotations from types import SimpleNamespace import pytest from fastapi import FastAPI from httpx import ASGITransport, AsyncClient from app.api.ai_steward_api import routes_capture from app.api.ai_steward_api.schemas import ProjectFromTextResponse from app.core.database import get_db from app.dependencies.auth import get_current_user from app.dependencies.tenant import get_tenant_id from app.services.ai_steward_service import AIStewardService @pytest.mark.asyncio async def test_project_from_text_returns_enveloped_signal_sources(monkeypatch) -> None: async def fake_create_project_from_natural_language(self, **kwargs): # noqa: ANN001 _ = (self, kwargs) return { "project": {"id": "11", "name": "AI 客服", "title": "AI 客服平台", "description": "demo", "status": "active"}, "signal": {"id": "21", "content": "main signal", "impact_score": 0.91}, "charter": {"id": "31", "title": "宪章", "content": {"summary": "demo"}}, "tasks": [], "risks": [], "analysis": { "project_name": "AI 客服", "project_title": "AI 客服平台", "tech_stack": ["FastAPI", "React"], "timeline": "3 周", "primary_role": "backend", "requirement_items": [ { "section": "核心痛点", "title": "内容本地化", "description": "翻译公司贵、慢、不地道", } ], }, "signal_sources": [ { "id": "101", "source_kind": "custom", "source_name": "核心痛点 - 内容本地化", "source_identifier": "custom:core-pain-content-localization", "display_source_kind": "自定义来源", "technical_identifier": "custom:core-pain-content-localization", "description": "翻译公司贵、慢、不地道", "section": "核心痛点", } ], } async def _fake_get_db(): yield object() monkeypatch.setattr(AIStewardService, "create_project_from_natural_language", fake_create_project_from_natural_language) app = FastAPI() app.include_router(routes_capture.router) app.dependency_overrides[get_db] = _fake_get_db app.dependency_overrides[get_tenant_id] = lambda: 1 app.dependency_overrides[get_current_user] = lambda: SimpleNamespace( id=9, tenant_id=1, name="管理员", username="admin", role="ADMIN", ) transport = ASGITransport(app=app) async with AsyncClient(transport=transport, base_url="http://test") as client: response = await client.post("/ai-steward/project-from-text", json={"input_text": "我们要做一个 AI 客服工具"}) assert response.status_code == 200 payload = response.json() assert payload["code"] == 0 assert payload["message"] == "success" assert payload["data"]["project"]["id"] == "11" assert payload["data"]["signal_sources"][0]["source_name"] == "核心痛点 - 内容本地化" assert payload["data"]["signal_sources"][0]["display_source_kind"] == "自定义来源" assert payload["data"]["signal_sources"][0]["technical_identifier"] == "custom:core-pain-content-localization" def test_project_from_text_response_schema_exposes_signal_source_display_fields() -> None: schema = ProjectFromTextResponse.model_json_schema() signal_source = schema["$defs"]["ProjectFromTextSignalSource"] assert "display_source_kind" in signal_source["properties"] assert "technical_identifier" in signal_source["properties"] @pytest.mark.asyncio async def test_project_requirement_analysis_uses_canonical_function_name(monkeypatch) -> None: service = AIStewardService(object()) captured: dict[str, object] = {} async def fake_generate(prompt: str, **kwargs): # noqa: ANN001 _ = prompt captured["function_name"] = kwargs.get("function_name") return '{"project_title":"AI 客服","requirement_items":[]}' monkeypatch.setattr(service.llm, "generate", fake_generate) analysis = await service._analyze_project_input("请创建一个 AI 客服项目", tenant_id=1) assert analysis["project_title"] == "AI 客服" assert captured["function_name"] == "project_requirement_extraction"