Files
mdg-filius/packages/sim/test/localSimHost.test.ts
T
julian.kiedaischandClaude Opus 5.5 0fd42f10e2 feat(sim): F4.2 the sim protocol, the simulation room and LocalSimHost; M9 measured
packages/protocol holds the one list of /sim messages and checks every
field, naming what is wrong. packages/sim has the simulation room for F and
the web worker: the document's snapshot at start, the engine's clock on the
wall clock with Java's speed as cable delay, subscriptions to cable
activity, a card's new frames and a program's new window lines, capture and
clock kept over a stop, the command line as first action, a core failure
told to every client. LocalSimHost mirrors the document into the worker and
back without echo. M9 in Chromium: 17 examples give the same capture as in
Node, character for character; the room runs in a real worker.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 08:32:20 +00:00

143 lines
6.7 KiB
TypeScript

// LocalSimHost and the worker's side (serveRoom) over a MessageChannel, which
// clones as a worker's port does: the editor's document reaches the room,
// changes to it follow, and the room's messages come back parsed.
import { readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { describe, expect, it } from 'vitest';
import * as Y from 'yjs';
import { readProject, writeProject } from '@mdg-filius/doc';
import { openProjectArchive } from '@mdg-filius/fls';
import type { HostMessage } from '@mdg-filius/protocol';
import { CHANNEL_ORIGIN, LocalSimHost, serveRoom, type Port } from '../src/index.js';
const corpus = join(dirname(fileURLToPath(import.meta.url)), '../../../corpus');
function exampleDoc(): Y.Doc {
let counter = 0;
const file = join(corpus, 'java-examples/internetwork_komplett.fls');
const project = openProjectArchive(new Uint8Array(readFileSync(file)), () => `00000000-0000-0000-0000-${(++counter).toString(16).padStart(12, '0')}`);
const doc = new Y.Doc();
writeProject(doc, project);
return doc;
}
/** Waits until the messages so far hold one the test wants (the channel delivers on later turns). */
async function until<T>(find: () => T | undefined): Promise<T> {
for (let attempt = 0; attempt < 200; attempt += 1) {
const found = find();
if (found !== undefined) return found;
await new Promise((resolve) => setTimeout(resolve, 5));
}
throw new Error('never came');
}
function setUp() {
const doc = exampleDoc();
const channel = new MessageChannel();
let now = 0;
let tick = () => {};
const room = serveRoom(channel.port2 as unknown as Port, { now: () => now, seed: () => 14, every: (_, each) => (tick = each) });
const host = new LocalSimHost(doc, channel.port1 as unknown as Port, () => channel.port1.close());
const got: HostMessage[] = [];
host.onMessage((message) => got.push(message));
const run = (milliseconds: number) => {
for (let elapsed = 0; elapsed < milliseconds; elapsed += 200) {
now += 200;
tick();
}
};
return { doc, room, host, got, run, close: () => channel.port2.close() };
}
describe('the simulation in a worker of the page', () => {
it('runs the document the editor has, and sends back what comes of it', async () => {
const { doc, host, got, run, close } = setUp();
const notebook = readProject(doc).graph.nodes.find((node) => node.displayName === 'Notebook 1.10')!;
host.send({ v: 1, kind: 'sub', topics: [`trace:${notebook.ports[0]!.id}`] });
host.send({ v: 1, kind: 'start' });
await until(() => got.find((message) => message.kind === 'state' && message.mode === 'simulation'));
host.send({ v: 1, kind: 'input', node: notebook.id, app: 'terminal', action: 'enter', args: { line: 'ping 141.99.5.10' }, reqId: 'r1' });
await until(() => got.find((message) => message.kind === 'ack'));
run(6_000);
const frames = await until(() => {
const all = got.flatMap((message) => (message.kind === 'trace' ? message.frames : []));
return all.filter((frame) => frame.rows.some((row) => row.protocol === 'ICMP')).length >= 8 ? all : undefined;
});
expect(frames[0]!.rows[0]!.source).toMatch(/^[0-9A-F]{2}(:[0-9A-F]{2}){5}$/);
host.close();
close();
});
it("gives the room the editor's changes, which count from the next start", async () => {
const { doc, host, got, close } = setUp();
const notebook = readProject(doc).graph.nodes.find((node) => node.displayName === 'Notebook 1.10')!;
doc.getMap('nodes').delete(notebook.id);
host.send({ v: 1, kind: 'start' });
host.send({ v: 1, kind: 'input', node: notebook.id, app: 'terminal', action: 'enter', args: { line: 'ipconfig' }, reqId: 'r1' });
expect(await until(() => got.find((message) => message.kind === 'error'))).toMatchObject({ code: 'unknownNode', reqId: 'r1' });
host.close();
close();
});
it("takes the room's changes to the document without sending them back", async () => {
const doc = exampleDoc();
const channel = new MessageChannel();
const sent: unknown[] = [];
// A stand-in for the worker: it records what the page sends and changes its replica as a room would.
const replica = new Y.Doc();
channel.port2.addEventListener('message', (event) => {
const data = (event as MessageEvent).data as { type: string; update?: Uint8Array };
sent.push(data.type);
if (data.type === 'doc') Y.applyUpdate(replica, data.update!, CHANNEL_ORIGIN);
});
channel.port2.start();
const host = new LocalSimHost(doc, channel.port1 as unknown as Port, () => channel.port1.close());
await until(() => (sent.length > 0 ? true : undefined));
const before = sent.length;
const update = (() => {
let captured: Uint8Array | undefined;
replica.on('update', (each: Uint8Array) => (captured = each));
replica.getMap('meta').set('note', 'from the room');
return captured!;
})();
channel.port2.postMessage({ type: 'doc', update });
await until(() => (doc.getMap('meta').get('note') === 'from the room' ? true : undefined));
await new Promise((resolve) => setTimeout(resolve, 20));
expect(sent.length).toBe(before);
host.close();
channel.port2.close();
});
it('tells the editor of a message it does not know, as an error', async () => {
const doc = exampleDoc();
const channel = new MessageChannel();
channel.port2.start();
const host = new LocalSimHost(doc, channel.port1 as unknown as Port, () => channel.port1.close());
const got: HostMessage[] = [];
host.onMessage((message) => got.push(message));
channel.port2.postMessage({ type: 'host', message: { v: 1, kind: 'node', id: 'x' } });
expect(await until(() => got[0])).toEqual({ v: 1, kind: 'error', code: 'unknownKind', detail: 'kind node' });
host.close();
channel.port2.close();
});
it("sends the editor's changes to the worker, and nothing of them back", async () => {
const doc = exampleDoc();
const channel = new MessageChannel();
const fromWorker: string[] = [];
channel.port1.addEventListener('message', (event) => fromWorker.push(((event as MessageEvent).data as { type: string }).type));
const room = serveRoom(channel.port2 as unknown as Port, { now: () => 0, seed: () => 14, every: () => {} });
const host = new LocalSimHost(doc, channel.port1 as unknown as Port, () => channel.port1.close());
await until(() => (fromWorker.includes('host') ? true : undefined));
doc.getMap('meta').set('note', 'changed');
host.send({ v: 1, kind: 'start' });
await until(() => (room.currentMode === 'simulation' ? true : undefined));
await new Promise((resolve) => setTimeout(resolve, 20));
expect(fromWorker.filter((type) => type === 'doc')).toEqual([]);
host.close();
channel.port2.close();
});
});