"""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", )