from __future__ import annotations import pytest from pydantic import ValidationError from yuxi.gateway.protocol.handshake import ConnectParams, HelloFeatures, HelloOk, handle_handshake class TestConnectParams: def test_default_construction(self): params = ConnectParams() assert params.min_protocol == 1 assert params.max_protocol == 1 assert params.client_id == "" assert params.client_version == "" assert params.client_platform == "" assert params.auth_token is None assert params.caps == [] assert params.client_display_name == "" assert params.client_device_family == "" assert params.client_model_identifier == "" assert params.client_mode == "default" assert params.client_instance_id == "" assert params.role == "" assert params.locale == "" assert params.user_agent == "" def test_full_construction(self): params = ConnectParams( min_protocol=1, max_protocol=1, client_id="web-client-001", client_version="2.3.0", client_platform="web", auth_token="jwt-token-here", caps=["streaming", "commands"], client_display_name="TestBot", client_device_family="desktop", client_model_identifier="win-x64", client_mode="default", client_instance_id="inst-001", commands=["/start", "/help"], permissions={"admin": True}, path_env="/api/v1", role="bot", scopes=["messages.read", "messages.write"], locale="zh-CN", user_agent="ForcePilot/2.3.0", ) assert params.client_id == "web-client-001" assert params.client_version == "2.3.0" assert params.client_platform == "web" assert params.auth_token == "jwt-token-here" assert params.caps == ["streaming", "commands"] assert params.client_display_name == "TestBot" assert params.client_device_family == "desktop" assert params.scopes == ["messages.read", "messages.write"] assert params.locale == "zh-CN" def test_client_platform_values(self): for platform in ["web", "cli", "android", "ios"]: params = ConnectParams(client_platform=platform) assert params.client_platform == platform def test_min_protocol_int(self): with pytest.raises(ValidationError): ConnectParams(min_protocol="invalid") def test_max_protocol_int(self): with pytest.raises(ValidationError): ConnectParams(max_protocol="invalid") def test_auth_token_optional(self): params = ConnectParams() assert params.auth_token is None def test_caps_empty_default(self): params = ConnectParams() assert params.caps == [] def test_caps_from_list(self): params = ConnectParams(caps=["streaming", "reactions"]) assert params.caps == ["streaming", "reactions"] def test_json_serialization(self): params = ConnectParams(client_id="cli-1", client_platform="cli", locale="en") data = params.model_dump() assert data["client_id"] == "cli-1" assert data["client_platform"] == "cli" assert data["locale"] == "en" def test_json_deserialization(self): data = { "min_protocol": 1, "max_protocol": 1, "client_id": "web-1", "client_version": "1.0", "client_platform": "web", "auth_token": None, "caps": ["streaming"], "client_display_name": "WebBot", } params = ConnectParams.model_validate(data) assert params.client_id == "web-1" assert params.client_platform == "web" assert params.client_display_name == "WebBot" def test_field_count_gte_17(self): params = ConnectParams() fields = list(ConnectParams.model_fields.keys()) assert len(fields) >= 17, f"ConnectParams has {len(fields)} fields, need >= 17: {fields}" class TestHelloFeatures: def test_default_construction(self): features = HelloFeatures() assert features.methods == [] assert features.events == [] def test_full_construction(self): features = HelloFeatures( methods=["ping", "chat.send"], events=["message.received", "typing"], ) assert features.methods == ["ping", "chat.send"] assert features.events == ["message.received", "typing"] def test_json_serialization(self): features = HelloFeatures(methods=["ping"], events=["tick"]) data = features.model_dump() assert data["methods"] == ["ping"] assert data["events"] == ["tick"] class TestHelloOk: def test_minimal_construction(self): hello = HelloOk(protocol=1, server_version="1.0.0", server_id="gw-1", session_id="sess-1") assert hello.protocol == 1 assert hello.server_version == "1.0.0" assert hello.server_id == "gw-1" assert hello.session_id == "sess-1" assert hello.features.methods == [] assert hello.features.events == [] assert hello.type == "hello-ok" assert hello.snapshot is None assert hello.dm_policy == "pairing" assert hello.group_policy == "allowlist" assert hello.conn_id == "" assert hello.max_payload == 1048576 assert hello.max_buffered_bytes == 10485760 assert hello.tick_interval_ms == 5000 def test_full_construction(self): features = HelloFeatures(methods=["ping"], events=["tick"]) hello = HelloOk( protocol=1, server_version="2.0.0", server_id="forcepilot-gw", session_id="abc-123", features=features, snapshot={"threads": 5}, dm_policy="pairing", group_policy="allowlist", conn_id="conn-abc12345", max_payload=2097152, max_buffered_bytes=20971520, tick_interval_ms=3000, ) assert hello.features.methods == ["ping"] assert hello.snapshot == {"threads": 5} assert hello.conn_id == "conn-abc12345" assert hello.max_payload == 2097152 assert hello.tick_interval_ms == 3000 def test_protocol_is_required(self): with pytest.raises(ValidationError): HelloOk(server_version="1.0.0", server_id="gw", session_id="s1") def test_server_version_is_required(self): with pytest.raises(ValidationError): HelloOk(protocol=1, server_id="gw", session_id="s1") def test_server_id_is_required(self): with pytest.raises(ValidationError): HelloOk(protocol=1, server_version="1.0.0", session_id="s1") def test_session_id_is_required(self): with pytest.raises(ValidationError): HelloOk(protocol=1, server_version="1.0.0", server_id="gw") def test_default_dm_policy(self): hello = HelloOk(protocol=1, server_version="1.0.0", server_id="gw", session_id="s1") assert hello.dm_policy == "pairing" def test_default_group_policy(self): hello = HelloOk(protocol=1, server_version="1.0.0", server_id="gw", session_id="s1") assert hello.group_policy == "allowlist" def test_json_serialization(self): hello = HelloOk(protocol=1, server_version="1.0.0", server_id="gw-1", session_id="sess-1") data = hello.model_dump() assert data["protocol"] == 1 assert data["type"] == "hello-ok" assert data["dm_policy"] == "pairing" assert data["max_payload"] == 1048576 def test_deserialization(self): data = { "protocol": 1, "server_version": "1.0.0", "server_id": "gw-1", "session_id": "sess-1", "features": {"methods": [], "events": []}, "snapshot": None, "dm_policy": "pairing", "group_policy": "allowlist", "conn_id": "", "max_payload": 1048576, "max_buffered_bytes": 10485760, "tick_interval_ms": 5000, } hello = HelloOk.model_validate(data) assert hello.server_version == "1.0.0" assert hello.max_payload == 1048576 def test_field_count_gte_11(self): hello = HelloOk(protocol=1, server_version="1.0.0", server_id="gw", session_id="s1") fields = list(HelloOk.model_fields.keys()) assert len(fields) >= 11, f"HelloOk has {len(fields)} fields, need >= 11: {fields}" class TestHandleHandshake: @pytest.mark.asyncio async def test_successful_handshake(self): params = ConnectParams(min_protocol=1, max_protocol=1, client_id="cli-1") hello = await handle_handshake(params) assert isinstance(hello, HelloOk) assert hello.protocol == 1 assert hello.server_version == "1.0.0" assert hello.server_id == "forcepilot-gateway" assert len(hello.session_id) > 0 assert hello.conn_id.startswith("conn-") @pytest.mark.asyncio async def test_version_negotiation_fails(self): params = ConnectParams(min_protocol=3, max_protocol=5) with pytest.raises(ValueError, match="Protocol version negotiation failed"): await handle_handshake(params) @pytest.mark.asyncio async def test_features_structured(self): params = ConnectParams(min_protocol=1, max_protocol=1) hello = await handle_handshake(params) assert isinstance(hello.features, HelloFeatures) assert len(hello.features.methods) > 0 assert len(hello.features.events) > 0 @pytest.mark.asyncio async def test_session_id_unique(self): params = ConnectParams(min_protocol=1, max_protocol=1) hello1 = await handle_handshake(params) hello2 = await handle_handshake(params) assert hello1.session_id != hello2.session_id @pytest.mark.asyncio async def test_session_id_is_uuid_format(self): params = ConnectParams(min_protocol=1, max_protocol=1) hello = await handle_handshake(params) parts = hello.session_id.split("-") assert len(parts) == 5 @pytest.mark.asyncio async def test_default_dm_policy(self): params = ConnectParams(min_protocol=1, max_protocol=1) hello = await handle_handshake(params) assert hello.dm_policy == "pairing" @pytest.mark.asyncio async def test_default_group_policy(self): params = ConnectParams(min_protocol=1, max_protocol=1) hello = await handle_handshake(params) assert hello.group_policy == "allowlist" @pytest.mark.asyncio async def test_hello_type_is_hello_ok(self): params = ConnectParams(min_protocol=1, max_protocol=1) hello = await handle_handshake(params) assert hello.type == "hello-ok" @pytest.mark.asyncio async def test_policy_defaults(self): params = ConnectParams(min_protocol=1, max_protocol=1) hello = await handle_handshake(params) assert hello.max_payload == 1048576 assert hello.max_buffered_bytes == 10485760 assert hello.tick_interval_ms == 5000