deerflow-code/offline-backend-20260512/backend/tests/test_report_collaboration_contracts.py
2026-09-07 18:24:55 +08:00

192 lines
5.6 KiB
Python

"""Domain contracts aligned with the frontend frozen types."""
from __future__ import annotations
import pytest
from pydantic import ValidationError
from app.report_collaboration.contracts import (
ALL_COLLABORATION_EVENT_TYPES,
CollaborationEventEnvelope,
CollaborationMessage,
IntentDecision,
NodeContractDetail,
ReportRequirementSnapshot,
SessionSnapshot,
make_node_run_id,
parse_node_run_id,
)
FRONTEND_EVENT_TYPES = {
"session.snapshot",
"message.created",
"message.delta",
"message.completed",
"tool_call.created",
"tool_call.delta",
"tool_call.completed",
"tool_result.created",
"tool_result.delta",
"tool_result.completed",
"team_message.created",
"team_message.delta",
"team_message.completed",
"requirement.updated",
"clarification.requested",
"clarification.resolved",
"plan.progress",
"plan.proposed",
"plan.selected",
"run.status.changed",
"node.created",
"node.status.changed",
"node.progress",
"node.attempt.created",
"artifact.created",
"artifact.validated",
"artifact.rejected",
"source.added",
"command.classified",
"command.confirmation.required",
"command.accepted",
"command.completed",
"command.failed",
"command.cancelled",
"report.delta",
"report.version.created",
"review.created",
"error",
"heartbeat",
}
def test_event_types_match_frontend_frozen_set() -> None:
assert set(ALL_COLLABORATION_EVENT_TYPES) == FRONTEND_EVENT_TYPES
assert "agent_run.started" not in ALL_COLLABORATION_EVENT_TYPES
def test_event_envelope_serializes_camel_case() -> None:
envelope = CollaborationEventEnvelope(
eventId="evt_1",
sessionId="session_1",
runId="run_1",
seq=1,
type="message.created",
timestamp="2026-09-05T04:00:00Z",
data={"message": {"message_id": "msg_1"}},
)
dumped = envelope.to_sse_dict()
assert dumped["eventId"] == "evt_1"
assert dumped["sessionId"] == "session_1"
assert dumped["runId"] == "run_1"
assert dumped["seq"] == 1
assert dumped["type"] == "message.created"
assert dumped["data"]["message"]["message_id"] == "msg_1"
def test_event_envelope_accepts_snake_or_camel_aliases() -> None:
envelope = CollaborationEventEnvelope.model_validate(
{
"event_id": "evt_2",
"session_id": "session_1",
"run_id": None,
"seq": 2,
"type": "heartbeat",
"timestamp": "2026-09-05T04:00:01Z",
"data": {},
}
)
assert envelope.event_id == "evt_2"
assert envelope.run_id is None
def test_node_run_id_is_deterministic() -> None:
assert make_node_run_id("rcr_x", "research_brand", 2) == "rcr_x-research_brand-attempt2"
assert parse_node_run_id("rcr_x-research_brand-attempt2") == ("rcr_x-research_brand", 2)
with pytest.raises(ValueError):
make_node_run_id("rcr_x", "research_brand", 0)
with pytest.raises(ValueError):
make_node_run_id("", "research_brand", 1)
with pytest.raises(ValueError):
parse_node_run_id("random-uuid")
def test_requirement_revision_required() -> None:
with pytest.raises(ValidationError):
ReportRequirementSnapshot(topic="新能源汽车", revision=0)
def test_session_snapshot_round_trip() -> None:
snapshot = SessionSnapshot.model_validate(
{
"session": {
"id": "session_1",
"title": "新能源趋势",
"status": "empty",
"requirement_revision": 1,
"selected_plan_id": None,
"active_run_id": None,
"current_report_version_id": None,
"created_at": "2026-09-05T04:00:00Z",
"updated_at": "2026-09-05T04:00:00Z",
},
"messages": [],
"plans": [],
"run": None,
"nodes": [],
"agent_runs": [],
"commands": [],
"report_versions": [],
"last_seq": 0,
}
)
dumped = snapshot.model_dump(mode="json")
assert dumped["session"]["requirement_revision"] == 1
assert dumped["last_seq"] == 0
assert SessionSnapshot.model_validate(dumped).session.id == "session_1"
def test_node_contract_detail_has_no_execution_body() -> None:
detail = NodeContractDetail(
node_id="n1",
label="品牌研究",
role="Researcher",
angle="市场份额",
status="planned",
attempt=1,
mission="收集 2024 至今份额",
output_artifact_type="EvidenceBundle",
max_attempts=3,
)
fields = set(detail.model_dump().keys())
assert "content" not in fields
assert "markdown" not in fields
assert "artifact_body" not in fields
assert "tool_results" not in fields
assert "mission" in fields
assert "acceptance_criteria" in fields
def test_user_message_carries_idempotency_key_in_metadata() -> None:
message = CollaborationMessage(
message_id="msg_user_1",
role="human",
content="写一份新能源报告",
visibility="visible",
metadata={"idempotency_key": "idem-1"},
created_at="2026-09-05T04:00:00Z",
completed_at="2026-09-05T04:00:00Z",
)
assert message.metadata["idempotency_key"] == "idem-1"
def test_intent_decision_rejects_unknown_intent() -> None:
with pytest.raises(ValidationError):
IntentDecision(
intent="do_whatever", # type: ignore[arg-type]
confidence=0.9,
normalized_instruction="x",
scope="conversation",
reason="nope",
)