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 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-09-22 06:13:08 +02:00
commit 021e3f1bba
3 changed files with 50 additions and 1 deletions

View file

@ -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(

View file

@ -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:

View file

@ -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