from __future__ import annotations import pytest from app.schemas.pmflow_ai_turn_runtime import ( PMFlowAIAnswerState, PMFlowAIEvidenceFinding, PMFlowAIGoalFrame, PMFlowAIRuntimeAction, PMFlowAITargetScope, PMFlowAITurnActionState, PMFlowAITurnEvidenceState, PMFlowAITurnObjectRef, PMFlowAITurnObjectState, PMFlowAITurnState, ) from app.schemas.rag import RAGAskRequest from app.services.pmflow_ai_evidence_absorber import PMFlowAIEvidenceAbsorber from app.services import pmflow_ai_evidence_tool_registry as tool_registry_module from app.services.pmflow_ai_evidence_tool_registry import PMFlowAIEvidenceToolRegistry from app.services.pmflow_ai_turn_runtime_service import ( PMFlowAITurnRuntimeService, _apply_project_operating_answer_verification_to_answer, _apply_project_operating_answer_verification_to_canvas, _apply_project_operating_answer_verification_to_packet, _project_operating_answer_verification_packet, _render_project_operating_answer_from_turn_state, ) class _FakeProjectionPayload: def __init__(self, payload: dict): self.payload = payload def model_dump(self, *, mode: str = "json") -> dict: _ = mode return self.payload class _FakeProjectionService: def __init__(self, db, *, tenant_id: int): _ = db, tenant_id async def get_truth_state(self, *, project_id: int, current_user_id: int): _ = current_user_id return _FakeProjectionPayload( { "loopStage": "command_idle", "project": { "id": str(project_id), "name": "童析", "stage": "development", "status": "active", }, "projectMemory": { "truthNodeFacts": [ {"summary": "MVP聚焦单次视频到单次报告闭环"}, ], }, "changeSets": [ {"id": "cs-1", "title": "补齐加密方案评审", "status": "pending_review"}, ], "changeItems": [ {"id": "ci-1", "title": "传输加密验收标准", "riskLevel": "high"}, ], "approvals": [ {"id": "approval-1", "state": "requested"}, ], "reviewerRouting": { "summary": { "totalPendingReview": 1, "highRiskCount": 1, "missingReviewerCount": 0, }, "items": [ { "reviewReason": "高风险儿童数据传输方案需要技术审查", "suggestedReviewer": { "displayName": "安全负责人", "reason": "需要确认数据保护方案", }, } ], }, "feed": [ { "title": "加密方案进入评审", "summary": "等待测试证据和技术审查", "kind": "review", } ], "risks": [ {"title": "儿童数据合规风险", "summary": "需要补齐加密和匿名化证据"}, ], } ) async def get_execution_summary(self, *, project_id: int, current_user_id: int): _ = project_id, current_user_id return _FakeProjectionPayload( { "execution": { "taskId": "194", "status": "pr_open", "prompt": "", }, "acceptance": { "status": "waiting_test_evidence", "requiredAction": "run_required_tests", "blockingReasons": ["缺少 test_passed 或 checks_passed 证据。"], }, "releaseGate": { "status": "needs_confirmation", "title": "需要人工确认", "summary": "已有执行回写,但缺少通过测试证据。", "blockingReasons": ["缺少 PR 证据。"], "nextActions": ["补充测试回写。"], }, "qualityContract": { "summary": {"releaseBlockingMissing": 4}, "blockingReasons": ["代码仓库联调与 PR 证据"], }, "releaseSignals": { "feedback": [{"title": "家长样本反馈"}], "recommendations": [{"title": "优先补齐安全声明"}], }, "recentProgressEvents": [ {"title": "PR 已打开", "status": "pr_open"}, {"title": "测试证据待回写", "status": "waiting"}, ], "recentDeliveries": [ {"title": "加密方案评审包", "status": "submitted"}, ], "deliveryArtifacts": { "test_outputs": [ {"name": "pytest", "status": "passed"}, ], }, "artifactReview": { "items": [ {"title": "PR 证据", "status": "needs_review"}, ], "blockingReasons": ["缺少截图证据"], }, "activeDevTasks": [ { "id": "194", "title": "完成儿童数据匿名化与传输加密方案评审", "status": "in_progress", }, { "id": "188", "title": "设计视频上传-分析-报告三步信息架构与低保真原型", "status": "todo", }, ], } ) def _project_execution_state() -> PMFlowAITurnState: frame = PMFlowAIGoalFrame( frame_id="project-execution", operation_kind="inspect_object", target_scope=PMFlowAITargetScope(scope_type="current_project", binding_strength="explicit"), answer_shape="execution_guidance", target_object_requirement="single", ) return PMFlowAITurnState( selected_frame=frame, candidate_frames=[frame], answer_state=PMFlowAIAnswerState(answer_shape="execution_guidance"), object_state=PMFlowAITurnObjectState( primary_object=PMFlowAITurnObjectRef( object_type="project", object_ref="project:141", title="童析", binding_strength="strong", ) ), ) @pytest.mark.asyncio async def test_packet_k_project_status_tool_preserves_execution_projection(monkeypatch) -> None: monkeypatch.setattr(tool_registry_module, "PMFlowOSProjectionService", _FakeProjectionService) state = _project_execution_state() payload = await PMFlowAIEvidenceToolRegistry(object(), tenant_id=12).run( action_type="inspect_project_status", request=RAGAskRequest( question="现在我的项目怎么推进好呢", surface_context={ "space_runtime_context": { "workbench": { "projects": [ {"id": 141, "object_ref": "project:141", "title": "童析", "status": "active"}, ] } } }, ), current_user=object(), turn_state=state, ) result = payload["runtime_evidence_tool_result"] facts = "\n".join(item["fact"] for item in result["findings"]) packet = result["structured_evidence"]["project_operating_evidence_packet"] assert "项目概况:童析,阶段:development" in facts assert "当前执行任务:完成儿童数据匿名化与传输加密方案评审" in facts assert "发布/验收门禁:门禁状态:needs_confirmation" in facts assert "阻塞项:缺少 test_passed 或 checks_passed 证据。" in facts assert "候选任务:当前有 2 个执行/待执行任务" in facts assert "项目状态 active" not in facts assert packet["packet_version"] == "project_operating_evidence.v1" assert packet["project_ref"] == "project:141" assert packet["command_state"]["coverage_status"] == "covered" assert packet["execution_state"]["coverage_status"] == "covered" assert packet["quality_test_state"]["coverage_status"] == "covered" assert packet["release_growth_state"]["coverage_status"] == "covered" assert packet["team_state"]["coverage_status"] == "covered" assert packet["recent_timeline_state"]["coverage_status"] == "covered" assert packet["project_memory_state"]["coverage_status"] == "covered" assert packet["risk_blocker_state"]["facts"][0] == "缺少 test_passed 或 checks_passed 证据。" assert "team_state" not in packet["coverage"]["missing_facets"] assert any("待审路由" in fact for fact in packet["team_state"]["facts"]) assert any("进展事件" in fact for fact in packet["execution_state"]["facts"]) assert any("反馈输入" in fact for fact in packet["release_growth_state"]["facts"]) assert packet["coverage"]["coverage_score"] > 0.5 assert payload["runtime_observation"]["items"][0]["business_values"]["coverage_score"] == packet["coverage"]["coverage_score"] assert payload["runtime_observation"]["can_answer_operations"] == ["inspect_object"] @pytest.mark.asyncio async def test_packet_q_absorber_consumes_project_operating_evidence_packet(monkeypatch) -> None: monkeypatch.setattr(tool_registry_module, "PMFlowOSProjectionService", _FakeProjectionService) state = _project_execution_state() payload = await PMFlowAIEvidenceToolRegistry(object(), tenant_id=12).run( action_type="inspect_project_status", request=RAGAskRequest( question="现在我的项目怎么推进好呢", surface_context={ "space_runtime_context": { "workbench": { "projects": [ {"id": 141, "object_ref": "project:141", "title": "童析", "status": "active"}, ] } } }, ), current_user=object(), turn_state=state, ) absorption = PMFlowAIEvidenceAbsorber().absorb( turn_state=state, action=PMFlowAIRuntimeAction( action_id="inspect_project_status", action_type="inspect_project_status", tool_name="project_status_evidence_tool", ), payload=payload, ) absorbed_facts = "\n".join(item.fact for item in absorption.findings) assert "童析 / 测试与质量" in absorbed_facts assert "童析 / 发布与增长" in absorbed_facts assert "童析 / 团队层" in absorbed_facts assert "team_state_missing" not in absorption.remaining_gaps assert absorption.structured_evidence["project_operating_evidence_packet"]["project_ref"] == "project:141" def test_packet_l_coverage_gate_forces_project_execution_probe_before_answer() -> None: state = _project_execution_state() state.sufficiency_state.can_answer_now = True state.action_state = PMFlowAITurnActionState( candidate_actions=[ PMFlowAIRuntimeAction( action_id="synthesize_answer", action_type="synthesize_answer", tool_name="turn_state_answer_synthesizer", ) ] ) service = PMFlowAITurnRuntimeService(object(), tenant_id=12) decision = service.assess_sufficiency(state) action = service.select_next_minimal_action(state) assert decision.needs_action is True assert decision.next_action_type == "inspect_project_status" assert action is not None assert action.action_type == "inspect_project_status" def test_packet_s_coverage_gate_ignores_legacy_text_findings_without_profile() -> None: state = _project_execution_state() state.sufficiency_state.can_answer_now = True state.evidence_state = PMFlowAITurnEvidenceState( known_facts=[ PMFlowAIEvidenceFinding( fact="项目概况:童析,阶段:development,状态:active。", source="PMFlow 项目执行态证据", ), PMFlowAIEvidenceFinding( fact="当前执行任务:完成儿童数据匿名化与传输加密方案评审,任务状态:in_progress,执行状态:pr_open。", source="PMFlow 项目执行态证据", ), PMFlowAIEvidenceFinding( fact="发布/验收门禁:门禁状态:needs_confirmation,需要人工确认。", source="PMFlow 项目执行态证据", ), PMFlowAIEvidenceFinding( fact="候选任务:当前有 2 个执行/待执行任务。", source="PMFlow 项目执行态证据", ), ] ) decision = PMFlowAITurnRuntimeService(object(), tenant_id=12).assess_sufficiency(state) assert decision.needs_action is True assert decision.next_action_type == "inspect_project_status" def test_packet_s_coverage_gate_allows_answer_from_structured_profile() -> None: state = _project_execution_state() state.sufficiency_state.can_answer_now = True state.evidence_state = PMFlowAITurnEvidenceState( structured_evidence={ "project_operating_evidence_packets": [_covered_project_operating_packet()], "project_operating_evidence_packet": _covered_project_operating_packet(), } ) decision = PMFlowAITurnRuntimeService(object(), tenant_id=12).assess_sufficiency(state) assert decision.can_answer_now is True assert decision.needs_action is False def test_packet_t_project_operating_verification_bounds_final_answer_surface() -> None: packet = _verification_project_operating_packet() state = _project_execution_state() state.answer_state = PMFlowAIAnswerState(answer_shape="execution_guidance") state.object_state = PMFlowAITurnObjectState( primary_object=PMFlowAITurnObjectRef( object_type="project", object_ref="project:141", title="童析", binding_strength="explicit", ) ) state.evidence_state = PMFlowAITurnEvidenceState( structured_evidence={ "project_operating_evidence_packets": [packet], "project_operating_evidence_packet": packet, } ) verification = _project_operating_answer_verification_packet(state) assert verification["packet_version"] == "project_operating_answer_verification.v1" assert verification["can_claim_full_execution_guidance"] is False assert verification["missing_or_partial_facets"][0]["display_label"] == "测试质量" assert verification["safe_next_actions"][0]["display_label"] == "整理加密评审材料" assert verification["blocked_actions"][0]["display_label"] == "直接发布" customer_packet = _apply_project_operating_answer_verification_to_packet( packet={"limitations": [], "suggested_actions": []}, verification=verification, ) canvas = _apply_project_operating_answer_verification_to_canvas( canvas={"limitations": [], "next_actions": []}, verification=verification, ) answer = _apply_project_operating_answer_verification_to_answer( answer="当前可以继续推进,但要先补齐证据。", verification=verification, ) assert any("测试质量" in item for item in customer_packet["limitations"]) assert customer_packet["suggested_actions"][0]["title"] == "整理加密评审材料" assert canvas["next_actions"][0]["title"] == "整理加密评审材料" assert not any(action.get("title") == "直接发布" for action in canvas["next_actions"]) assert "可推进:整理加密评审材料" in answer assert "不能直接做:直接发布" in answer assert "需要注意:测试质量" in answer def test_packet_ab_project_operating_packet_renders_concrete_project_answer() -> None: packet = _verification_project_operating_packet() state = _project_execution_state() state.answer_state = PMFlowAIAnswerState(answer_shape="execution_guidance") state.evidence_state = PMFlowAITurnEvidenceState( structured_evidence={ "project_operating_evidence_packets": [packet], "project_operating_evidence_packet": packet, } ) answer = _render_project_operating_answer_from_turn_state( question="现在这个项目怎么样了,详细跟我说下", turn_state=state, ) assert "童析" in answer assert "开发执行:PR 已打开" in answer assert "风险阻塞:发布前需解除加密评审阻塞" in answer assert "整理加密评审材料" in answer assert "直接发布" in answer assert "无法提供" not in answer assert "请补充" not in answer def _covered_project_operating_packet() -> dict: return { "packet_version": "project_operating_evidence.v1", "project_ref": "project:141", "project_title": "童析", "command_state": {"coverage_status": "covered", "summary": "阶段:development"}, "execution_state": {"coverage_status": "covered", "summary": "执行状态:pr_open"}, "workbench_state": {"coverage_status": "covered", "summary": "执行/待执行任务 2 个"}, "risk_blocker_state": {"coverage_status": "covered", "summary": "缺少测试证据"}, "decision_review_state": {"coverage_status": "missing", "summary": ""}, "release_growth_state": {"coverage_status": "missing", "summary": ""}, } def _verification_project_operating_packet() -> dict: def facet(key: str, label: str, status: str, summary: str = "", gaps: list[str] | None = None) -> dict: return { "facet_key": key, "display_label": label, "coverage_status": status, "summary": summary, "facts": [summary] if summary else [], "remaining_gaps": gaps or [], } return { "packet_version": "project_operating_evidence.v1", "project_id": 141, "project_ref": "project:141", "project_title": "童析", "project_status": "active", "command_state": facet("command_state", "指挥台", "covered", "阶段:development"), "workbench_state": facet("workbench_state", "项目工作台", "covered", "有 2 个下一步"), "decision_review_state": facet("decision_review_state", "决策与待确认", "covered", "有高风险评审"), "execution_state": facet("execution_state", "开发执行", "covered", "PR 已打开"), "quality_test_state": facet("quality_test_state", "测试质量", "partial", "缺少测试运行记录", ["缺少测试运行记录"]), "release_growth_state": facet("release_growth_state", "发布增长", "missing", "", ["缺少发布门禁结果"]), "team_state": facet("team_state", "团队层", "covered", "建议安全负责人评审"), "project_memory_state": facet("project_memory_state", "项目记忆", "covered", "已记录 MVP 边界"), "risk_blocker_state": facet("risk_blocker_state", "风险阻塞", "covered", "发布前需解除加密评审阻塞"), "recent_timeline_state": facet("recent_timeline_state", "最近动态", "covered", "最近进入评审"), "safe_next_actions": [ { "action_key": "prepare_review", "display_label": "整理加密评审材料", "safety": "prepare_only", "reason": "来自项目执行态的准备动作,执行前仍需确认。", "target_ref": "project:141", } ], "blocked_actions": [ { "action_key": "release_now", "display_label": "直接发布", "safety": "blocked", "reason": "当前证据显示发布门禁未解除。", "target_ref": "project:141", } ], }