PMFlow/backend/tests/test_space_runtime_context_...

239 lines
11 KiB
Python

from types import SimpleNamespace
import pytest
from app.api import rag_ask_contract
from app.api.space_api import _attach_space_operating_conversation
from app.schemas.rag import RAGAskRequest
from app.services.customer_answer_presentation_service import CustomerAnswerPresentationService
from app.services.space_runtime_context_builder import SpaceRuntimeContextBuilder
@pytest.mark.asyncio
async def test_space_runtime_context_builder_merges_backend_projects_into_surface_context(monkeypatch) -> None:
builder = SpaceRuntimeContextBuilder(object(), tenant_id=1)
project = SimpleNamespace(
id=141,
title="童析-学龄前儿童行为特质AI评估助手",
name="童析",
description="儿童行为特质评估",
status="active",
updated_at=None,
)
async def load_projects(*, current_user, limit):
return [project]
async def load_task_counts(*, project_ids):
return {141: {"total": 3, "completed": 1, "in_progress": 1, "pending": 1, "blocked": 0}}
async def load_saved_ideas(*, user_id, limit):
return [
{
"id": "idea_1",
"object_ref": "saved_idea:idea_1",
"title": "AI 教练想法",
"summary": "先暂存,等待整理",
"status": "saved",
"readiness_score": 0.4,
"href": "/space",
}
]
monkeypatch.setattr(builder, "_load_visible_projects", load_projects)
monkeypatch.setattr(builder, "_load_task_counts", load_task_counts)
monkeypatch.setattr(builder, "_load_saved_ideas", load_saved_ideas)
runtime_context = await builder.build(
current_user=SimpleNamespace(id=7, tenant_id=1, role="MEMBER"),
surface_context={"workbench": {"counts": {"pendingConfirmations": 2}}},
)
merged = builder.merge_surface_context(surface_context={}, runtime_context=runtime_context)
assert runtime_context["source"] == "backend_authoritative"
assert runtime_context["space_operating_packet"]["packet_version"] == "space_operating_packet.v1"
assert runtime_context["space_operating_packet"]["visible_projects"][0]["object_ref"] == "project:141"
assert runtime_context["space_operating_packet"]["attention_queue"][0]["href"] == "/pmflow-os?projectId=141"
assert runtime_context["space_operating_packet"]["attention_queue"][0]["object_anchor"] == {
"object_ref": "project:141",
"object_type": "project",
"object_id": "141",
"surface": "command_center",
"project_id": "141",
"project_title": "童析-学龄前儿童行为特质AI评估助手",
"href": "/pmflow-os?projectId=141",
"parent_object_ref": None,
}
forbidden_actions = set(runtime_context["space_operating_packet"]["forbidden_actions"])
assert {
"project_create_without_confirmation",
"project_memory_write",
"changeset_commit",
"execution_handoff",
"external_command_execution",
"publish_or_deploy",
"provider_model_prompt_diagnosis",
}.issubset(forbidden_actions)
assert runtime_context["workbench"]["counts"]["projects"] == 1
assert runtime_context["workbench"]["counts"]["savedIdeas"] == 1
assert runtime_context["workbench"]["attention_queue"][0]["kind"] == "next_action"
assert merged["workbench"]["recent_projects"][0]["object_ref"] == "project:141"
assert merged["workbench"]["recent_projects"][0]["next_action"] == "继续推进待处理任务"
assert merged["workbench"]["top_next_actions"][0]["href"] == "/pmflow-os?projectId=141"
assert merged["workbench"]["top_next_actions"][0]["object_ref"] == "project:141"
assert merged["workbench"]["proactive_suggestions"][0]["requires_user_confirm"] is True
assert merged["workbench"]["proactive_suggestions"][0]["why_now"]
assert merged["workbench"]["proactive_suggestions"][0]["required_confirmation"] == "human_confirm_on_target_surface"
assert merged["workbench"]["proactive_suggestions"][0]["audit_event"] == "space_ai.proactive_suggestion.created"
assert merged["space_operating_packet"]["automation_policy"]["mode"] == "suggest_only"
assert "external_command_execution" in merged["space_operating_packet"]["forbidden_actions"]
assert merged["space_runtime_context"]["source_mix"]["projects"] == "backend"
assert merged["space_operating_packet"]["packet_version"] == "space_operating_packet.v1"
def test_enriched_runtime_context_feeds_surface_direct_project_answer() -> None:
builder = SpaceRuntimeContextBuilder(object(), tenant_id=1)
runtime_context = {
"packet_version": "space_runtime_context.v1",
"visibility": "customer",
"source": "backend_authoritative",
"workbench": {
"counts": {"projects": 1, "savedIdeas": 0, "nextActions": 1},
"recent_projects": [
{
"id": 141,
"project_id": 141,
"object_ref": "project:141",
"title": "童析-学龄前儿童行为特质AI评估助手",
"status": "active",
"stage": "execution",
"next_action": "检查进行中任务进展",
"href": "/pmflow-os?projectId=141",
}
],
"top_next_actions": [
{
"title": "检查进行中任务进展",
"project": "童析-学龄前儿童行为特质AI评估助手",
"priority": "normal",
"href": "/execution-bridge?projectId=141&taskId=task-1&evidenceId=progress-1",
"object_ref": "task:task-1",
"object_anchor": {
"object_ref": "task:task-1",
"object_type": "task",
"object_id": "task-1",
"surface": "execution_bridge",
"project_id": "141",
"project_title": "童析-学龄前儿童行为特质AI评估助手",
"href": "/execution-bridge?projectId=141&taskId=task-1&evidenceId=progress-1",
"parent_object_ref": "project:141",
},
}
],
},
}
request = RAGAskRequest(
question="现在我有哪些项目",
surface_context=builder.merge_surface_context(surface_context={}, runtime_context=runtime_context),
)
payload = rag_ask_contract.build_surface_direct_answer(
request=request,
current_user=SimpleNamespace(id=7, tenant_id=1),
presentation_service=CustomerAnswerPresentationService(object(), tenant_id=1),
pmflow_ai_route_plan={
"source_strategy": "internal_only",
"surface_direct_eligible": True,
"allowed_executors": ["surface_direct"],
"internal_plan": {"objects": ["projects"], "expected_answer_shape": "list"},
},
)
assert payload is not None
assert "童析-学龄前儿童行为特质AI评估助手" in payload["answer"]
assert payload["answer_canvas"]["object_refs"][0]["object_ref"] == "project:141"
assert payload["answer_canvas"]["next_actions"][0]["href"] == "/execution-bridge?projectId=141&taskId=task-1&evidenceId=progress-1"
assert payload["answer_canvas"]["next_actions"][0]["object_ref"] == "task:task-1"
assert payload["answer_canvas"]["next_actions"][0]["object_anchor"]["parent_object_ref"] == "project:141"
def test_surface_direct_refuses_project_mvp_gap_contract() -> None:
request = RAGAskRequest(
question="这个项目的MVP还差多少做完",
surface_context={
"workbench": {
"recent_projects": [
{
"id": 141,
"object_ref": "project:141",
"title": "童析-学龄前儿童行为特质AI评估助手",
"status": "active",
"next_action": "继续推进待处理任务",
}
]
}
},
)
payload = rag_ask_contract.build_surface_direct_answer(
request=request,
current_user=SimpleNamespace(id=7, tenant_id=1),
presentation_service=CustomerAnswerPresentationService(object(), tenant_id=1),
pmflow_ai_route_plan={
"source_strategy": "internal_only",
"answer_contract": "project_mvp_gap_assessment",
"analysis_task": "gap_assessment",
"business_frame": "mvp_delivery",
"surface_direct_eligible": True,
"surface_direct_allowed": False,
"allowed_executors": ["surface_direct", "internal_query"],
},
)
assert payload is None
def test_space_operating_conversation_contract_adds_mode_and_blocks() -> None:
request = RAGAskRequest(
question="我今天应该先处理什么?",
surface_context={
"surface": "personal_space",
"workbench": {
"counts": {"nextActions": 1, "pendingConfirmations": 1, "aiProgress": 0, "savedIdeas": 1},
},
"space_operating_packet": {
"attention_queue": [
{
"kind": "pending_confirmation",
"object_ref": "change_item:21",
"title": "高风险需求待确认",
"reason": "范围会影响执行计划",
"urgency": "high",
"next_action": "进入待确认处理",
"href": "/change-review?projectId=141&itemId=21",
"object_anchor": {
"object_ref": "change_item:21",
"object_type": "change_item",
"object_id": "21",
"surface": "change_review",
"project_id": "141",
"project_title": "童析",
"href": "/change-review?projectId=141&itemId=21",
"parent_object_ref": "project:141",
},
}
],
"pending_confirmations": [
{"object_ref": "change_item:21", "title": "高风险需求待确认", "href": "/change-review?projectId=141&itemId=21"}
],
"saved_ideas": [{"object_ref": "saved_idea:idea-1", "title": "AI 教练", "readiness_score": 0.7}],
},
},
)
payload = _attach_space_operating_conversation({"answer": "今天先处理高风险需求。", "answer_canvas": {}}, request=request)
assert payload["pmflow_ai_mode"] == "space_operating_partner"
assert payload["conversation_frame"]["frame_type"] == "agent_run"
assert payload["answer_canvas"]["conversation_mode"] == "space_operating_partner"
assert "conversation_blocks" not in payload["answer_canvas"]