Add Cut 5's four QUIC realtime provider interop lanes

TypeScript provider vs C# native connector (the Unity path, gated to
Windows since the native connector under test is); TypeScript provider vs
C# managed connector, with a golden-bytes check comparing the C# test's
re-encoded frame against the TypeScript peer's own encoding of the same
frame; latest-only under a stalled consumer (two dials against one 200-frame
provider, one paused mid-stream via the peer script's new --pause-after),
asserting the provider's broadcasts stay bounded regardless of the pause and
both consumers converge on sequence 200.

Both dotnet-test lanes run `dotnet test --filter FullyQualifiedName~<test>
--logger "console;verbosity=detailed"`: detailed verbosity is what surfaces
a passed test's own TestContext.Out lines, which the golden-bytes lane reads
back out of stdout.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-09-23 15:10:44 +02:00
commit d526286d60

View file

@ -59,6 +59,8 @@ const csharpDllPath = resolve(
);
const cultMeshTsRoot = resolve(cultLibRoot, "packages", "cultmesh-ts");
const cultMeshQuicPeerScript = resolve(cultMeshTsRoot, "dist-test", "test", "interop", "cultmesh-quic-peer.js");
const quicNativeTestsProject = resolve(cultLibRoot, "tests", "GameCult.Mesh.Quic.Native.Tests", "GameCult.Mesh.Quic.Native.Tests.csproj");
const quicManagedTestsProject = resolve(cultLibRoot, "tests", "GameCult.Mesh.Quic.Tests", "GameCult.Mesh.Quic.Tests.csproj");
const rustBinaryPath = resolve(
cultnetRsRoot,
"target",
@ -450,6 +452,180 @@ test("CultMesh QUIC realtime: C# managed provider and TypeScript consumer", asyn
}
});
// ---------------------------------------------------------------------------
// Cut 5: the TypeScript QUIC realtime provider (the StreamPixels role). Four
// lanes, spec section 10. Lane 1 is the headline: it proves the Unity
// overlay's transport, and is gated to Windows because the native connector
// under test (`CultMeshNativeQuicRealtimeTransportConnector`) is.
// ---------------------------------------------------------------------------
test("CultMesh QUIC realtime: TypeScript provider and C# native connector (the Unity path)", async (t) => {
if (process.platform !== "win32") {
t.skip("The native connector is Windows-only; this lane runs on Windows.");
return;
}
if (!process.env.CULTMESH_QUIC_NATIVE_DIR) {
t.skip("CULTMESH_QUIC_NATIVE_DIR is not set to a built native bridge.");
return;
}
logInteropPhase("quic-provider-native", "build the typescript quic peer");
await buildCultMeshTsQuicPeer();
const servers: RunningServeProcess[] = [];
t.after(async () => {
for (const server of servers) {
server.child.stdin?.end();
server.child.kill();
}
await delay(processCleanupTimeoutMs);
});
logInteropPhase("quic-provider-native", "start the typescript QUIC provider");
servers.push(await spawnServeProcess("ts-quic-provider", {
command: process.execPath,
args: [cultMeshQuicPeerScript, "serve", "--frames", "5", "--delivery", "latest-only"],
cwd: cultMeshTsRoot,
env: { CULTMESH_QUIC_NATIVE_DIR: process.env.CULTMESH_QUIC_NATIVE_DIR ?? "" },
stdin: "pipe",
}));
const ready = (await servers[servers.length - 1].ready) as { status: string; endpoint: string };
assert.equal(ready.status, "ready");
assert.match(ready.endpoint, /^cultmesh-state\+quic:\/\/127\.0\.0\.1:\d+\?cert-sha256=[0-9A-F]{64}$/);
logInteropPhase("quic-provider-native", "dotnet test: the native connector dials the typescript provider");
const { stdout } = await runDotnetTestFilter(
quicNativeTestsProject,
"NativeConnectorReceivesFromExternalProvider",
{ CULTMESH_NATIVE_EXTERNAL_ENDPOINT: ready.endpoint },
);
assert.match(stdout, /Passed!/);
assert.doesNotMatch(stdout, /Skipped:\s*1/, `the env var should have made the test run, not skip.\n${stdout}`);
});
test("CultMesh QUIC realtime: TypeScript provider and C# managed connector, with golden bytes", async (t) => {
if (!process.env.CULTMESH_QUIC_NATIVE_DIR) {
t.skip("CULTMESH_QUIC_NATIVE_DIR is not set to a built native bridge.");
return;
}
logInteropPhase("quic-provider-managed", "build the typescript quic peer");
await buildCultMeshTsQuicPeer();
const servers: RunningServeProcess[] = [];
t.after(async () => {
for (const server of servers) {
server.child.stdin?.end();
server.child.kill();
}
await delay(processCleanupTimeoutMs);
});
logInteropPhase("quic-provider-managed", "start the typescript QUIC provider");
servers.push(await spawnServeProcess("ts-quic-provider-managed", {
command: process.execPath,
args: [cultMeshQuicPeerScript, "serve", "--frames", "5", "--delivery", "latest-only"],
cwd: cultMeshTsRoot,
env: { CULTMESH_QUIC_NATIVE_DIR: process.env.CULTMESH_QUIC_NATIVE_DIR ?? "" },
stdin: "pipe",
}));
const server = servers[servers.length - 1];
const ready = (await server.ready) as { status: string; endpoint: string };
assert.equal(ready.status, "ready");
logInteropPhase("quic-provider-managed", "dotnet test: the managed connector dials the typescript provider");
const { stdout } = await runDotnetTestFilter(
quicManagedTestsProject,
"ManagedConnectorReceivesFromExternalProvider",
{ CULTMESH_NATIVE_EXTERNAL_ENDPOINT: ready.endpoint },
);
assert.match(stdout, /Passed!/);
assert.doesNotMatch(stdout, /Skipped:\s*1/, `the env var should have made the test run, not skip.\n${stdout}`);
const goldenMatch = /CULTMESH_GOLDEN_HEX=([0-9A-Fa-f]+)/.exec(stdout);
assert.ok(goldenMatch, `expected the C# test to print its golden hex line.\n${stdout}`);
const csharpHex = goldenMatch![1].toLowerCase();
// The typescript peer logs the hex of its own encoding of frame 1 (the
// first, and here the only, frame the managed connector's single
// ReceiveAsync call can have gotten) to stderr as it sends it.
const tsSentLine = server.stderr
.join("")
.split("\n")
.map((line) => line.trim())
.filter(Boolean)
.map((line) => {
try {
return JSON.parse(line) as { event?: string; sent?: number; hex?: string };
} catch {
return undefined;
}
})
.find((entry) => entry?.event === "quic-realtime-serve" && entry.sent === 1);
assert.ok(tsSentLine?.hex, "expected the typescript peer to log frame 1's hex encoding");
assert.equal(csharpHex, tsSentLine!.hex, "TypeScript and C# must encode the same frame identically");
});
test("CultMesh QUIC realtime: latest-only under a stalled consumer converges without blocking the provider", async (t) => {
if (!process.env.CULTMESH_QUIC_NATIVE_DIR) {
t.skip("CULTMESH_QUIC_NATIVE_DIR is not set to a built native bridge.");
return;
}
logInteropPhase("quic-provider-stalled", "build the typescript quic peer");
await buildCultMeshTsQuicPeer();
const servers: RunningServeProcess[] = [];
t.after(async () => {
for (const server of servers) {
server.child.stdin?.end();
server.child.kill();
}
await delay(processCleanupTimeoutMs);
});
logInteropPhase("quic-provider-stalled", "start the typescript QUIC provider");
const started = Date.now();
servers.push(await spawnServeProcess("ts-quic-provider-stalled", {
command: process.execPath,
args: [cultMeshQuicPeerScript, "serve", "--frames", "200", "--delivery", "latest-only", "--interval-ms", "1"],
cwd: cultMeshTsRoot,
env: { CULTMESH_QUIC_NATIVE_DIR: process.env.CULTMESH_QUIC_NATIVE_DIR ?? "" },
stdin: "pipe",
}));
const ready = (await servers[servers.length - 1].ready) as { status: string; endpoint: string };
logInteropPhase("quic-provider-stalled", "dial twice: one healthy, one paused mid-stream");
const env = { CULTMESH_QUIC_NATIVE_DIR: process.env.CULTMESH_QUIC_NATIVE_DIR ?? "" };
const [healthy, paused] = await Promise.all([
runJsonCommand("ts-quic-dial-healthy", process.execPath, [
cultMeshQuicPeerScript,
"dial",
"--endpoint", ready.endpoint,
"--expect-sequence", "200",
"--timeout-ms", "20000",
], cultMeshTsRoot, env),
runJsonCommand("ts-quic-dial-paused", process.execPath, [
cultMeshQuicPeerScript,
"dial",
"--endpoint", ready.endpoint,
"--expect-sequence", "200",
"--pause-after", "5",
"--pause-ms", "500",
"--timeout-ms", "20000",
], cultMeshTsRoot, env),
]);
// The provider's 200 broadcasts (bounded per-frame by --interval-ms 1, so
// ~200ms of pacing) plus the 500ms pause plus process/network overhead
// must still land well inside the bound below regardless of the pause: the
// provider itself was never blocked by the paused consumer.
assert.ok(Date.now() - started < 15_000, "the provider's broadcasts must not be blocked by a stalled peer");
assert.equal(healthy.frames.at(-1).sequence, "200");
assert.equal(paused.frames.at(-1).sequence, "200");
// Coalescing means the paused consumer saw far fewer than 200 frames.
assert.ok(paused.frames.length < 200, `expected coalescing under the pause, got ${paused.frames.length} frames`);
});
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();
@ -2501,6 +2677,40 @@ async function buildCultMeshTsQuicPeer(): Promise<void> {
await cultMeshTsInteropPeerBuild;
}
/**
* Runs `dotnet test` against one project, filtered to a single test by name,
* with extra environment (normally `CULTMESH_NATIVE_EXTERNAL_ENDPOINT`).
* `verbosity=detailed` is what makes VSTest's console logger print a passed
* test's own `TestContext.Out` lines (the golden-bytes hex line, section
* 10's lane 4) instead of only failures.
*/
async function runDotnetTestFilter(
projectPath: string,
testName: string,
env: NodeJS.ProcessEnv,
): Promise<{ stdout: string; stderr: string }> {
try {
return await execFileAsync(
dotnetCommand,
[
"test",
projectPath,
"--filter",
`FullyQualifiedName~${testName}`,
"--logger",
"console;verbosity=detailed",
"-nologo",
],
{ cwd: cultLibRoot, env: { ...process.env, ...env }, timeout: jsonCommandTimeoutMs },
);
} catch (error) {
const execError = error as { stdout?: string; stderr?: string; message: string };
throw new Error(
`dotnet test ${testName} failed.\nstdout:\n${execError.stdout ?? ""}\nstderr:\n${execError.stderr ?? execError.message}`,
);
}
}
async function buildKotlinInteropPeer(): Promise<void> {
kotlinInteropPeerBuild ??= execFileAsync(
"powershell",