PMFlow/backend/tests/test_pmflow_ai_observation_...

152 lines
6.0 KiB
Python

from __future__ import annotations
import pytest
from app.schemas.pmflow_ai_turn_runtime import (
PMFlowAIGoalFrame,
PMFlowAITargetCollection,
PMFlowAITargetScope,
PMFlowAITurnActionState,
PMFlowAITurnState,
)
from app.schemas.rag import RAGAskRequest
from app.services.pmflow_ai_evidence_tool_registry import PMFlowAIEvidenceToolRegistry
from app.services.pmflow_ai_turn_runtime_service import PMFlowAITurnRuntimeService
from app.services.pmflow_ai_turn_state_reviser import PMFlowAITurnStateReviser
def _project_collection_state() -> PMFlowAITurnState:
scope = PMFlowAITargetScope(scope_type="current_space", binding_strength="surface")
frame = PMFlowAIGoalFrame(
frame_id="current-space-projects",
operation_kind="list_collection",
target_scope=scope,
target_collection=PMFlowAITargetCollection(object_type="project", required=True, scope=scope),
target_object_requirement="none",
answer_shape="fact_list",
)
return PMFlowAITurnState(selected_frame=frame, candidate_frames=[frame])
@pytest.mark.asyncio
async def test_packet_c_surface_tool_emits_runtime_observation_for_collection() -> None:
state = _project_collection_state()
payload = await PMFlowAIEvidenceToolRegistry(object(), tenant_id=1).run(
action_type="inspect_surface_context",
request=RAGAskRequest(
question="现在有哪些项目",
surface_context={
"space_runtime_context": {
"workbench": {
"projects": [
{"id": 141, "title": "童析", "status": "进行中"},
{"id": 142, "title": "星河", "status": "待确认"},
]
}
}
},
),
current_user=object(),
turn_state=state,
)
assert payload is not None
observation = payload["runtime_observation"]
assert observation["observation_type"] == "collection_snapshot"
assert observation["object_type"] == "project"
assert observation["can_answer_operations"] == ["list_collection"]
assert observation["count"] >= 2
assert any("童析" in item["title"] for item in observation["items"])
parsed = PMFlowAITurnStateReviser().observe_payload(
turn_state=state,
action_type="inspect_surface_context",
payload=payload,
)
PMFlowAITurnStateReviser().revise_from_observation(turn_state=state, observation=parsed)
assert state.sufficiency_state.can_answer_now is True
assert state.customer_packet_draft["operation_kind"] == "list_collection"
@pytest.mark.asyncio
async def test_packet_c_surface_tool_reads_capacity_packet_as_evidence_only() -> None:
state = _project_collection_state()
payload = await PMFlowAIEvidenceToolRegistry(object(), tenant_id=1).run(
action_type="inspect_surface_context",
request=RAGAskRequest(
question="我的工作负载怎么样,有什么任务安排吗",
surface_context={
"space_operating_packet": {
"capacity_packet": {
"packet_version": "space_capacity_packet.v1",
"project_capacity": [
{
"project_id": 141,
"project_title": "童析",
"team": {"effective_minutes": 480, "planned_minutes": 90},
"personal": {
"status": "normal",
"effective_minutes": 480,
"planned_minutes": 90,
"remaining_minutes": 318,
},
"ai_reduction_potential_minutes": 30,
"recommendations": ["AI 预处理预计可节省 30 分钟人工处理。"],
}
],
}
}
},
),
current_user=object(),
turn_state=state,
)
assert payload is not None
result = payload["runtime_evidence_tool_result"]
facts = [item["fact"] for item in result["findings"]]
assert payload["answer_mode"] == "turn_runtime_evidence_tool"
assert "answer_canvas" not in payload
assert "quick_actions" not in payload
assert any("已计划 90 分钟 / 有效 480 分钟" in fact for fact in facts)
assert any("AI 可减少约 30 分钟人工处理" in fact for fact in facts)
@pytest.mark.asyncio
async def test_packet_c_empty_tool_observation_does_not_mark_state_answerable() -> None:
state = _project_collection_state()
payload = await PMFlowAIEvidenceToolRegistry(object(), tenant_id=1).run(
action_type="inspect_surface_context",
request=RAGAskRequest(question="现在有哪些项目", surface_context={"space_runtime_context": {"workbench": {}}}),
current_user=object(),
turn_state=state,
)
assert payload is not None
observation = payload["runtime_observation"]
assert observation["can_answer_operations"] == []
assert observation["remaining_gaps"][0]["gap_type"] == "system_evidence_gap"
parsed = PMFlowAITurnStateReviser().observe_payload(
turn_state=state,
action_type="inspect_surface_context",
payload=payload,
)
PMFlowAITurnStateReviser().revise_from_observation(turn_state=state, observation=parsed)
assert state.sufficiency_state.can_answer_now is False
assert state.operation_gaps
assert all(gap.user_actionable is False for gap in state.operation_gaps)
def test_packet_c_sufficiency_honors_observation_answerability_before_remaining_candidates() -> None:
state = _project_collection_state()
state.sufficiency_state.can_answer_now = True
state.action_state = PMFlowAITurnActionState()
decision = PMFlowAITurnRuntimeService(object(), tenant_id=1).assess_sufficiency(state)
assert decision.can_answer_now is True
assert decision.needs_action is False