xcore-protocol 0.5.1__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 (38) hide show
  1. xcore_protocol-0.5.1/PKG-INFO +92 -0
  2. xcore_protocol-0.5.1/README.md +79 -0
  3. xcore_protocol-0.5.1/pyproject.toml +32 -0
  4. xcore_protocol-0.5.1/python/tests/test_canonical_protocol_fixtures.py +171 -0
  5. xcore_protocol-0.5.1/python/tests/test_chat_fixtures.py +145 -0
  6. xcore_protocol-0.5.1/python/tests/test_discord_fixtures.py +87 -0
  7. xcore_protocol-0.5.1/python/tests/test_envelope_schemas.py +396 -0
  8. xcore_protocol-0.5.1/python/tests/test_generated_chat_models.py +251 -0
  9. xcore_protocol-0.5.1/python/tests/test_generated_discord_models.py +123 -0
  10. xcore_protocol-0.5.1/python/tests/test_generated_maps_models.py +120 -0
  11. xcore_protocol-0.5.1/python/tests/test_generated_moderation_models.py +223 -0
  12. xcore_protocol-0.5.1/python/tests/test_generated_telemetry_models.py +32 -0
  13. xcore_protocol-0.5.1/python/tests/test_generator_bootstrap.py +121 -0
  14. xcore_protocol-0.5.1/python/tests/test_java_python_route_compatibility.py +125 -0
  15. xcore_protocol-0.5.1/python/tests/test_java_python_serialized_compatibility.py +68 -0
  16. xcore_protocol-0.5.1/python/tests/test_maps_fixtures.py +81 -0
  17. xcore_protocol-0.5.1/python/tests/test_moderation_fixtures.py +113 -0
  18. xcore_protocol-0.5.1/python/tests/test_normalized_route_model.py +264 -0
  19. xcore_protocol-0.5.1/python/tests/test_telemetry_fixtures.py +49 -0
  20. xcore_protocol-0.5.1/python/xcore_protocol/__init__.py +1 -0
  21. xcore_protocol-0.5.1/python/xcore_protocol/generated/__init__.py +195 -0
  22. xcore_protocol-0.5.1/python/xcore_protocol/generated/chat.py +747 -0
  23. xcore_protocol-0.5.1/python/xcore_protocol/generated/discord.py +389 -0
  24. xcore_protocol-0.5.1/python/xcore_protocol/generated/envelopes.py +695 -0
  25. xcore_protocol-0.5.1/python/xcore_protocol/generated/maps.py +312 -0
  26. xcore_protocol-0.5.1/python/xcore_protocol/generated/moderation.py +478 -0
  27. xcore_protocol-0.5.1/python/xcore_protocol/generated/routes.py +634 -0
  28. xcore_protocol-0.5.1/python/xcore_protocol/generated/shared.py +605 -0
  29. xcore_protocol-0.5.1/python/xcore_protocol/generated/telemetry.py +177 -0
  30. xcore_protocol-0.5.1/python/xcore_protocol/models/__init__.py +1 -0
  31. xcore_protocol-0.5.1/python/xcore_protocol/paths.py +15 -0
  32. xcore_protocol-0.5.1/python/xcore_protocol/schema_validation.py +66 -0
  33. xcore_protocol-0.5.1/python/xcore_protocol.egg-info/PKG-INFO +92 -0
  34. xcore_protocol-0.5.1/python/xcore_protocol.egg-info/SOURCES.txt +36 -0
  35. xcore_protocol-0.5.1/python/xcore_protocol.egg-info/dependency_links.txt +1 -0
  36. xcore_protocol-0.5.1/python/xcore_protocol.egg-info/requires.txt +7 -0
  37. xcore_protocol-0.5.1/python/xcore_protocol.egg-info/top_level.txt +2 -0
  38. xcore_protocol-0.5.1/setup.cfg +4 -0
@@ -0,0 +1,92 @@
1
+ Metadata-Version: 2.4
2
+ Name: xcore-protocol
3
+ Version: 0.5.1
4
+ Summary: Canonical cross-service protocol for the XCore ecosystem
5
+ Requires-Python: >=3.11
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: jsonschema>=4.23.0
8
+ Requires-Dist: rfc3339-validator>=0.1.4
9
+ Requires-Dist: referencing>=0.35.0
10
+ Requires-Dist: PyYAML>=6.0.2
11
+ Provides-Extra: dev
12
+ Requires-Dist: pytest>=8.3.0; extra == "dev"
13
+
14
+ # xcore-protocol
15
+
16
+ Canonical cross-service protocol for the XCore ecosystem.
17
+
18
+ ## Mission
19
+ `xcore-protocol` is the canonical source of truth for XCore cross-service communication.
20
+
21
+ It defines:
22
+ - message schemas
23
+ - envelope structure
24
+ - route and stream metadata
25
+ - versioning and compatibility rules
26
+ - canonical fixtures
27
+ - generation inputs and tooling
28
+ - generated Java and Python protocol DTO/model artifacts
29
+ - thin handwritten validation and repository-tooling support around generated artifacts
30
+ - cross-language compatibility tests
31
+
32
+ ## Scope
33
+ This repository contains only **cross-process / cross-service wire protocol artifacts**.
34
+
35
+ ### In scope
36
+ - event, command, and RPC contracts
37
+ - envelope definitions
38
+ - route metadata
39
+ - shared payload subtypes
40
+ - protocol fixtures and validation helpers
41
+ - generation configuration and generated Java/Python protocol artifacts
42
+
43
+ ### Out of scope
44
+ - application business logic
45
+ - Discord handlers or presentation code
46
+ - Mindustry runtime integration
47
+ - Mongo persistence
48
+ - app-specific reconnect/worker orchestration
49
+
50
+ ## Repository Layout
51
+ - `docs/` — ADRs, architecture notes, policies, migration notes
52
+ - `spec/` — protocol specs, shared types, envelopes, routes, generation inputs
53
+ - `fixtures/` — valid, invalid, and legacy examples
54
+ - `generators/` — language generation config/templates/scripts
55
+ - `java/` — generated Java protocol artifacts for the canonical model surface
56
+ - `python/` — generated Python protocol package and thin helpers
57
+ - `compat/` — cross-language compatibility checks
58
+ - `scripts/` — validation and generation helper scripts
59
+
60
+ ## Migrated Families
61
+
62
+ All planned message families are fully migrated and operational:
63
+
64
+ - **Moderation** — ban, mute, vote-kick, kick-banned, pardon, audit
65
+ - **Discord** — link-confirm, unlink, link-status, admin-access, link-code
66
+ - **Maps** — list request/response, remove request/response
67
+ - **Chat/Heartbeat** — chat messages, global chat, heartbeat, join/leave, server actions, player state commands
68
+ - **Telemetry** — Redis TTL snapshot payloads for Prometheus/Gateway telemetry aggregation
69
+
70
+ ## Current Status
71
+
72
+ All canonical message families are defined with JSON Schema specs and canonical fixtures. Generated Java records (`org.xcore.protocol.generated.*`) and Python frozen dataclasses (`xcore_protocol.generated.*`) cover the supported generated protocol surface, including route-less telemetry snapshot payloads. `XCore-plugin` and `XCore-discord-bot` both consume generated artifacts as their sole transport model where applicable. Cross-language compatibility checks and full CI validation chain are in place.
73
+
74
+ ## Releases
75
+
76
+ Each `vX.Y.Z` tag publishes the same version of both supported artifacts:
77
+
78
+ - Java: `org.xcore:xcore-protocol-java:X.Y.Z` from `https://maven.x-core.org/releases`
79
+ - Python: `xcore-protocol==X.Y.Z` from PyPI
80
+
81
+ Production consumers must use an exact released version. Git revisions and `-SNAPSHOT`
82
+ versions are only acceptable while developing the protocol itself. The release workflow validates
83
+ schemas, generated output, Java tests, Python tests, and distributable packages before publishing.
84
+
85
+ Before the first PyPI release, configure the `pypi` GitHub environment as a PyPI Trusted Publisher
86
+ for this repository's `Publish Release` workflow. No Python package registry is self-hosted.
87
+
88
+ ## See Also
89
+ - `docs/adr/ADR-001-protocol-first.md`
90
+ - `docs/architecture/protocol-overview.md`
91
+ - `docs/policies/versioning.md`
92
+ - `docs/policies/compatibility.md`
@@ -0,0 +1,79 @@
1
+ # xcore-protocol
2
+
3
+ Canonical cross-service protocol for the XCore ecosystem.
4
+
5
+ ## Mission
6
+ `xcore-protocol` is the canonical source of truth for XCore cross-service communication.
7
+
8
+ It defines:
9
+ - message schemas
10
+ - envelope structure
11
+ - route and stream metadata
12
+ - versioning and compatibility rules
13
+ - canonical fixtures
14
+ - generation inputs and tooling
15
+ - generated Java and Python protocol DTO/model artifacts
16
+ - thin handwritten validation and repository-tooling support around generated artifacts
17
+ - cross-language compatibility tests
18
+
19
+ ## Scope
20
+ This repository contains only **cross-process / cross-service wire protocol artifacts**.
21
+
22
+ ### In scope
23
+ - event, command, and RPC contracts
24
+ - envelope definitions
25
+ - route metadata
26
+ - shared payload subtypes
27
+ - protocol fixtures and validation helpers
28
+ - generation configuration and generated Java/Python protocol artifacts
29
+
30
+ ### Out of scope
31
+ - application business logic
32
+ - Discord handlers or presentation code
33
+ - Mindustry runtime integration
34
+ - Mongo persistence
35
+ - app-specific reconnect/worker orchestration
36
+
37
+ ## Repository Layout
38
+ - `docs/` — ADRs, architecture notes, policies, migration notes
39
+ - `spec/` — protocol specs, shared types, envelopes, routes, generation inputs
40
+ - `fixtures/` — valid, invalid, and legacy examples
41
+ - `generators/` — language generation config/templates/scripts
42
+ - `java/` — generated Java protocol artifacts for the canonical model surface
43
+ - `python/` — generated Python protocol package and thin helpers
44
+ - `compat/` — cross-language compatibility checks
45
+ - `scripts/` — validation and generation helper scripts
46
+
47
+ ## Migrated Families
48
+
49
+ All planned message families are fully migrated and operational:
50
+
51
+ - **Moderation** — ban, mute, vote-kick, kick-banned, pardon, audit
52
+ - **Discord** — link-confirm, unlink, link-status, admin-access, link-code
53
+ - **Maps** — list request/response, remove request/response
54
+ - **Chat/Heartbeat** — chat messages, global chat, heartbeat, join/leave, server actions, player state commands
55
+ - **Telemetry** — Redis TTL snapshot payloads for Prometheus/Gateway telemetry aggregation
56
+
57
+ ## Current Status
58
+
59
+ All canonical message families are defined with JSON Schema specs and canonical fixtures. Generated Java records (`org.xcore.protocol.generated.*`) and Python frozen dataclasses (`xcore_protocol.generated.*`) cover the supported generated protocol surface, including route-less telemetry snapshot payloads. `XCore-plugin` and `XCore-discord-bot` both consume generated artifacts as their sole transport model where applicable. Cross-language compatibility checks and full CI validation chain are in place.
60
+
61
+ ## Releases
62
+
63
+ Each `vX.Y.Z` tag publishes the same version of both supported artifacts:
64
+
65
+ - Java: `org.xcore:xcore-protocol-java:X.Y.Z` from `https://maven.x-core.org/releases`
66
+ - Python: `xcore-protocol==X.Y.Z` from PyPI
67
+
68
+ Production consumers must use an exact released version. Git revisions and `-SNAPSHOT`
69
+ versions are only acceptable while developing the protocol itself. The release workflow validates
70
+ schemas, generated output, Java tests, Python tests, and distributable packages before publishing.
71
+
72
+ Before the first PyPI release, configure the `pypi` GitHub environment as a PyPI Trusted Publisher
73
+ for this repository's `Publish Release` workflow. No Python package registry is self-hosted.
74
+
75
+ ## See Also
76
+ - `docs/adr/ADR-001-protocol-first.md`
77
+ - `docs/architecture/protocol-overview.md`
78
+ - `docs/policies/versioning.md`
79
+ - `docs/policies/compatibility.md`
@@ -0,0 +1,32 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "xcore-protocol"
7
+ version = "0.5.1"
8
+ description = "Canonical cross-service protocol for the XCore ecosystem"
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ dependencies = [
12
+ "jsonschema>=4.23.0",
13
+ "rfc3339-validator>=0.1.4",
14
+ "referencing>=0.35.0",
15
+ "PyYAML>=6.0.2"
16
+ ]
17
+
18
+ [project.optional-dependencies]
19
+ dev = [
20
+ "pytest>=8.3.0"
21
+ ]
22
+
23
+ [tool.pytest.ini_options]
24
+ testpaths = ["python/tests"]
25
+ pythonpath = ["python", "."]
26
+ addopts = "-q"
27
+
28
+ [tool.setuptools]
29
+ package-dir = {"" = "python"}
30
+
31
+ [tool.setuptools.packages.find]
32
+ where = ["python"]
@@ -0,0 +1,171 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from collections.abc import Callable, Mapping
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ import pytest
9
+
10
+ from xcore_protocol.generated import (
11
+ ActorRefV1ActorType,
12
+ DiscordAdminAccessChangedCommandV1,
13
+ DiscordUnlinkCommandV1,
14
+ MetricsSnapshotV1,
15
+ ServerHeartbeatV1,
16
+ )
17
+ from xcore_protocol.paths import fixtures_root, spec_root
18
+ from xcore_protocol.schema_validation import assert_valid
19
+
20
+
21
+ REPO_ROOT = fixtures_root().parent
22
+ CANONICAL_FIXTURES_ROOT = REPO_ROOT / "spec" / "fixtures" / "valid"
23
+
24
+
25
+ def _load_fixture(path: Path) -> dict[str, Any]:
26
+ return json.loads(path.read_text(encoding="utf-8"))
27
+
28
+
29
+ def _model_dump_json(model: Any) -> dict[str, Any]:
30
+ model_dump = getattr(model, "model_dump", None)
31
+ if callable(model_dump):
32
+ payload = model_dump(mode="json")
33
+ if not isinstance(payload, dict):
34
+ raise TypeError("model_dump(mode='json') must return a dict payload")
35
+ return payload
36
+
37
+ to_payload = getattr(model, "to_payload", None)
38
+ if callable(to_payload):
39
+ payload = to_payload()
40
+ if not isinstance(payload, dict):
41
+ raise TypeError("to_payload() must return a dict payload")
42
+ return payload
43
+
44
+ raise TypeError("Generated model must provide model_dump(mode='json') or to_payload()")
45
+
46
+
47
+ def _assert_no_snake_case_keys(value: Any) -> None:
48
+ if isinstance(value, Mapping):
49
+ for key, nested_value in value.items():
50
+ assert "_" not in key, f"Found legacy snake_case key: {key}"
51
+ _assert_no_snake_case_keys(nested_value)
52
+ return
53
+
54
+ if isinstance(value, list):
55
+ for item in value:
56
+ _assert_no_snake_case_keys(item)
57
+
58
+
59
+ CanonicalAssertion = Callable[[Any, dict[str, Any]], None]
60
+
61
+
62
+ def _assert_admin_access_semantics(model: DiscordAdminAccessChangedCommandV1, payload: dict[str, Any]) -> None:
63
+ assert model.source.actorType is ActorRefV1ActorType.SYSTEM
64
+ assert payload["source"]["actorType"] == "system"
65
+ assert model.actor.actorType is ActorRefV1ActorType.DISCORD
66
+ assert model.actor.actorName == payload["actor"]["actorName"]
67
+
68
+
69
+ def _assert_unlink_semantics(model: DiscordUnlinkCommandV1, payload: dict[str, Any]) -> None:
70
+ assert model.actor.actorType is ActorRefV1ActorType.DISCORD
71
+ assert model.actor.actorName == payload["actor"]["actorName"]
72
+ assert payload["actor"]["actorName"] == "Beta Display"
73
+
74
+
75
+ def _assert_heartbeat_semantics(model: ServerHeartbeatV1, payload: dict[str, Any]) -> None:
76
+ assert model.serverName == payload["serverName"]
77
+ assert model.discordChannelId == payload["discordChannelId"]
78
+
79
+
80
+ def _assert_telemetry_semantics(model: MetricsSnapshotV1, payload: dict[str, Any]) -> None:
81
+ assert model.server == payload["server"]
82
+ assert model.SCHEMA_VERSION == payload["schemaVersion"]
83
+ assert model.samples[2].count == payload["samples"][2]["count"]
84
+
85
+
86
+ CANONICAL_CASES: list[tuple[str, Path, Path, type[Any], CanonicalAssertion]] = [
87
+ (
88
+ "discord_admin_access_grant",
89
+ spec_root() / "messages" / "discord" / "discord.admin-access.changed.command.v1.json",
90
+ CANONICAL_FIXTURES_ROOT / "discord" / "discord.admin-access.changed.command.v1.grant.json",
91
+ DiscordAdminAccessChangedCommandV1,
92
+ _assert_admin_access_semantics,
93
+ ),
94
+ (
95
+ "discord_admin_access_revoke",
96
+ spec_root() / "messages" / "discord" / "discord.admin-access.changed.command.v1.json",
97
+ CANONICAL_FIXTURES_ROOT / "discord" / "discord.admin-access.changed.command.v1.revoke.json",
98
+ DiscordAdminAccessChangedCommandV1,
99
+ _assert_admin_access_semantics,
100
+ ),
101
+ (
102
+ "discord_unlink",
103
+ spec_root() / "messages" / "discord" / "discord.unlink.command.v1.json",
104
+ CANONICAL_FIXTURES_ROOT / "discord" / "discord.unlink.command.v1.json",
105
+ DiscordUnlinkCommandV1,
106
+ _assert_unlink_semantics,
107
+ ),
108
+ (
109
+ "server_heartbeat",
110
+ spec_root() / "messages" / "chat" / "server.heartbeat.v1.json",
111
+ CANONICAL_FIXTURES_ROOT / "chat" / "server.heartbeat.v1.json",
112
+ ServerHeartbeatV1,
113
+ _assert_heartbeat_semantics,
114
+ ),
115
+ (
116
+ "metrics_snapshot",
117
+ spec_root() / "messages" / "telemetry" / "metrics.snapshot.v1.json",
118
+ CANONICAL_FIXTURES_ROOT / "telemetry" / "metrics.snapshot.v1.json",
119
+ MetricsSnapshotV1,
120
+ _assert_telemetry_semantics,
121
+ ),
122
+ ]
123
+
124
+
125
+ def test_canonical_fixture_inventory_exists() -> None:
126
+ assert fixtures_root().exists()
127
+
128
+ for _, schema_path, fixture_path, _, _ in CANONICAL_CASES:
129
+ assert schema_path.exists()
130
+ assert fixture_path.exists()
131
+
132
+
133
+ @pytest.mark.parametrize(
134
+ ("_case_name", "schema_path", "fixture_path", "model_type", "assert_semantics"),
135
+ CANONICAL_CASES,
136
+ )
137
+ def test_canonical_fixtures_remain_schema_valid(
138
+ _case_name: str,
139
+ schema_path: Path,
140
+ fixture_path: Path,
141
+ model_type: type[Any],
142
+ assert_semantics: CanonicalAssertion,
143
+ ) -> None:
144
+ del model_type, assert_semantics
145
+ assert_valid(schema_path, fixture_path)
146
+
147
+
148
+ @pytest.mark.parametrize(
149
+ ("_case_name", "schema_path", "fixture_path", "model_type", "assert_semantics"),
150
+ CANONICAL_CASES,
151
+ )
152
+ def test_canonical_generated_models_roundtrip(
153
+ _case_name: str,
154
+ schema_path: Path,
155
+ fixture_path: Path,
156
+ model_type: type[Any],
157
+ assert_semantics: CanonicalAssertion,
158
+ ) -> None:
159
+ del schema_path
160
+ payload = _load_fixture(fixture_path)
161
+
162
+ model = model_type.from_payload(payload)
163
+ roundtrip_payload = _model_dump_json(model)
164
+
165
+ assert roundtrip_payload == payload
166
+ assert_semantics(model, payload)
167
+
168
+
169
+ def test_canonical_fixtures_do_not_use_legacy_snake_case_keys() -> None:
170
+ for _, _, fixture_path, _, _ in CANONICAL_CASES:
171
+ _assert_no_snake_case_keys(_load_fixture(fixture_path))
@@ -0,0 +1,145 @@
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
+ spec_root() / "messages" / "chat" / "server.action.v1.json",
26
+ fixtures_root() / "valid" / "chat" / "server.action.v1.json",
27
+ ),
28
+ (
29
+ spec_root() / "messages" / "chat" / "player.join-leave.v1.json",
30
+ fixtures_root() / "valid" / "chat" / "player.join-leave.v1.json",
31
+ ),
32
+ (
33
+ spec_root() / "messages" / "chat" / "player.custom-nickname.changed.command.v1.json",
34
+ fixtures_root() / "valid" / "chat" / "player.custom-nickname.changed.command.v1.json",
35
+ ),
36
+ (
37
+ spec_root() / "messages" / "chat" / "player.active-badge.changed.command.v1.json",
38
+ fixtures_root() / "valid" / "chat" / "player.active-badge.changed.command.v1.json",
39
+ ),
40
+ (
41
+ spec_root() / "messages" / "chat" / "player.badge-inventory.changed.command.v1.json",
42
+ fixtures_root() / "valid" / "chat" / "player.badge-inventory.changed.command.v1.json",
43
+ ),
44
+ (
45
+ spec_root() / "messages" / "chat" / "player.badge-symbol-color-mode.changed.command.v1.json",
46
+ fixtures_root() / "valid" / "chat" / "player.badge-symbol-color-mode.changed.command.v1.json",
47
+ ),
48
+ (
49
+ spec_root() / "messages" / "chat" / "player.password-reset.command.v1.json",
50
+ fixtures_root() / "valid" / "chat" / "player.password-reset.command.v1.json",
51
+ ),
52
+ (
53
+ spec_root() / "messages" / "chat" / "server.heartbeat.v1.json",
54
+ fixtures_root() / "valid" / "chat" / "server.heartbeat.v1.json",
55
+ ),
56
+ ]
57
+
58
+
59
+ INVALID_CASES = [
60
+ (
61
+ spec_root() / "messages" / "chat" / "chat.message.v1.json",
62
+ fixtures_root() / "invalid" / "chat" / "chat.message.v1.snake-author.json",
63
+ ),
64
+ (
65
+ spec_root() / "messages" / "chat" / "chat.private.v1.json",
66
+ fixtures_root() / "invalid" / "chat" / "chat.private.v1.snake-from.json",
67
+ ),
68
+ (
69
+ spec_root() / "messages" / "chat" / "player.join-leave.v1.json",
70
+ fixtures_root()
71
+ / "invalid"
72
+ / "chat"
73
+ / "player.join-leave.v1.legacy-join.json",
74
+ ),
75
+ (
76
+ spec_root() / "messages" / "chat" / "player.custom-nickname.changed.command.v1.json",
77
+ fixtures_root()
78
+ / "invalid"
79
+ / "chat"
80
+ / "player.custom-nickname.changed.command.v1.legacy-uuid.json",
81
+ ),
82
+ (
83
+ spec_root() / "messages" / "chat" / "player.active-badge.changed.command.v1.json",
84
+ fixtures_root()
85
+ / "invalid"
86
+ / "chat"
87
+ / "player.active-badge.changed.command.v1.legacy-uuid.json",
88
+ ),
89
+ (
90
+ spec_root() / "messages" / "chat" / "player.badge-inventory.changed.command.v1.json",
91
+ fixtures_root()
92
+ / "invalid"
93
+ / "chat"
94
+ / "player.badge-inventory.changed.command.v1.legacy-uuid.json",
95
+ ),
96
+ (
97
+ spec_root() / "messages" / "chat" / "player.badge-symbol-color-mode.changed.command.v1.json",
98
+ fixtures_root()
99
+ / "invalid"
100
+ / "chat"
101
+ / "player.badge-symbol-color-mode.changed.command.v1.legacy-uuid.json",
102
+ ),
103
+ (
104
+ spec_root() / "messages" / "chat" / "player.password-reset.command.v1.json",
105
+ fixtures_root()
106
+ / "invalid"
107
+ / "chat"
108
+ / "player.password-reset.command.v1.legacy-uuid.json",
109
+ ),
110
+ (
111
+ spec_root() / "messages" / "chat" / "server.heartbeat.v1.json",
112
+ fixtures_root()
113
+ / "invalid"
114
+ / "chat"
115
+ / "server.heartbeat.v1.server-host-alias.json",
116
+ ),
117
+ (
118
+ spec_root() / "messages" / "chat" / "server.heartbeat.v1.json",
119
+ fixtures_root()
120
+ / "invalid"
121
+ / "chat"
122
+ / "server.heartbeat.v1.missing-channel.json",
123
+ ),
124
+ ]
125
+
126
+
127
+ def test_valid_chat_fixtures_pass() -> None:
128
+ for schema_path, fixture_path in VALID_CASES:
129
+ assert_valid(schema_path, fixture_path)
130
+
131
+
132
+ def test_invalid_chat_fixtures_fail() -> None:
133
+ for schema_path, fixture_path in INVALID_CASES:
134
+ error = assert_invalid(schema_path, fixture_path)
135
+ assert error is not None
136
+
137
+
138
+ def test_chat_fixture_inventory_exists() -> None:
139
+ chat_valid_dir = fixtures_root() / "valid" / "chat"
140
+ chat_invalid_dir = fixtures_root() / "invalid" / "chat"
141
+
142
+ assert chat_valid_dir.exists()
143
+ assert chat_invalid_dir.exists()
144
+ assert any(chat_valid_dir.iterdir())
145
+ assert any(chat_invalid_dir.iterdir())
@@ -0,0 +1,87 @@
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" / "discord" / "discord.link-code-created.v1.json",
10
+ fixtures_root() / "valid" / "discord" / "discord.link-code-created.v1.json",
11
+ ),
12
+ (
13
+ spec_root() / "messages" / "discord" / "discord.link.confirm.command.v1.json",
14
+ fixtures_root() / "valid" / "discord" / "discord.link.confirm.command.v1.json",
15
+ ),
16
+ (
17
+ spec_root() / "messages" / "discord" / "discord.unlink.command.v1.json",
18
+ fixtures_root() / "valid" / "discord" / "discord.unlink.command.v1.json",
19
+ ),
20
+ (
21
+ spec_root() / "messages" / "discord" / "discord.link.status-changed.v1.json",
22
+ fixtures_root() / "valid" / "discord" / "discord.link.status-changed.v1.json",
23
+ ),
24
+ (
25
+ spec_root()
26
+ / "messages"
27
+ / "discord"
28
+ / "discord.admin-access.changed.command.v1.json",
29
+ fixtures_root()
30
+ / "valid"
31
+ / "discord"
32
+ / "discord.admin-access.changed.command.v1.json",
33
+ ),
34
+ ]
35
+
36
+
37
+ INVALID_CASES = [
38
+ (
39
+ spec_root() / "messages" / "discord" / "discord.link-code-created.v1.json",
40
+ fixtures_root() / "invalid" / "discord" / "discord.link-code-created.v1.bad-time.json",
41
+ ),
42
+ (
43
+ spec_root() / "messages" / "discord" / "discord.link.confirm.command.v1.json",
44
+ fixtures_root()
45
+ / "invalid"
46
+ / "discord"
47
+ / "discord.link.confirm.command.v1.bad-time.json",
48
+ ),
49
+ (
50
+ spec_root() / "messages" / "discord" / "discord.unlink.command.v1.json",
51
+ fixtures_root()
52
+ / "invalid"
53
+ / "discord"
54
+ / "discord.unlink.command.v1.missing-discord.json",
55
+ ),
56
+ (
57
+ spec_root()
58
+ / "messages"
59
+ / "discord"
60
+ / "discord.admin-access.changed.command.v1.json",
61
+ fixtures_root()
62
+ / "invalid"
63
+ / "discord"
64
+ / "discord.admin-access.changed.command.v1.missing-reason.json",
65
+ ),
66
+ ]
67
+
68
+
69
+ def test_valid_discord_fixtures_pass() -> None:
70
+ for schema_path, fixture_path in VALID_CASES:
71
+ assert_valid(schema_path, fixture_path)
72
+
73
+
74
+ def test_invalid_discord_fixtures_fail() -> None:
75
+ for schema_path, fixture_path in INVALID_CASES:
76
+ error = assert_invalid(schema_path, fixture_path)
77
+ assert error is not None
78
+
79
+
80
+ def test_discord_fixture_inventory_exists() -> None:
81
+ discord_valid_dir = fixtures_root() / "valid" / "discord"
82
+ discord_invalid_dir = fixtures_root() / "invalid" / "discord"
83
+
84
+ assert discord_valid_dir.exists()
85
+ assert discord_invalid_dir.exists()
86
+ assert any(discord_valid_dir.iterdir())
87
+ assert any(discord_invalid_dir.iterdir())