feat(core): modems run — the pair in the project, and a modem of another project hands its frames on (K0–K2)

Measured first: tools/java-oracle's ModemRun joins two networks by a pair
of Java modems over TCP on localhost. The modem has no card and adds no
delay of its own — a frame takes the cables on both sides, nothing more.

The core pairs the two enabled modems on a port (Swift's inProject, with
its conditions and cycle suppression) on Java's timing: no 20 ms. The
modem example of FILIUS now pings through instead of failing the core.
A modem of another project (relay) gives its frames to whoever runs the
engine and puts on its cable what comes back, with this engine's frame
and packet numbers; a frame dies after MAXIMUM_RELAY_HOPS trips.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-11 18:12:27 +00:00
co-authored by Claude Opus 5.5
parent 6f583616f6
commit c85b0e45da
19 changed files with 1743 additions and 44 deletions
+40 -10
View File
@@ -7,7 +7,7 @@
import { readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { describe, expect, it } from 'vitest';
import { afterEach, describe, expect, it, vi } from 'vitest';
import * as Y from 'yjs';
import { NetworkRuntimeEngine, type Id } from '@mdg-filius/core';
import { docSnapshot, readProject, writeProject } from '@mdg-filius/doc';
@@ -27,6 +27,22 @@ function exampleDoc(file = 'java-examples/internetwork_komplett.fls'): Y.Doc {
}
/** A room on a clock the test moves, and a client that keeps what it gets. */
/**
* A core that fails at the first frame `node` sends — as it once did at a
* modem, before the core had modems. Every test lets it go again.
*/
function failingAt(node: Id): void {
const send = NetworkRuntimeEngine.prototype.sendEthernetFrame;
vi.spyOn(NetworkRuntimeEngine.prototype, 'sendEthernetFrame').mockImplementation(function (this: NetworkRuntimeEngine, from, port, frame) {
if (from === node) throw new Error('core broke');
send.call(this, from, port, frame);
});
}
afterEach(() => {
vi.restoreAllMocks();
});
function setUp(doc = exampleDoc()) {
let now = 1_000_000;
const room = new SimulationRoom(doc, { now: () => now, seed: () => SEED, epoch: () => 1_700_000_000_000 + now });
@@ -102,11 +118,12 @@ describe('a simulation room', () => {
});
it('forgets who started it when the core fails', () => {
const { room, run, node } = setUp(exampleDoc('java-examples/modem_beispiel.fls'));
const { room, run, node } = setUp();
failingAt(node('Webserver 5.10').id);
const mia: HostMessage[] = [];
const miaId = room.connect((message) => mia.push(message), 'Mia');
room.receive(miaId, { v: 1, kind: 'start' });
room.receive(miaId, { v: 1, kind: 'input', node: node('Notebook 0.10').id, app: 'terminal', action: 'enter', args: { line: 'ping 192.168.0.11' }, reqId: 'r' });
room.receive(miaId, { v: 1, kind: 'input', node: node('Notebook 1.10').id, app: 'terminal', action: 'enter', args: { line: 'ping 141.99.5.10' }, reqId: 'r' });
run(10_000);
expect(ofKind(mia, 'state').at(-1)).toMatchObject({ mode: 'design' });
expect(ofKind(mia, 'state').at(-1)).not.toHaveProperty('startedBy');
@@ -287,16 +304,16 @@ describe('a simulation room', () => {
});
it('tells every client when the core fails, and goes back to design mode', () => {
// FILIUS' modem example: remote links are not in the core yet, it throws at the first frame to a modem.
const { room, got, send, run, node } = setUp(exampleDoc('java-examples/modem_beispiel.fls'));
const { room, got, send, run, node } = setUp();
failingAt(node('Webserver 5.10').id);
const other: HostMessage[] = [];
room.connect((message) => other.push(message));
const ping = () => send({ kind: 'input', node: node('Notebook 0.10').id, app: 'terminal', action: 'enter', args: { line: 'ping 192.168.0.11' }, reqId: 'r' });
const ping = () => send({ kind: 'input', node: node('Notebook 1.10').id, app: 'terminal', action: 'enter', args: { line: 'ping 141.99.5.10' }, reqId: 'r' });
send({ kind: 'start' });
ping();
run(10_000);
for (const each of [got, other]) {
expect(ofKind(each, 'error')).toEqual([{ v: 1, kind: 'error', code: 'engineFailed', detail: expect.stringContaining('remote links') }]);
expect(ofKind(each, 'error')).toEqual([{ v: 1, kind: 'error', code: 'engineFailed', detail: 'core broke' }]);
expect(ofKind(each, 'state').at(-1)).toMatchObject({ mode: 'design' });
}
expect(room.currentMode).toBe('design');
@@ -307,6 +324,18 @@ describe('a simulation room', () => {
expect(ofKind(got, 'error')).toHaveLength(2);
});
it("runs FILIUS' modem example: a ping goes through the pair of modems", () => {
const { got, send, run, node } = setUp(exampleDoc('java-examples/modem_beispiel.fls'));
const notebook = node('Notebook 0.10');
send({ kind: 'sub', topics: [`app:${notebook.id}/terminal`] });
send({ kind: 'start' });
send({ kind: 'input', node: notebook.id, app: 'terminal', action: 'enter', args: { line: 'ping 192.168.0.11' }, reqId: 'r' });
run(10_000);
expect(ofKind(got, 'error')).toEqual([]);
const text = ofKind(got, 'app').flatMap((each) => each.lines as { text?: string }[]).map((line) => line.text ?? '').join('');
expect(text).toContain('4 Paket(e) gesendet, 4 Paket(e) empfangen');
});
it('sends a card anew to a client that watches it again', () => {
const { got, send, node, run } = setUp();
const notebook = node('Notebook 1.10');
@@ -340,14 +369,15 @@ describe('a simulation room', () => {
});
it('reads the document anew at the start after a failure', () => {
const doc = exampleDoc('java-examples/modem_beispiel.fls');
const doc = exampleDoc();
const { got, send, run, node } = setUp(doc);
const ping = () => send({ kind: 'input', node: node('Notebook 0.10').id, app: 'terminal', action: 'enter', args: { line: 'ping 192.168.0.11' }, reqId: 'r' });
failingAt(node('Webserver 5.10').id);
const ping = () => send({ kind: 'input', node: node('Notebook 1.10').id, app: 'terminal', action: 'enter', args: { line: 'ping 141.99.5.10' }, reqId: 'r' });
send({ kind: 'start' });
ping();
run(10_000);
expect(ofKind(got, 'error')).toHaveLength(1);
// Without the modems' cables, nothing reaches a modem: the next start runs.
// Without the cables, nothing reaches the web server, which fails the core: the next start runs.
doc.getMap('links').clear();
send({ kind: 'start' });
ping();