Files
mdg-filius/packages/core/test/webJava.test.ts
T
julian.kiedaischandClaude Opus 5.5 7dac57cbbe feat(core): the pages of routers and home routers after Java, measured against Java
Routers and home routers start a web server on port 80 with Java's index,
log and routes pages in the project's language (routerPages.ts); the
firewall keeps a log per node that outlives a new start; RIP routes carry
the telling router's public address. TCP counts sequence numbers and
segments in Java's characters, not bytes. RouterWebRun records six stories,
with firewalls now built by ScenarioNetwork. Not taken from Java: its router
server sometimes never listening (a race in setAktiv), and </titel>.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 17:41:52 +00:00

290 lines
13 KiB
TypeScript

// The web server and the web browser as Java FILIUS runs them.
// tools/java-oracle's WebRun played stories of the two programs on web-lan's
// network in Java FILIUS and wrote every frame each card saw, Java's tables
// of them, every line the server wrote into its window, everything the
// browser told its window, and the server's files (web-java-<story>.java.json).
// The engine plays the same stories with its programs (apps/web.ts), on a
// server with the same files: each card's frames must come in the same
// order, with the same content and rows, at about the same time; the
// server's lines and the browser's pages and images in the same order, at
// about the same time.
//
// The browser's error pages are its own (the editor's): for a status other
// than 200 the page is compared by its status and content type, not by
// Java's text for it.
import { readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { describe, expect, it } from 'vitest';
import {
javaBase64Decode,
parentPath,
VirtualFileSystem,
withSnapshot,
type LogLine,
type NetworkRuntimeEngine,
} from '../src/index.js';
import { expectSameCaptures, watchedEngine, withClientPortText, type JavaFrame, type JavaTables, type SeenFrame } from './javaRun.js';
import { browserText, clientText, isPolled, POLL_MILLISECONDS, serverText } from './programLines.js';
import { snapshotOf, type Scenario } from './scenario.js';
const scenarioDirectory = join(dirname(fileURLToPath(import.meta.url)), 'scenarios');
const BROWSER = '00000000-0000-0000-0000-000000000201';
const SERVER = '00000000-0000-0000-0000-000000000203';
type Program = 'server' | 'browser' | 'client';
/**
* Java's threads wake a millisecond or two late at each segment; over a page
* with its images (some 70 frames, one after the other) Java falls behind by
* up to some 130 ms (measured: byName, post, caseInsensitive).
*/
const TOLERANCE_MILLISECONDS = 200;
/** Java's timeout of a TCP attempt (3 round trips at 90 %). */
const JAVA_TIMEOUT_MILLISECONDS = 3 * 2_750;
interface JavaRun extends JavaTables {
frames: Record<string, JavaFrame[]>;
events: [number, Program, string][];
/** The server's files at the end: path, Java's type, content (images in Base64). */
files: [string, string, string][];
}
type Act = (engine: NetworkRuntimeEngine) => void;
const browser = (engine: NetworkRuntimeEngine) => engine.webBrowser(BROWSER)!;
const client = (engine: NetworkRuntimeEngine) => engine.simpleClient(BROWSER)!;
const open = (typed: string, post?: string): Act => (engine) => browser(engine).open(typed, post);
/** Each story's calls at their times, as WebRun makes them. */
const STORIES: Record<string, [number, Act][]> = {
page: [[500, open('192.168.0.30')]],
byName: [
[500, open('www.filius.de/seite.html')],
[6000, open('http:///text.txt')],
],
notFound: [[500, open('http://www.filius.de/nichts.html')]],
vhost: [
[500, open('seite.filius.de')],
[5000, open('andere.filius.de')],
[9500, open('www.filius.de/vhost1/')],
],
noServer: [[500, open('leer.filius.de')]],
unresolved: [[500, open('unbekannt.filius.de')]],
post: [[500, open('www.filius.de/seite.html', 'name=Filius&x=1')]],
encoded: [
[500, open('www.filius.de/mein bild.html')],
[5000, open('www.filius.de/grüße.html')],
],
traversal: [[500, open('www.filius.de/../geheim.txt')]],
caseInsensitive: [[500, open('WWW.Filius.de/SEITE.HTML')]],
raw: [
[500, (engine) => client(engine).connect('192.168.0.30', 80)],
[2500, (engine) => client(engine).send('GET /text.txt HTTP/1.1\nHost: 192.168.0.30\n')],
[4500, (engine) => client(engine).send('HALLO')],
[7000, (engine) => client(engine).connect('192.168.0.30', 80)],
[9000, (engine) => client(engine).send('GET /')],
],
};
const IMAGE_TYPES = new Set(['png', 'gif', 'jpg', 'bmp']);
/** The server's files as Java had them: text as it is, images decoded. */
function fileSystemOf(files: [string, string, string][]): VirtualFileSystem {
const fileSystem = new VirtualFileSystem();
for (const [path, type, content] of files) {
const parent = parentPath(path);
if (parent !== '/' && !fileSystem.contains(parent)) fileSystem.createDirectory(parent, true);
if (IMAGE_TYPES.has(type)) {
fileSystem.writeImageFile(path, javaBase64Decode(content)!, `image/${type === 'jpg' ? 'jpeg' : type}`);
} else {
fileSystem.writeTextFile(path, content);
}
}
return fileSystem;
}
type Lines = Record<Program, [number, string][]>;
function javaLines(java: JavaRun): Lines {
const lines: Lines = { server: [], browser: [], client: [] };
const firstCall = Math.min(...java.events.filter(([, , text]) => text.startsWith('call ')).map(([time]) => time));
for (const [time, program, text] of java.events) {
// Before the first call: Anwendung.starten's empty notice (null, or the browser's "update").
if (time < firstCall || text === 'null' || text.startsWith('call ') || text === 'no url') continue;
// A page other than 200 carries Java's error page; the editor makes its own.
const page = /^page (\d+) (\S+)\n/.exec(text);
lines[program].push([time, withClientPortText(page !== null && page[1] !== '200' ? `page ${page[1]} ${page[2]}` : text)]);
}
return lines;
}
function engineLines(engine: NetworkRuntimeEngine): Lines {
const lines = <E>(log: readonly LogLine<E>[] | undefined, text: (entry: E) => string | undefined): [number, string][] =>
(log ?? []).flatMap((line) => {
const each = text(line);
return each === undefined ? [] : [[line.timeMilliseconds, withClientPortText(each)] as [number, string]];
});
return {
server: lines(engine.webServer(SERVER)!.log.all, serverText),
browser: lines(browser(engine).log.all, browserText),
client: lines(engine.simpleClient(BROWSER)?.log.all, clientText),
};
}
/** When the story first made the call. */
/** A frame's text with placeholders: free ports, and DNS message IDs (drawn at random in Java, from the seed in the engine). */
const frameText = (text: string) => withClientPortText(text).replace(/ID=\d+/g, 'ID=x');
const callTime = (java: JavaRun, call: string) => java.events.find(([, , text]) => text === `call ${call}`)![0];
/**
* Where Java errs, per story: its run corrected, as Java means it.
*
* traversal: the server resolves "/../geheim.txt" below its document root to
* /geheim.txt and serves it; any file of the host is open to every browser.
* The engine's server answers 404.
*
* raw: "HALLO" (no request) makes HTTPNachricht throw; the server's thread
* closes the connection and writes "null" (which its window skips). The
* engine's server answers 501 Not Implemented — what WebServerMitarbeiter
* means for a method it does not know — and stays connected.
*
* noServer: Java's browser sends one SYN, not three (tcpJava.test.ts,
* closedPort); its page "server not reachable" comes two attempts later in
* the engine.
*/
const JAVA_BUGS: Record<string, (java: JavaRun) => JavaRun> = {
noServer: (java) => ({
...java,
events: java.events.map(([time, who, text]) => (who === 'browser' && text.startsWith('page 0') ? [time + 2 * JAVA_TIMEOUT_MILLISECONDS, who, text] : [time, who, text])),
}),
traversal: (java) => ({
...java,
events: java.events.map(([time, who, text]) =>
who === 'browser' && text.startsWith('page 200')
? [time, who, 'page 404 null\n']
: who === 'server' && text.startsWith('<<HTTP/1.1 200')
? [time, who, '<<HTTP/1.1 404 Not Found\n']
: [time, who, text],
),
}),
raw: (java) => {
const unreadable = java.events.findIndex(([, who, text]) => who === 'server' && text === '>>HALLO');
const [time] = java.events[unreadable]!;
const answer = 'HTTP/1.1 501 Not Implemented\n';
return {
...java,
events: [
...java.events.slice(0, unreadable + 1),
[time + 111, 'client', '<<HALLO'],
[time + 167, 'client', `>>${answer}`],
[time + 279, 'server', `<<${answer}`],
],
};
},
};
/**
* Where a story is compared only up to the request whose answer Java gets
* wrong (each card's frames up to it, the lines up to its arrival): after
* it, the engine goes on beyond Java's error. traversal: the answer differs
* (404, not the file), and with it every later sequence number. raw: after
* "HALLO" Java's connection is gone, the engine's lives on.
*/
const UNTIL_REQUEST: Record<string, string> = { traversal: 'GET /../geheim.txt', raw: 'HALLO' };
/** Java's tables of each card up to the frame that carries the request (with it). */
function tablesUpTo(java: JavaRun, text: string | undefined): JavaTables {
if (text === undefined) return java;
const upToRequest = (rows: Record<string, (string | null)[][]> | undefined) =>
Object.fromEntries(
Object.entries(rows ?? {}).map(([card, cardRows]) => {
const index = java.frames[card]!.findIndex((frame) => frame.payload?.startsWith(text) === true);
return [card, index < 0 ? cardRows : cardRows.filter((row) => Number(row[0]) <= index + 1)];
}),
);
return { rows: upToRequest(java.rows), layeredRows: upToRequest(java.layeredRows) };
}
/**
* Where a story is compared only up to a time (Java's). noServer: the
* engine's second SYN, which Java does not send (the switch floods it to
* every card).
*/
const UNTIL: Record<string, (java: JavaRun) => number> = {
noServer: (java) => java.events.find(([, who, text]) => who === 'browser' && text.startsWith('page 0'))![0] - 250,
};
/** Where the lines are compared only up to a time (Java's). raw: the story's second connection, which the engine's client does not need. */
const LINES_UNTIL: Record<string, (java: JavaRun) => number> = {
raw: (java) => java.events.filter(([, , text]) => text === 'call verbinden')[1]![0],
};
/** Each card's frames up to the one that carries the request in Java's list (with it); all where it does not show. */
function upTo(java: JavaRun, text: string | undefined) {
return (frames: SeenFrame[], card: string) => {
const index = text === undefined ? -1 : java.frames[card]!.findIndex((frame) => frame.payload?.startsWith(text) === true);
return index < 0 ? frames : frames.slice(0, index + 1);
};
}
describe('the web server and the web browser as Java FILIUS runs them', () => {
it('corrects Java only where it errs, and cuts only after the story began', () => {
for (const [name, correct] of Object.entries(JAVA_BUGS)) {
const java = JSON.parse(readFileSync(join(scenarioDirectory, `web-java-${name}.java.json`), 'utf8')) as JavaRun;
expect(JSON.stringify(correct(java)), name).not.toBe(JSON.stringify(java));
const firstCall = java.events.find(([, , text]) => text.startsWith('call '))![0];
for (const until of [UNTIL[name], LINES_UNTIL[name]]) if (until !== undefined) expect(until(java), name).toBeGreaterThan(firstCall + 500);
}
});
const scenario = JSON.parse(readFileSync(join(scenarioDirectory, 'web-lan.scenario.json'), 'utf8')) as Scenario;
for (const [name, acts] of Object.entries(STORIES)) {
it(name, () => {
const measured = JSON.parse(readFileSync(join(scenarioDirectory, `web-java-${name}.java.json`), 'utf8')) as JavaRun;
const java = JAVA_BUGS[name]?.(measured) ?? measured;
const snapshot = withSnapshot(snapshotOf(scenario), {
webServersByNodeId: new Map([[SERVER, { port: 80, documentRoot: '/webserver', virtualHosts: false, isActive: true }]]),
webBrowserNodeIds: new Set([BROWSER]),
fileSystemsByNodeId: new Map([[SERVER, fileSystemOf(measured.files)]]),
simpleClientsByNodeId: name === 'raw' ? new Map([[BROWSER, { targetAddress: '', targetPort: 80 }]]) : new Map(),
});
const { engine } = watchedEngine(scenario, snapshot);
for (const [time, act] of acts) {
engine.handle({ type: 'advance', toMilliseconds: time });
act(engine);
}
engine.handle({ type: 'advance', toMilliseconds: Math.max(...java.events.map(([time]) => time)) + 5_000 });
const until = UNTIL[name]?.(measured);
expectSameCaptures(engine, scenario, java.frames, {
text: frameText,
normalized: upTo(measured, UNTIL_REQUEST[name]),
until,
tables: tablesUpTo(measured, UNTIL_REQUEST[name]),
tolerance: TOLERANCE_MILLISECONDS,
});
const linesUntil = LINES_UNTIL[name]?.(measured) ?? until;
const before = (lines: [number, string][]) => lines.filter(([time]) => linesUntil === undefined || time < linesUntil);
const expected = javaLines(java);
const actual = engineLines(engine);
for (const program of ['server', 'browser', 'client'] as const) {
const expectedLines = before(expected[program]);
const actualLines = before(actual[program]);
expect(actualLines.map(([, text]) => text), `${program}'s lines`).toEqual(expectedLines.map(([, text]) => text));
for (const [index, [javaTime, text]] of expectedLines.entries()) {
const late = javaTime - actualLines[index]![0];
const most = (isPolled(text) ? POLL_MILLISECONDS : 0) + TOLERANCE_MILLISECONDS;
expect(late, `${program}'s line ${JSON.stringify(text.slice(0, 60))}: Java ${late} ms later`).toBeGreaterThanOrEqual(-TOLERANCE_MILLISECONDS);
expect(late, `${program}'s line ${JSON.stringify(text.slice(0, 60))}: Java ${late} ms later`).toBeLessThanOrEqual(most);
}
}
});
}
});