xcore-protocol 0.6.2__tar.gz → 0.7.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. {xcore_protocol-0.6.2 → xcore_protocol-0.7.0}/PKG-INFO +1 -1
  2. {xcore_protocol-0.6.2 → xcore_protocol-0.7.0}/pyproject.toml +1 -1
  3. {xcore_protocol-0.6.2 → xcore_protocol-0.7.0}/python/tests/test_canonical_protocol_fixtures.py +2 -2
  4. xcore_protocol-0.7.0/python/tests/test_chat_fixtures.py +57 -0
  5. xcore_protocol-0.7.0/python/tests/test_generated_chat_models.py +89 -0
  6. xcore_protocol-0.7.0/python/tests/test_generated_identity_models.py +100 -0
  7. xcore_protocol-0.7.0/python/tests/test_generated_security_models.py +39 -0
  8. xcore_protocol-0.7.0/python/tests/test_generated_server_models.py +66 -0
  9. xcore_protocol-0.7.0/python/tests/test_identity_fixtures.py +73 -0
  10. {xcore_protocol-0.6.2 → xcore_protocol-0.7.0}/python/tests/test_normalized_route_model.py +4 -51
  11. xcore_protocol-0.7.0/python/tests/test_security_fixtures.py +44 -0
  12. xcore_protocol-0.7.0/python/tests/test_server_fixtures.py +55 -0
  13. {xcore_protocol-0.6.2 → xcore_protocol-0.7.0}/python/xcore_protocol/generated/__init__.py +16 -10
  14. xcore_protocol-0.7.0/python/xcore_protocol/generated/chat.py +298 -0
  15. xcore_protocol-0.7.0/python/xcore_protocol/generated/identity.py +332 -0
  16. {xcore_protocol-0.6.2 → xcore_protocol-0.7.0}/python/xcore_protocol/generated/routes.py +69 -60
  17. xcore_protocol-0.7.0/python/xcore_protocol/generated/security.py +149 -0
  18. xcore_protocol-0.7.0/python/xcore_protocol/generated/server.py +323 -0
  19. {xcore_protocol-0.6.2 → xcore_protocol-0.7.0}/python/xcore_protocol.egg-info/PKG-INFO +1 -1
  20. {xcore_protocol-0.6.2 → xcore_protocol-0.7.0}/python/xcore_protocol.egg-info/SOURCES.txt +9 -0
  21. xcore_protocol-0.6.2/python/tests/test_chat_fixtures.py +0 -145
  22. xcore_protocol-0.6.2/python/tests/test_generated_chat_models.py +0 -251
  23. xcore_protocol-0.6.2/python/xcore_protocol/generated/chat.py +0 -747
  24. {xcore_protocol-0.6.2 → xcore_protocol-0.7.0}/README.md +0 -0
  25. {xcore_protocol-0.6.2 → xcore_protocol-0.7.0}/python/tests/test_discord_fixtures.py +0 -0
  26. {xcore_protocol-0.6.2 → xcore_protocol-0.7.0}/python/tests/test_envelope_schemas.py +0 -0
  27. {xcore_protocol-0.6.2 → xcore_protocol-0.7.0}/python/tests/test_generated_discord_models.py +0 -0
  28. {xcore_protocol-0.6.2 → xcore_protocol-0.7.0}/python/tests/test_generated_maps_models.py +0 -0
  29. {xcore_protocol-0.6.2 → xcore_protocol-0.7.0}/python/tests/test_generated_moderation_models.py +0 -0
  30. {xcore_protocol-0.6.2 → xcore_protocol-0.7.0}/python/tests/test_generated_sentinel_models.py +0 -0
  31. {xcore_protocol-0.6.2 → xcore_protocol-0.7.0}/python/tests/test_generated_telemetry_models.py +0 -0
  32. {xcore_protocol-0.6.2 → xcore_protocol-0.7.0}/python/tests/test_generator_bootstrap.py +0 -0
  33. {xcore_protocol-0.6.2 → xcore_protocol-0.7.0}/python/tests/test_java_python_route_compatibility.py +0 -0
  34. {xcore_protocol-0.6.2 → xcore_protocol-0.7.0}/python/tests/test_java_python_serialized_compatibility.py +0 -0
  35. {xcore_protocol-0.6.2 → xcore_protocol-0.7.0}/python/tests/test_maps_fixtures.py +0 -0
  36. {xcore_protocol-0.6.2 → xcore_protocol-0.7.0}/python/tests/test_moderation_fixtures.py +0 -0
  37. {xcore_protocol-0.6.2 → xcore_protocol-0.7.0}/python/tests/test_telemetry_fixtures.py +0 -0
  38. {xcore_protocol-0.6.2 → xcore_protocol-0.7.0}/python/xcore_protocol/__init__.py +0 -0
  39. {xcore_protocol-0.6.2 → xcore_protocol-0.7.0}/python/xcore_protocol/generated/discord.py +0 -0
  40. {xcore_protocol-0.6.2 → xcore_protocol-0.7.0}/python/xcore_protocol/generated/envelopes.py +0 -0
  41. {xcore_protocol-0.6.2 → xcore_protocol-0.7.0}/python/xcore_protocol/generated/maps.py +0 -0
  42. {xcore_protocol-0.6.2 → xcore_protocol-0.7.0}/python/xcore_protocol/generated/moderation.py +0 -0
  43. {xcore_protocol-0.6.2 → xcore_protocol-0.7.0}/python/xcore_protocol/generated/sentinel.py +0 -0
  44. {xcore_protocol-0.6.2 → xcore_protocol-0.7.0}/python/xcore_protocol/generated/shared.py +0 -0
  45. {xcore_protocol-0.6.2 → xcore_protocol-0.7.0}/python/xcore_protocol/generated/telemetry.py +0 -0
  46. {xcore_protocol-0.6.2 → xcore_protocol-0.7.0}/python/xcore_protocol/models/__init__.py +0 -0
  47. {xcore_protocol-0.6.2 → xcore_protocol-0.7.0}/python/xcore_protocol/paths.py +0 -0
  48. {xcore_protocol-0.6.2 → xcore_protocol-0.7.0}/python/xcore_protocol/schema_validation.py +0 -0
  49. {xcore_protocol-0.6.2 → xcore_protocol-0.7.0}/python/xcore_protocol.egg-info/dependency_links.txt +0 -0
  50. {xcore_protocol-0.6.2 → xcore_protocol-0.7.0}/python/xcore_protocol.egg-info/requires.txt +0 -0
  51. {xcore_protocol-0.6.2 → xcore_protocol-0.7.0}/python/xcore_protocol.egg-info/top_level.txt +0 -0
  52. {xcore_protocol-0.6.2 → xcore_protocol-0.7.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: xcore-protocol
3
- Version: 0.6.2
3
+ Version: 0.7.0
4
4
  Summary: Canonical cross-service protocol for the XCore ecosystem
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "xcore-protocol"
7
- version = "0.6.2"
7
+ version = "0.7.0"
8
8
  description = "Canonical cross-service protocol for the XCore ecosystem"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -107,8 +107,8 @@ CANONICAL_CASES: list[tuple[str, Path, Path, type[Any], CanonicalAssertion]] = [
107
107
  ),
108
108
  (
109
109
  "server_heartbeat",
110
- spec_root() / "messages" / "chat" / "server.heartbeat.v1.json",
111
- CANONICAL_FIXTURES_ROOT / "chat" / "server.heartbeat.v1.json",
110
+ spec_root() / "messages" / "server" / "server.heartbeat.v1.json",
111
+ CANONICAL_FIXTURES_ROOT / "server" / "server.heartbeat.v1.json",
112
112
  ServerHeartbeatV1,
113
113
  _assert_heartbeat_semantics,
114
114
  ),
@@ -0,0 +1,57 @@
1
+ from __future__ import annotations
2
+
3
+ from xcore_protocol.paths import fixtures_root, spec_root
4
+ from xcore_protocol.schema_validation import assert_invalid, assert_valid
5
+
6
+
7
+ VALID_CASES = [
8
+ (
9
+ spec_root() / "messages" / "chat" / "chat.message.v1.json",
10
+ fixtures_root() / "valid" / "chat" / "chat.message.v1.json",
11
+ ),
12
+ (
13
+ spec_root() / "messages" / "chat" / "chat.global.v1.json",
14
+ fixtures_root() / "valid" / "chat" / "chat.global.v1.json",
15
+ ),
16
+ (
17
+ spec_root() / "messages" / "chat" / "chat.discord-ingress.command.v1.json",
18
+ fixtures_root() / "valid" / "chat" / "chat.discord-ingress.command.v1.json",
19
+ ),
20
+ (
21
+ spec_root() / "messages" / "chat" / "chat.private.v1.json",
22
+ fixtures_root() / "valid" / "chat" / "chat.private.v1.json",
23
+ ),
24
+ ]
25
+
26
+
27
+ INVALID_CASES = [
28
+ (
29
+ spec_root() / "messages" / "chat" / "chat.message.v1.json",
30
+ fixtures_root() / "invalid" / "chat" / "chat.message.v1.snake-author.json",
31
+ ),
32
+ (
33
+ spec_root() / "messages" / "chat" / "chat.private.v1.json",
34
+ fixtures_root() / "invalid" / "chat" / "chat.private.v1.snake-from.json",
35
+ ),
36
+ ]
37
+
38
+
39
+ def test_valid_chat_fixtures_pass() -> None:
40
+ for schema_path, fixture_path in VALID_CASES:
41
+ assert_valid(schema_path, fixture_path)
42
+
43
+
44
+ def test_invalid_chat_fixtures_fail() -> None:
45
+ for schema_path, fixture_path in INVALID_CASES:
46
+ error = assert_invalid(schema_path, fixture_path)
47
+ assert error is not None
48
+
49
+
50
+ def test_chat_fixture_inventory_exists() -> None:
51
+ chat_valid_dir = fixtures_root() / "valid" / "chat"
52
+ chat_invalid_dir = fixtures_root() / "invalid" / "chat"
53
+
54
+ assert chat_valid_dir.exists()
55
+ assert chat_invalid_dir.exists()
56
+ assert any(chat_valid_dir.iterdir())
57
+ assert any(chat_invalid_dir.iterdir())
@@ -0,0 +1,89 @@
1
+ from __future__ import annotations
2
+
3
+ from xcore_protocol.generated import (
4
+ CHAT_DISCORD_INGRESS_COMMAND_V1,
5
+ CHAT_GLOBAL_V1,
6
+ CHAT_MESSAGE_V1,
7
+ CHAT_PRIVATE_V1,
8
+ ChatDiscordIngressCommandV1,
9
+ ChatGlobalV1,
10
+ ChatMessageV1,
11
+ ChatPrivateV1,
12
+ ROUTES_BY_MESSAGE,
13
+ )
14
+ from xcore_protocol.paths import fixtures_root, spec_root
15
+ from xcore_protocol.schema_validation import load_json, validate_instance
16
+
17
+
18
+ def test_generated_chat_message_roundtrip_matches_fixture() -> None:
19
+ payload = load_json(fixtures_root() / "valid" / "chat" / "chat.message.v1.json")
20
+
21
+ model = ChatMessageV1.from_payload(payload)
22
+
23
+ assert model == ChatMessageV1(authorName="PlayerOne", message="Hello there", server="mini-pvp")
24
+ assert model.to_payload() == payload
25
+ validate_instance(spec_root() / "messages" / "chat" / "chat.message.v1.json", model.to_payload())
26
+
27
+
28
+ def test_generated_chat_global_roundtrip_matches_fixture() -> None:
29
+ payload = load_json(fixtures_root() / "valid" / "chat" / "chat.global.v1.json")
30
+
31
+ model = ChatGlobalV1.from_payload(payload)
32
+
33
+ assert model.to_payload() == payload
34
+ validate_instance(spec_root() / "messages" / "chat" / "chat.global.v1.json", model.to_payload())
35
+
36
+
37
+ def test_generated_chat_discord_ingress_roundtrip_matches_fixture() -> None:
38
+ payload = load_json(fixtures_root() / "valid" / "chat" / "chat.discord-ingress.command.v1.json")
39
+
40
+ model = ChatDiscordIngressCommandV1.from_payload(payload)
41
+
42
+ assert model.to_payload() == payload
43
+ validate_instance(
44
+ spec_root() / "messages" / "chat" / "chat.discord-ingress.command.v1.json",
45
+ model.to_payload(),
46
+ )
47
+
48
+
49
+ def test_generated_chat_private_roundtrip_matches_fixture() -> None:
50
+ payload = load_json(fixtures_root() / "valid" / "chat" / "chat.private.v1.json")
51
+
52
+ model = ChatPrivateV1.from_payload(payload)
53
+
54
+ assert model.to_payload() == payload
55
+ validate_instance(
56
+ spec_root() / "messages" / "chat" / "chat.private.v1.json",
57
+ model.to_payload(),
58
+ )
59
+
60
+
61
+ def test_generated_chat_models_remain_strict() -> None:
62
+ invalid_payload = load_json(fixtures_root() / "invalid" / "chat" / "chat.message.v1.snake-author.json")
63
+
64
+ try:
65
+ ChatMessageV1.from_payload(invalid_payload)
66
+ except ValueError as error:
67
+ assert "missing required fields" in str(error) or "unexpected fields" in str(error)
68
+ else:
69
+ raise AssertionError("Expected strict generated model parsing to reject non-canonical keys")
70
+
71
+ invalid_private_payload = load_json(fixtures_root() / "invalid" / "chat" / "chat.private.v1.snake-from.json")
72
+
73
+ try:
74
+ ChatPrivateV1.from_payload(invalid_private_payload)
75
+ except ValueError as error:
76
+ assert "missing required fields" in str(error) or "unexpected fields" in str(error)
77
+ else:
78
+ raise AssertionError("Expected strict generated private-message parsing to reject non-canonical keys")
79
+
80
+
81
+ def test_generated_route_registry_includes_chat_messages() -> None:
82
+ assert CHAT_MESSAGE_V1.payloadType is ChatMessageV1
83
+ assert CHAT_GLOBAL_V1.payloadType is ChatGlobalV1
84
+ assert CHAT_DISCORD_INGRESS_COMMAND_V1.payloadType is ChatDiscordIngressCommandV1
85
+ assert CHAT_PRIVATE_V1.payloadType is ChatPrivateV1
86
+ assert ROUTES_BY_MESSAGE[("chat.message", 1)].stream == "xcore:evt:chat:message"
87
+ assert ROUTES_BY_MESSAGE[("chat.global", 1)].stream == "xcore:evt:chat:global"
88
+ assert ROUTES_BY_MESSAGE[("chat.private", 1)].stream == "xcore:evt:chat:private"
89
+ assert ROUTES_BY_MESSAGE[("chat.discord-ingress.command", 1)].bindings == {"server": "payload.server"}
@@ -0,0 +1,100 @@
1
+ from __future__ import annotations
2
+
3
+ from xcore_protocol.generated import (
4
+ PLAYER_ACTIVE_BADGE_CHANGED_COMMAND_V1,
5
+ PLAYER_BADGE_INVENTORY_CHANGED_COMMAND_V1,
6
+ PLAYER_BADGE_SYMBOL_COLOR_MODE_CHANGED_COMMAND_V1,
7
+ PLAYER_CUSTOM_NICKNAME_CHANGED_COMMAND_V1,
8
+ PLAYER_JOIN_LEAVE_V1,
9
+ PlayerActiveBadgeChangedCommandV1,
10
+ PlayerBadgeInventoryChangedCommandV1,
11
+ PlayerBadgeSymbolColorModeChangedCommandV1,
12
+ PlayerCustomNicknameChangedCommandV1,
13
+ PlayerJoinLeaveV1,
14
+ ROUTES_BY_MESSAGE,
15
+ )
16
+ from xcore_protocol.paths import fixtures_root, spec_root
17
+ from xcore_protocol.schema_validation import load_json, validate_instance
18
+
19
+
20
+ def test_generated_player_join_leave_roundtrip_matches_fixture() -> None:
21
+ payload = load_json(fixtures_root() / "valid" / "identity" / "player.join-leave.v1.json")
22
+
23
+ model = PlayerJoinLeaveV1.from_payload(payload)
24
+
25
+ assert model.to_payload() == payload
26
+ validate_instance(spec_root() / "messages" / "identity" / "player.join-leave.v1.json", model.to_payload())
27
+
28
+
29
+ def test_generated_player_custom_nickname_changed_command_roundtrip_matches_fixture() -> None:
30
+ payload = load_json(fixtures_root() / "valid" / "identity" / "player.custom-nickname.changed.command.v1.json")
31
+
32
+ model = PlayerCustomNicknameChangedCommandV1.from_payload(payload)
33
+
34
+ assert model.to_payload() == payload
35
+ validate_instance(
36
+ spec_root() / "messages" / "identity" / "player.custom-nickname.changed.command.v1.json",
37
+ model.to_payload(),
38
+ )
39
+
40
+
41
+ def test_generated_player_active_badge_changed_command_roundtrip_matches_fixture() -> None:
42
+ payload = load_json(fixtures_root() / "valid" / "identity" / "player.active-badge.changed.command.v1.json")
43
+
44
+ model = PlayerActiveBadgeChangedCommandV1.from_payload(payload)
45
+
46
+ assert model.to_payload() == payload
47
+ validate_instance(
48
+ spec_root() / "messages" / "identity" / "player.active-badge.changed.command.v1.json",
49
+ model.to_payload(),
50
+ )
51
+
52
+
53
+ def test_generated_player_badge_inventory_changed_command_roundtrip_matches_fixture() -> None:
54
+ payload = load_json(fixtures_root() / "valid" / "identity" / "player.badge-inventory.changed.command.v1.json")
55
+
56
+ model = PlayerBadgeInventoryChangedCommandV1.from_payload(payload)
57
+
58
+ assert model.to_payload() == payload
59
+ validate_instance(
60
+ spec_root() / "messages" / "identity" / "player.badge-inventory.changed.command.v1.json",
61
+ model.to_payload(),
62
+ )
63
+
64
+
65
+ def test_generated_player_badge_symbol_color_mode_changed_command_roundtrip_matches_fixture() -> None:
66
+ payload = load_json(fixtures_root() / "valid" / "identity" / "player.badge-symbol-color-mode.changed.command.v1.json")
67
+
68
+ model = PlayerBadgeSymbolColorModeChangedCommandV1.from_payload(payload)
69
+
70
+ assert model.to_payload() == payload
71
+ validate_instance(
72
+ spec_root() / "messages" / "identity" / "player.badge-symbol-color-mode.changed.command.v1.json",
73
+ model.to_payload(),
74
+ )
75
+
76
+
77
+ def test_generated_identity_models_remain_strict() -> None:
78
+ invalid_player_payload = load_json(
79
+ fixtures_root() / "invalid" / "identity" / "player.custom-nickname.changed.command.v1.legacy-uuid.json"
80
+ )
81
+
82
+ try:
83
+ PlayerCustomNicknameChangedCommandV1.from_payload(invalid_player_payload)
84
+ except ValueError as error:
85
+ assert "missing required fields" in str(error) or "unexpected fields" in str(error)
86
+ else:
87
+ raise AssertionError("Expected strict generated player-session command parsing to reject legacy uuid field")
88
+
89
+
90
+ def test_generated_route_registry_includes_identity_messages() -> None:
91
+ assert PLAYER_JOIN_LEAVE_V1.payloadType is PlayerJoinLeaveV1
92
+ assert PLAYER_CUSTOM_NICKNAME_CHANGED_COMMAND_V1.payloadType is PlayerCustomNicknameChangedCommandV1
93
+ assert PLAYER_ACTIVE_BADGE_CHANGED_COMMAND_V1.payloadType is PlayerActiveBadgeChangedCommandV1
94
+ assert PLAYER_BADGE_INVENTORY_CHANGED_COMMAND_V1.payloadType is PlayerBadgeInventoryChangedCommandV1
95
+ assert PLAYER_BADGE_SYMBOL_COLOR_MODE_CHANGED_COMMAND_V1.payloadType is PlayerBadgeSymbolColorModeChangedCommandV1
96
+ assert ROUTES_BY_MESSAGE[("player.join-leave", 1)].stream == "xcore:evt:player:joinleave"
97
+ assert ROUTES_BY_MESSAGE[("player.custom-nickname.changed.command", 1)].stream == "xcore:cmd:player-custom-nickname:{server}"
98
+ assert ROUTES_BY_MESSAGE[("player.active-badge.changed.command", 1)].stream == "xcore:cmd:player-active-badge:{server}"
99
+ assert ROUTES_BY_MESSAGE[("player.badge-inventory.changed.command", 1)].stream == "xcore:cmd:player-badge-inventory:{server}"
100
+ assert ROUTES_BY_MESSAGE[("player.badge-symbol-color-mode.changed.command", 1)].stream == "xcore:cmd:player-badge-symbol-color-mode:{server}"
@@ -0,0 +1,39 @@
1
+ from __future__ import annotations
2
+
3
+ from xcore_protocol.generated import (
4
+ PLAYER_PASSWORD_RESET_COMMAND_V1,
5
+ PlayerPasswordResetCommandV1,
6
+ ROUTES_BY_MESSAGE,
7
+ )
8
+ from xcore_protocol.paths import fixtures_root, spec_root
9
+ from xcore_protocol.schema_validation import load_json, validate_instance
10
+
11
+
12
+ def test_generated_player_password_reset_command_roundtrip_matches_fixture() -> None:
13
+ payload = load_json(fixtures_root() / "valid" / "security" / "player.password-reset.command.v1.json")
14
+
15
+ model = PlayerPasswordResetCommandV1.from_payload(payload)
16
+
17
+ assert model.to_payload() == payload
18
+ validate_instance(
19
+ spec_root() / "messages" / "security" / "player.password-reset.command.v1.json",
20
+ model.to_payload(),
21
+ )
22
+
23
+
24
+ def test_generated_security_models_remain_strict() -> None:
25
+ invalid_password_reset_payload = load_json(
26
+ fixtures_root() / "invalid" / "security" / "player.password-reset.command.v1.legacy-uuid.json"
27
+ )
28
+
29
+ try:
30
+ PlayerPasswordResetCommandV1.from_payload(invalid_password_reset_payload)
31
+ except ValueError as error:
32
+ assert "missing required fields" in str(error) or "unexpected fields" in str(error)
33
+ else:
34
+ raise AssertionError("Expected strict generated password-reset parsing to reject legacy uuid field")
35
+
36
+
37
+ def test_generated_route_registry_includes_security_messages() -> None:
38
+ assert PLAYER_PASSWORD_RESET_COMMAND_V1.payloadType is PlayerPasswordResetCommandV1
39
+ assert ROUTES_BY_MESSAGE[("player.password-reset.command", 1)].stream == "xcore:cmd:player-password-reset:{server}"
@@ -0,0 +1,66 @@
1
+ from __future__ import annotations
2
+
3
+ from xcore_protocol.generated import (
4
+ PLAYER_DATA_CACHE_RELOAD_COMMAND_V1,
5
+ SERVER_ACTION_V1,
6
+ SERVER_COMMAND_EXECUTE_COMMAND_V1,
7
+ SERVER_HEARTBEAT_V1,
8
+ PlayerDataCacheReloadCommandV1,
9
+ ServerActionV1,
10
+ ServerCommandExecuteCommandV1,
11
+ ServerHeartbeatV1,
12
+ ROUTES_BY_MESSAGE,
13
+ )
14
+ from xcore_protocol.paths import fixtures_root, spec_root
15
+ from xcore_protocol.schema_validation import load_json, validate_instance
16
+
17
+
18
+ def test_generated_server_action_roundtrip_matches_fixture() -> None:
19
+ payload = load_json(fixtures_root() / "valid" / "server" / "server.action.v1.json")
20
+
21
+ model = ServerActionV1.from_payload(payload)
22
+
23
+ assert model.to_payload() == payload
24
+ validate_instance(spec_root() / "messages" / "server" / "server.action.v1.json", model.to_payload())
25
+
26
+
27
+ def test_generated_server_heartbeat_roundtrip_matches_fixture() -> None:
28
+ payload = load_json(fixtures_root() / "valid" / "server" / "server.heartbeat.v1.json")
29
+
30
+ model = ServerHeartbeatV1.from_payload(payload)
31
+
32
+ assert model == ServerHeartbeatV1(
33
+ serverName="mini-pvp",
34
+ discordChannelId=1234567890123456789,
35
+ players=4,
36
+ maxPlayers=12,
37
+ version="146.1",
38
+ host="play.example.com",
39
+ port=6567,
40
+ )
41
+ assert model.to_payload() == payload
42
+ validate_instance(spec_root() / "messages" / "server" / "server.heartbeat.v1.json", model.to_payload())
43
+
44
+
45
+ def test_generated_server_heartbeat_rejects_alias_fields() -> None:
46
+ invalid_payload = load_json(
47
+ fixtures_root() / "invalid" / "server" / "server.heartbeat.v1.server-host-alias.json"
48
+ )
49
+
50
+ try:
51
+ ServerHeartbeatV1.from_payload(invalid_payload)
52
+ except ValueError as error:
53
+ assert "unexpected fields" in str(error)
54
+ else:
55
+ raise AssertionError("Expected strict generated heartbeat parsing to reject alias fields")
56
+
57
+
58
+ def test_generated_route_registry_includes_server_messages() -> None:
59
+ assert SERVER_ACTION_V1.payloadType is ServerActionV1
60
+ assert SERVER_HEARTBEAT_V1.payloadType is ServerHeartbeatV1
61
+ assert SERVER_COMMAND_EXECUTE_COMMAND_V1.payloadType is ServerCommandExecuteCommandV1
62
+ assert PLAYER_DATA_CACHE_RELOAD_COMMAND_V1.payloadType is PlayerDataCacheReloadCommandV1
63
+ assert ROUTES_BY_MESSAGE[("server.action", 1)].stream == "xcore:evt:server:action"
64
+ assert ROUTES_BY_MESSAGE[("server.heartbeat", 1)].stream == "xcore:evt:server:heartbeat"
65
+ assert ROUTES_BY_MESSAGE[("server-command.execute.command", 1)].stream == "xcore:cmd:execute-command:broadcast"
66
+ assert ROUTES_BY_MESSAGE[("player-data-cache.reload.command", 1)].stream == "xcore:cmd:reload-cache:{server}"
@@ -0,0 +1,73 @@
1
+ from __future__ import annotations
2
+
3
+ from xcore_protocol.paths import fixtures_root, spec_root
4
+ from xcore_protocol.schema_validation import assert_invalid, assert_valid
5
+
6
+
7
+ VALID_CASES = [
8
+ (
9
+ spec_root() / "messages" / "identity" / "player.join-leave.v1.json",
10
+ fixtures_root() / "valid" / "identity" / "player.join-leave.v1.json",
11
+ ),
12
+ (
13
+ spec_root() / "messages" / "identity" / "player.custom-nickname.changed.command.v1.json",
14
+ fixtures_root() / "valid" / "identity" / "player.custom-nickname.changed.command.v1.json",
15
+ ),
16
+ (
17
+ spec_root() / "messages" / "identity" / "player.active-badge.changed.command.v1.json",
18
+ fixtures_root() / "valid" / "identity" / "player.active-badge.changed.command.v1.json",
19
+ ),
20
+ (
21
+ spec_root() / "messages" / "identity" / "player.badge-inventory.changed.command.v1.json",
22
+ fixtures_root() / "valid" / "identity" / "player.badge-inventory.changed.command.v1.json",
23
+ ),
24
+ (
25
+ spec_root() / "messages" / "identity" / "player.badge-symbol-color-mode.changed.command.v1.json",
26
+ fixtures_root() / "valid" / "identity" / "player.badge-symbol-color-mode.changed.command.v1.json",
27
+ ),
28
+ ]
29
+
30
+
31
+ INVALID_CASES = [
32
+ (
33
+ spec_root() / "messages" / "identity" / "player.join-leave.v1.json",
34
+ fixtures_root() / "invalid" / "identity" / "player.join-leave.v1.legacy-join.json",
35
+ ),
36
+ (
37
+ spec_root() / "messages" / "identity" / "player.custom-nickname.changed.command.v1.json",
38
+ fixtures_root() / "invalid" / "identity" / "player.custom-nickname.changed.command.v1.legacy-uuid.json",
39
+ ),
40
+ (
41
+ spec_root() / "messages" / "identity" / "player.active-badge.changed.command.v1.json",
42
+ fixtures_root() / "invalid" / "identity" / "player.active-badge.changed.command.v1.legacy-uuid.json",
43
+ ),
44
+ (
45
+ spec_root() / "messages" / "identity" / "player.badge-inventory.changed.command.v1.json",
46
+ fixtures_root() / "invalid" / "identity" / "player.badge-inventory.changed.command.v1.legacy-uuid.json",
47
+ ),
48
+ (
49
+ spec_root() / "messages" / "identity" / "player.badge-symbol-color-mode.changed.command.v1.json",
50
+ fixtures_root() / "invalid" / "identity" / "player.badge-symbol-color-mode.changed.command.v1.legacy-uuid.json",
51
+ ),
52
+ ]
53
+
54
+
55
+ def test_valid_identity_fixtures_pass() -> None:
56
+ for schema_path, fixture_path in VALID_CASES:
57
+ assert_valid(schema_path, fixture_path)
58
+
59
+
60
+ def test_invalid_identity_fixtures_fail() -> None:
61
+ for schema_path, fixture_path in INVALID_CASES:
62
+ error = assert_invalid(schema_path, fixture_path)
63
+ assert error is not None
64
+
65
+
66
+ def test_identity_fixture_inventory_exists() -> None:
67
+ valid_dir = fixtures_root() / "valid" / "identity"
68
+ invalid_dir = fixtures_root() / "invalid" / "identity"
69
+
70
+ assert valid_dir.exists()
71
+ assert invalid_dir.exists()
72
+ assert any(valid_dir.iterdir())
73
+ assert any(invalid_dir.iterdir())
@@ -92,80 +92,33 @@ def test_generation_plan_only_uses_reachable_maps_shared_types() -> None:
92
92
  def test_generation_plan_supports_chat_family_without_shared_refs() -> None:
93
93
  plan = load_generation_plan(family="chat")
94
94
 
95
- assert [schema.title for schema in plan.shared_schemas] == [
96
- "MapEntryV1",
97
- "MapFileSourceV1",
98
- ]
95
+ assert [schema.title for schema in plan.shared_schemas] == []
99
96
  assert [schema.title for schema in plan.message_schemas_for("chat")] == [
100
97
  "ChatDiscordIngressCommandV1",
101
98
  "ChatGlobalV1",
102
99
  "ChatMessageV1",
103
100
  "ChatPrivateV1",
104
- "PlayerDataCacheReloadCommandV1",
105
- "PlayerActiveBadgeChangedCommandV1",
106
- "PlayerBadgeInventoryChangedCommandV1",
107
- "PlayerBadgeSymbolColorModeChangedCommandV1",
108
- "PlayerCustomNicknameChangedCommandV1",
109
- "PlayerJoinLeaveV1",
110
- "PlayerPasswordResetCommandV1",
111
- "ServerCommandExecuteCommandV1",
112
- "ServerActionV1",
113
- "ServerHeartbeatV1",
114
101
  ]
115
102
  assert [route.message_type for route in plan.routes_for("chat")] == [
116
103
  "chat.message",
117
104
  "chat.global",
118
105
  "chat.discord-ingress.command",
119
106
  "chat.private",
120
- "server.action",
121
- "player.join-leave",
122
- "player.custom-nickname.changed.command",
123
- "player.active-badge.changed.command",
124
- "player.badge-inventory.changed.command",
125
- "player.badge-symbol-color-mode.changed.command",
126
- "player.password-reset.command",
127
- "player-data-cache.reload.command",
128
- "server-command.execute.command",
129
- "server.heartbeat",
130
- ]
131
- assert [schema.title for schema in plan.map_schemas] == [
132
- "MapsListRequestV1",
133
- "MapsListResponseV1",
134
- "MapsLoadCommandV1",
135
- "MapsRemoveRequestV1",
136
- "MapsRemoveResponseV1",
137
107
  ]
138
108
  assert [route.constant_name for route in plan.chat_routes] == [
139
109
  "CHAT_MESSAGE_V1",
140
110
  "CHAT_GLOBAL_V1",
141
111
  "CHAT_DISCORD_INGRESS_COMMAND_V1",
142
112
  "CHAT_PRIVATE_V1",
143
- "SERVER_ACTION_V1",
144
- "PLAYER_JOIN_LEAVE_V1",
145
- "PLAYER_CUSTOM_NICKNAME_CHANGED_COMMAND_V1",
146
- "PLAYER_ACTIVE_BADGE_CHANGED_COMMAND_V1",
147
- "PLAYER_BADGE_INVENTORY_CHANGED_COMMAND_V1",
148
- "PLAYER_BADGE_SYMBOL_COLOR_MODE_CHANGED_COMMAND_V1",
149
- "PLAYER_PASSWORD_RESET_COMMAND_V1",
150
- "PLAYER_DATA_CACHE_RELOAD_COMMAND_V1",
151
- "SERVER_COMMAND_EXECUTE_COMMAND_V1",
152
- "SERVER_HEARTBEAT_V1",
153
- ]
154
- assert [route.constant_name for route in plan.map_routes] == [
155
- "MAPS_LIST_REQUEST_V1",
156
- "MAPS_REMOVE_REQUEST_V1",
157
- "MAPS_LOAD_COMMAND_V1",
158
113
  ]
159
114
 
160
115
 
161
116
  def test_generation_plan_keeps_family_inputs_separate() -> None:
162
117
  plan = load_generation_plan(family="chat")
163
118
 
164
- assert [family_input.family for family_input in plan.family_inputs] == ["maps", "chat"]
165
- assert plan.family_inputs[0].message_schemas == plan.message_schemas_for("maps")
166
- assert plan.family_inputs[0].routes == plan.routes_for("maps")
167
- assert plan.family_inputs[1].message_schemas == plan.message_schemas_for("chat")
168
- assert plan.family_inputs[1].routes == plan.routes_for("chat")
119
+ assert [family_input.family for family_input in plan.family_inputs] == ["chat"]
120
+ assert plan.family_inputs[0].message_schemas == plan.message_schemas_for("chat")
121
+ assert plan.family_inputs[0].routes == plan.routes_for("chat")
169
122
 
170
123
 
171
124
  def test_generation_plan_supports_discord_family_with_nested_shared_refs() -> None:
@@ -0,0 +1,44 @@
1
+ from __future__ import annotations
2
+
3
+ from xcore_protocol.paths import fixtures_root, spec_root
4
+ from xcore_protocol.schema_validation import assert_invalid, assert_valid
5
+
6
+
7
+ VALID_CASES = [
8
+ (
9
+ spec_root() / "messages" / "security" / "player.password-reset.command.v1.json",
10
+ fixtures_root() / "valid" / "security" / "player.password-reset.command.v1.json",
11
+ ),
12
+ ]
13
+
14
+
15
+ INVALID_CASES = [
16
+ (
17
+ spec_root() / "messages" / "security" / "player.password-reset.command.v1.json",
18
+ fixtures_root()
19
+ / "invalid"
20
+ / "security"
21
+ / "player.password-reset.command.v1.legacy-uuid.json",
22
+ ),
23
+ ]
24
+
25
+
26
+ def test_valid_security_fixtures_pass() -> None:
27
+ for schema_path, fixture_path in VALID_CASES:
28
+ assert_valid(schema_path, fixture_path)
29
+
30
+
31
+ def test_invalid_security_fixtures_fail() -> None:
32
+ for schema_path, fixture_path in INVALID_CASES:
33
+ error = assert_invalid(schema_path, fixture_path)
34
+ assert error is not None
35
+
36
+
37
+ def test_security_fixture_inventory_exists() -> None:
38
+ valid_dir = fixtures_root() / "valid" / "security"
39
+ invalid_dir = fixtures_root() / "invalid" / "security"
40
+
41
+ assert valid_dir.exists()
42
+ assert invalid_dir.exists()
43
+ assert any(valid_dir.iterdir())
44
+ assert any(invalid_dir.iterdir())
@@ -0,0 +1,55 @@
1
+ from __future__ import annotations
2
+
3
+ from xcore_protocol.paths import fixtures_root, spec_root
4
+ from xcore_protocol.schema_validation import assert_invalid, assert_valid
5
+
6
+
7
+ VALID_CASES = [
8
+ (
9
+ spec_root() / "messages" / "server" / "server.action.v1.json",
10
+ fixtures_root() / "valid" / "server" / "server.action.v1.json",
11
+ ),
12
+ (
13
+ spec_root() / "messages" / "server" / "server.heartbeat.v1.json",
14
+ fixtures_root() / "valid" / "server" / "server.heartbeat.v1.json",
15
+ ),
16
+ ]
17
+
18
+
19
+ INVALID_CASES = [
20
+ (
21
+ spec_root() / "messages" / "server" / "server.heartbeat.v1.json",
22
+ fixtures_root()
23
+ / "invalid"
24
+ / "server"
25
+ / "server.heartbeat.v1.server-host-alias.json",
26
+ ),
27
+ (
28
+ spec_root() / "messages" / "server" / "server.heartbeat.v1.json",
29
+ fixtures_root()
30
+ / "invalid"
31
+ / "server"
32
+ / "server.heartbeat.v1.missing-channel.json",
33
+ ),
34
+ ]
35
+
36
+
37
+ def test_valid_server_fixtures_pass() -> None:
38
+ for schema_path, fixture_path in VALID_CASES:
39
+ assert_valid(schema_path, fixture_path)
40
+
41
+
42
+ def test_invalid_server_fixtures_fail() -> None:
43
+ for schema_path, fixture_path in INVALID_CASES:
44
+ error = assert_invalid(schema_path, fixture_path)
45
+ assert error is not None
46
+
47
+
48
+ def test_server_fixture_inventory_exists() -> None:
49
+ valid_dir = fixtures_root() / "valid" / "server"
50
+ invalid_dir = fixtures_root() / "invalid" / "server"
51
+
52
+ assert valid_dir.exists()
53
+ assert invalid_dir.exists()
54
+ assert any(valid_dir.iterdir())
55
+ assert any(invalid_dir.iterdir())