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:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user