Files
nucleic/cloud/nucleic-edge/test/room.test.ts
T

239 lines
8.7 KiB
TypeScript

// node --experimental-strip-types --test
//
// Exercises the Room DO's per-peer routing (mesh P2): client→host frames get a sender tag, and
// host→client frames are demuxed by tag to exactly one client. `fetch` isn't tested here (it needs
// the runtime's WebSocketPair); we drive `webSocketMessage` against fake sockets pushed into a fake
// DurableObjectState, which is where the routing logic lives.
import { test } from "node:test";
import assert from "node:assert/strict";
import { Room } from "../src/room.ts";
interface Meta {
role: "host" | "client";
deviceId: string;
/** Precomputed routing-tag hex (optional — old attachments lack it; routing backfills). */
tagHex?: string;
}
class FakeWS {
tags: string[] = [];
sent: Array<ArrayBuffer | string> = [];
closed: { code: number; reason?: string } | null = null;
meta: Meta;
constructor(meta: Meta) {
this.meta = meta;
}
serializeAttachment(a: Meta): void {
this.meta = a;
}
deserializeAttachment(): Meta {
return this.meta;
}
send(m: ArrayBuffer | string): void {
if (this.closed) throw new Error("closed");
this.sent.push(m);
}
close(code: number, reason?: string): void {
this.closed = { code, reason };
}
}
class FakeState {
sockets: FakeWS[] = [];
id = { toString: () => "room-do-id" };
add(ws: FakeWS): void {
ws.tags = [ws.deserializeAttachment().role, ws.deserializeAttachment().deviceId];
this.sockets.push(ws);
}
getWebSockets(tag?: string): FakeWS[] {
return tag ? this.sockets.filter((s) => s.tags.includes(tag)) : this.sockets;
}
acceptWebSocket(): void {
/* not exercised here */
}
}
function room(state: FakeState): Room {
return new Room(state as unknown as DurableObjectState, {} as never);
}
async function deviceTag(deviceId: string): Promise<Uint8Array> {
const d = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(deviceId));
return new Uint8Array(d).slice(0, 8);
}
function ws(role: "host" | "client", deviceId: string): FakeWS {
const w = new FakeWS({ role, deviceId });
return w;
}
// MARK: - Runner wake (relay presence → pool wake, CARBON_RUNNER §3)
function roomWithEnv(state: FakeState, env: Record<string, string>): Room {
return new Room(state as unknown as DurableObjectState, env as never);
}
const RUNNER_ROOM = "a".repeat(64);
test("runner wake fires once with the bearer + roomId, then throttles", async () => {
const r = roomWithEnv(new FakeState(), {
RUNNER_WAKE_URL: "https://runner/v1/hook/wake",
RUNNER_WAKE_SECRET: "s3cret",
});
const calls: Array<{ url: string; init: RequestInit }> = [];
const fetcher = (async (url: RequestInfo | URL, init?: RequestInit) => {
calls.push({ url: String(url), init: init ?? {} });
return new Response("ok");
}) as typeof fetch;
assert.equal(await r.maybeWakeRunner(RUNNER_ROOM, 1_000, fetcher), true);
assert.equal(calls.length, 1);
assert.equal(calls[0].url, "https://runner/v1/hook/wake");
const headers = calls[0].init.headers as Record<string, string>;
assert.equal(headers.authorization, "Bearer s3cret");
assert.deepEqual(JSON.parse(String(calls[0].init.body)), { roomId: RUNNER_ROOM });
// Inside the 30s window: suppressed (a reconnect storm must not hammer the hook)…
assert.equal(await r.maybeWakeRunner(RUNNER_ROOM, 20_000, fetcher), false);
assert.equal(calls.length, 1);
// …after it: fires again.
assert.equal(await r.maybeWakeRunner(RUNNER_ROOM, 40_000, fetcher), true);
assert.equal(calls.length, 2);
});
test("runner wake is a no-op when unconfigured or the roomId is missing", async () => {
const fetcher = (async () => {
throw new Error("must not be called");
}) as unknown as typeof fetch;
// No env at all (an ordinary relay deploy).
assert.equal(await roomWithEnv(new FakeState(), {}).maybeWakeRunner(RUNNER_ROOM, 0, fetcher), false);
// URL without secret, secret without URL, missing roomId.
assert.equal(
await roomWithEnv(new FakeState(), { RUNNER_WAKE_URL: "https://r" }).maybeWakeRunner(RUNNER_ROOM, 0, fetcher),
false,
);
assert.equal(
await roomWithEnv(new FakeState(), { RUNNER_WAKE_SECRET: "s" }).maybeWakeRunner(RUNNER_ROOM, 0, fetcher),
false,
);
assert.equal(
await roomWithEnv(new FakeState(), {
RUNNER_WAKE_URL: "https://r",
RUNNER_WAKE_SECRET: "s",
}).maybeWakeRunner(null, 0, fetcher),
false,
);
});
test("runner wake swallows an unreachable control plane", async () => {
const r = roomWithEnv(new FakeState(), {
RUNNER_WAKE_URL: "https://runner/v1/hook/wake",
RUNNER_WAKE_SECRET: "s3cret",
});
const fetcher = (async () => {
throw new Error("connect refused");
}) as unknown as typeof fetch;
// Still counts as an attempt (throttled) and never throws into the connect path.
assert.equal(await r.maybeWakeRunner(RUNNER_ROOM, 0, fetcher), true);
});
test("client→host frame is delivered to the host with a sender tag prepended", async () => {
const state = new FakeState();
const host = ws("host", "host-dev");
const phone = ws("client", "phone-A");
state.add(host);
state.add(phone);
const payload = new Uint8Array([1, 2, 3, 4]).buffer;
await room(state).webSocketMessage(phone as never, payload);
assert.equal(host.sent.length, 1);
const framed = new Uint8Array(host.sent[0] as ArrayBuffer);
assert.equal(framed[0], 0x01); // envelope version
assert.deepEqual(framed.subarray(1, 9), await deviceTag("phone-A")); // sender tag
assert.deepEqual(framed.subarray(9), new Uint8Array([1, 2, 3, 4])); // untouched payload
assert.equal(phone.sent.length, 0); // not echoed back to the sender
});
test("host→client frame is demuxed by tag to exactly the addressed client", async () => {
const state = new FakeState();
const host = ws("host", "host-dev");
const phoneA = ws("client", "phone-A");
const phoneB = ws("client", "phone-B");
state.add(host);
state.add(phoneA);
state.add(phoneB);
const raw = new Uint8Array([9, 8, 7]);
const tag = await deviceTag("phone-B");
const framed = new Uint8Array(9 + raw.length);
framed[0] = 0x01;
framed.set(tag, 1);
framed.set(raw, 9);
await room(state).webSocketMessage(host as never, framed.buffer);
assert.equal(phoneB.sent.length, 1);
assert.deepEqual(new Uint8Array(phoneB.sent[0] as ArrayBuffer), raw); // envelope stripped
assert.equal(phoneA.sent.length, 0); // NOT broadcast to the other client
assert.equal(host.sent.length, 0);
});
test("host frame with an unknown tag is dropped, not broadcast", async () => {
const state = new FakeState();
const host = ws("host", "host-dev");
const phoneA = ws("client", "phone-A");
state.add(host);
state.add(phoneA);
const framed = new Uint8Array(9 + 2);
framed[0] = 0x01;
framed.set(await deviceTag("phone-ghost"), 1);
framed.set(new Uint8Array([5, 5]), 9);
await room(state).webSocketMessage(host as never, framed.buffer);
assert.equal(phoneA.sent.length, 0);
});
test("routing backfills tagHex into a pre-tagHex attachment and then routes off it", async () => {
const state = new FakeState();
const host = ws("host", "host-dev");
const phone = ws("client", "phone-A"); // attachment has no tagHex (old hibernated socket)
state.add(host);
state.add(phone);
await room(state).webSocketMessage(phone as never, new Uint8Array([1]).buffer);
const expected = [...(await deviceTag("phone-A"))].map((b) => b.toString(16).padStart(2, "0")).join("");
assert.equal(phone.deserializeAttachment().tagHex, expected); // backfilled once
// A precomputed (even attacker-supplied-nonsense) tagHex is what routing trusts — pin that
// the fast path reads the attachment rather than re-deriving from deviceId.
const phoneOdd = new FakeWS({ role: "client", deviceId: "phone-B", tagHex: "00".repeat(8) });
state.add(phoneOdd);
const framed = new Uint8Array(9 + 1);
framed[0] = 0x01; // version + all-zero tag targets phoneOdd's stored tagHex
framed.set(new Uint8Array([7]), 9);
await room(state).webSocketMessage(host as never, framed.buffer);
assert.equal(phoneOdd.sent.length, 1);
});
test("client→host frame is dropped when no host is present", async () => {
const state = new FakeState();
const phoneA = ws("client", "phone-A");
state.add(phoneA);
await room(state).webSocketMessage(phoneA as never, new Uint8Array([1]).buffer);
assert.equal(phoneA.sent.length, 0); // nothing to route to; no throw
});
test("an oversized frame closes the socket with a policy code", async () => {
const state = new FakeState();
const host = ws("host", "host-dev");
const phone = ws("client", "phone-A");
state.add(host);
state.add(phone);
const huge = new ArrayBuffer(20 * 1024 * 1024 + 1);
await room(state).webSocketMessage(phone as never, huge);
assert.equal(phone.closed?.code, 1008);
assert.equal(host.sent.length, 0);
});