152 lines
6.0 KiB
Python
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
|