Files
mdg-filius/packages/core/test/apps.test.ts
T
julian.kiedaischandClaude Opus 5.5 e31da62ae0 feat: F4.6g1 the echo server and the generic client windows
Oracle EchoWindowRun uses Java's echo server and client windows as a
person does; SaveAfterRunRun echo saves what Java keeps after the run.
The room takes setActive with a port, connect/disconnect/send, sends
both views and writes the server's port and the client's target back.
The editor's windows show Java's buttons, fields and lines; a port that
is no port is marked and not handed over (Java's button throws).
Core fix: an empty message (one empty segment with the push, as Java's
erstelleSegmente makes) counted as a repeat and never arrived.

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

245 lines
11 KiB
TypeScript

// Programs' threads (threads.ts) and the echo server and generic client
// (apps/echo.ts) where echoJava.test.ts's stories do not go: how threads
// wake, a taken port, a name, no route, lost data, a new start.
import { describe, expect, it } from 'vitest';
import { deviceConfiguration, pause, waitUntil, type NetworkRuntimeEngine, type Steps, type TopologySnapshotInput } from '../src/index.js';
import { link, pc, startedEngine, uuid } from './fixtures.js';
const SERVER = uuid(0x2001);
const CLIENT = uuid(0x2002);
/** 10.0.0.1 (echo server, DNS server with echo.filius.de) and 10.0.0.2 (client), on one cable. */
function network(changes: Partial<TopologySnapshotInput> = {}): TopologySnapshotInput {
return {
nodes: [pc(SERVER, uuid(0x2011)), pc(CLIENT, uuid(0x2012))],
links: [link(SERVER, uuid(0x2011), CLIENT, uuid(0x2012))],
deviceConfigurations: new Map([
[SERVER, deviceConfiguration('10.0.0.1', '255.255.255.0', '', '')],
[CLIENT, deviceConfiguration('10.0.0.2', '255.255.255.0', '', '10.0.0.1')],
]),
echoServersByNodeId: new Map([[SERVER, { port: 55555, isActive: true }]]),
simpleClientsByNodeId: new Map([[CLIENT, { targetAddress: '', targetPort: 55555 }]]),
...changes,
};
}
const advance = (engine: NetworkRuntimeEngine, toMilliseconds: number) => engine.handle({ type: 'advance', toMilliseconds });
const kinds = (lines: readonly { kind: string }[]) => lines.map((line) => line.kind);
describe("programs' threads", () => {
it('wake when their condition holds, right after the event that made it so', () => {
const engine = startedEngine(network({ echoServersByNodeId: new Map(), simpleClientsByNodeId: new Map() }));
let flag = false;
const woken: number[] = [];
engine.startThread(
(function* (): Steps {
yield* waitUntil(() => flag);
woken.push(engine.state.currentTimeMilliseconds);
})(),
);
engine.handle({ type: 'schedule', deadlineMilliseconds: 300, kind: { kind: 'parityMarker', label: 'a' } });
advance(engine, 200);
flag = true;
expect(woken).toEqual([]);
advance(engine, 1_000);
expect(woken).toEqual([300]);
});
it('go on after a pause, after the threads that are ready at that time', () => {
const engine = startedEngine(network({ echoServersByNodeId: new Map(), simpleClientsByNodeId: new Map() }));
const order: string[] = [];
let ready = false;
engine.startThread(
(function* (): Steps {
yield pause(engine.state.currentTimeMilliseconds);
order.push(`paused ${engine.state.currentTimeMilliseconds}`);
})(),
);
engine.startThread(
(function* (): Steps {
yield* waitUntil(() => ready);
order.push(`waited ${engine.state.currentTimeMilliseconds}`);
})(),
);
ready = true;
engine.resumeThreads();
advance(engine, 0);
expect(order).toEqual(['waited 0', 'paused 0']);
});
it('wake from a wait by its time only once, not again in a later wait', () => {
const engine = startedEngine(network({ echoServersByNodeId: new Map(), simpleClientsByNodeId: new Map() }));
let flag = false;
const woken: string[] = [];
engine.startThread(
(function* (): Steps {
const first = yield* waitUntil(() => flag, 500);
woken.push(`first ${first} ${engine.state.currentTimeMilliseconds}`);
const second = yield* waitUntil(() => false, 2_000);
woken.push(`second ${second} ${engine.state.currentTimeMilliseconds}`);
})(),
);
engine.handle({ type: 'schedule', deadlineMilliseconds: 100, kind: { kind: 'parityMarker', label: 'a' } });
flag = true;
advance(engine, 3_000);
// The first wait ended at 100 by its condition; its wake-up at 500 does not end the second.
expect(woken).toEqual(['first true 100', 'second false 2000']);
});
it('resume in the order they began when the same event wakes them', () => {
const engine = startedEngine(network({ echoServersByNodeId: new Map(), simpleClientsByNodeId: new Map() }));
let flag = false;
const order: string[] = [];
for (const name of ['first', 'second', 'third']) {
engine.startThread(
(function* (): Steps {
yield* waitUntil(() => flag);
order.push(name);
})(),
);
}
flag = true;
engine.resumeThreads();
expect(order).toEqual(['first', 'second', 'third']);
});
it('do not wait for what already holds: no other thread runs in between', () => {
const engine = startedEngine(network({ echoServersByNodeId: new Map(), simpleClientsByNodeId: new Map() }));
let flag = false;
const order: string[] = [];
engine.startThread(
(function* (): Steps {
yield* waitUntil(() => flag);
order.push('woken');
})(),
);
engine.startThread(
(function* (): Steps {
flag = true;
yield* waitUntil(() => true);
order.push('went on');
})(),
);
expect(order).toEqual(['went on', 'woken']);
});
it('end with the simulation', () => {
const engine = startedEngine(network());
expect(engine.echoServer(SERVER)).toBeDefined();
engine.handle({ type: 'stop' });
expect(engine.echoServer(SERVER)).toBeUndefined();
expect(engine.simpleClient(CLIENT)).toBeUndefined();
});
});
describe('the echo server and the generic client', () => {
it('switches the server off when its port is taken (sw_serveranwendung_msg6)', () => {
const engine = startedEngine(network({ echoServersByNodeId: new Map([[SERVER, { port: 7, isActive: false }]]) }));
expect(engine.openTcpServerSocket(SERVER, 7)).toBeDefined();
const server = engine.echoServer(SERVER)!;
server.setActive(true);
advance(engine, 100);
expect(kinds(server.log.all)).toEqual(['acceptingStarted', 'serverSocketFailed', 'acceptingStopped']);
expect(server.isActive).toBe(false);
});
it('connects to a name through the DNS server', () => {
const engine = startedEngine(
network({
dnsServerConfigurationsByNodeId: new Map([[SERVER, { recursiveResolutionEnabled: false, records: [{ name: 'echo.filius.de', type: 'A', ttlSeconds: 3600, target: '10.0.0.1' }] }]]),
}),
);
const client = engine.simpleClient(CLIENT)!;
client.connect('echo.filius.de', 55555);
advance(engine, 3_000);
client.send('Hallo');
advance(engine, 6_000);
expect(kinds(client.log.all)).toEqual(['connected', 'sent', 'received']);
expect(engine.echoServer(SERVER)!.log.all[0]).toMatchObject({ kind: 'connectionAccepted', address: '10.0.0.2' });
});
it('fails to connect to a name no server knows, without a frame (sw_socket_msg1)', () => {
const engine = startedEngine(network({ deviceConfigurations: new Map([[CLIENT, deviceConfiguration('10.0.0.2', '255.255.255.0', '', '')]]) }));
const client = engine.simpleClient(CLIENT)!;
client.connect('echo.filius.de', 55555);
advance(engine, 5_000);
expect(client.log.all).toEqual([{ kind: 'connectFailed', failure: 'unresolved', timeMilliseconds: 0 }]);
expect(engine.capture.frames(uuid(0x2012))).toEqual([]);
});
it('fails at once to connect to an address without a route, without a frame (Java waits for ever)', () => {
const engine = startedEngine(network());
const client = engine.simpleClient(CLIENT)!;
advance(engine, 100);
client.connect('10.9.9.9', 55555);
expect(client.log.all).toEqual([{ kind: 'connectFailed', failure: 'failed', timeMilliseconds: 100 }]);
advance(engine, 5_000);
expect(engine.capture.frames(uuid(0x2012))).toEqual([]);
});
it('reports a send that no one acknowledges and the connection it ends (sw_tcpsocket_msg8)', () => {
const engine = startedEngine(network());
const client = engine.simpleClient(CLIENT)!;
client.connect('10.0.0.1', 55555);
advance(engine, 3_000);
engine.setGlobalPacketLossEnabled(true);
client.send('verloren');
advance(engine, 40_000);
expect(client.log.all.filter((line) => line.kind === 'sendFailed')).toMatchObject([{ kind: 'sendFailed', failure: 'timedOut' }]);
expect(kinds(client.log.all)).toContain('connectionClosed');
expect(client.isConnected).toBe(false);
});
it('starts anew with a new simulation: from the snapshot, with empty windows', () => {
const engine = startedEngine(network());
engine.echoServer(SERVER)!.setActive(false);
engine.simpleClient(CLIENT)!.send('Hallo');
engine.handle({ type: 'stop' });
engine.handle({ type: 'start', snapshot: engine.state.topologySnapshot, seed: 14, initialTimeMilliseconds: 0 });
expect(engine.echoServer(SERVER)!.isActive).toBe(true);
expect(engine.echoServer(SERVER)!.log.all).toEqual([]);
expect(engine.simpleClient(CLIENT)!.log.all).toEqual([]);
});
it('hands out a message that came before the connection closed (Java drops it)', () => {
const engine = startedEngine(network({ echoServersByNodeId: new Map(), simpleClientsByNodeId: new Map() }));
const listener = engine.openTcpServerSocket(SERVER, 80)!;
const client = engine.openTcpClientSocket(CLIENT, '10.0.0.1', 80)!;
expect(engine.connectTcpWithResult(client)).toBe('connected');
const accepted = engine.state.tcpAcceptedSocketIdsByListenerId.get(listener)![0]!;
advance(engine, engine.state.currentTimeMilliseconds + 100);
expect(engine.sendTcp(accepted, new TextEncoder().encode('zuletzt'))).toBe(true);
engine.closeTcpSocket(accepted);
advance(engine, engine.state.currentTimeMilliseconds + 1_000);
expect(engine.state.socketsById.get(client)!.tcpState).not.toBe('ESTABLISHED');
const received: string[] = [];
engine.startThread(
(function* (): Steps {
for (;;) {
const next = yield* engine.receiveTcpSteps(client);
received.push(next.type === 'message' ? new TextDecoder().decode(next.payload) : next.type);
if (next.type !== 'message') return;
}
})(),
);
expect(received).toEqual(['zuletzt', 'notConnected']);
});
// Measured with EchoWindowRun (packages/sim/test/echoWindow.java.json): Java's echo answers an empty message.
it('echoes an empty message: one empty segment with the push, once, and the next message after it', () => {
const engine = startedEngine(network());
const client = engine.simpleClient(CLIENT)!;
client.connect('10.0.0.1', 55555);
advance(engine, 6_000);
client.send('');
advance(engine, 10_000);
client.send('danach');
advance(engine, 14_000);
const server = engine.echoServer(SERVER)!;
expect(server.log.all.filter((line) => line.kind === 'received' || line.kind === 'sent').map((line) => `${line.kind}:${'text' in line ? line.text : ''}`)).toEqual(['received:', 'sent:', 'received:danach', 'sent:danach']);
expect(client.log.all.filter((line) => line.kind === 'received').map((line) => ('text' in line ? line.text : ''))).toEqual(['', 'danach']);
});
});