From 021e3f1bba368ec56d75127ebd73795145001e9e Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Tue, 22 Sep 2026 06:13:08 +0200 Subject: [PATCH] CultNet selection cut1, commit 2 fix batch: R-2, Python refuses v1 selection messages R-2 was owed by Cut 1 and not yet done (docs/cultnet-selection-cut.md, section 9, Self's rulings 2026-09-22): a v0-only Python peer must refuse a cultnet.selection.v1 carrier message loudly instead of silently answering with an empty list. - packages/cultnet-py/src/cultnet_py/interop_peer.py: handle_server_message's fallback now recognises any cultnet.*.v1 schema version and replies cultnet.error.v0 { code: "unsupported_schema_version", details: { schemaVersion } } via the new unsupported_schema_version_error helper, matching cultmesh_py's existing error shape. One test (test_python_interop_peer_refuses_v1_selection_messages) proves a snapshot_request.v1 and a database_subscribe.v1 message both get the refusal, not an empty list. - packages/cultmesh-py/src/cultmesh_py/server.py's delegating path (_handle_connection_message -> handle_message) already refused every unsupported schema version generically, v1 included - no code change needed there. test_cultmesh_local_server_returns_errors_for_bad_requests gains an explicit assertion for a snapshot_request.v1 message through that same path, so the coverage is named rather than only implied by the generic case. This is the one exception to "do not touch packages/" in this fix batch: only these two Python packages' interop/server files and their tests. Co-Authored-By: Claude Sonnet 5 --- packages/cultmesh-py/tests/test_cultmesh.py | 11 +++++++++ .../cultnet-py/src/cultnet_py/interop_peer.py | 16 +++++++++++++ packages/cultnet-py/tests/test_cultnet.py | 24 ++++++++++++++++++- 3 files changed, 50 insertions(+), 1 deletion(-) diff --git a/packages/cultmesh-py/tests/test_cultmesh.py b/packages/cultmesh-py/tests/test_cultmesh.py index 8cb2801f..9a697370 100644 --- a/packages/cultmesh-py/tests/test_cultmesh.py +++ b/packages/cultmesh-py/tests/test_cultmesh.py @@ -4317,6 +4317,14 @@ class CultMeshTests(unittest.TestCase): {"schemaVersion": "cultnet.document_delete.v0", "messageId": "bad-delete", "schemaId": "mesh.error_note.v1"}, expected_schema_version="cultnet.error.v0", ) + # R-2, owed by Cut 1 (docs/cultnet-selection-cut.md, section 9): this v0-only Mesh server + # refuses a cultnet.selection.v1 carrier message through the same delegating path + # (_handle_connection_message -> handle_message) as any other unsupported schema, not a + # silent empty response. + unsupported_v1 = raw_client.request( + {"schemaVersion": "cultnet.snapshot_request.v1", "messageId": "bad-v1-selection"}, + expected_schema_version="cultnet.error.v0", + ) finally: server.stop() @@ -4328,6 +4336,9 @@ class CultMeshTests(unittest.TestCase): self.assertEqual(malformed_put["code"], "malformed_document_put") self.assertEqual(malformed_delete["messageId"], "bad-delete") self.assertEqual(malformed_delete["code"], "malformed_document_delete") + self.assertEqual(unsupported_v1["messageId"], "bad-v1-selection") + self.assertEqual(unsupported_v1["code"], "unsupported_schema_version") + self.assertEqual(unsupported_v1["details"]["schemaVersion"], "cultnet.snapshot_request.v1") def test_cultmesh_authority_lease_requires_live_matching_lease(self) -> None: peer = CultMeshPeerCard( diff --git a/packages/cultnet-py/src/cultnet_py/interop_peer.py b/packages/cultnet-py/src/cultnet_py/interop_peer.py index 2eaaf0ac..4ff89314 100644 --- a/packages/cultnet-py/src/cultnet_py/interop_peer.py +++ b/packages/cultnet-py/src/cultnet_py/interop_peer.py @@ -384,9 +384,25 @@ def handle_server_message(state: PeerState, message: dict[str, Any], subscriptio return [state.verse_catalog.create_response(message)] if schema_version == PEER_EXCHANGE_REQUEST: return [state.peer_catalog.create_response(message)] + if isinstance(schema_version, str) and schema_version.startswith("cultnet.") and schema_version.endswith(".v1"): + # R-2, owed by Cut 1 (docs/cultnet-selection-cut.md, section 9): this peer speaks + # cultnet.selection.v1's carrier messages (snapshot_request.v1, database_subscribe.v1) not at + # all - it is v0-only. A v1 message is refused loudly, not answered with a silent empty list. + return [unsupported_schema_version_error(message)] return [] +def unsupported_schema_version_error(message: dict[str, Any]) -> dict[str, Any]: + schema_version = message.get("schemaVersion") + return { + "schemaVersion": "cultnet.error.v0", + "messageId": str(message.get("messageId") or ""), + "error": f"Unsupported CultNet message schema: {schema_version!r}.", + "code": "unsupported_schema_version", + "details": {"schemaVersion": schema_version}, + } + + def handle_database_subscribe(state: PeerState, message: dict[str, Any], subscriptions: dict[str, DatabaseSubscription]) -> list[dict[str, Any]]: subscription_id = str(message.get("subscriptionId") or message.get("messageId") or "") if not subscription_id: diff --git a/packages/cultnet-py/tests/test_cultnet.py b/packages/cultnet-py/tests/test_cultnet.py index 4c57f42b..b95686be 100644 --- a/packages/cultnet-py/tests/test_cultnet.py +++ b/packages/cultnet-py/tests/test_cultnet.py @@ -22,7 +22,12 @@ from cultnet_py.compare_csharp import ( _median_result, _parity_status, ) -from cultnet_py.interop_peer import append_shard_log_put, build_state, raw_snapshot_response +from cultnet_py.interop_peer import ( + append_shard_log_put, + build_state, + handle_server_message, + raw_snapshot_response, +) from cultnet_py import ( compute_simulation_claim_hash, CultNetDatabaseChange, @@ -1948,6 +1953,23 @@ class CultNetTests(unittest.TestCase): self.assertEqual(response["shardLogSequence"], 1) self.assertEqual([record["recordKey"] for record in response["documents"]], ["note:logged"]) + def test_python_interop_peer_refuses_v1_selection_messages(self) -> None: + # R-2, owed by Cut 1 (docs/cultnet-selection-cut.md, section 9): a v0-only peer refuses a v1 + # message loudly - cultnet.error.v0 { code: "unsupported_schema_version" } - rather than + # silently answering with an empty list. handle_server_message never touches `state` before + # this refusal fires, so no PeerState fixture is needed here. + for schema_version in ("cultnet.snapshot_request.v1", "cultnet.database_subscribe.v1"): + with self.subTest(schema_version=schema_version): + responses = handle_server_message( + None, # type: ignore[arg-type] + {"schemaVersion": schema_version, "messageId": "v1-refused"}, + {}, + ) + self.assertEqual(len(responses), 1) + self.assertEqual(responses[0]["schemaVersion"], "cultnet.error.v0") + self.assertEqual(responses[0]["code"], "unsupported_schema_version") + self.assertEqual(responses[0]["messageId"], "v1-refused") + def test_cultnet_simulation_consensus_dedupes_witnesses_and_requires_quorum(self) -> None: from cultnet_py import CultNetSimulationConsensus