CultLib/packages/cultnet-ts/test/interop/cultnet-interop.test.ts
Meta Vens 95493862bd Split the Python runtime into cultcache-py, cultnet-py, cultmesh-py
One distribution named cultcache-py carried three libraries. Rust and
TypeScript layer them as cultcache < cultnet < cultmesh; Python now does
the same, one package per layer, module names unchanged so every
`from cultmesh_py import ...` keeps working.

Two imports ran against that layering and are resolved by moving code to
where the dependency direction says it lives, not by adding a shim:

- cultnet_py.interop_peer imported the Verse/peer catalog, authority
  lease, stream, and simulation-fact contracts from cultmesh_py. They are
  wire contracts a bare peer has to speak without owning a node, which is
  why cultnet-rs and cultnet-ts keep them in the net layer. They now live
  in cultnet_py.cultmesh_contracts; cultmesh_py re-exports them.
- cultcache_py.benchmark imported cultnet_py. It measures framed CultNet
  parse beside cache paths, so it and compare_csharp move to cultnet_py.
  verify checks all three packages and moves to cultmesh_py.

The single 6,693-line test module is split by what each test touches: a
test using any cultmesh_py name lands in cultmesh-py, else cultnet_py in
cultnet-py, else cultcache-py. Each suite runs with only its layer and the
layers below on the path, so a test that reaches up fails to import
rather than quietly binding the packages back together. 11 / 53 / 78 =
the same 142.

Workflows install all three; the publish job is one matrix over the
three packages keyed by tag prefix. The TS test harnesses put all three
src directories on PYTHONPATH. cultcache-py bumps to 0.2.0 because it no
longer contains cultnet_py or cultmesh_py; the new packages start at
0.1.0. Nothing was ever published, so no PyPI consumer moves.

  cultcache-py  11 tests, path: cultcache-py/src
  cultnet-py    53 tests, path: + cultnet-py/src
  cultmesh-py   78 tests, path: + cultmesh-py/src
  python -m cultmesh_py.verify -> ok, all five checks
  cultnet_py.interop_peer imports with cultmesh_py absent

Not run here: the cultnet-ts interop ring and cultcache-ts store matrix
need dotnet/kotlin peers; their PYTHONPATH change is four lines.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-10 13:10:47 +02:00

4019 lines
146 KiB
TypeScript

import assert from "node:assert/strict";
import { execFile, spawn, spawnSync, type ChildProcessByStdio } from "node:child_process";
import { createHash } from "node:crypto";
import dgram, { type Socket } from "node:dgram";
import { once } from "node:events";
import { existsSync, rmSync } from "node:fs";
import { connect as connectTcp, createServer } from "node:net";
import { homedir, networkInterfaces, tmpdir } from "node:os";
import { delimiter, dirname, join, resolve } from "node:path";
import type { Readable } from "node:stream";
import { test } from "node:test";
import { setTimeout as delay } from "node:timers/promises";
import { promisify } from "node:util";
import { decode, encode } from "@msgpack/msgpack";
import { encodeFrame, LengthPrefixedMessageFramer } from "../../src/framing";
import { CultNetRudpSocketTransportConnection, decodeRudpPacket } from "../../src";
import { INTEROP_SCHEMA_VERSION } from "./cultnet-interop-shared";
const execFileAsync = promisify(execFile);
const cargoCommand = process.env.CARGO ?? (process.platform === "win32" ? join(homedir(), ".cargo", "bin", "cargo.exe") : "cargo");
const dotnetCommand = process.env.DOTNET ?? (process.platform === "win32" ? join("C:", "Program Files", "dotnet", "dotnet.exe") : "dotnet");
const pythonCommand = process.env.PYTHON ?? resolvePythonCommand();
const cultNetTsRoot = resolve(__dirname, "../../..");
const cultLibRoot = findAncestor(cultNetTsRoot, "CultLib.sln") ?? resolve(cultNetTsRoot, "..", "CultLib");
const cultcachePyRoot = resolve(cultLibRoot, "packages", "cultcache-py");
const cultcachePySrc = ["cultcache-py", "cultnet-py", "cultmesh-py"]
.map((name) => resolve(cultLibRoot, "packages", name, "src"))
.join(delimiter);
const cultmeshKotlinRoot = resolve(cultLibRoot, "packages", "cultmesh-kotlin");
const kotlinHome = process.env.KOTLIN_HOME ?? "C:\\Program Files\\Android\\Android Studio\\plugins\\Kotlin\\kotlinc";
const kotlinJavaHome = process.env.KOTLIN_JAVA_HOME ?? "C:\\Program Files\\Android\\Android Studio\\jbr";
const kotlinJavaCommand = process.env.JAVA ?? join(kotlinJavaHome, "bin", process.platform === "win32" ? "java.exe" : "java");
const kotlinStdlibPath = join(kotlinHome, "lib", "kotlin-stdlib.jar");
const kotlinJarPath = resolve(cultLibRoot, "artifacts", "cultmesh-kotlin", "cultmesh-kotlin.jar");
const kotlinClasspath = [kotlinJarPath, kotlinStdlibPath].join(delimiter);
const cultnetRsRoot = existsSync(resolve(cultLibRoot, "packages", "cultnet-rs"))
? resolve(cultLibRoot, "packages", "cultnet-rs")
: resolve(cultNetTsRoot, "..", "cultnet-rs");
const tsPeerScript = resolve(cultNetTsRoot, "dist-test", "test", "interop", "cultnet-interop-peer.js");
const interopSchemaPath = resolve(cultNetTsRoot, "integration", "contracts", "cultnet.interop-note.schema.json");
const interopNoteSchemaId = "https://github.com/GameCult/cultnet-ts/integration/contracts/cultnet.interop-note.schema.json";
const witnessArtifactBundleSchemaId = "https://github.com/GameCult/cultnet-ts/contracts/cultnet.witness-artifact-bundle.schema.json";
const simulationFactSchemaId = "gamecult.mesh.simulation_fact";
const csharpProjectPath = resolve(
cultLibRoot,
"tests",
"GameCult.Networking.InteropPeer",
"GameCult.Networking.InteropPeer.csproj",
);
const csharpDllPath = resolve(
cultLibRoot,
"bin",
"GameCult.Networking.InteropPeer",
"Debug",
"net10.0",
"GameCult.Networking.InteropPeer.dll",
);
const rustBinaryPath = resolve(
cultnetRsRoot,
"target",
"debug",
"examples",
process.platform === "win32" ? "cultnet_interop_peer.exe" : "cultnet_interop_peer",
);
const discoveryGroup = "239.77.44.11";
const serveReadyTimeoutMs = 90_000;
const jsonCommandTimeoutMs = 180_000;
const processCleanupTimeoutMs = 5_000;
let rustInteropPeerBuild: Promise<void> | undefined;
let csharpInteropPeerBuild: Promise<void> | undefined;
let kotlinInteropPeerBuild: Promise<void> | undefined;
function resolvePythonCommand(): string {
const codexPythonCommand = join(homedir(), ".cache", "codex-runtimes", "codex-primary-runtime", "dependencies", "python", "python.exe");
const candidates = process.platform === "win32"
? [
...(existsSync(codexPythonCommand) ? [codexPythonCommand] : []),
"py",
"python",
"python3",
]
: ["python3", "python"];
for (const candidate of candidates) {
const result = spawnSync(candidate, ["--version"], { stdio: "ignore" });
if (result.status === 0) {
return candidate;
}
}
return process.platform === "win32" ? "python" : "python3";
}
interface RudpServerInteropPeerSpec {
name: string;
connectionId: number;
build: () => Promise<void>;
spawn: (options?: RudpServerSpawnOptions) => RunningPythonRudpPeer;
joinPayload: string;
clientPayload: string;
expectedPayload: string;
}
interface RudpServerSpawnOptions {
clientPayload?: string;
serverPayload?: string;
serverExtraPayload?: string;
disconnectReason?: string;
maxFragmentBytes?: number;
}
function rudpServerInteropPeers(): RudpServerInteropPeerSpec[] {
return [
{
name: "Python",
connectionId: 0x55667788,
build: async () => undefined,
spawn: spawnPythonRudpPeer,
joinPayload: "ts-join",
clientPayload: "client-state",
expectedPayload: "python-state",
},
{
name: "Rust",
connectionId: 0x22446688,
build: buildRustInteropPeer,
spawn: spawnRustRudpServer,
joinPayload: "ts-rust-join",
clientPayload: "ts-rust-client-state",
expectedPayload: "rust-server-state",
},
{
name: "C#",
connectionId: 0x33557799,
build: buildCSharpInteropPeer,
spawn: spawnCSharpRudpServer,
joinPayload: "ts-csharp-join",
clientPayload: "ts-csharp-client-state",
expectedPayload: "csharp-server-state",
},
{
name: "Kotlin",
connectionId: 0x446688aa,
build: buildKotlinInteropPeer,
spawn: spawnKotlinRudpServer,
joinPayload: "ts-kotlin-join",
clientPayload: "ts-kotlin-client-state",
expectedPayload: "kotlin-server-state",
},
];
}
const pythonRudpPeerScript = String.raw`
import os
import socket
import time
from cultnet_py import (
CultNetRudpSocketMode,
CultNetRudpSocketTransportConnection,
CultNetRudpSocketTransportOptions,
)
expected_client_payload = os.environ.get("RUDP_CLIENT_PAYLOAD", "client-state").encode()
server_payload = os.environ.get("RUDP_SERVER_PAYLOAD", "python-state").encode()
server_extra_payload_raw = os.environ.get("RUDP_SERVER_EXTRA_PAYLOAD")
server_extra_payload = server_extra_payload_raw.encode() if server_extra_payload_raw else None
disconnect_reason_raw = os.environ.get("RUDP_DISCONNECT_REASON")
disconnect_reason = disconnect_reason_raw.encode() if disconnect_reason_raw else None
max_fragment_bytes_raw = os.environ.get("RUDP_MAX_FRAGMENT_BYTES")
max_fragment_bytes = int(max_fragment_bytes_raw) if max_fragment_bytes_raw else None
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind(("127.0.0.1", 0))
sock.settimeout(0.02)
transport = CultNetRudpSocketTransportConnection(CultNetRudpSocketTransportOptions(
runtime_id="python-rudp-interop",
socket=sock,
mode=CultNetRudpSocketMode.SERVER,
connection_id=0x55667788,
initial_sequence=100,
resend_delay_ms=25,
max_fragment_bytes=max_fragment_bytes,
))
print(f"PORT {sock.getsockname()[1]}", flush=True)
deadline = time.time() + 5
while time.time() < deadline:
frame = transport.receive_once()
transport.poll_resends()
if frame is None:
time.sleep(0.005)
continue
if frame.channel_id != "schema" or frame.payload != expected_client_payload:
transport.close()
raise SystemExit(f"unexpected RUDP frame: {frame.channel_id!r} {frame.payload!r}")
transport.send("schema", server_payload)
if server_extra_payload is not None:
transport.send("schema", server_extra_payload)
if disconnect_reason is not None:
transport.disconnect(disconnect_reason)
print("OK", flush=True)
ack_deadline = time.time() + 0.25
while time.time() < ack_deadline:
transport.receive_once()
transport.poll_resends()
time.sleep(0.005)
transport.close()
raise SystemExit(0)
transport.close()
raise SystemExit("timed out waiting for RUDP frame")
`;
const pythonRudpClientScript = String.raw`
import socket
import sys
import time
from cultnet_py import (
CultNetRudpSocketMode,
CultNetRudpSocketTransportConnection,
CultNetRudpSocketTransportOptions,
)
remote_port = int(sys.argv[1])
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind(("127.0.0.1", 0))
sock.settimeout(0.02)
transport = CultNetRudpSocketTransportConnection(CultNetRudpSocketTransportOptions(
runtime_id="python-rudp-client-interop",
socket=sock,
mode=CultNetRudpSocketMode.CLIENT,
remote_addr=("127.0.0.1", remote_port),
connection_id=0x66554433,
initial_sequence=1,
resend_delay_ms=25,
))
transport.connect(b"python-join")
sent = False
deadline = time.time() + 5
while time.time() < deadline:
frame = transport.receive_once()
transport.poll_resends()
if transport.connected and not sent:
transport.send("schema", b"python-client-state")
sent = True
if frame is None:
time.sleep(0.005)
continue
if frame.channel_id != "schema" or frame.payload != b"ts-server-state":
transport.close()
raise SystemExit(f"unexpected RUDP frame: {frame.channel_id!r} {frame.payload!r}")
print("OK", flush=True)
transport.close()
raise SystemExit(0)
transport.close()
raise SystemExit("timed out waiting for TypeScript RUDP frame")
`;
const pythonRudpMessagePeerScript = String.raw`
import msgpack
import socket
import time
from cultnet_py import (
CultNetRudpSocketMode,
CultNetRudpSocketTransportConnection,
CultNetRudpSocketTransportOptions,
create_rudp_transport_profile,
)
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind(("127.0.0.1", 0))
sock.settimeout(0.02)
transport = CultNetRudpSocketTransportConnection(CultNetRudpSocketTransportOptions(
runtime_id="python-rudp-message-interop",
socket=sock,
mode=CultNetRudpSocketMode.SERVER,
connection_id=0x55667789,
initial_sequence=100,
resend_delay_ms=25,
))
print(f"PORT {sock.getsockname()[1]}", flush=True)
deadline = time.time() + 5
while time.time() < deadline:
frame = transport.receive_once()
transport.poll_resends()
if frame is None:
time.sleep(0.005)
continue
if frame.channel_id != "schema":
transport.close()
raise SystemExit(f"unexpected RUDP channel: {frame.channel_id!r}")
message = msgpack.unpackb(frame.payload, raw=False)
if message.get("schemaVersion") != "cultnet.schema_catalog_request.v0":
transport.close()
raise SystemExit(f"unexpected schema message: {message!r}")
response = {
"schemaVersion": "cultnet.hello.v0",
"runtimeId": "python-rudp-message-interop",
"runtimeKind": "python",
"supportedDocumentTypes": [],
"supportedMutationContracts": [],
"supportedMessageVersions": ["cultnet.hello.v0", "cultnet.schema_catalog_request.v0"],
"transportProfiles": [create_rudp_transport_profile(
"python-rudp-message-interop",
host="127.0.0.1",
port=sock.getsockname()[1],
)],
"supportsSchemaCatalog": True,
}
transport.send("schema", msgpack.packb(response, use_bin_type=True))
print("OK", flush=True)
ack_deadline = time.time() + 0.25
while time.time() < ack_deadline:
transport.receive_once()
transport.poll_resends()
time.sleep(0.005)
transport.close()
raise SystemExit(0)
transport.close()
raise SystemExit("timed out waiting for RUDP schema-v0 message")
`;
const pythonRudpMessageClientScript = String.raw`
import msgpack
import socket
import sys
import time
from cultnet_py import (
CultNetRudpSocketMode,
CultNetRudpSocketTransportConnection,
CultNetRudpSocketTransportOptions,
)
remote_port = int(sys.argv[1])
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind(("127.0.0.1", 0))
sock.settimeout(0.02)
transport = CultNetRudpSocketTransportConnection(CultNetRudpSocketTransportOptions(
runtime_id="python-rudp-message-client-interop",
socket=sock,
mode=CultNetRudpSocketMode.CLIENT,
remote_addr=("127.0.0.1", remote_port),
connection_id=0x66554434,
initial_sequence=1,
resend_delay_ms=25,
))
transport.connect(b"python-message-join")
sent = False
deadline = time.time() + 5
while time.time() < deadline:
frame = transport.receive_once()
transport.poll_resends()
if transport.connected and not sent:
transport.send("schema", msgpack.packb({
"schemaVersion": "cultnet.schema_catalog_request.v0",
"messageId": "python-ts-schema-message",
"includeSchemaJson": False,
"schemaIds": [],
"kinds": ["wire_message"],
}, use_bin_type=True))
sent = True
if frame is None:
time.sleep(0.005)
continue
if frame.channel_id != "schema":
transport.close()
raise SystemExit(f"unexpected RUDP channel: {frame.channel_id!r}")
message = msgpack.unpackb(frame.payload, raw=False)
if message.get("schemaVersion") != "cultnet.hello.v0":
transport.close()
raise SystemExit(f"unexpected schema message: {message!r}")
if message.get("runtimeId") != "ts-python-rudp-message-server":
transport.close()
raise SystemExit(f"unexpected runtime id: {message!r}")
print("OK", flush=True)
transport.close()
raise SystemExit(0)
transport.close()
raise SystemExit("timed out waiting for TypeScript RUDP schema-v0 message")
`;
test("CultNet TS/Rust/C#/Python peers discover each other and exchange raw state over the shared schema-v0 lane", async (t) => {
logInteropPhase("schema-v0", "build peers");
await buildInteropPeers();
cleanInteropStores([
"rust-peer",
"rust-client-dial",
"ts-peer",
"ts-client-dial",
"ts-rudp-client-dial",
"ts-rust-dial",
"ts-csharp-dial",
"python-peer",
"python-client-dial",
"python-rudp-client-dial",
]);
const discoveryPort = await getFreePort();
const tsPort = await getFreePort();
const rustPort = await getFreePort();
const csharpPort = await getFreePort();
const pythonPort = await getFreePort();
const advertiseHost = findAdvertiseHost();
const servers: RunningServeProcess[] = [];
// Registered before anything is spawned. A peer whose readiness rejects
// would otherwise skip this and strand every peer already running.
t.after(async () => {
await Promise.all(servers.map(stopProcess));
});
logInteropPhase("schema-v0", "start rust peer");
servers.push(await spawnServeProcess("rust", {
command: rustBinaryPath,
args: [
"serve",
"--runtime-id", "rust-peer",
"--runtime-kind", "rust",
"--display-name", "Rust Peer",
"--agent-id", "rust-agent",
"--bind-host", "127.0.0.1",
"--advertise-host", advertiseHost,
"--tcp-port", String(rustPort),
"--discovery-port", String(discoveryPort),
"--discovery-group", discoveryGroup,
"--schema-path", interopSchemaPath,
],
cwd: cultnetRsRoot,
}));
await servers[servers.length - 1].ready;
logInteropPhase("schema-v0", "start typescript peer");
servers.push(await spawnServeProcess("ts", {
command: process.execPath,
args: [
tsPeerScript,
"serve",
"--runtime-id", "ts-peer",
"--runtime-kind", "node",
"--display-name", "TypeScript Peer",
"--agent-id", "ts-agent",
"--bind-host", "127.0.0.1",
"--advertise-host", advertiseHost,
"--tcp-port", String(tsPort),
"--discovery-port", String(discoveryPort),
"--discovery-group", discoveryGroup,
"--schema-path", interopSchemaPath,
],
cwd: cultNetTsRoot,
}));
await servers[servers.length - 1].ready;
logInteropPhase("schema-v0", "start csharp peer");
servers.push(await spawnServeProcess("csharp", {
command: dotnetCommand,
args: [
csharpDllPath,
"serve",
"--runtime-id", "csharp-peer",
"--runtime-kind", "dotnet",
"--display-name", "CSharp Peer",
"--agent-id", "csharp-agent",
"--bind-host", "127.0.0.1",
"--advertise-host", advertiseHost,
"--tcp-port", String(csharpPort),
"--discovery-port", String(discoveryPort),
"--discovery-group", discoveryGroup,
"--schema-path", interopSchemaPath,
],
cwd: cultLibRoot,
}));
await servers[servers.length - 1].ready;
logInteropPhase("schema-v0", "start python peer");
servers.push(await spawnServeProcess("python", {
command: pythonCommand,
args: [
"-m", "cultnet_py.interop_peer",
"serve",
"--runtime-id", "python-peer",
"--runtime-kind", "python",
"--display-name", "Python Peer",
"--agent-id", "python-agent",
"--bind-host", "127.0.0.1",
"--advertise-host", advertiseHost,
"--tcp-port", String(pythonPort),
"--discovery-port", String(discoveryPort),
"--discovery-group", discoveryGroup,
"--schema-path", interopSchemaPath,
],
cwd: cultcachePyRoot,
env: { PYTHONPATH: cultcachePySrc },
}));
await servers[servers.length - 1].ready;
logInteropPhase("schema-v0", "typescript dials rust");
const tsDial = await runJsonCommand("ts-dial", process.execPath, [
tsPeerScript,
"dial",
"--runtime-id", "ts-client",
"--runtime-kind", "node",
"--display-name", "TS Dialer",
"--agent-id", "ts-client-agent",
"--target-host", "127.0.0.1",
"--target-port", String(rustPort),
"--schema-path", interopSchemaPath,
], cultNetTsRoot);
assert.equal(tsDial.remoteHello.runtimeId, "rust-peer");
assert.equal(tsDial.hasInteropSchema, true);
assert.equal(tsDial.retrievedNote.authorRuntimeId, "rust-peer");
assert.ok(tsDial.mutatedNote.tags.includes("decorated:ts-client"));
assert.equal(tsDial.mutationReceipt.accepted, true);
assert.equal(tsDial.fireReceipt.shotsFired, 1);
logInteropPhase("schema-v0", "typescript dials typescript over rudp");
const tsRudpDial = await runJsonCommand("ts-rudp-dial", process.execPath, [
tsPeerScript,
"dial",
"--runtime-id", "ts-rudp-client",
"--runtime-kind", "node",
"--display-name", "TS RUDP Dialer",
"--agent-id", "ts-rudp-client-agent",
"--target-host", "127.0.0.1",
"--target-rudp-port", String(tsPort),
"--schema-path", interopSchemaPath,
], cultNetTsRoot);
assert.equal(tsRudpDial.transport, "rudp");
assert.equal(tsRudpDial.remoteHello.runtimeId, "ts-peer");
assert.equal(tsRudpDial.hasInteropSchema, true);
assert.equal(tsRudpDial.retrievedNote.authorRuntimeId, "ts-peer");
assert.ok(tsRudpDial.mutatedNote.tags.includes("decorated:ts-rudp-client"));
assert.equal(tsRudpDial.fireReceipt.accepted, true);
logInteropPhase("schema-v0", "rust dials csharp");
const rustDial = await runJsonCommand("rust-dial", rustBinaryPath, [
"dial",
"--runtime-id", "rust-client",
"--runtime-kind", "rust",
"--display-name", "Rust Dialer",
"--agent-id", "rust-client-agent",
"--target-host", "127.0.0.1",
"--target-port", String(csharpPort),
"--schema-path", interopSchemaPath,
], cultnetRsRoot);
assert.equal(rustDial.remoteHello.runtimeId, "csharp-peer");
assert.equal(rustDial.hasInteropSchema, true);
assert.equal(rustDial.retrievedNote.authorRuntimeId, "csharp-peer");
assert.ok(rustDial.mutatedNote.tags.includes("decorated:rust-client"));
assert.equal(rustDial.fireReceipt.ammoRemaining, 29);
logInteropPhase("schema-v0", "csharp dials typescript");
const csharpDial = await runJsonCommand("csharp-dial", dotnetCommand, [
csharpDllPath,
"dial",
"--runtime-id", "csharp-client",
"--runtime-kind", "dotnet",
"--display-name", "CSharp Dialer",
"--agent-id", "csharp-client-agent",
"--target-host", "127.0.0.1",
"--target-port", String(tsPort),
"--schema-path", interopSchemaPath,
], cultLibRoot);
assert.equal(csharpDial.remoteHello.runtimeId, "ts-peer");
assert.equal(csharpDial.hasInteropSchema, true);
assert.equal(csharpDial.retrievedNote.authorRuntimeId, "ts-peer");
assert.ok(csharpDial.mutatedNote.tags.includes("decorated:csharp-client"));
assert.equal(csharpDial.fireReceipt.accepted, true);
logInteropPhase("schema-v0", "python dials rust");
const pythonDial = await runJsonCommand("python-dial", pythonCommand, [
"-m", "cultnet_py.interop_peer",
"dial",
"--runtime-id", "python-client",
"--runtime-kind", "python",
"--display-name", "Python Dialer",
"--agent-id", "python-client-agent",
"--target-host", "127.0.0.1",
"--target-port", String(rustPort),
"--schema-path", interopSchemaPath,
], cultcachePyRoot, { PYTHONPATH: cultcachePySrc });
assert.equal(pythonDial.remoteHello.runtimeId, "rust-peer");
assert.equal(pythonDial.hasInteropSchema, true);
assert.equal(pythonDial.retrievedNote.authorRuntimeId, "rust-peer");
assert.ok(pythonDial.mutatedNote.tags.includes("decorated:python-client"));
assert.equal(pythonDial.fireReceipt.accepted, true);
logInteropPhase("schema-v0", "python dials python over rudp");
const pythonRudpDial = await runJsonCommand("python-rudp-dial", pythonCommand, [
"-m", "cultnet_py.interop_peer",
"dial",
"--runtime-id", "python-rudp-client",
"--runtime-kind", "python",
"--display-name", "Python RUDP Dialer",
"--agent-id", "python-rudp-client-agent",
"--target-host", "127.0.0.1",
"--target-rudp-port", String(pythonPort),
"--schema-path", interopSchemaPath,
], cultcachePyRoot, { PYTHONPATH: cultcachePySrc });
assert.equal(pythonRudpDial.transport, "rudp");
assert.equal(pythonRudpDial.remoteHello.runtimeId, "python-peer");
assert.equal(pythonRudpDial.hasInteropSchema, true);
assert.equal(pythonRudpDial.retrievedNote.authorRuntimeId, "python-peer");
assert.ok(pythonRudpDial.mutatedNote.tags.includes("decorated:python-rudp-client"));
assert.equal(pythonRudpDial.fireReceipt.accepted, true);
logInteropPhase("schema-v0", "typescript dials python");
const tsDialPython = await runJsonCommand("ts-dial-python", process.execPath, [
tsPeerScript,
"dial",
"--runtime-id", "ts-python-client",
"--runtime-kind", "node",
"--display-name", "TS Python Dialer",
"--agent-id", "ts-python-client-agent",
"--target-host", "127.0.0.1",
"--target-port", String(pythonPort),
"--schema-path", interopSchemaPath,
], cultNetTsRoot);
assert.equal(tsDialPython.remoteHello.runtimeId, "python-peer");
assert.equal(tsDialPython.hasInteropSchema, true);
assert.equal(tsDialPython.retrievedNote.authorRuntimeId, "python-peer");
assert.ok(tsDialPython.mutatedNote.tags.includes("decorated:ts-python-client"));
assert.equal(tsDialPython.fireReceipt.ammoRemaining, 29);
logInteropPhase("schema-v0", "python wire catalog");
const pythonWireCatalog = await requestCultMeshFromPython(pythonPort, {
schemaVersion: "cultnet.schema_catalog_request.v0",
messageId: "python-wire-catalog",
includeSchemaJson: true,
kinds: ["wire_message"],
});
assert.equal(pythonWireCatalog.schemaVersion, "cultnet.schema_catalog_response.v0");
assert.equal(pythonWireCatalog.messageId, "python-wire-catalog");
const pythonWireVersions = new Set(pythonWireCatalog.schemas.map((schema: any) => schema.schemaVersion));
for (const schemaVersion of [
"cultnet.document_delete.v0",
"cultnet.database_change_raw.v0",
"cultnet.shard_log_response.v0",
"cultnet.simulation_consensus_candidate.v0",
"cultmesh.peer_exchange_response.v0",
]) {
assert.ok(pythonWireVersions.has(schemaVersion), `Python wire catalog should include ${schemaVersion}`);
}
const pythonDeleteDescriptor = pythonWireCatalog.schemas.find((schema: any) => schema.schemaVersion === "cultnet.document_delete.v0");
assert.equal(pythonDeleteDescriptor.kind, "wire_message");
assert.ok(pythonDeleteDescriptor.wireContracts.includes("cultnet.schema.v0"));
assert.ok(String(pythonDeleteDescriptor.schemaJson).includes("cultnet.document_delete.v0"));
const pythonDeleteSchema = JSON.parse(String(pythonDeleteDescriptor.schemaJson));
assert.deepEqual(pythonDeleteSchema.required, ["schemaVersion", "messageId", "schemaId", "recordKey"]);
assert.equal(pythonDeleteSchema.properties.recordKey.type, "string");
logInteropPhase("schema-v0", "python verse catalog");
const pythonVerseCatalog = await requestCultMeshFromPython(pythonPort, {
schemaVersion: "cultmesh.verse_catalog_request.v0",
messageId: "python-verses",
transportVersion: "cultmesh.v0",
});
assert.equal(pythonVerseCatalog.schemaVersion, "cultmesh.verse_catalog_response.v0");
assert.equal(pythonVerseCatalog.messageId, "python-verses");
assert.equal(pythonVerseCatalog.verses[0].verseId, "python-interop");
assert.equal(pythonVerseCatalog.verses[0].compatibility.transportVersion, "cultmesh.v0");
const pythonPeerExchange = await requestCultMeshFromPython(pythonPort, {
schemaVersion: "cultmesh.peer_exchange_request.v0",
messageId: "python-peers",
verseId: "python-interop",
roles: ["read-replica"],
});
assert.equal(pythonPeerExchange.schemaVersion, "cultmesh.peer_exchange_response.v0");
assert.equal(pythonPeerExchange.messageId, "python-peers");
assert.equal(pythonPeerExchange.peers[0].peerId, "python-peer");
assert.ok(pythonPeerExchange.peers[0].roles.includes("read-replica"));
logInteropPhase("schema-v0", "python client helpers");
const pythonCultMeshClient = await runPythonCultMeshClient(pythonPort);
assert.equal(pythonCultMeshClient.verses[0].verseId, "python-interop");
assert.equal(pythonCultMeshClient.verses[0].transportVersion, "cultmesh.v0");
assert.equal(pythonCultMeshClient.peers[0].peerId, "python-peer");
assert.ok(pythonCultMeshClient.peers[0].roles.includes("read-replica"));
const pythonCultNetClient = await runPythonCultNetRawClient(pythonPort);
assert.ok(pythonCultNetClient.wireSchemaCount >= 1);
assert.ok(pythonCultNetClient.snapshotRecordKeys.includes("note:python-peer"));
assert.equal(pythonCultNetClient.shards[0].shardId, "interop");
const pythonCultMeshNodeSync = await runPythonCultMeshNodeSync(pythonPort);
assert.equal(pythonCultMeshNodeSync.synced[0].recordKey, "note:python-peer");
assert.equal(pythonCultMeshNodeSync.note.authorRuntimeId, "python-peer");
const pythonCultMeshNodeEmit = await runPythonCultMeshNodeEmit(pythonPort);
assert.equal(pythonCultMeshNodeEmit.synced[0].recordKey, "note:python-node-emit");
assert.equal(pythonCultMeshNodeEmit.note.authorRuntimeId, "python-node-emit");
assert.equal(pythonCultMeshNodeEmit.note.title, "Python node emitted raw put");
logInteropPhase("schema-v0", "python prediction/subscription");
const pythonPrediction = await runPythonCultMeshPredictionReconcile(pythonPort);
assert.ok(pythonPrediction.reconciledKeys.includes("note:python-node-emit"));
assert.equal(pythonPrediction.note.title, "Python node emitted raw put");
assert.equal(pythonPrediction.note.authorRuntimeId, "python-node-emit");
const pythonSubscription = await runPythonCultNetSubscription(pythonPort);
assert.equal(pythonSubscription.changeKind, "added");
assert.equal(pythonSubscription.recordKey, "note:python-sub-client");
assert.equal(pythonSubscription.subscriptionId, "python-public-sub");
const pythonChange = await requestPythonDatabaseChange(pythonPort, {
schemaVersion: "cultnet.database_subscribe.v0",
messageId: "python-subscribe",
subscriptionId: "python-sub",
schemaIds: [interopNoteSchemaId],
recordKeys: ["note:ts-python-sub"],
includeSnapshot: false,
}, {
schemaVersion: "cultnet.document_put_raw.v0",
messageId: "python-sub-put",
document: {
schemaId: interopNoteSchemaId,
recordKey: "note:ts-python-sub",
storedAt: "2026-06-13T00:00:00Z",
payloadEncoding: "messagepack",
payload: encode([
INTEROP_SCHEMA_VERSION,
"note:ts-python-sub",
"ts-python-sub",
"Subscription note",
"Python streamed this raw change through the CultNet database lane.",
["interop", "subscription"],
]),
sourceRuntimeId: "ts-python-sub",
},
});
assert.equal(pythonChange.schemaVersion, "cultnet.database_change_raw.v0");
assert.equal(pythonChange.subscriptionId, "python-sub");
assert.equal(pythonChange.changeKind, "added");
assert.equal(pythonChange.document.schemaId, interopNoteSchemaId);
assert.equal(pythonChange.document.recordKey, "note:ts-python-sub");
const pythonShard = await requestPythonShardCatalogAndLog(pythonPort, {
schemaVersion: "cultnet.shard_catalog_request.v0",
messageId: "python-shards",
schemaIds: [interopNoteSchemaId],
recordKeys: ["note:ts-python-log"],
}, {
schemaVersion: "cultnet.document_put_raw.v0",
messageId: "python-shard-put",
document: {
schemaId: interopNoteSchemaId,
recordKey: "note:ts-python-log",
storedAt: "2026-06-13T00:00:01Z",
payloadEncoding: "messagepack",
payload: encode([
INTEROP_SCHEMA_VERSION,
"note:ts-python-log",
"ts-python-log",
"Shard log note",
"Python exposes accepted raw puts through the CultNet shard log lane.",
["interop", "shard-log"],
]),
sourceRuntimeId: "ts-python-log",
},
}, {
schemaVersion: "cultnet.shard_log_request.v0",
messageId: "python-log",
shardId: "interop",
shardEpoch: 1,
afterSequence: 0,
});
assert.equal(pythonShard.catalog.schemaVersion, "cultnet.shard_catalog_response.v0");
assert.equal(pythonShard.catalog.messageId, "python-shards");
assert.equal(pythonShard.catalog.shards[0].shardId, "interop");
assert.equal(pythonShard.catalog.shards[0].ownerRuntimeId, "python-peer");
assert.equal(pythonShard.catalog.shards[0].epoch, 1);
assert.equal(pythonShard.log.schemaVersion, "cultnet.shard_log_response.v0");
assert.equal(pythonShard.log.messageId, "python-log");
assert.equal(pythonShard.log.shardId, "interop");
assert.equal(pythonShard.log.shardEpoch, 1);
assert.equal(pythonShard.log.resyncRequired, false);
assert.ok(pythonShard.log.entries.some((entry: any) =>
entry.changeKind === "added"
&& entry.put?.messageId === "python-shard-put"
&& entry.put?.document?.recordKey === "note:ts-python-log"
));
logInteropPhase("schema-v0", "python delete/simulation/witness");
const pythonDelete = await requestPythonDatabaseDelete(pythonPort, {
schemaVersion: "cultnet.database_subscribe.v0",
messageId: "python-delete-subscribe",
subscriptionId: "python-delete-sub",
schemaIds: [interopNoteSchemaId],
recordKeys: ["note:ts-python-delete"],
includeSnapshot: false,
}, {
schemaVersion: "cultnet.document_put_raw.v0",
messageId: "python-delete-put",
document: {
schemaId: interopNoteSchemaId,
recordKey: "note:ts-python-delete",
storedAt: "2026-06-13T00:00:01Z",
payloadEncoding: "messagepack",
payload: encode([
INTEROP_SCHEMA_VERSION,
"note:ts-python-delete",
"ts-python-delete",
"Delete note",
"Python should remove this through the CultNet delete lane.",
["interop", "delete"],
]),
sourceRuntimeId: "ts-python-delete",
},
}, {
schemaVersion: "cultnet.document_delete.v0",
messageId: "python-delete",
schemaId: interopNoteSchemaId,
recordKey: "note:ts-python-delete",
shardId: "interop",
shardEpoch: 1,
}, {
schemaVersion: "cultnet.shard_log_request.v0",
messageId: "python-delete-log",
shardId: "interop",
shardEpoch: 1,
afterSequence: 0,
});
assert.equal(pythonDelete.change.schemaVersion, "cultnet.database_change_raw.v0");
assert.equal(pythonDelete.change.subscriptionId, "python-delete-sub");
assert.equal(pythonDelete.change.changeKind, "removed");
assert.equal(pythonDelete.change.schemaId, interopNoteSchemaId);
assert.equal(pythonDelete.change.recordKey, "note:ts-python-delete");
assert.ok(pythonDelete.log.entries.some((entry: any) =>
entry.changeKind === "removed"
&& entry.delete?.messageId === "python-delete"
&& entry.delete?.schemaId === interopNoteSchemaId
&& entry.delete?.recordKey === "note:ts-python-delete"
&& entry.delete?.shardEpoch === 1
));
const claimHash = computeSimulationClaimHash("frame:42", "subject:python-target", "hit");
const pythonCandidate = await requestPythonSimulationCandidate(pythonPort, {
schemaVersion: "cultnet.simulation_observation.v0",
messageId: "python-observation",
observation: {
witnessRuntimeId: "ts-witness",
shardId: "interop",
shardEpoch: 1,
frame: 42,
subjectId: "python-target",
claimKind: "hit",
claimHash,
claimSummary: "python-target was hit by the TypeScript witness",
weight: 1,
observedAt: "2026-06-13T00:00:02Z",
},
});
assert.equal(pythonCandidate.schemaVersion, "cultnet.simulation_consensus_candidate.v0");
assert.equal(pythonCandidate.messageId, "python-observation");
assert.equal(pythonCandidate.shardId, "interop");
assert.equal(pythonCandidate.shardEpoch, 1);
assert.equal(pythonCandidate.frame, 42);
assert.equal(pythonCandidate.subjectId, "python-target");
assert.equal(pythonCandidate.claimKind, "hit");
assert.equal(pythonCandidate.claimHash, claimHash);
assert.equal(pythonCandidate.witnessCount, 1);
assert.equal(pythonCandidate.supportWeight, 1);
assert.equal(pythonCandidate.totalWeight, 1);
assert.equal(pythonCandidate.hasQuorum, true);
assert.equal(pythonCandidate.confidence, 1);
const simulationFactKey = createSimulationFactKey(pythonCandidate);
const pythonFactSnapshot = await requestPythonSnapshot(pythonPort, {
schemaVersion: "cultnet.snapshot_request.v0",
messageId: "python-simulation-fact-snapshot",
schemaIds: [simulationFactSchemaId],
recordKeys: [simulationFactKey],
});
assert.equal(pythonFactSnapshot.schemaVersion, "cultnet.snapshot_response_raw.v0");
assert.equal(pythonFactSnapshot.documents[0].schemaId, simulationFactSchemaId);
assert.equal(pythonFactSnapshot.documents[0].recordKey, simulationFactKey);
const simulationFactSlots = decode(pythonFactSnapshot.documents[0].payload) as any[];
assert.equal(simulationFactSlots[0], simulationFactKey.slice("simulation:".length));
assert.equal(simulationFactSlots[1], "interop");
assert.equal(simulationFactSlots[3], 42);
assert.equal(simulationFactSlots[4], "python-target");
assert.equal(simulationFactSlots[5], "hit");
assert.equal(simulationFactSlots[6], claimHash);
assert.equal(simulationFactSlots[8], 1);
const pythonWitness = await putAndSnapshotPythonWitnessBundle(pythonPort, {
schemaVersion: "cultnet.document_put_raw.v0",
messageId: "python-witness-put",
document: {
schemaId: witnessArtifactBundleSchemaId,
recordKey: "bundle:ts-python-witness",
storedAt: "2026-06-13T00:00:03Z",
payloadEncoding: "messagepack",
payload: encode([
"bundle:ts-python-witness",
"interop-proof",
"2026-06-13T00:00:03Z",
{
documentType: "cultnet.interop-note",
subjectId: "note:python-peer",
schemaVersion: INTEROP_SCHEMA_VERSION,
schemaId: interopNoteSchemaId,
},
[{ role: "payload", schemaId: interopNoteSchemaId, schemaVersion: INTEROP_SCHEMA_VERSION }],
[{ role: "log", uri: "cultcache://bundle:ts-python-witness/log", mediaType: "text/plain" }],
[{ stage: "roundtrip", startedAt: "2026-06-13T00:00:03Z", completedAt: "2026-06-13T00:00:04Z", latencyMs: 1 }],
{ pipelineId: "interop", runId: "ts-python-witness", runtimeId: "ts-witness", agentId: "ts-agent" },
]),
sourceRuntimeId: "ts-witness",
},
});
assert.equal(pythonWitness.catalog.schemaVersion, "cultnet.schema_catalog_response.v0");
assert.equal(pythonWitness.catalog.schemas[0].schemaId, witnessArtifactBundleSchemaId);
assert.equal(pythonWitness.catalog.schemas[0].documentType, "cultnet.witness_artifact_bundle");
assert.ok(String(pythonWitness.catalog.schemas[0].schemaJson).includes("CultNet Witness Artifact Bundle"));
assert.equal(pythonWitness.snapshot.schemaVersion, "cultnet.snapshot_response_raw.v0");
assert.equal(pythonWitness.snapshot.documents[0].schemaId, witnessArtifactBundleSchemaId);
assert.equal(pythonWitness.snapshot.documents[0].recordKey, "bundle:ts-python-witness");
const witnessSlots = decode(pythonWitness.snapshot.documents[0].payload) as any[];
assert.equal(witnessSlots[0], "bundle:ts-python-witness");
assert.equal(witnessSlots[1], "interop-proof");
assert.equal(witnessSlots[3].subjectId, "note:python-peer");
assert.equal(witnessSlots[7].runtimeId, "ts-witness");
const expectedPeers = ["csharp-peer", "python-peer", "rust-peer", "ts-peer"];
logInteropPhase("schema-v0", "typescript discovery probe");
await expectProbePeers("ts-probe", process.execPath, [
tsPeerScript,
"probe",
"--runtime-id", "ts-prober",
"--discovery-port", String(discoveryPort),
"--discovery-group", discoveryGroup,
"--timeout-ms", "3000",
], cultNetTsRoot, {}, expectedPeers);
logInteropPhase("schema-v0", "rust discovery probe");
await expectProbePeers("rust-probe", rustBinaryPath, [
"probe",
"--runtime-id", "rust-prober",
"--discovery-port", String(discoveryPort),
"--discovery-group", discoveryGroup,
"--timeout-ms", "3000",
], cultnetRsRoot, {}, expectedPeers);
logInteropPhase("schema-v0", "csharp discovery probe");
await expectProbePeers("csharp-probe", dotnetCommand, [
csharpDllPath,
"probe",
"--runtime-id", "csharp-prober",
"--discovery-port", String(discoveryPort),
"--discovery-group", discoveryGroup,
"--timeout-ms", "3000",
], cultLibRoot, {}, expectedPeers);
logInteropPhase("schema-v0", "python discovery probe");
await expectProbePeers("python-probe", pythonCommand, [
"-m", "cultnet_py.interop_peer",
"probe",
"--runtime-id", "python-prober",
"--discovery-port", String(discoveryPort),
"--discovery-group", discoveryGroup,
"--timeout-ms", "3000",
], cultcachePyRoot, { PYTHONPATH: cultcachePySrc }, expectedPeers);
});
test("CultNet TypeScript and Rust full interop peers exchange schema-v0 over RUDP", async (t) => {
logInteropPhase("ts-rust-full-rudp", "build peers");
await buildRustInteropPeer();
cleanInteropStores([
"rust-rudp-peer",
"rust-rudp-client-dial",
"ts-rust-rudp-peer",
"ts-rust-rudp-client-dial",
]);
const rustTcpPort = await getFreePort();
const rustRudpPort = await getFreePort();
const tsTcpPort = await getFreePort();
const tsRudpPort = await getFreePort();
const discoveryPort = await getFreePort();
const advertiseHost = findAdvertiseHost();
const servers: RunningServeProcess[] = [];
// Registered before anything is spawned. A peer whose readiness rejects
// would otherwise skip this and strand every peer already running.
t.after(async () => {
await Promise.all(servers.map(stopProcess));
});
servers.push(await spawnServeProcess("rust-rudp", {
command: rustBinaryPath,
args: [
"serve",
"--runtime-id", "rust-rudp-peer",
"--runtime-kind", "rust",
"--display-name", "Rust RUDP Peer",
"--agent-id", "rust-rudp-agent",
"--bind-host", "127.0.0.1",
"--advertise-host", advertiseHost,
"--tcp-port", String(rustTcpPort),
"--rudp-port", String(rustRudpPort),
"--discovery-port", String(discoveryPort),
"--discovery-group", discoveryGroup,
"--schema-path", interopSchemaPath,
],
cwd: cultnetRsRoot,
}));
await servers[servers.length - 1].ready;
servers.push(await spawnServeProcess("ts-rust-rudp", {
command: process.execPath,
args: [
tsPeerScript,
"serve",
"--runtime-id", "ts-rust-rudp-peer",
"--runtime-kind", "node",
"--display-name", "TypeScript Rust RUDP Peer",
"--agent-id", "ts-rust-rudp-agent",
"--bind-host", "127.0.0.1",
"--advertise-host", advertiseHost,
"--tcp-port", String(tsTcpPort),
"--rudp-port", String(tsRudpPort),
"--discovery-port", String(discoveryPort),
"--discovery-group", discoveryGroup,
"--schema-path", interopSchemaPath,
],
cwd: cultNetTsRoot,
}));
await servers[servers.length - 1].ready;
logInteropPhase("ts-rust-full-rudp", "typescript dials rust over rudp");
const tsRudpDial = await runJsonCommand("ts-rust-rudp-dial", process.execPath, [
tsPeerScript,
"dial",
"--runtime-id", "ts-rust-rudp-client",
"--runtime-kind", "node",
"--display-name", "TS Rust RUDP Dialer",
"--agent-id", "ts-rust-rudp-client-agent",
"--target-host", "127.0.0.1",
"--target-rudp-port", String(rustRudpPort),
"--schema-path", interopSchemaPath,
], cultNetTsRoot);
assert.equal(tsRudpDial.transport, "rudp");
assert.equal(tsRudpDial.remoteHello.runtimeId, "rust-rudp-peer");
assert.equal(tsRudpDial.hasInteropSchema, true);
assert.equal(tsRudpDial.retrievedNote.authorRuntimeId, "rust-rudp-peer");
assert.ok(tsRudpDial.mutatedNote.tags.includes("decorated:ts-rust-rudp-client"));
assert.equal(tsRudpDial.fireReceipt.accepted, true);
logInteropPhase("ts-rust-full-rudp", "rust dials typescript over rudp");
const rustRudpDial = await runJsonCommand("rust-rudp-dial", rustBinaryPath, [
"dial",
"--runtime-id", "rust-rudp-client",
"--runtime-kind", "rust",
"--display-name", "Rust RUDP Dialer",
"--agent-id", "rust-rudp-client-agent",
"--target-host", "127.0.0.1",
"--target-rudp-port", String(tsRudpPort),
"--schema-path", interopSchemaPath,
], cultnetRsRoot);
assert.equal(rustRudpDial.transport, "rudp");
assert.equal(rustRudpDial.remoteHello.runtimeId, "ts-rust-rudp-peer");
assert.equal(rustRudpDial.hasInteropSchema, true);
assert.equal(rustRudpDial.retrievedNote.authorRuntimeId, "ts-rust-rudp-peer");
assert.ok(rustRudpDial.mutatedNote.tags.includes("decorated:rust-rudp-client"));
assert.equal(rustRudpDial.fireReceipt.accepted, true);
});
test("CultNet TypeScript and Python exchange schema frames over the shared RUDP socket transport", async () => {
const pythonPeer = spawnPythonRudpPeer();
const clientSocket = await bindUdpSocket();
let client: CultNetRudpSocketTransportConnection | undefined;
try {
const pythonPort = await pythonPeer.ready;
client = new CultNetRudpSocketTransportConnection({
runtimeId: "ts-rudp-interop",
socket: clientSocket,
mode: "client",
remoteHost: "127.0.0.1",
remotePort: pythonPort,
connectionId: 0x55667788,
initialSequence: 1,
resendDelayMs: 25,
resendPollMs: 5,
});
const receivedFrame = once(client, "frame");
client.connect(Buffer.from("ts-join"));
await waitFor(() => client?.connected === true, "TypeScript RUDP client to complete Python handshake");
client.send("schema", Buffer.from("client-state"));
const [frame] = await withTimeout(receivedFrame, 2_000, "Python RUDP response frame");
assert.equal(frame.channelId, "schema");
assert.deepEqual(Buffer.from(frame.payload), Buffer.from("python-state"));
const [exitCode] = await withTimeout(once(pythonPeer.child, "exit"), 2_000, "Python RUDP peer exit");
assert.equal(exitCode, 0, pythonPeer.stderr.join(""));
} finally {
if (client) {
client.close();
} else {
clientSocket.close();
}
if (pythonPeer.child.exitCode === null && !pythonPeer.child.killed) {
pythonPeer.child.kill("SIGTERM");
await once(pythonPeer.child, "exit").catch(() => undefined);
}
}
});
test("CultNet Python and TypeScript exchange schema frames when Python dials a TypeScript RUDP server", async () => {
const serverSocket = await bindUdpSocket();
const server = new CultNetRudpSocketTransportConnection({
runtimeId: "ts-rudp-server-interop",
socket: serverSocket,
mode: "server",
connectionId: 0x66554433,
initialSequence: 100,
resendDelayMs: 25,
resendPollMs: 5,
});
const pythonClient = spawnPythonRudpClient(udpSocketPort(serverSocket));
try {
const receivedFrame = once(server, "frame");
const [frame] = await withTimeout(receivedFrame, 5_000, "Python RUDP client frame");
assert.equal(frame.channelId, "schema");
assert.deepEqual(Buffer.from(frame.payload), Buffer.from("python-client-state"));
server.send("schema", Buffer.from("ts-server-state"));
const [exitCode] = await withTimeout(once(pythonClient.child, "exit"), 5_000, "Python RUDP client exit");
assert.equal(exitCode, 0, pythonClient.stderr.join(""));
} finally {
server.close();
closeUdpSocket(serverSocket);
if (pythonClient.child.exitCode === null && !pythonClient.child.killed) {
pythonClient.child.kill("SIGTERM");
await once(pythonClient.child, "exit").catch(() => undefined);
}
}
});
test("CultNet TypeScript and Python exchange schema-v0 MessagePack messages over RUDP", async () => {
const pythonPeer = spawnPythonRudpMessagePeer();
const clientSocket = await bindUdpSocket();
let client: CultNetRudpSocketTransportConnection | undefined;
try {
const pythonPort = await pythonPeer.ready;
client = new CultNetRudpSocketTransportConnection({
runtimeId: "ts-python-rudp-message-interop",
socket: clientSocket,
mode: "client",
remoteHost: "127.0.0.1",
remotePort: pythonPort,
connectionId: 0x55667789,
initialSequence: 1,
resendDelayMs: 25,
resendPollMs: 5,
});
const receivedFrame = once(client, "frame");
client.connect(Buffer.from("ts-python-message-join"));
await waitFor(() => client?.connected === true, "TypeScript RUDP client to complete Python message handshake");
client.send("schema", Buffer.from(encode({
schemaVersion: "cultnet.schema_catalog_request.v0",
messageId: "ts-python-schema-message",
includeSchemaJson: false,
schemaIds: [],
kinds: ["wire_message"],
})));
const [frame] = await withTimeout(receivedFrame, 2_000, "Python RUDP schema-v0 response frame");
assert.equal(frame.channelId, "schema");
const message = decode(frame.payload) as {
schemaVersion?: string;
runtimeId?: string;
supportsSchemaCatalog?: boolean;
transportProfiles?: Array<{ transports?: Array<{ protocol?: string; reconnectPolicy?: unknown; channels?: Array<{ channelId?: string }> }> }>;
};
assert.equal(message.schemaVersion, "cultnet.hello.v0");
assert.equal(message.runtimeId, "python-rudp-message-interop");
assert.equal(message.supportsSchemaCatalog, true);
assert.equal(message.transportProfiles?.[0]?.transports?.[0]?.protocol, "rudp");
assertRudpReconnectPolicy("Python", message.transportProfiles?.[0]?.transports?.[0]?.reconnectPolicy);
assert.equal(message.transportProfiles?.[0]?.transports?.[0]?.channels?.[0]?.channelId, "schema");
const [exitCode] = await withTimeout(once(pythonPeer.child, "exit"), 2_000, "Python RUDP message peer exit");
assert.equal(exitCode, 0, pythonPeer.stderr.join(""));
} finally {
if (client) {
client.close();
} else {
clientSocket.close();
}
if (pythonPeer.child.exitCode === null && !pythonPeer.child.killed) {
pythonPeer.child.kill("SIGTERM");
await once(pythonPeer.child, "exit").catch(() => undefined);
}
}
});
test("CultNet Python and TypeScript exchange schema-v0 MessagePack messages over RUDP when Python dials", async () => {
const serverSocket = await bindUdpSocket();
const server = new CultNetRudpSocketTransportConnection({
runtimeId: "ts-python-rudp-message-server",
socket: serverSocket,
mode: "server",
connectionId: 0x66554434,
initialSequence: 100,
resendDelayMs: 25,
resendPollMs: 5,
});
const pythonClient = spawnPythonRudpMessageClient(udpSocketPort(serverSocket));
try {
const receivedFrame = once(server, "frame");
const [frame] = await withTimeout(receivedFrame, 5_000, "Python RUDP schema-v0 client frame");
assert.equal(frame.channelId, "schema");
const message = decode(frame.payload) as {
schemaVersion?: string;
messageId?: string;
kinds?: string[];
};
assert.equal(message.schemaVersion, "cultnet.schema_catalog_request.v0");
assert.equal(message.messageId, "python-ts-schema-message");
assert.deepEqual(message.kinds, ["wire_message"]);
server.send("schema", Buffer.from(encode({
schemaVersion: "cultnet.hello.v0",
runtimeId: "ts-python-rudp-message-server",
runtimeKind: "typescript",
supportedDocumentTypes: [],
supportedMutationContracts: [],
supportedMessageVersions: ["cultnet.hello.v0", "cultnet.schema_catalog_request.v0"],
transportProfiles: [],
supportsSchemaCatalog: true,
})));
const [exitCode] = await withTimeout(once(pythonClient.child, "exit"), 5_000, "Python RUDP message client exit");
assert.equal(exitCode, 0, pythonClient.stderr.join(""));
} finally {
server.close();
closeUdpSocket(serverSocket);
if (pythonClient.child.exitCode === null && !pythonClient.child.killed) {
pythonClient.child.kill("SIGTERM");
await once(pythonClient.child, "exit").catch(() => undefined);
}
}
});
test("CultNet TypeScript and Rust exchange schema frames over the shared RUDP socket transport", async () => {
await buildRustInteropPeer();
const rustPeer = spawnRustRudpServer();
const clientSocket = await bindUdpSocket();
let client: CultNetRudpSocketTransportConnection | undefined;
try {
const rustPort = await rustPeer.ready;
client = new CultNetRudpSocketTransportConnection({
runtimeId: "ts-rust-rudp-interop",
socket: clientSocket,
mode: "client",
remoteHost: "127.0.0.1",
remotePort: rustPort,
connectionId: 0x22446688,
initialSequence: 1,
resendDelayMs: 25,
resendPollMs: 5,
});
const receivedFrame = once(client, "frame");
client.connect(Buffer.from("ts-rust-join"));
await waitFor(() => client?.connected === true, "TypeScript RUDP client to complete Rust handshake");
client.send("schema", Buffer.from("ts-rust-client-state"));
const [frame] = await withTimeout(receivedFrame, 2_000, "Rust RUDP response frame");
assert.equal(frame.channelId, "schema");
assert.deepEqual(Buffer.from(frame.payload), Buffer.from("rust-server-state"));
const [exitCode] = await withTimeout(once(rustPeer.child, "exit"), 2_000, "Rust RUDP peer exit");
assert.equal(exitCode, 0, rustPeer.stderr.join(""));
} finally {
if (client) {
client.close();
} else {
clientSocket.close();
}
if (rustPeer.child.exitCode === null && !rustPeer.child.killed) {
rustPeer.child.kill("SIGTERM");
await once(rustPeer.child, "exit").catch(() => undefined);
}
}
});
test("CultNet Rust and TypeScript exchange schema frames when Rust dials a TypeScript RUDP server", async () => {
await buildRustInteropPeer();
const serverSocket = await bindUdpSocket();
const server = new CultNetRudpSocketTransportConnection({
runtimeId: "ts-rust-rudp-server-interop",
socket: serverSocket,
mode: "server",
connectionId: 0x88664422,
initialSequence: 100,
resendDelayMs: 25,
resendPollMs: 5,
});
const rustClient = spawnRustRudpClient(udpSocketPort(serverSocket));
try {
const receivedFrame = once(server, "frame");
const [frame] = await withTimeout(receivedFrame, 2_000, "Rust RUDP client frame");
assert.equal(frame.channelId, "schema");
assert.deepEqual(Buffer.from(frame.payload), Buffer.from("rust-client-state"));
server.send("schema", Buffer.from("ts-rust-server-state"));
const [exitCode] = await withTimeout(once(rustClient.child, "exit"), 2_000, "Rust RUDP client exit");
assert.equal(exitCode, 0, rustClient.stderr.join(""));
} finally {
server.close();
closeUdpSocket(serverSocket);
if (rustClient.child.exitCode === null && !rustClient.child.killed) {
rustClient.child.kill("SIGTERM");
await once(rustClient.child, "exit").catch(() => undefined);
}
}
});
test("CultNet TypeScript and Rust exchange schema-v0 MessagePack messages over RUDP", async () => {
await buildRustInteropPeer();
const rustPeer = spawnRustRudpMessageServer();
const clientSocket = await bindUdpSocket();
let client: CultNetRudpSocketTransportConnection | undefined;
try {
const rustPort = await rustPeer.ready;
client = new CultNetRudpSocketTransportConnection({
runtimeId: "ts-rust-rudp-message-interop",
socket: clientSocket,
mode: "client",
remoteHost: "127.0.0.1",
remotePort: rustPort,
connectionId: 0x22446689,
initialSequence: 1,
resendDelayMs: 25,
resendPollMs: 5,
});
const receivedFrame = once(client, "frame");
client.connect(Buffer.from("ts-rust-message-join"));
await waitFor(() => client?.connected === true, "TypeScript RUDP client to complete Rust message handshake");
client.send("schema", Buffer.from(encode({
schemaVersion: "cultnet.schema_catalog_request.v0",
messageId: "ts-rust-schema-message",
includeSchemaJson: false,
schemaIds: [],
kinds: ["wire_message"],
})));
const [frame] = await withTimeout(receivedFrame, 2_000, "Rust RUDP schema-v0 response frame");
assert.equal(frame.channelId, "schema");
const message = decode(frame.payload) as {
schemaVersion?: string;
runtimeId?: string;
supportsSchemaCatalog?: boolean;
transportProfiles?: Array<{ transports?: Array<{ protocol?: string; reconnectPolicy?: unknown; channels?: Array<{ channelId?: string }> }> }>;
};
assert.equal(message.schemaVersion, "cultnet.hello.v0");
assert.equal(message.runtimeId, "rust-rudp-message-interop");
assert.equal(message.supportsSchemaCatalog, true);
assert.equal(message.transportProfiles?.[0]?.transports?.[0]?.protocol, "rudp");
assertRudpReconnectPolicy("Rust", message.transportProfiles?.[0]?.transports?.[0]?.reconnectPolicy);
assert.equal(message.transportProfiles?.[0]?.transports?.[0]?.channels?.[0]?.channelId, "schema");
const [exitCode] = await withTimeout(once(rustPeer.child, "exit"), 2_000, "Rust RUDP message peer exit");
assert.equal(exitCode, 0, rustPeer.stderr.join(""));
} finally {
if (client) {
client.close();
} else {
clientSocket.close();
}
if (rustPeer.child.exitCode === null && !rustPeer.child.killed) {
rustPeer.child.kill("SIGTERM");
await once(rustPeer.child, "exit").catch(() => undefined);
}
}
});
test("CultNet Rust and TypeScript exchange schema-v0 MessagePack messages over RUDP when Rust dials", async () => {
await buildRustInteropPeer();
const serverSocket = await bindUdpSocket();
const server = new CultNetRudpSocketTransportConnection({
runtimeId: "ts-rust-rudp-message-server",
socket: serverSocket,
mode: "server",
connectionId: 0x88664423,
initialSequence: 100,
resendDelayMs: 25,
resendPollMs: 5,
});
const rustClient = spawnRustRudpMessageClient(udpSocketPort(serverSocket));
try {
const receivedFrame = once(server, "frame");
const [frame] = await withTimeout(receivedFrame, 2_000, "Rust RUDP schema-v0 client frame");
assert.equal(frame.channelId, "schema");
const message = decode(frame.payload) as {
schemaVersion?: string;
messageId?: string;
kinds?: string[];
};
assert.equal(message.schemaVersion, "cultnet.schema_catalog_request.v0");
assert.equal(message.messageId, "rust-ts-schema-message");
assert.deepEqual(message.kinds, ["wire_message"]);
server.send("schema", Buffer.from(encode({
schemaVersion: "cultnet.hello.v0",
runtimeId: "ts-rust-rudp-message-server",
runtimeKind: "typescript",
supportedDocumentTypes: [],
supportedMutationContracts: [],
supportedMessageVersions: ["cultnet.hello.v0", "cultnet.schema_catalog_request.v0"],
transportProfiles: [],
supportsSchemaCatalog: true,
})));
const [exitCode] = await withTimeout(once(rustClient.child, "exit"), 2_000, "Rust RUDP message client exit");
assert.equal(exitCode, 0, rustClient.stderr.join(""));
} finally {
server.close();
closeUdpSocket(serverSocket);
if (rustClient.child.exitCode === null && !rustClient.child.killed) {
rustClient.child.kill("SIGTERM");
await once(rustClient.child, "exit").catch(() => undefined);
}
}
});
test("CultNet TypeScript and C# exchange schema frames over the shared RUDP socket transport", async () => {
await buildCSharpInteropPeer();
const csharpPeer = spawnCSharpRudpServer();
const clientSocket = await bindUdpSocket();
let client: CultNetRudpSocketTransportConnection | undefined;
try {
const csharpPort = await csharpPeer.ready;
client = new CultNetRudpSocketTransportConnection({
runtimeId: "ts-csharp-rudp-interop",
socket: clientSocket,
mode: "client",
remoteHost: "127.0.0.1",
remotePort: csharpPort,
connectionId: 0x33557799,
initialSequence: 1,
resendDelayMs: 25,
resendPollMs: 5,
});
const receivedFrame = once(client, "frame");
client.connect(Buffer.from("ts-csharp-join"));
await waitFor(() => client?.connected === true, "TypeScript RUDP client to complete C# handshake");
client.send("schema", Buffer.from("ts-csharp-client-state"));
const [frame] = await withTimeout(receivedFrame, 2_000, "C# RUDP response frame");
assert.equal(frame.channelId, "schema");
assert.deepEqual(Buffer.from(frame.payload), Buffer.from("csharp-server-state"));
const [exitCode] = await withTimeout(once(csharpPeer.child, "exit"), 2_000, "C# RUDP peer exit");
assert.equal(exitCode, 0, csharpPeer.stderr.join(""));
} finally {
if (client) {
client.close();
} else {
clientSocket.close();
}
if (csharpPeer.child.exitCode === null && !csharpPeer.child.killed) {
csharpPeer.child.kill("SIGTERM");
await once(csharpPeer.child, "exit").catch(() => undefined);
}
}
});
test("CultNet C# and TypeScript exchange schema frames when C# dials a TypeScript RUDP server", async () => {
await buildCSharpInteropPeer();
const serverSocket = await bindUdpSocket();
const server = new CultNetRudpSocketTransportConnection({
runtimeId: "ts-csharp-rudp-server-interop",
socket: serverSocket,
mode: "server",
connectionId: 0x99775533,
initialSequence: 100,
resendDelayMs: 25,
resendPollMs: 5,
});
const csharpClient = spawnCSharpRudpClient(udpSocketPort(serverSocket));
try {
const receivedFrame = once(server, "frame");
const [frame] = await withTimeout(receivedFrame, 2_000, "C# RUDP client frame");
assert.equal(frame.channelId, "schema");
assert.deepEqual(Buffer.from(frame.payload), Buffer.from("csharp-client-state"));
server.send("schema", Buffer.from("ts-csharp-server-state"));
const [exitCode] = await withTimeout(once(csharpClient.child, "exit"), 2_000, "C# RUDP client exit");
assert.equal(exitCode, 0, csharpClient.stderr.join(""));
} finally {
server.close();
closeUdpSocket(serverSocket);
if (csharpClient.child.exitCode === null && !csharpClient.child.killed) {
csharpClient.child.kill("SIGTERM");
await once(csharpClient.child, "exit").catch(() => undefined);
}
}
});
test("CultNet TypeScript and C# exchange schema-v0 MessagePack messages over RUDP", async () => {
await buildCSharpInteropPeer();
const csharpPeer = spawnCSharpRudpMessageServer();
const clientSocket = await bindUdpSocket();
let client: CultNetRudpSocketTransportConnection | undefined;
try {
const csharpPort = await csharpPeer.ready;
client = new CultNetRudpSocketTransportConnection({
runtimeId: "ts-csharp-rudp-message-interop",
socket: clientSocket,
mode: "client",
remoteHost: "127.0.0.1",
remotePort: csharpPort,
connectionId: 0x3355779a,
initialSequence: 1,
resendDelayMs: 25,
resendPollMs: 5,
});
const receivedFrame = once(client, "frame");
client.connect(Buffer.from("ts-csharp-message-join"));
await waitFor(() => client?.connected === true, "TypeScript RUDP client to complete C# message handshake");
client.send("schema", Buffer.from(encode({
schemaVersion: "cultnet.schema_catalog_request.v0",
messageId: "ts-csharp-schema-message",
includeSchemaJson: false,
schemaIds: [],
kinds: ["wire_message"],
})));
const [frame] = await withTimeout(receivedFrame, 2_000, "C# RUDP schema-v0 response frame");
assert.equal(frame.channelId, "schema");
const message = decode(frame.payload) as {
schemaVersion?: string;
runtimeId?: string;
supportsSchemaCatalog?: boolean;
transportProfiles?: Array<{ transports?: Array<{ protocol?: string; reconnectPolicy?: unknown; channels?: Array<{ channelId?: string }> }> }>;
};
assert.equal(message.schemaVersion, "cultnet.hello.v0");
assert.equal(message.runtimeId, "csharp-rudp-message-interop");
assert.equal(message.supportsSchemaCatalog, true);
assert.equal(message.transportProfiles?.[0]?.transports?.[0]?.protocol, "rudp");
assertRudpReconnectPolicy("C#", message.transportProfiles?.[0]?.transports?.[0]?.reconnectPolicy);
assert.equal(message.transportProfiles?.[0]?.transports?.[0]?.channels?.[0]?.channelId, "schema");
const [exitCode] = await withTimeout(once(csharpPeer.child, "exit"), 2_000, "C# RUDP message peer exit");
assert.equal(exitCode, 0, csharpPeer.stderr.join(""));
} finally {
if (client) {
client.close();
} else {
clientSocket.close();
}
if (csharpPeer.child.exitCode === null && !csharpPeer.child.killed) {
csharpPeer.child.kill("SIGTERM");
await once(csharpPeer.child, "exit").catch(() => undefined);
}
}
});
test("CultNet C# and TypeScript exchange schema-v0 MessagePack messages over RUDP when C# dials", async () => {
await buildCSharpInteropPeer();
const serverSocket = await bindUdpSocket();
const server = new CultNetRudpSocketTransportConnection({
runtimeId: "ts-csharp-rudp-message-server",
socket: serverSocket,
mode: "server",
connectionId: 0x99775534,
initialSequence: 100,
resendDelayMs: 25,
resendPollMs: 5,
});
const csharpClient = spawnCSharpRudpMessageClient(udpSocketPort(serverSocket));
try {
const receivedFrame = once(server, "frame");
const [frame] = await withTimeout(receivedFrame, 2_000, "C# RUDP schema-v0 client frame");
assert.equal(frame.channelId, "schema");
const message = decode(frame.payload) as {
schemaVersion?: string;
messageId?: string;
kinds?: string[];
};
assert.equal(message.schemaVersion, "cultnet.schema_catalog_request.v0");
assert.equal(message.messageId, "csharp-ts-schema-message");
assert.deepEqual(message.kinds, ["wire_message"]);
server.send("schema", Buffer.from(encode({
schemaVersion: "cultnet.hello.v0",
runtimeId: "ts-csharp-rudp-message-server",
runtimeKind: "typescript",
supportedDocumentTypes: [],
supportedMutationContracts: [],
supportedMessageVersions: ["cultnet.hello.v0", "cultnet.schema_catalog_request.v0"],
transportProfiles: [],
supportsSchemaCatalog: true,
})));
const [exitCode] = await withTimeout(once(csharpClient.child, "exit"), 2_000, "C# RUDP message client exit");
assert.equal(exitCode, 0, csharpClient.stderr.join(""));
} finally {
server.close();
closeUdpSocket(serverSocket);
if (csharpClient.child.exitCode === null && !csharpClient.child.killed) {
csharpClient.child.kill("SIGTERM");
await once(csharpClient.child, "exit").catch(() => undefined);
}
}
});
test("CultNet TypeScript and Kotlin exchange schema frames over the shared RUDP socket transport", async () => {
await buildKotlinInteropPeer();
const kotlinPeer = spawnKotlinRudpServer();
const clientSocket = await bindUdpSocket();
let client: CultNetRudpSocketTransportConnection | undefined;
try {
const kotlinPort = await kotlinPeer.ready;
client = new CultNetRudpSocketTransportConnection({
runtimeId: "ts-kotlin-rudp-interop",
socket: clientSocket,
mode: "client",
remoteHost: "127.0.0.1",
remotePort: kotlinPort,
connectionId: 0x446688aa,
initialSequence: 1,
resendDelayMs: 25,
resendPollMs: 5,
});
const receivedFrame = once(client, "frame");
client.connect(Buffer.from("ts-kotlin-join"));
await waitFor(() => client?.connected === true, "TypeScript RUDP client to complete Kotlin handshake");
client.send("schema", Buffer.from("ts-kotlin-client-state"));
const [frame] = await withTimeout(receivedFrame, 2_000, "Kotlin RUDP response frame");
assert.equal(frame.channelId, "schema");
assert.deepEqual(Buffer.from(frame.payload), Buffer.from("kotlin-server-state"));
const [exitCode] = await withTimeout(once(kotlinPeer.child, "exit"), 2_000, "Kotlin RUDP peer exit");
assert.equal(exitCode, 0, kotlinPeer.stderr.join(""));
} finally {
if (client) {
client.close();
} else {
clientSocket.close();
}
if (kotlinPeer.child.exitCode === null && !kotlinPeer.child.killed) {
kotlinPeer.child.kill("SIGTERM");
await once(kotlinPeer.child, "exit").catch(() => undefined);
}
}
});
test("CultNet Kotlin and TypeScript exchange schema frames when Kotlin dials a TypeScript RUDP server", async () => {
await buildKotlinInteropPeer();
const serverSocket = await bindUdpSocket();
const server = new CultNetRudpSocketTransportConnection({
runtimeId: "ts-kotlin-rudp-server-interop",
socket: serverSocket,
mode: "server",
connectionId: 0xaa886644,
initialSequence: 100,
resendDelayMs: 25,
resendPollMs: 5,
});
const kotlinClient = spawnKotlinRudpClient(udpSocketPort(serverSocket));
try {
const receivedFrame = once(server, "frame");
const [frame] = await withTimeout(receivedFrame, 2_000, "Kotlin RUDP client frame");
assert.equal(frame.channelId, "schema");
assert.deepEqual(Buffer.from(frame.payload), Buffer.from("kotlin-client-state"));
server.send("schema", Buffer.from("ts-kotlin-server-state"));
const [exitCode] = await withTimeout(once(kotlinClient.child, "exit"), 2_000, "Kotlin RUDP client exit");
assert.equal(exitCode, 0, kotlinClient.stderr.join(""));
} finally {
server.close();
closeUdpSocket(serverSocket);
if (kotlinClient.child.exitCode === null && !kotlinClient.child.killed) {
kotlinClient.child.kill("SIGTERM");
await once(kotlinClient.child, "exit").catch(() => undefined);
}
}
});
test("CultNet TypeScript and Kotlin exchange schema-v0 MessagePack messages over RUDP", async () => {
await buildKotlinInteropPeer();
const kotlinPeer = spawnKotlinRudpMessageServer();
const clientSocket = await bindUdpSocket();
let client: CultNetRudpSocketTransportConnection | undefined;
try {
const kotlinPort = await kotlinPeer.ready;
client = new CultNetRudpSocketTransportConnection({
runtimeId: "ts-kotlin-rudp-message-interop",
socket: clientSocket,
mode: "client",
remoteHost: "127.0.0.1",
remotePort: kotlinPort,
connectionId: 0x446688ab,
initialSequence: 1,
resendDelayMs: 25,
resendPollMs: 5,
});
const receivedFrame = once(client, "frame");
client.connect(Buffer.from("ts-kotlin-message-join"));
await waitFor(() => client?.connected === true, "TypeScript RUDP client to complete Kotlin message handshake");
client.send("schema", Buffer.from(encode({
schemaVersion: "cultnet.schema_catalog_request.v0",
messageId: "ts-kotlin-schema-message",
includeSchemaJson: false,
schemaIds: [],
kinds: ["wire_message"],
})));
const [frame] = await withTimeout(receivedFrame, 2_000, "Kotlin RUDP schema-v0 response frame");
assert.equal(frame.channelId, "schema");
const message = decode(frame.payload) as {
schemaVersion?: string;
runtimeId?: string;
supportsSchemaCatalog?: boolean;
transportProfiles?: Array<{ transports?: Array<{ protocol?: string; reconnectPolicy?: unknown; channels?: Array<{ channelId?: string }> }> }>;
};
assert.equal(message.schemaVersion, "cultnet.hello.v0");
assert.equal(message.runtimeId, "kotlin-rudp-message-interop");
assert.equal(message.supportsSchemaCatalog, true);
assert.equal(message.transportProfiles?.[0]?.transports?.[0]?.protocol, "rudp");
assertRudpReconnectPolicy("Kotlin", message.transportProfiles?.[0]?.transports?.[0]?.reconnectPolicy);
assert.equal(message.transportProfiles?.[0]?.transports?.[0]?.channels?.[0]?.channelId, "schema");
const [exitCode] = await withTimeout(once(kotlinPeer.child, "exit"), 2_000, "Kotlin RUDP message peer exit");
assert.equal(exitCode, 0, kotlinPeer.stderr.join(""));
} finally {
if (client) {
client.close();
} else {
clientSocket.close();
}
if (kotlinPeer.child.exitCode === null && !kotlinPeer.child.killed) {
kotlinPeer.child.kill("SIGTERM");
await once(kotlinPeer.child, "exit").catch(() => undefined);
}
}
});
test("CultNet Kotlin and TypeScript exchange schema-v0 MessagePack messages over RUDP when Kotlin dials", async () => {
await buildKotlinInteropPeer();
const serverSocket = await bindUdpSocket();
const server = new CultNetRudpSocketTransportConnection({
runtimeId: "ts-kotlin-rudp-message-server",
socket: serverSocket,
mode: "server",
connectionId: 0xaa886645,
initialSequence: 100,
resendDelayMs: 25,
resendPollMs: 5,
});
const kotlinClient = spawnKotlinRudpMessageClient(udpSocketPort(serverSocket));
try {
const receivedFrame = once(server, "frame");
const [frame] = await withTimeout(receivedFrame, 2_000, "Kotlin RUDP schema-v0 client frame");
assert.equal(frame.channelId, "schema");
const message = decode(frame.payload) as {
schemaVersion?: string;
messageId?: string;
kinds?: string[];
};
assert.equal(message.schemaVersion, "cultnet.schema_catalog_request.v0");
assert.equal(message.messageId, "kotlin-ts-schema-message");
assert.deepEqual(message.kinds, ["wire_message"]);
server.send("schema", Buffer.from(encode({
schemaVersion: "cultnet.hello.v0",
runtimeId: "ts-kotlin-rudp-message-server",
runtimeKind: "typescript",
supportedDocumentTypes: [],
supportedMutationContracts: [],
supportedMessageVersions: ["cultnet.hello.v0", "cultnet.schema_catalog_request.v0"],
transportProfiles: [],
supportsSchemaCatalog: true,
})));
const [exitCode] = await withTimeout(once(kotlinClient.child, "exit"), 2_000, "Kotlin RUDP message client exit");
assert.equal(exitCode, 0, kotlinClient.stderr.join(""));
} finally {
server.close();
closeUdpSocket(serverSocket);
if (kotlinClient.child.exitCode === null && !kotlinClient.child.killed) {
kotlinClient.child.kill("SIGTERM");
await once(kotlinClient.child, "exit").catch(() => undefined);
}
}
});
test("CultNet TypeScript and C# full interop peers exchange schema-v0 over RUDP", async (t) => {
logInteropPhase("ts-csharp-full-rudp", "build csharp peer");
await buildCSharpInteropPeer();
cleanInteropStores([
"ts-csharp-rudp-peer",
"ts-csharp-rudp-client-dial",
]);
const csharpTcpPort = await getFreePort();
const csharpRudpPort = await getFreePort();
const tsTcpPort = await getFreePort();
const tsRudpPort = await getFreePort();
const discoveryPort = await getFreePort();
const advertiseHost = findAdvertiseHost();
const servers: RunningServeProcess[] = [];
// Registered before anything is spawned. A peer whose readiness rejects
// would otherwise skip this and strand every peer already running.
t.after(async () => {
await Promise.all(servers.map(stopProcess));
});
servers.push(await spawnServeProcess("csharp-rudp", {
command: dotnetCommand,
args: [
csharpDllPath,
"serve",
"--runtime-id", "csharp-rudp-peer",
"--runtime-kind", "dotnet",
"--display-name", "CSharp RUDP Peer",
"--agent-id", "csharp-rudp-agent",
"--bind-host", "127.0.0.1",
"--advertise-host", advertiseHost,
"--tcp-port", String(csharpTcpPort),
"--rudp-port", String(csharpRudpPort),
"--discovery-port", String(discoveryPort),
"--discovery-group", discoveryGroup,
"--schema-path", interopSchemaPath,
],
cwd: cultLibRoot,
}));
await servers[servers.length - 1].ready;
servers.push(await spawnServeProcess("ts-csharp-rudp", {
command: process.execPath,
args: [
tsPeerScript,
"serve",
"--runtime-id", "ts-csharp-rudp-peer",
"--runtime-kind", "node",
"--display-name", "TypeScript CSharp RUDP Peer",
"--agent-id", "ts-csharp-rudp-agent",
"--bind-host", "127.0.0.1",
"--advertise-host", advertiseHost,
"--tcp-port", String(tsTcpPort),
"--rudp-port", String(tsRudpPort),
"--discovery-port", String(discoveryPort),
"--discovery-group", discoveryGroup,
"--schema-path", interopSchemaPath,
],
cwd: cultNetTsRoot,
}));
await servers[servers.length - 1].ready;
logInteropPhase("ts-csharp-full-rudp", "typescript dials csharp over rudp");
const tsRudpDial = await runJsonCommand("ts-csharp-rudp-dial", process.execPath, [
tsPeerScript,
"dial",
"--runtime-id", "ts-csharp-rudp-client",
"--runtime-kind", "node",
"--display-name", "TS CSharp RUDP Dialer",
"--agent-id", "ts-csharp-rudp-client-agent",
"--target-host", "127.0.0.1",
"--target-rudp-port", String(csharpRudpPort),
"--schema-path", interopSchemaPath,
], cultNetTsRoot);
assert.equal(tsRudpDial.transport, "rudp");
assert.equal(tsRudpDial.remoteHello.runtimeId, "csharp-rudp-peer");
assert.equal(tsRudpDial.hasInteropSchema, true);
assert.equal(tsRudpDial.retrievedNote.authorRuntimeId, "csharp-rudp-peer");
assert.ok(tsRudpDial.mutatedNote.tags.includes("decorated:ts-csharp-rudp-client"));
assert.equal(tsRudpDial.fireReceipt.accepted, true);
logInteropPhase("ts-csharp-full-rudp", "csharp dials typescript over rudp");
const csharpRudpDial = await runJsonCommand("csharp-rudp-dial", dotnetCommand, [
csharpDllPath,
"dial",
"--runtime-id", "csharp-rudp-client",
"--runtime-kind", "dotnet",
"--display-name", "CSharp RUDP Dialer",
"--agent-id", "csharp-rudp-client-agent",
"--target-host", "127.0.0.1",
"--target-rudp-port", String(tsRudpPort),
"--schema-path", interopSchemaPath,
], cultLibRoot);
assert.equal(csharpRudpDial.transport, "rudp");
assert.equal(csharpRudpDial.remoteHello.runtimeId, "ts-csharp-rudp-peer");
assert.equal(csharpRudpDial.hasInteropSchema, true);
assert.equal(csharpRudpDial.retrievedNote.authorRuntimeId, "ts-csharp-rudp-peer");
assert.ok(csharpRudpDial.mutatedNote.tags.includes("decorated:csharp-rudp-client"));
assert.equal(csharpRudpDial.fireReceipt.accepted, true);
});
test("CultNet TypeScript and Kotlin full interop peers exchange schema-v0 over RUDP", async (t) => {
logInteropPhase("ts-kotlin-full-rudp", "build kotlin peer");
await buildKotlinInteropPeer();
cleanInteropStores([
"ts-kotlin-rudp-peer",
"ts-kotlin-rudp-client-dial",
]);
const kotlinTcpPort = await getFreePort();
const kotlinRudpPort = await getFreePort();
const tsTcpPort = await getFreePort();
const tsRudpPort = await getFreePort();
const discoveryPort = await getFreePort();
const advertiseHost = findAdvertiseHost();
const servers: RunningServeProcess[] = [];
// Registered before anything is spawned. A peer whose readiness rejects
// would otherwise skip this and strand every peer already running.
t.after(async () => {
await Promise.all(servers.map(stopProcess));
});
servers.push(await spawnServeProcess("kotlin-rudp", {
command: kotlinJavaCommand,
args: [
"-cp", kotlinClasspath,
"org.gamecult.cultmesh.CultMeshKt",
"serve",
"--runtime-id", "kotlin-rudp-peer",
"--runtime-kind", "kotlin",
"--display-name", "Kotlin RUDP Peer",
"--agent-id", "kotlin-rudp-agent",
"--bind-host", "127.0.0.1",
"--advertise-host", advertiseHost,
"--tcp-port", String(kotlinTcpPort),
"--rudp-port", String(kotlinRudpPort),
"--discovery-port", String(discoveryPort),
"--discovery-group", discoveryGroup,
"--schema-path", interopSchemaPath,
],
cwd: cultLibRoot,
}));
await servers[servers.length - 1].ready;
servers.push(await spawnServeProcess("ts-kotlin-rudp", {
command: process.execPath,
args: [
tsPeerScript,
"serve",
"--runtime-id", "ts-kotlin-rudp-peer",
"--runtime-kind", "node",
"--display-name", "TypeScript Kotlin RUDP Peer",
"--agent-id", "ts-kotlin-rudp-agent",
"--bind-host", "127.0.0.1",
"--advertise-host", advertiseHost,
"--tcp-port", String(tsTcpPort),
"--rudp-port", String(tsRudpPort),
"--discovery-port", String(discoveryPort),
"--discovery-group", discoveryGroup,
"--schema-path", interopSchemaPath,
],
cwd: cultNetTsRoot,
}));
await servers[servers.length - 1].ready;
logInteropPhase("ts-kotlin-full-rudp", "typescript dials kotlin over rudp");
const tsRudpDial = await runJsonCommand("ts-kotlin-rudp-dial", process.execPath, [
tsPeerScript,
"dial",
"--runtime-id", "ts-kotlin-rudp-client",
"--runtime-kind", "node",
"--display-name", "TS Kotlin RUDP Dialer",
"--agent-id", "ts-kotlin-rudp-client-agent",
"--target-host", "127.0.0.1",
"--target-rudp-port", String(kotlinRudpPort),
"--schema-path", interopSchemaPath,
], cultNetTsRoot);
assert.equal(tsRudpDial.transport, "rudp");
assert.equal(tsRudpDial.remoteHello.runtimeId, "kotlin-rudp-peer");
assert.equal(tsRudpDial.hasInteropSchema, true);
assert.equal(tsRudpDial.retrievedNote.authorRuntimeId, "kotlin-rudp-peer");
assert.ok(tsRudpDial.mutatedNote.tags.includes("decorated:ts-kotlin-rudp-client"));
assert.equal(tsRudpDial.fireReceipt.accepted, true);
logInteropPhase("ts-kotlin-full-rudp", "kotlin dials typescript over rudp");
const kotlinRudpDial = await runJsonCommand("kotlin-rudp-dial", kotlinJavaCommand, [
"-cp", kotlinClasspath,
"org.gamecult.cultmesh.CultMeshKt",
"dial",
"--runtime-id", "kotlin-rudp-client",
"--runtime-kind", "kotlin",
"--display-name", "Kotlin RUDP Dialer",
"--agent-id", "kotlin-rudp-client-agent",
"--target-host", "127.0.0.1",
"--target-rudp-port", String(tsRudpPort),
"--schema-path", interopSchemaPath,
], cultLibRoot);
assert.equal(kotlinRudpDial.transport, "rudp");
assert.equal(kotlinRudpDial.remoteHello.runtimeId, "ts-kotlin-rudp-peer");
assert.equal(kotlinRudpDial.hasInteropSchema, true);
assert.equal(kotlinRudpDial.retrievedNote.authorRuntimeId, "ts-kotlin-rudp-peer");
assert.ok(kotlinRudpDial.mutatedNote.tags.includes("decorated:kotlin-rudp-client"));
assert.equal(kotlinRudpDial.fireReceipt.accepted, true);
});
test("CultNet RUDP reliable schema delivery survives one dropped client data packet across runtimes", async () => {
for (const peer of rudpServerInteropPeers()) {
await peer.build();
const serverPeer = peer.spawn();
const clientSocket = await bindUdpSocket();
const lossyBridge = await createRudpLossyBridge({
remoteHost: "127.0.0.1",
remotePort: await serverPeer.ready,
shouldDropClientPacket: (wire) => {
const packet = decodeRudpPacket(wire);
return packet.packetType === "data"
&& packet.channelId === "schema"
&& Buffer.from(packet.payload ?? []).equals(Buffer.from(peer.clientPayload));
},
});
let client: CultNetRudpSocketTransportConnection | undefined;
try {
client = new CultNetRudpSocketTransportConnection({
runtimeId: `ts-lossy-${peer.name.replace(/\W/g, "").toLowerCase()}-rudp-interop`,
socket: clientSocket,
mode: "client",
remoteHost: "127.0.0.1",
remotePort: lossyBridge.port,
connectionId: peer.connectionId,
initialSequence: 1,
resendDelayMs: 25,
resendPollMs: 5,
});
const receivedFrame = once(client, "frame");
client.connect(Buffer.from(peer.joinPayload));
await waitFor(() => client?.connected === true, `TypeScript RUDP client to complete ${peer.name} handshake through lossy bridge`);
client.send("schema", Buffer.from(peer.clientPayload));
const [frame] = await withTimeout(receivedFrame, 2_000, `${peer.name} RUDP response frame after dropped data packet`);
assert.equal(frame.channelId, "schema");
assert.deepEqual(Buffer.from(frame.payload), Buffer.from(peer.expectedPayload));
assert.equal(lossyBridge.droppedClientPackets, 1, `${peer.name} bridge should drop exactly one client data packet`);
const [exitCode] = await withTimeout(once(serverPeer.child, "exit"), 2_000, `${peer.name} RUDP peer exit after lossy delivery`);
assert.equal(exitCode, 0, serverPeer.stderr.join(""));
} finally {
if (client) {
client.close();
} else {
clientSocket.close();
}
lossyBridge.close();
if (serverPeer.child.exitCode === null && !serverPeer.child.killed) {
serverPeer.child.kill("SIGTERM");
await once(serverPeer.child, "exit").catch(() => undefined);
}
}
}
});
test("CultNet RUDP reliable schema response survives one dropped server data packet across runtimes", async () => {
for (const peer of rudpServerInteropPeers()) {
await peer.build();
const serverPeer = peer.spawn();
const clientSocket = await bindUdpSocket();
const lossyBridge = await createRudpLossyBridge({
remoteHost: "127.0.0.1",
remotePort: await serverPeer.ready,
shouldDropServerPacket: (wire) => {
const packet = decodeRudpPacket(wire);
return packet.packetType === "data"
&& packet.channelId === "schema"
&& Buffer.from(packet.payload ?? []).equals(Buffer.from(peer.expectedPayload));
},
});
let client: CultNetRudpSocketTransportConnection | undefined;
try {
client = new CultNetRudpSocketTransportConnection({
runtimeId: `ts-server-loss-${peer.name.replace(/\W/g, "").toLowerCase()}-rudp-interop`,
socket: clientSocket,
mode: "client",
remoteHost: "127.0.0.1",
remotePort: lossyBridge.port,
connectionId: peer.connectionId,
initialSequence: 1,
resendDelayMs: 25,
resendPollMs: 5,
});
const receivedFrame = once(client, "frame");
client.connect(Buffer.from(peer.joinPayload));
await waitFor(() => client?.connected === true, `TypeScript RUDP client to complete ${peer.name} handshake through server-loss bridge`);
client.send("schema", Buffer.from(peer.clientPayload));
const [frame] = await withTimeout(receivedFrame, 2_000, `${peer.name} RUDP response frame after dropped server data packet`);
assert.equal(frame.channelId, "schema");
assert.deepEqual(Buffer.from(frame.payload), Buffer.from(peer.expectedPayload));
assert.equal(lossyBridge.droppedServerPackets, 1, `${peer.name} bridge should drop exactly one server data packet`);
const [exitCode] = await withTimeout(once(serverPeer.child, "exit"), 2_000, `${peer.name} RUDP peer exit after server resend`);
assert.equal(exitCode, 0, serverPeer.stderr.join(""));
} finally {
if (client) {
client.close();
} else {
clientSocket.close();
}
lossyBridge.close();
if (serverPeer.child.exitCode === null && !serverPeer.child.killed) {
serverPeer.child.kill("SIGTERM");
await once(serverPeer.child, "exit").catch(() => undefined);
}
}
}
});
test("CultNet RUDP reliable schema delivery survives reordered client data packets across runtimes", async () => {
for (const peer of rudpServerInteropPeers()) {
await peer.build();
const serverPeer = peer.spawn();
const clientSocket = await bindUdpSocket();
const lossyBridge = await createRudpLossyBridge({
remoteHost: "127.0.0.1",
remotePort: await serverPeer.ready,
shouldDelayClientPacket: (wire) => {
const packet = decodeRudpPacket(wire);
return packet.packetType === "data"
&& packet.channelId === "schema"
&& Buffer.from(packet.payload ?? []).equals(Buffer.from(peer.clientPayload));
},
});
let client: CultNetRudpSocketTransportConnection | undefined;
try {
client = new CultNetRudpSocketTransportConnection({
runtimeId: `ts-reordered-${peer.name.replace(/\W/g, "").toLowerCase()}-rudp-interop`,
socket: clientSocket,
mode: "client",
remoteHost: "127.0.0.1",
remotePort: lossyBridge.port,
connectionId: peer.connectionId,
initialSequence: 1,
resendDelayMs: 25,
resendPollMs: 5,
});
const receivedFrame = once(client, "frame");
client.connect(Buffer.from(peer.joinPayload));
await waitFor(() => client?.connected === true, `TypeScript RUDP client to complete ${peer.name} handshake through reorder bridge`);
client.send("schema", Buffer.from(peer.clientPayload));
client.send("schema", Buffer.from(`${peer.clientPayload}:second`));
const [frame] = await withTimeout(receivedFrame, 2_000, `${peer.name} RUDP response frame after reordered client data packets`);
assert.equal(frame.channelId, "schema");
assert.deepEqual(Buffer.from(frame.payload), Buffer.from(peer.expectedPayload));
assert.equal(lossyBridge.delayedClientPackets, 1, `${peer.name} bridge should delay exactly one client data packet`);
const [exitCode] = await withTimeout(once(serverPeer.child, "exit"), 2_000, `${peer.name} RUDP peer exit after reordered delivery`);
assert.equal(exitCode, 0, serverPeer.stderr.join(""));
} finally {
if (client) {
client.close();
} else {
clientSocket.close();
}
lossyBridge.close();
if (serverPeer.child.exitCode === null && !serverPeer.child.killed) {
serverPeer.child.kill("SIGTERM");
await once(serverPeer.child, "exit").catch(() => undefined);
}
}
}
});
test("CultNet RUDP reliable schema response survives reordered server data packets across runtimes", async () => {
for (const peer of rudpServerInteropPeers()) {
await peer.build();
const serverPeer = peer.spawn({
serverExtraPayload: `${peer.expectedPayload}:second`,
});
const clientSocket = await bindUdpSocket();
const lossyBridge = await createRudpLossyBridge({
remoteHost: "127.0.0.1",
remotePort: await serverPeer.ready,
shouldDelayServerPacket: (wire) => {
const packet = decodeRudpPacket(wire);
return packet.packetType === "data"
&& packet.channelId === "schema"
&& Buffer.from(packet.payload ?? []).equals(Buffer.from(peer.expectedPayload));
},
});
let client: CultNetRudpSocketTransportConnection | undefined;
try {
client = new CultNetRudpSocketTransportConnection({
runtimeId: `ts-server-reordered-${peer.name.replace(/\W/g, "").toLowerCase()}-rudp-interop`,
socket: clientSocket,
mode: "client",
remoteHost: "127.0.0.1",
remotePort: lossyBridge.port,
connectionId: peer.connectionId,
initialSequence: 1,
resendDelayMs: 25,
resendPollMs: 5,
});
const receivedFrame = once(client, "frame");
client.connect(Buffer.from(peer.joinPayload));
await waitFor(() => client?.connected === true, `TypeScript RUDP client to complete ${peer.name} handshake through server reorder bridge`);
client.send("schema", Buffer.from(peer.clientPayload));
const [frame] = await withTimeout(receivedFrame, 2_000, `${peer.name} RUDP response frame after reordered server data packets`);
assert.equal(frame.channelId, "schema");
assert.deepEqual(Buffer.from(frame.payload), Buffer.from(peer.expectedPayload));
assert.equal(lossyBridge.delayedServerPackets, 1, `${peer.name} bridge should delay exactly one server data packet`);
const [exitCode] = await withTimeout(once(serverPeer.child, "exit"), 2_000, `${peer.name} RUDP peer exit after server reorder`);
assert.equal(exitCode, 0, serverPeer.stderr.join(""));
} finally {
if (client) {
client.close();
} else {
clientSocket.close();
}
lossyBridge.close();
if (serverPeer.child.exitCode === null && !serverPeer.child.killed) {
serverPeer.child.kill("SIGTERM");
await once(serverPeer.child, "exit").catch(() => undefined);
}
}
}
});
test("CultNet RUDP fragmented schema frames cross runtime boundaries", async () => {
for (const peer of rudpServerInteropPeers()) {
await peer.build();
const clientPayload = `${peer.clientPayload}:fragmented:${"x".repeat(96)}`;
const serverPayload = `${peer.expectedPayload}:fragmented:${"y".repeat(96)}`;
const serverPeer = peer.spawn({
clientPayload,
serverPayload,
maxFragmentBytes: 16,
});
const clientSocket = await bindUdpSocket();
let client: CultNetRudpSocketTransportConnection | undefined;
try {
client = new CultNetRudpSocketTransportConnection({
runtimeId: `ts-fragmented-${peer.name.replace(/\W/g, "").toLowerCase()}-rudp-interop`,
socket: clientSocket,
mode: "client",
remoteHost: "127.0.0.1",
remotePort: await serverPeer.ready,
connectionId: peer.connectionId,
initialSequence: 1,
resendDelayMs: 25,
resendPollMs: 5,
maxFragmentBytes: 16,
});
const receivedFrame = once(client, "frame");
client.connect(Buffer.from(peer.joinPayload));
await waitFor(() => client?.connected === true, `TypeScript RUDP client to complete ${peer.name} fragmented handshake`);
client.send("schema", Buffer.from(clientPayload));
const [frame] = await withTimeout(receivedFrame, 2_000, `${peer.name} fragmented RUDP response frame`);
assert.equal(frame.channelId, "schema");
assert.deepEqual(Buffer.from(frame.payload), Buffer.from(serverPayload));
const [exitCode] = await withTimeout(once(serverPeer.child, "exit"), 2_000, `${peer.name} RUDP peer exit after fragmented exchange`);
assert.equal(exitCode, 0, serverPeer.stderr.join(""));
} finally {
if (client) {
client.close();
} else {
clientSocket.close();
}
if (serverPeer.child.exitCode === null && !serverPeer.child.killed) {
serverPeer.child.kill("SIGTERM");
await once(serverPeer.child, "exit").catch(() => undefined);
}
}
}
});
test("CultNet RUDP disconnect reasons cross runtime boundaries", async () => {
for (const peer of rudpServerInteropPeers()) {
await peer.build();
const disconnectReason = `${peer.name.toLowerCase()}-done`;
const serverPeer = peer.spawn({ disconnectReason });
const clientSocket = await bindUdpSocket();
let client: CultNetRudpSocketTransportConnection | undefined;
try {
client = new CultNetRudpSocketTransportConnection({
runtimeId: `ts-disconnect-${peer.name.replace(/\W/g, "").toLowerCase()}-rudp-interop`,
socket: clientSocket,
mode: "client",
remoteHost: "127.0.0.1",
remotePort: await serverPeer.ready,
connectionId: peer.connectionId,
initialSequence: 1,
resendDelayMs: 25,
resendPollMs: 5,
});
const receivedFrame = once(client, "frame");
const disconnected = once(client, "disconnect");
client.connect(Buffer.from(peer.joinPayload));
await waitFor(() => client?.connected === true, `TypeScript RUDP client to complete ${peer.name} disconnect handshake`);
client.send("schema", Buffer.from(peer.clientPayload));
const [frame] = await withTimeout(receivedFrame, 2_000, `${peer.name} RUDP response frame before disconnect`);
assert.equal(frame.channelId, "schema");
assert.deepEqual(Buffer.from(frame.payload), Buffer.from(peer.expectedPayload));
const [disconnect] = await withTimeout(disconnected, 2_000, `${peer.name} RUDP disconnect reason`);
assert.deepEqual(Buffer.from(disconnect.reason), Buffer.from(disconnectReason));
assert.equal(client.connected, false);
const [exitCode] = await withTimeout(once(serverPeer.child, "exit"), 2_000, `${peer.name} RUDP peer exit after disconnect`);
assert.equal(exitCode, 0, serverPeer.stderr.join(""));
} finally {
if (client) {
client.close();
} else {
clientSocket.close();
}
if (serverPeer.child.exitCode === null && !serverPeer.child.killed) {
serverPeer.child.kill("SIGTERM");
await once(serverPeer.child, "exit").catch(() => undefined);
}
}
}
});
test("CultNet RUDP ping/pong keepalive crosses runtime boundaries", async () => {
for (const peer of rudpServerInteropPeers()) {
await peer.build();
const serverPeer = peer.spawn();
const clientSocket = await bindUdpSocket();
let client: CultNetRudpSocketTransportConnection | undefined;
try {
client = new CultNetRudpSocketTransportConnection({
runtimeId: `ts-ping-${peer.name.replace(/\W/g, "").toLowerCase()}-rudp-interop`,
socket: clientSocket,
mode: "client",
remoteHost: "127.0.0.1",
remotePort: await serverPeer.ready,
connectionId: peer.connectionId,
initialSequence: 1,
resendDelayMs: 25,
resendPollMs: 5,
});
const receivedPong = once(client, "pong");
const receivedFrame = once(client, "frame");
client.connect(Buffer.from(peer.joinPayload));
await waitFor(() => client?.connected === true, `TypeScript RUDP client to complete ${peer.name} ping handshake`);
const pingPayload = Buffer.from(`${peer.name.toLowerCase()}-ping`);
client.ping(pingPayload);
const [pong] = await withTimeout(receivedPong, 2_000, `${peer.name} RUDP pong`);
assert.deepEqual(Buffer.from(pong.payload), pingPayload);
assert.equal(client.checkTimeout(1_000), false);
client.send("schema", Buffer.from(peer.clientPayload));
const [frame] = await withTimeout(receivedFrame, 2_000, `${peer.name} RUDP response frame after ping`);
assert.equal(frame.channelId, "schema");
assert.deepEqual(Buffer.from(frame.payload), Buffer.from(peer.expectedPayload));
const [exitCode] = await withTimeout(once(serverPeer.child, "exit"), 2_000, `${peer.name} RUDP peer exit after ping`);
assert.equal(exitCode, 0, serverPeer.stderr.join(""));
} finally {
if (client) {
client.close();
} else {
clientSocket.close();
}
if (serverPeer.child.exitCode === null && !serverPeer.child.killed) {
serverPeer.child.kill("SIGTERM");
await once(serverPeer.child, "exit").catch(() => undefined);
}
}
}
});
async function buildInteropPeers(): Promise<void> {
await buildRustInteropPeer();
await buildCSharpInteropPeer();
}
async function buildRustInteropPeer(): Promise<void> {
rustInteropPeerBuild ??= execFileAsync(cargoCommand, ["build", "--quiet", "--example", "cultnet_interop_peer"], {
cwd: cultnetRsRoot,
}).then(() => undefined);
await rustInteropPeerBuild;
}
async function buildCSharpInteropPeer(): Promise<void> {
csharpInteropPeerBuild ??= execFileAsync(dotnetCommand, ["build", csharpProjectPath, "-nologo"], {
cwd: cultLibRoot,
}).then(() => undefined);
await csharpInteropPeerBuild;
}
async function buildKotlinInteropPeer(): Promise<void> {
kotlinInteropPeerBuild ??= execFileAsync(
"powershell",
["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", resolve(cultmeshKotlinRoot, "build.ps1")],
{
cwd: cultLibRoot,
timeout: 180_000,
},
).then(() => undefined);
await kotlinInteropPeerBuild;
}
function cleanInteropStores(runtimeIds: readonly string[]): void {
for (const runtimeId of runtimeIds) {
rmSync(join(tmpdir(), `cultnet-rs-interop-${runtimeId}.msgpack`), { force: true });
rmSync(join(tmpdir(), `cultnet-ts-interop-${runtimeId}.msgpack`), { force: true });
rmSync(join(tmpdir(), `cultnet-py-interop-${runtimeId}.msgpack`), { force: true });
}
}
function logInteropPhase(lane: string, phase: string): void {
console.error(`[interop:${lane}] ${new Date().toISOString()} ${phase}`);
}
function findAncestor(start: string, marker: string): string | undefined {
let current = start;
while (true) {
if (existsSync(resolve(current, marker))) {
return current;
}
const parent = dirname(current);
if (parent === current) {
return undefined;
}
current = parent;
}
}
interface ServeCommand {
command: string;
args: string[];
cwd: string;
env?: NodeJS.ProcessEnv;
}
interface RunningServeProcess {
name: string;
child: ChildProcessByStdio<null, Readable, Readable>;
ready: Promise<unknown>;
stderr: string[];
}
async function spawnServeProcess(name: string, command: ServeCommand): Promise<RunningServeProcess> {
const child = spawn(command.command, command.args, {
cwd: command.cwd,
env: { ...process.env, ...command.env },
stdio: ["ignore", "pipe", "pipe"],
});
const stderr: string[] = [];
let stdoutBuffer = "";
const ready = new Promise<unknown>((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error(`${name} serve process did not become ready within ${serveReadyTimeoutMs}ms.\n${stderr.join("")}`));
}, serveReadyTimeoutMs);
const resolveReady = (value: unknown) => {
clearTimeout(timeout);
resolve(value);
};
const rejectReady = (error: Error) => {
clearTimeout(timeout);
reject(error);
};
child.stdout.setEncoding("utf8");
child.stdout.on("data", (chunk: string) => {
stdoutBuffer += chunk;
while (true) {
const newline = stdoutBuffer.indexOf("\n");
if (newline === -1) {
break;
}
const line = stdoutBuffer.slice(0, newline).trim();
stdoutBuffer = stdoutBuffer.slice(newline + 1);
if (!line) {
continue;
}
try {
const parsed = JSON.parse(line) as { status?: string };
if (parsed.status === "ready") {
resolveReady(parsed);
return;
}
} catch (error) {
rejectReady(new Error(`${name} emitted non-JSON stdout while starting: ${line}`));
return;
}
}
});
child.stderr.setEncoding("utf8");
child.stderr.on("data", (chunk: string) => {
stderr.push(chunk);
});
child.once("exit", (code, signal) => {
rejectReady(new Error(`${name} serve process exited before becoming ready (code=${code}, signal=${signal}).\n${stderr.join("")}`));
});
child.once("error", rejectReady);
});
return { name, child, ready, stderr };
}
async function runJsonCommand(
name: string,
command: string,
args: string[],
cwd: string,
env: NodeJS.ProcessEnv = {},
): Promise<any> {
const { stdout, stderr } = await execFileAsync(command, args, {
cwd,
env: { ...process.env, ...env },
timeout: jsonCommandTimeoutMs,
});
const trimmed = stdout.trim();
if (!trimmed) {
throw new Error(`${name} produced no stdout.\n${stderr}`);
}
const lines = trimmed.split(/\r?\n/).filter(Boolean);
try {
return JSON.parse(lines.at(-1) as string);
} catch (error) {
throw new Error(`${name} did not end with JSON stdout.\nstdout:\n${stdout}\nstderr:\n${stderr}`);
}
}
interface RunningPythonRudpPeer {
child: ChildProcessByStdio<null, Readable, Readable>;
ready: Promise<number>;
stderr: string[];
}
function spawnPythonRudpPeer(options: RudpServerSpawnOptions = {}): RunningPythonRudpPeer {
const child = spawn(pythonCommand, ["-c", pythonRudpPeerScript], {
cwd: cultcachePyRoot,
env: {
...process.env,
PYTHONPATH: cultcachePySrc,
...(options.clientPayload ? { RUDP_CLIENT_PAYLOAD: options.clientPayload } : {}),
...(options.serverPayload ? { RUDP_SERVER_PAYLOAD: options.serverPayload } : {}),
...(options.serverExtraPayload ? { RUDP_SERVER_EXTRA_PAYLOAD: options.serverExtraPayload } : {}),
...(options.disconnectReason ? { RUDP_DISCONNECT_REASON: options.disconnectReason } : {}),
...(options.maxFragmentBytes ? { RUDP_MAX_FRAGMENT_BYTES: String(options.maxFragmentBytes) } : {}),
},
stdio: ["ignore", "pipe", "pipe"],
});
const stderr: string[] = [];
let stdoutBuffer = "";
const ready = new Promise<number>((resolveReady, rejectReady) => {
child.stdout.setEncoding("utf8");
child.stdout.on("data", (chunk: string) => {
stdoutBuffer += chunk;
while (true) {
const newline = stdoutBuffer.indexOf("\n");
if (newline === -1) {
break;
}
const line = stdoutBuffer.slice(0, newline).trim();
stdoutBuffer = stdoutBuffer.slice(newline + 1);
if (!line) {
continue;
}
const portMatch = /^PORT\s+(\d+)$/.exec(line);
if (portMatch) {
resolveReady(Number(portMatch[1]));
continue;
}
if (line !== "OK") {
rejectReady(new Error(`Python RUDP peer emitted unexpected stdout: ${line}`));
}
}
});
child.stderr.setEncoding("utf8");
child.stderr.on("data", (chunk: string) => {
stderr.push(chunk);
});
child.once("exit", (code, signal) => {
rejectReady(new Error(`Python RUDP peer exited before publishing a port (code=${code}, signal=${signal}).\n${stderr.join("")}`));
});
child.once("error", rejectReady);
});
return { child, ready, stderr };
}
function spawnPythonRudpMessagePeer(): RunningPythonRudpPeer {
const child = spawn(pythonCommand, ["-c", pythonRudpMessagePeerScript], {
cwd: cultcachePyRoot,
env: { ...process.env, PYTHONPATH: cultcachePySrc },
stdio: ["ignore", "pipe", "pipe"],
});
const stderr: string[] = [];
let stdoutBuffer = "";
const ready = new Promise<number>((resolveReady, rejectReady) => {
child.stdout.setEncoding("utf8");
child.stdout.on("data", (chunk: string) => {
stdoutBuffer += chunk;
while (true) {
const newline = stdoutBuffer.indexOf("\n");
if (newline === -1) {
break;
}
const line = stdoutBuffer.slice(0, newline).trim();
stdoutBuffer = stdoutBuffer.slice(newline + 1);
if (!line) {
continue;
}
const portMatch = /^PORT\s+(\d+)$/.exec(line);
if (portMatch) {
resolveReady(Number(portMatch[1]));
continue;
}
if (line !== "OK") {
rejectReady(new Error(`Python RUDP message peer emitted unexpected stdout: ${line}`));
}
}
});
child.stderr.setEncoding("utf8");
child.stderr.on("data", (chunk: string) => {
stderr.push(chunk);
});
child.once("exit", (code, signal) => {
rejectReady(new Error(`Python RUDP message peer exited before publishing a port (code=${code}, signal=${signal}).\n${stderr.join("")}`));
});
child.once("error", rejectReady);
});
return { child, ready, stderr };
}
function spawnRustRudpServer(options: RudpServerSpawnOptions = {}): RunningPythonRudpPeer {
const child = spawn(rustBinaryPath, [
"rudp-serve-once",
"--bind-host", "127.0.0.1",
...(options.clientPayload ? ["--client-payload", options.clientPayload] : []),
...(options.serverPayload ? ["--server-payload", options.serverPayload] : []),
...(options.serverExtraPayload ? ["--server-extra-payload", options.serverExtraPayload] : []),
...(options.disconnectReason ? ["--disconnect-reason", options.disconnectReason] : []),
...(options.maxFragmentBytes ? ["--max-fragment-bytes", String(options.maxFragmentBytes)] : []),
], {
cwd: cultnetRsRoot,
env: process.env,
stdio: ["ignore", "pipe", "pipe"],
});
const stderr: string[] = [];
let stdoutBuffer = "";
const ready = new Promise<number>((resolveReady, rejectReady) => {
child.stdout.setEncoding("utf8");
child.stdout.on("data", (chunk: string) => {
stdoutBuffer += chunk;
while (true) {
const newline = stdoutBuffer.indexOf("\n");
if (newline === -1) {
break;
}
const line = stdoutBuffer.slice(0, newline).trim();
stdoutBuffer = stdoutBuffer.slice(newline + 1);
if (!line) {
continue;
}
try {
const parsed = JSON.parse(line) as { status?: string; port?: number };
if (parsed.status === "ready" && typeof parsed.port === "number") {
resolveReady(parsed.port);
} else if (parsed.status !== "ok") {
rejectReady(new Error(`Rust RUDP peer emitted unexpected stdout: ${line}`));
}
} catch (error) {
rejectReady(new Error(`Rust RUDP peer emitted non-JSON stdout: ${line}`));
}
}
});
child.stderr.setEncoding("utf8");
child.stderr.on("data", (chunk: string) => {
stderr.push(chunk);
});
child.once("exit", (code, signal) => {
rejectReady(new Error(`Rust RUDP peer exited before publishing a port (code=${code}, signal=${signal}).\n${stderr.join("")}`));
});
child.once("error", rejectReady);
});
return { child, ready, stderr };
}
function spawnRustRudpMessageServer(): RunningPythonRudpPeer {
const child = spawn(rustBinaryPath, [
"rudp-serve-message-once",
"--bind-host", "127.0.0.1",
], {
cwd: cultnetRsRoot,
env: process.env,
stdio: ["ignore", "pipe", "pipe"],
});
const stderr: string[] = [];
let stdoutBuffer = "";
const ready = new Promise<number>((resolveReady, rejectReady) => {
child.stdout.setEncoding("utf8");
child.stdout.on("data", (chunk: string) => {
stdoutBuffer += chunk;
while (true) {
const newline = stdoutBuffer.indexOf("\n");
if (newline === -1) {
break;
}
const line = stdoutBuffer.slice(0, newline).trim();
stdoutBuffer = stdoutBuffer.slice(newline + 1);
if (!line) {
continue;
}
try {
const parsed = JSON.parse(line) as { status?: string; port?: number };
if (parsed.status === "ready" && typeof parsed.port === "number") {
resolveReady(parsed.port);
} else if (parsed.status !== "ok") {
rejectReady(new Error(`Rust RUDP message peer emitted unexpected stdout: ${line}`));
}
} catch (error) {
rejectReady(new Error(`Rust RUDP message peer emitted non-JSON stdout: ${line}`));
}
}
});
child.stderr.setEncoding("utf8");
child.stderr.on("data", (chunk: string) => {
stderr.push(chunk);
});
child.once("exit", (code, signal) => {
rejectReady(new Error(`Rust RUDP message peer exited before publishing a port (code=${code}, signal=${signal}).\n${stderr.join("")}`));
});
child.once("error", rejectReady);
});
return { child, ready, stderr };
}
function spawnCSharpRudpServer(options: RudpServerSpawnOptions = {}): RunningPythonRudpPeer {
const child = spawn(dotnetCommand, [
csharpDllPath,
"rudp-serve-once",
"--bind-host", "127.0.0.1",
...(options.clientPayload ? ["--client-payload", options.clientPayload] : []),
...(options.serverPayload ? ["--server-payload", options.serverPayload] : []),
...(options.serverExtraPayload ? ["--server-extra-payload", options.serverExtraPayload] : []),
...(options.disconnectReason ? ["--disconnect-reason", options.disconnectReason] : []),
...(options.maxFragmentBytes ? ["--max-fragment-bytes", String(options.maxFragmentBytes)] : []),
], {
cwd: cultLibRoot,
env: process.env,
stdio: ["ignore", "pipe", "pipe"],
});
const stderr: string[] = [];
let stdoutBuffer = "";
const ready = new Promise<number>((resolveReady, rejectReady) => {
child.stdout.setEncoding("utf8");
child.stdout.on("data", (chunk: string) => {
stdoutBuffer += chunk;
while (true) {
const newline = stdoutBuffer.indexOf("\n");
if (newline === -1) {
break;
}
const line = stdoutBuffer.slice(0, newline).trim();
stdoutBuffer = stdoutBuffer.slice(newline + 1);
if (!line) {
continue;
}
try {
const parsed = JSON.parse(line) as { status?: string; port?: number };
if (parsed.status === "ready" && typeof parsed.port === "number") {
resolveReady(parsed.port);
} else if (parsed.status !== "ok") {
rejectReady(new Error(`C# RUDP peer emitted unexpected stdout: ${line}`));
}
} catch (error) {
rejectReady(new Error(`C# RUDP peer emitted non-JSON stdout: ${line}`));
}
}
});
child.stderr.setEncoding("utf8");
child.stderr.on("data", (chunk: string) => {
stderr.push(chunk);
});
child.once("exit", (code, signal) => {
rejectReady(new Error(`C# RUDP peer exited before publishing a port (code=${code}, signal=${signal}).\n${stderr.join("")}`));
});
child.once("error", rejectReady);
});
return { child, ready, stderr };
}
function spawnCSharpRudpMessageServer(): RunningPythonRudpPeer {
const child = spawn(dotnetCommand, [
csharpDllPath,
"rudp-serve-message-once",
"--bind-host", "127.0.0.1",
], {
cwd: cultLibRoot,
env: process.env,
stdio: ["ignore", "pipe", "pipe"],
});
const stderr: string[] = [];
let stdoutBuffer = "";
const ready = new Promise<number>((resolveReady, rejectReady) => {
child.stdout.setEncoding("utf8");
child.stdout.on("data", (chunk: string) => {
stdoutBuffer += chunk;
while (true) {
const newline = stdoutBuffer.indexOf("\n");
if (newline === -1) {
break;
}
const line = stdoutBuffer.slice(0, newline).trim();
stdoutBuffer = stdoutBuffer.slice(newline + 1);
if (!line) {
continue;
}
try {
const parsed = JSON.parse(line) as { status?: string; port?: number };
if (parsed.status === "ready" && typeof parsed.port === "number") {
resolveReady(parsed.port);
} else if (parsed.status !== "ok") {
rejectReady(new Error(`C# RUDP message peer emitted unexpected stdout: ${line}`));
}
} catch (error) {
rejectReady(new Error(`C# RUDP message peer emitted non-JSON stdout: ${line}`));
}
}
});
child.stderr.setEncoding("utf8");
child.stderr.on("data", (chunk: string) => {
stderr.push(chunk);
});
child.once("exit", (code, signal) => {
rejectReady(new Error(`C# RUDP message peer exited before publishing a port (code=${code}, signal=${signal}).\n${stderr.join("")}`));
});
child.once("error", rejectReady);
});
return { child, ready, stderr };
}
function spawnKotlinRudpServer(options: RudpServerSpawnOptions = {}): RunningPythonRudpPeer {
const child = spawn(kotlinJavaCommand, [
"-cp", kotlinClasspath,
"org.gamecult.cultmesh.CultMeshKt",
"rudp-serve-once",
"--bind-host", "127.0.0.1",
...(options.clientPayload ? ["--client-payload", options.clientPayload] : []),
...(options.serverPayload ? ["--server-payload", options.serverPayload] : []),
...(options.serverExtraPayload ? ["--server-extra-payload", options.serverExtraPayload] : []),
...(options.disconnectReason ? ["--disconnect-reason", options.disconnectReason] : []),
...(options.maxFragmentBytes ? ["--max-fragment-bytes", String(options.maxFragmentBytes)] : []),
], {
cwd: cultLibRoot,
env: process.env,
stdio: ["ignore", "pipe", "pipe"],
});
const stderr: string[] = [];
let stdoutBuffer = "";
const ready = new Promise<number>((resolveReady, rejectReady) => {
child.stdout.setEncoding("utf8");
child.stdout.on("data", (chunk: string) => {
stdoutBuffer += chunk;
while (true) {
const newline = stdoutBuffer.indexOf("\n");
if (newline === -1) {
break;
}
const line = stdoutBuffer.slice(0, newline).trim();
stdoutBuffer = stdoutBuffer.slice(newline + 1);
if (!line) {
continue;
}
try {
const parsed = JSON.parse(line) as { status?: string; port?: number };
if (parsed.status === "ready" && typeof parsed.port === "number") {
resolveReady(parsed.port);
} else if (parsed.status !== "ok") {
rejectReady(new Error(`Kotlin RUDP peer emitted unexpected stdout: ${line}`));
}
} catch (error) {
rejectReady(new Error(`Kotlin RUDP peer emitted non-JSON stdout: ${line}`));
}
}
});
child.stderr.setEncoding("utf8");
child.stderr.on("data", (chunk: string) => {
stderr.push(chunk);
});
child.once("exit", (code, signal) => {
rejectReady(new Error(`Kotlin RUDP peer exited before publishing a port (code=${code}, signal=${signal}).\n${stderr.join("")}`));
});
child.once("error", rejectReady);
});
return { child, ready, stderr };
}
function spawnKotlinRudpMessageServer(): RunningPythonRudpPeer {
const child = spawn(kotlinJavaCommand, [
"-cp", kotlinClasspath,
"org.gamecult.cultmesh.CultMeshKt",
"rudp-serve-message-once",
"--bind-host", "127.0.0.1",
], {
cwd: cultLibRoot,
env: process.env,
stdio: ["ignore", "pipe", "pipe"],
});
const stderr: string[] = [];
let stdoutBuffer = "";
const ready = new Promise<number>((resolveReady, rejectReady) => {
child.stdout.setEncoding("utf8");
child.stdout.on("data", (chunk: string) => {
stdoutBuffer += chunk;
while (true) {
const newline = stdoutBuffer.indexOf("\n");
if (newline === -1) {
break;
}
const line = stdoutBuffer.slice(0, newline).trim();
stdoutBuffer = stdoutBuffer.slice(newline + 1);
if (!line) {
continue;
}
try {
const parsed = JSON.parse(line) as { status?: string; port?: number };
if (parsed.status === "ready" && typeof parsed.port === "number") {
resolveReady(parsed.port);
} else if (parsed.status !== "ok") {
rejectReady(new Error(`Kotlin RUDP message peer emitted unexpected stdout: ${line}`));
}
} catch (error) {
rejectReady(new Error(`Kotlin RUDP message peer emitted non-JSON stdout: ${line}`));
}
}
});
child.stderr.setEncoding("utf8");
child.stderr.on("data", (chunk: string) => {
stderr.push(chunk);
});
child.once("exit", (code, signal) => {
rejectReady(new Error(`Kotlin RUDP message peer exited before publishing a port (code=${code}, signal=${signal}).\n${stderr.join("")}`));
});
child.once("error", rejectReady);
});
return { child, ready, stderr };
}
function spawnPythonRudpClient(remotePort: number): Omit<RunningPythonRudpPeer, "ready"> {
const child = spawn(pythonCommand, ["-c", pythonRudpClientScript, String(remotePort)], {
cwd: cultcachePyRoot,
env: { ...process.env, PYTHONPATH: cultcachePySrc },
stdio: ["ignore", "pipe", "pipe"],
});
const stderr: string[] = [];
child.stderr.setEncoding("utf8");
child.stderr.on("data", (chunk: string) => {
stderr.push(chunk);
});
return { child, stderr };
}
function spawnPythonRudpMessageClient(remotePort: number): Omit<RunningPythonRudpPeer, "ready"> {
const child = spawn(pythonCommand, ["-c", pythonRudpMessageClientScript, String(remotePort)], {
cwd: cultcachePyRoot,
env: { ...process.env, PYTHONPATH: cultcachePySrc },
stdio: ["ignore", "pipe", "pipe"],
});
const stderr: string[] = [];
child.stderr.setEncoding("utf8");
child.stderr.on("data", (chunk: string) => {
stderr.push(chunk);
});
return { child, stderr };
}
function spawnKotlinRudpClient(remotePort: number): Omit<RunningPythonRudpPeer, "ready"> {
const child = spawn(kotlinJavaCommand, [
"-cp", kotlinClasspath,
"org.gamecult.cultmesh.CultMeshKt",
"rudp-dial-once",
"--target-host", "127.0.0.1",
"--target-port", String(remotePort),
], {
cwd: cultLibRoot,
env: process.env,
stdio: ["ignore", "pipe", "pipe"],
});
const stderr: string[] = [];
child.stderr.setEncoding("utf8");
child.stderr.on("data", (chunk: string) => {
stderr.push(chunk);
});
return { child, stderr };
}
function spawnKotlinRudpMessageClient(remotePort: number): Omit<RunningPythonRudpPeer, "ready"> {
const child = spawn(kotlinJavaCommand, [
"-cp", kotlinClasspath,
"org.gamecult.cultmesh.CultMeshKt",
"rudp-dial-message-once",
"--target-host", "127.0.0.1",
"--target-port", String(remotePort),
], {
cwd: cultLibRoot,
env: process.env,
stdio: ["ignore", "pipe", "pipe"],
});
const stderr: string[] = [];
child.stderr.setEncoding("utf8");
child.stderr.on("data", (chunk: string) => {
stderr.push(chunk);
});
return { child, stderr };
}
function spawnCSharpRudpClient(remotePort: number): Omit<RunningPythonRudpPeer, "ready"> {
const child = spawn(dotnetCommand, [
csharpDllPath,
"rudp-dial-once",
"--target-host", "127.0.0.1",
"--target-port", String(remotePort),
], {
cwd: cultLibRoot,
env: process.env,
stdio: ["ignore", "pipe", "pipe"],
});
const stderr: string[] = [];
child.stderr.setEncoding("utf8");
child.stderr.on("data", (chunk: string) => {
stderr.push(chunk);
});
return { child, stderr };
}
function spawnCSharpRudpMessageClient(remotePort: number): Omit<RunningPythonRudpPeer, "ready"> {
const child = spawn(dotnetCommand, [
csharpDllPath,
"rudp-dial-message-once",
"--target-host", "127.0.0.1",
"--target-port", String(remotePort),
], {
cwd: cultLibRoot,
env: process.env,
stdio: ["ignore", "pipe", "pipe"],
});
const stderr: string[] = [];
child.stderr.setEncoding("utf8");
child.stderr.on("data", (chunk: string) => {
stderr.push(chunk);
});
return { child, stderr };
}
function spawnRustRudpClient(remotePort: number): Omit<RunningPythonRudpPeer, "ready"> {
const child = spawn(rustBinaryPath, [
"rudp-dial-once",
"--target-host", "127.0.0.1",
"--target-port", String(remotePort),
], {
cwd: cultnetRsRoot,
env: process.env,
stdio: ["ignore", "pipe", "pipe"],
});
const stderr: string[] = [];
child.stderr.setEncoding("utf8");
child.stderr.on("data", (chunk: string) => {
stderr.push(chunk);
});
return { child, stderr };
}
function spawnRustRudpMessageClient(remotePort: number): Omit<RunningPythonRudpPeer, "ready"> {
const child = spawn(rustBinaryPath, [
"rudp-dial-message-once",
"--target-host", "127.0.0.1",
"--target-port", String(remotePort),
], {
cwd: cultnetRsRoot,
env: process.env,
stdio: ["ignore", "pipe", "pipe"],
});
const stderr: string[] = [];
child.stderr.setEncoding("utf8");
child.stderr.on("data", (chunk: string) => {
stderr.push(chunk);
});
return { child, stderr };
}
/** Closes a socket this file bound, tolerating a transport that adopted and
* already closed it. A socket left open holds the event loop forever, which is
* invisible while tests pass and fatal to the runner's exit. */
function closeUdpSocket(socket: Socket | undefined): void {
if (!socket) {
return;
}
try {
socket.close();
} catch {
// already closed
}
}
async function bindUdpSocket(): Promise<Socket> {
const socket = dgram.createSocket("udp4");
await new Promise<void>((resolveBind, rejectBind) => {
socket.once("error", rejectBind);
socket.bind(0, "127.0.0.1", () => {
socket.off("error", rejectBind);
resolveBind();
});
});
return socket;
}
interface RudpLossyBridge {
port: number;
readonly droppedClientPackets: number;
readonly droppedServerPackets: number;
readonly delayedClientPackets: number;
readonly delayedServerPackets: number;
close(): void;
}
async function createRudpLossyBridge(options: {
remoteHost: string;
remotePort: number;
shouldDropClientPacket?: (wire: Buffer) => boolean;
shouldDropServerPacket?: (wire: Buffer) => boolean;
shouldDelayClientPacket?: (wire: Buffer) => boolean;
shouldDelayServerPacket?: (wire: Buffer) => boolean;
}): Promise<RudpLossyBridge> {
const socket = dgram.createSocket("udp4");
const remote = { address: options.remoteHost, port: options.remotePort };
let client: { address: string; port: number } | undefined;
let droppedClientPackets = 0;
let droppedServerPackets = 0;
let delayedClientPackets = 0;
let delayedServerPackets = 0;
let heldClientWire: Buffer | undefined;
let heldServerWire: Buffer | undefined;
let heldClientTimer: NodeJS.Timeout | undefined;
let heldServerTimer: NodeJS.Timeout | undefined;
const flushHeldClient = () => {
if (!heldClientWire) {
return;
}
const wire = heldClientWire;
heldClientWire = undefined;
if (heldClientTimer) {
clearTimeout(heldClientTimer);
heldClientTimer = undefined;
}
socket.send(wire, remote.port, remote.address);
};
const flushHeldServer = () => {
if (!heldServerWire || !client) {
return;
}
const wire = heldServerWire;
heldServerWire = undefined;
if (heldServerTimer) {
clearTimeout(heldServerTimer);
heldServerTimer = undefined;
}
socket.send(wire, client.port, client.address);
};
socket.on("message", (wire, sender) => {
if (sender.address === remote.address && sender.port === remote.port) {
if (droppedServerPackets === 0 && options.shouldDropServerPacket?.(wire)) {
droppedServerPackets += 1;
return;
}
if (delayedServerPackets === 0 && options.shouldDelayServerPacket?.(wire)) {
delayedServerPackets += 1;
heldServerWire = Buffer.from(wire);
heldServerTimer = setTimeout(flushHeldServer, 25);
return;
}
if (client) {
socket.send(wire, client.port, client.address);
flushHeldServer();
}
return;
}
client ??= { address: sender.address, port: sender.port };
if (droppedClientPackets === 0 && options.shouldDropClientPacket?.(wire)) {
droppedClientPackets += 1;
return;
}
if (delayedClientPackets === 0 && options.shouldDelayClientPacket?.(wire)) {
delayedClientPackets += 1;
heldClientWire = Buffer.from(wire);
heldClientTimer = setTimeout(flushHeldClient, 25);
return;
}
socket.send(wire, remote.port, remote.address);
flushHeldClient();
});
await new Promise<void>((resolveBind, rejectBind) => {
socket.once("error", rejectBind);
socket.bind(0, "127.0.0.1", () => {
socket.off("error", rejectBind);
resolveBind();
});
});
return {
port: udpSocketPort(socket),
get droppedClientPackets() {
return droppedClientPackets;
},
get droppedServerPackets() {
return droppedServerPackets;
},
get delayedClientPackets() {
return delayedClientPackets;
},
get delayedServerPackets() {
return delayedServerPackets;
},
close() {
if (heldClientTimer) {
clearTimeout(heldClientTimer);
}
if (heldServerTimer) {
clearTimeout(heldServerTimer);
}
socket.close();
},
};
}
function udpSocketPort(socket: Socket): number {
const address = socket.address();
if (typeof address === "string") {
throw new Error(`Expected UDP socket address info, got ${address}.`);
}
return address.port;
}
async function waitFor(predicate: () => boolean, description: string, timeoutMs = 2_000): Promise<void> {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
if (predicate()) {
return;
}
await delay(5);
}
throw new Error(`Timed out waiting for ${description}.`);
}
async function withTimeout<T>(promise: Promise<T>, timeoutMs: number, description: string): Promise<T> {
let timeout: NodeJS.Timeout | undefined;
try {
return await Promise.race([
promise,
new Promise<never>((_, reject) => {
timeout = setTimeout(() => reject(new Error(`Timed out waiting for ${description}.`)), timeoutMs);
}),
]);
} finally {
if (timeout) {
clearTimeout(timeout);
}
}
}
async function expectProbePeers(
name: string,
command: string,
args: string[],
cwd: string,
env: NodeJS.ProcessEnv,
expectedPeers: string[],
): Promise<void> {
const found = new Set<string>();
const profiles = new Map<string, unknown>();
for (let attempt = 0; attempt < 3; attempt += 1) {
const probe = await runJsonCommand(`${name}-${attempt + 1}`, command, args, cwd, env);
for (const peer of probe.peers as Array<{ runtimeId: string; transportProfiles?: unknown }>) {
found.add(peer.runtimeId);
profiles.set(peer.runtimeId, peer.transportProfiles);
}
if (expectedPeers.every((peer) => found.has(peer))) {
assert.deepEqual([...found].sort(), expectedPeers);
for (const peerId of expectedPeers) {
assertTcpFramedTransportProfile(peerId, profiles.get(peerId));
}
return;
}
}
assert.deepEqual([...found].sort(), expectedPeers);
}
function assertTcpFramedTransportProfile(peerId: string, value: unknown): void {
assert.ok(Array.isArray(value), `${peerId} did not advertise transport profiles`);
const firstProfile = value[0] as any;
assert.equal(firstProfile?.schemaVersion, "cultnet.transport_profile.v0");
assert.equal(firstProfile?.runtimeId, peerId);
const transport = firstProfile?.transports?.[0];
assert.equal(transport?.protocol, "tcp_framed");
assert.equal(transport?.channels?.[0]?.delivery, "reliable");
assert.equal(transport?.channels?.[0]?.ordering, "ordered");
}
function assertRudpReconnectPolicy(peerName: string, value: unknown): void {
assert.ok(value && typeof value === "object", `${peerName} did not advertise an RUDP reconnect policy`);
const policy = value as {
schemaVersion?: string;
policyId?: string;
baseDelayMs?: number;
maxDelayMs?: number;
maxJitterMs?: number;
};
assert.equal(policy.schemaVersion, "cultnet.reconnect_policy.v0");
assert.equal(policy.policyId, "default");
assert.equal(policy.baseDelayMs, 1_000);
assert.equal(policy.maxDelayMs, 30_000);
assert.equal(policy.maxJitterMs, 250);
}
async function requestCultMeshFromPython(port: number, request: Record<string, unknown>): Promise<any> {
const socket = connectTcp(port, "127.0.0.1");
const framer = new LengthPrefixedMessageFramer();
await once(socket, "connect");
return await new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
socket.destroy();
reject(new Error(`Timed out waiting for Python CultMesh response to ${request.schemaVersion}.`));
}, 5000);
socket.on("data", (chunk) => {
for (const frame of framer.push(chunk)) {
clearTimeout(timeout);
socket.end();
resolve(decode(frame));
}
});
socket.once("error", (error) => {
clearTimeout(timeout);
reject(error);
});
socket.write(encodeFrame(encode(request)));
});
}
async function runPythonCultMeshClient(port: number): Promise<any> {
const script = [
"import json",
"from cultmesh_py import CultMeshDiscoveryClient",
`client = CultMeshDiscoveryClient("127.0.0.1", ${port}, timeout_seconds=4.0)`,
"verses = client.fetch_verses(transport_version='cultmesh.v0')",
"peers = client.fetch_peers(verse_id='python-interop', roles=['read-replica'])",
"print(json.dumps({",
" 'verses': [{'verseId': verse.verse_id, 'transportVersion': verse.compatibility.transport_version} for verse in verses],",
" 'peers': [{'peerId': peer.peer_id, 'roles': list(peer.roles)} for peer in peers],",
"}))",
].join("\n");
const { stdout } = await execFileAsync(pythonCommand, ["-c", script], {
cwd: cultcachePyRoot,
env: {
...process.env,
PYTHONPATH: cultcachePySrc,
},
timeout: 8000,
});
return JSON.parse(stdout);
}
async function runPythonCultNetRawClient(port: number): Promise<any> {
const script = [
"import json",
"from cultnet_py import CultNetRawClient",
`client = CultNetRawClient("127.0.0.1", ${port}, timeout_seconds=4.0)`,
"catalog = client.fetch_schema_catalog(kinds=['wire_message'])",
`snapshot = client.fetch_snapshot(schema_ids=['${interopNoteSchemaId}'])`,
`shards = client.fetch_shard_catalog(schema_ids=['${interopNoteSchemaId}'])`,
"print(json.dumps({",
" 'wireSchemaCount': len(catalog.get('schemas', [])),",
" 'snapshotRecordKeys': [document.get('recordKey') for document in snapshot.get('documents', [])],",
" 'shards': [{'shardId': shard.get('shardId'), 'epoch': shard.get('epoch')} for shard in shards.get('shards', [])],",
"}))",
].join("\n");
const { stdout } = await execFileAsync(pythonCommand, ["-c", script], {
cwd: cultcachePyRoot,
env: {
...process.env,
PYTHONPATH: cultcachePySrc,
},
timeout: 8000,
});
return JSON.parse(stdout);
}
async function runPythonCultMeshNodeSync(port: number): Promise<any> {
const schemaPathJson = JSON.stringify(interopSchemaPath);
const script = [
"import json",
"from pathlib import Path",
"from cultcache_py import define_database_entry_type",
"from cultmesh_py import create_node",
"from cultnet_py import CultNetRawClient",
`schema_json = Path(${schemaPathJson}).read_text(encoding='utf-8')`,
"note_doc = define_database_entry_type(",
" 'cultnet.interop-note',",
" [('schemaVersion', 0), ('documentId', 1), ('authorRuntimeId', 2), ('title', 3), ('body', 4), ('tags', 5, [])],",
` schema_id='${interopNoteSchemaId}',`,
" schema_version='cultnet.interop_note.v0',",
" canonical_schema_json=schema_json,",
")",
"node = create_node(runtime_id='python-node-sync')",
"node.register_document(note_doc)",
`client = CultNetRawClient('127.0.0.1', ${port}, timeout_seconds=4.0)`,
`changes = node.sync_snapshot(client, schema_ids=['${interopNoteSchemaId}'], record_keys=['note:python-peer'])`,
"note = node.get_required(note_doc, 'note:python-peer')",
"print(json.dumps({",
" 'synced': [{'schemaId': change.schema_id, 'recordKey': change.record_key} for change in changes],",
" 'note': note,",
"}))",
].join("\n");
const { stdout } = await execFileAsync(pythonCommand, ["-c", script], {
cwd: cultcachePyRoot,
env: {
...process.env,
PYTHONPATH: cultcachePySrc,
},
timeout: 8000,
});
return JSON.parse(stdout);
}
async function runPythonCultMeshNodeEmit(port: number): Promise<any> {
const schemaPathJson = JSON.stringify(interopSchemaPath);
const script = [
"import json",
"import socket",
"import time",
"from pathlib import Path",
"from cultcache_py import define_database_entry_type",
"from cultmesh_py import create_node",
"from cultnet_py import CultNetRawClient, write_frame",
`schema_json = Path(${schemaPathJson}).read_text(encoding='utf-8')`,
"note_doc = define_database_entry_type(",
" 'cultnet.interop-note',",
" [('schemaVersion', 0), ('documentId', 1), ('authorRuntimeId', 2), ('title', 3), ('body', 4), ('tags', 5, [])],",
` schema_id='${interopNoteSchemaId}',`,
" schema_version='cultnet.interop_note.v0',",
" canonical_schema_json=schema_json,",
")",
"writer = create_node(runtime_id='python-node-emit')",
"writer.register_document(note_doc)",
"message = writer.put_raw_message(",
" note_doc,",
" 'note:python-node-emit',",
" {",
" 'schemaVersion': 'cultnet.interop_note.v0',",
" 'documentId': 'note:python-node-emit',",
" 'authorRuntimeId': 'python-node-emit',",
" 'title': 'Python node emitted raw put',",
" 'body': 'node-level Python emit reached the peer',",
" 'tags': ['python', 'node', 'emit'],",
" },",
" message_id='python-node-emit-put',",
" shard_id='interop',",
" shard_epoch=1,",
")",
`with socket.create_connection(('127.0.0.1', ${port}), timeout=4.0) as connection:`,
" stream = connection.makefile('rwb')",
" write_frame(stream, message.to_bytes())",
" stream.flush()",
"reader = create_node(runtime_id='python-node-emit-reader')",
"reader.register_document(note_doc)",
`client = CultNetRawClient('127.0.0.1', ${port}, timeout_seconds=4.0)`,
"changes = []",
"for _ in range(20):",
` changes = reader.sync_snapshot(client, schema_ids=['${interopNoteSchemaId}'], record_keys=['note:python-node-emit'])`,
" if changes:",
" break",
" time.sleep(0.1)",
"note = reader.get_required(note_doc, 'note:python-node-emit')",
"print(json.dumps({",
" 'synced': [{'schemaId': change.schema_id, 'recordKey': change.record_key} for change in changes],",
" 'note': note,",
"}))",
].join("\n");
const { stdout } = await execFileAsync(pythonCommand, ["-c", script], {
cwd: cultcachePyRoot,
env: {
...process.env,
PYTHONPATH: cultcachePySrc,
},
timeout: 8000,
});
return JSON.parse(stdout);
}
async function runPythonCultNetSubscription(port: number): Promise<any> {
const schemaPathJson = JSON.stringify(interopSchemaPath);
const script = [
"import json",
"from pathlib import Path",
"from cultcache_py import define_database_entry_type",
"from cultmesh_py import create_node",
"from cultnet_py import CultNetRawClient",
`schema_json = Path(${schemaPathJson}).read_text(encoding='utf-8')`,
"note_doc = define_database_entry_type(",
" 'cultnet.interop-note',",
" [('schemaVersion', 0), ('documentId', 1), ('authorRuntimeId', 2), ('title', 3), ('body', 4), ('tags', 5, [])],",
` schema_id='${interopNoteSchemaId}',`,
" schema_version='cultnet.interop_note.v0',",
" canonical_schema_json=schema_json,",
")",
"node = create_node(runtime_id='python-sub-client')",
"node.register_document(note_doc)",
"message = node.put_raw_message(",
" note_doc,",
" 'note:python-sub-client',",
" {",
" 'schemaVersion': 'cultnet.interop_note.v0',",
" 'documentId': 'note:python-sub-client',",
" 'authorRuntimeId': 'python-sub-client',",
" 'title': 'Python subscription client emitted raw put',",
" 'body': 'subscription client saw its own live change',",
" 'tags': ['python', 'subscription'],",
" },",
" message_id='python-sub-client-put',",
" shard_id='interop',",
" shard_epoch=1,",
")",
`client = CultNetRawClient('127.0.0.1', ${port}, timeout_seconds=4.0)`,
`with client.subscribe_database(subscription_id='python-public-sub', schema_ids=['${interopNoteSchemaId}'], record_keys=['note:python-sub-client'], include_snapshot=False) as subscription:`,
" subscription.send(message)",
" change = subscription.read_next()",
"print(json.dumps({",
" 'subscriptionId': change.get('subscriptionId'),",
" 'changeKind': change.get('changeKind'),",
" 'recordKey': change.get('document', {}).get('recordKey'),",
"}))",
].join("\n");
const { stdout } = await execFileAsync(pythonCommand, ["-c", script], {
cwd: cultcachePyRoot,
env: {
...process.env,
PYTHONPATH: cultcachePySrc,
},
timeout: 8000,
});
return JSON.parse(stdout);
}
async function runPythonCultMeshPredictionReconcile(port: number): Promise<any> {
const schemaPathJson = JSON.stringify(interopSchemaPath);
const script = [
"import json",
"import tempfile",
"from pathlib import Path",
"from cultcache_py import define_database_entry_type",
"from cultmesh_py import CultMesh, CultMeshGameSessionOptions",
"from cultnet_py import CultNetClientAuthorityScope, CultNetRawClient",
`schema_json = Path(${schemaPathJson}).read_text(encoding='utf-8')`,
"note_doc = define_database_entry_type(",
" 'cultnet.interop-note',",
" [('schemaVersion', 0), ('documentId', 1), ('authorRuntimeId', 2), ('title', 3), ('body', 4), ('tags', 5, [])],",
` schema_id='${interopNoteSchemaId}',`,
" schema_version='cultnet.interop_note.v0',",
" canonical_schema_json=schema_json,",
")",
"with tempfile.TemporaryDirectory() as tmp:",
" node = CultMesh.create_node(Path(tmp) / 'prediction.cc', runtime_id='python-predict-client')",
" node.register_document(note_doc)",
" session = CultMesh.create_game_session(",
" node,",
" CultMeshGameSessionOptions(client_authority_scopes=(",
` CultNetClientAuthorityScope('python-predict-client', schema_ids=('${interopNoteSchemaId}',), key_prefix='note:python-node-emit'),`,
" )),",
" )",
" session.predict(note_doc, 'note:python-node-emit', {",
" 'schemaVersion': 'cultnet.interop_note.v0',",
" 'documentId': 'note:python-node-emit',",
" 'authorRuntimeId': 'python-predict-client',",
" 'title': 'Predicted Python node emit',",
" 'body': 'local predicted body',",
" 'tags': ['python', 'prediction'],",
" })",
` client = CultNetRawClient('127.0.0.1', ${port}, timeout_seconds=4.0)`,
" changes = session.sync_shard_log(client, shard_id='interop', shard_epoch=1, after_sequence=0)",
" note = node.get_required(note_doc, 'note:python-node-emit')",
" print(json.dumps({",
" 'reconciledKeys': [change.record_key for change in changes if change.change_kind == 'reconciled'],",
" 'changes': [{'kind': change.change_kind, 'key': change.record_key} for change in changes],",
" 'note': note,",
" }))",
].join("\n");
const { stdout } = await execFileAsync(pythonCommand, ["-c", script], {
cwd: cultcachePyRoot,
env: {
...process.env,
PYTHONPATH: cultcachePySrc,
},
timeout: 8000,
});
return JSON.parse(stdout);
}
async function requestPythonDatabaseChange(port: number, subscribe: Record<string, unknown>, put: Record<string, unknown>): Promise<any> {
const socket = connectTcp(port, "127.0.0.1");
const framer = new LengthPrefixedMessageFramer();
await once(socket, "connect");
return await new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
socket.destroy();
reject(new Error("Timed out waiting for Python database change response."));
}, 5000);
socket.on("data", (chunk) => {
for (const frame of framer.push(chunk)) {
const decoded = decode(frame) as any;
if (decoded?.schemaVersion === "cultnet.database_change_raw.v0") {
clearTimeout(timeout);
socket.end();
resolve(decoded);
}
}
});
socket.once("error", (error) => {
clearTimeout(timeout);
reject(error);
});
socket.write(encodeFrame(encode(subscribe)));
socket.write(encodeFrame(encode(put)));
});
}
async function requestPythonDatabaseDelete(
port: number,
subscribe: Record<string, unknown>,
put: Record<string, unknown>,
remove: Record<string, unknown>,
logRequest: Record<string, unknown>,
): Promise<{ change: any; log: any }> {
const socket = connectTcp(port, "127.0.0.1");
const framer = new LengthPrefixedMessageFramer();
await once(socket, "connect");
return await new Promise((resolve, reject) => {
const result: { change?: any; log?: any } = {};
let sawAdded = false;
const timeout = setTimeout(() => {
socket.destroy();
reject(new Error("Timed out waiting for Python delete change/log responses."));
}, 5000);
socket.on("data", (chunk) => {
for (const frame of framer.push(chunk)) {
const decoded = decode(frame) as any;
if (decoded?.schemaVersion === "cultnet.database_change_raw.v0" && decoded.changeKind === "added") {
sawAdded = true;
socket.write(encodeFrame(encode(remove)));
socket.write(encodeFrame(encode(logRequest)));
} else if (decoded?.schemaVersion === "cultnet.database_change_raw.v0" && decoded.changeKind === "removed") {
result.change = decoded;
} else if (decoded?.schemaVersion === "cultnet.shard_log_response.v0") {
result.log = decoded;
}
if (sawAdded && result.change && result.log) {
clearTimeout(timeout);
socket.end();
resolve({ change: result.change, log: result.log });
}
}
});
socket.once("error", (error) => {
clearTimeout(timeout);
reject(error);
});
socket.write(encodeFrame(encode(subscribe)));
socket.write(encodeFrame(encode(put)));
});
}
async function requestPythonShardCatalogAndLog(
port: number,
catalogRequest: Record<string, unknown>,
put: Record<string, unknown>,
logRequest: Record<string, unknown>,
): Promise<{ catalog: any; log: any }> {
const socket = connectTcp(port, "127.0.0.1");
const framer = new LengthPrefixedMessageFramer();
await once(socket, "connect");
return await new Promise((resolve, reject) => {
const result: { catalog?: any; log?: any } = {};
const timeout = setTimeout(() => {
socket.destroy();
reject(new Error("Timed out waiting for Python shard catalog/log responses."));
}, 5000);
socket.on("data", (chunk) => {
for (const frame of framer.push(chunk)) {
const decoded = decode(frame) as any;
if (decoded?.schemaVersion === "cultnet.shard_catalog_response.v0") {
result.catalog = decoded;
socket.write(encodeFrame(encode(put)));
socket.write(encodeFrame(encode(logRequest)));
} else if (decoded?.schemaVersion === "cultnet.shard_log_response.v0") {
result.log = decoded;
}
if (result.catalog && result.log) {
clearTimeout(timeout);
socket.end();
resolve({ catalog: result.catalog, log: result.log });
}
}
});
socket.once("error", (error) => {
clearTimeout(timeout);
reject(error);
});
socket.write(encodeFrame(encode(catalogRequest)));
});
}
async function requestPythonSimulationCandidate(port: number, observation: Record<string, unknown>): Promise<any> {
const socket = connectTcp(port, "127.0.0.1");
const framer = new LengthPrefixedMessageFramer();
await once(socket, "connect");
return await new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
socket.destroy();
reject(new Error("Timed out waiting for Python simulation consensus candidate."));
}, 5000);
socket.on("data", (chunk) => {
for (const frame of framer.push(chunk)) {
const decoded = decode(frame) as any;
if (decoded?.schemaVersion === "cultnet.simulation_consensus_candidate.v0") {
clearTimeout(timeout);
socket.end();
resolve(decoded);
}
}
});
socket.once("error", (error) => {
clearTimeout(timeout);
reject(error);
});
socket.write(encodeFrame(encode(observation)));
});
}
function computeSimulationClaimHash(...parts: string[]): string {
return createHash("sha256").update(parts.join("\x1f"), "utf8").digest("hex");
}
function createSimulationFactKey(candidate: any): string {
const factId = createHash("sha256")
.update([
candidate.shardId,
String(candidate.shardEpoch),
String(candidate.frame),
candidate.subjectId,
candidate.claimKind,
].join("\x1f"), "utf8")
.digest("hex");
return `simulation:${factId}`;
}
async function requestPythonSnapshot(port: number, request: Record<string, unknown>): Promise<any> {
const socket = connectTcp(port, "127.0.0.1");
const framer = new LengthPrefixedMessageFramer();
await once(socket, "connect");
return await new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
socket.destroy();
reject(new Error("Timed out waiting for Python snapshot response."));
}, 5000);
socket.on("data", (chunk) => {
for (const frame of framer.push(chunk)) {
const decoded = decode(frame) as any;
if (decoded?.schemaVersion === "cultnet.snapshot_response_raw.v0") {
clearTimeout(timeout);
socket.end();
resolve(decoded);
}
}
});
socket.once("error", (error) => {
clearTimeout(timeout);
reject(error);
});
socket.write(encodeFrame(encode(request)));
});
}
async function putAndSnapshotPythonWitnessBundle(port: number, put: Record<string, unknown>): Promise<{ catalog: any; snapshot: any }> {
const socket = connectTcp(port, "127.0.0.1");
const framer = new LengthPrefixedMessageFramer();
await once(socket, "connect");
return await new Promise((resolve, reject) => {
const result: { catalog?: any; snapshot?: any } = {};
const timeout = setTimeout(() => {
socket.destroy();
reject(new Error("Timed out waiting for Python witness artifact bundle roundtrip."));
}, 5000);
socket.on("data", (chunk) => {
for (const frame of framer.push(chunk)) {
const decoded = decode(frame) as any;
if (decoded?.schemaVersion === "cultnet.schema_catalog_response.v0") {
result.catalog = decoded;
socket.write(encodeFrame(encode(put)));
socket.write(encodeFrame(encode({
schemaVersion: "cultnet.snapshot_request.v0",
messageId: "python-witness-snapshot",
schemaIds: [witnessArtifactBundleSchemaId],
recordKeys: ["bundle:ts-python-witness"],
})));
} else if (decoded?.schemaVersion === "cultnet.snapshot_response_raw.v0") {
result.snapshot = decoded;
}
if (result.catalog && result.snapshot) {
clearTimeout(timeout);
socket.end();
resolve({ catalog: result.catalog, snapshot: result.snapshot });
}
}
});
socket.once("error", (error) => {
clearTimeout(timeout);
reject(error);
});
socket.write(encodeFrame(encode({
schemaVersion: "cultnet.schema_catalog_request.v0",
messageId: "python-witness-catalog",
includeSchemaJson: true,
schemaIds: [witnessArtifactBundleSchemaId],
})));
});
}
async function stopProcess(processState: RunningServeProcess): Promise<void> {
if (processState.child.exitCode !== null) {
return;
}
if (process.platform === "win32") {
await execFileAsync("taskkill.exe", ["/PID", String(processState.child.pid), "/T", "/F"], {
timeout: processCleanupTimeoutMs,
})
.catch(() => undefined);
await Promise.race([
once(processState.child, "exit").then(() => true),
delay(processCleanupTimeoutMs).then(() => false),
]);
return;
}
const gracefulExit = once(processState.child, "exit").then(() => true);
processState.child.kill("SIGTERM");
const exited = await Promise.race([
gracefulExit,
delay(2_000).then(() => false),
]);
if (exited || processState.child.exitCode !== null) {
return;
}
const forcedExit = once(processState.child, "exit").then(() => true);
processState.child.kill("SIGKILL");
const killed = await Promise.race([
forcedExit,
delay(2_000).then(() => false),
]);
if (!killed && processState.child.exitCode === null) {
throw new Error(`${processState.name} serve process did not exit after SIGKILL.`);
}
}
async function getFreePort(): Promise<number> {
const server = createServer();
server.listen(0, "127.0.0.1");
await once(server, "listening");
const address = server.address();
if (!address || typeof address === "string") {
throw new Error("Failed to allocate an ephemeral port.");
}
const { port } = address;
await new Promise<void>((resolve, reject) => {
server.close((error) => {
if (error) {
reject(error);
return;
}
resolve();
});
});
return port;
}
function findAdvertiseHost(): string {
const interfaces = networkInterfaces();
for (const entries of Object.values(interfaces)) {
for (const entry of entries ?? []) {
if (entry.family === "IPv4" && !entry.internal) {
return entry.address;
}
}
}
return "127.0.0.1";
}