Files
mdg-filius/packages/core/test/emailJava.test.ts
T
julian.kiedaischandClaude Opus 5.5 015635926f feat(core): the e-mail server after Java, measured against Java
SMTP on port 25 stores mails for the domain's accounts as Java reads
them anew and tells a local sender of unknown recipients; POP3 on port
110 hands them out. The mailboxes are in the snapshot and go back into
the project. EmailRun plays four stories over the network; MailCases
feeds Java's workers without one (26 cases, de and en). 18 cases name
Java bugs that the core does not take.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 18:58:26 +00:00

161 lines
7.1 KiB
TypeScript

// The e-mail server as Java FILIUS runs it. tools/java-oracle's EmailRun let
// FILIUS' generic client talk SMTP and POP3 to an e-mail server on
// mail-lan's network and wrote every frame each card saw, Java's tables of
// them, the lines of the server's and the client's windows, and the
// server's accounts at the end (email-java-<story>.java.json). The engine
// plays the same stories with its programs (apps/emailServer.ts, the
// generic client of apps/echo.ts): each card's frames in the same order,
// with the same content and rows, at about the same time; each window's
// lines as Java's, at about the same time; the same accounts at the end.
// Java's errors in these dialogues are measured without a network
// (emailServer.test.ts); these stories keep to what Java does right.
import { readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { describe, expect, it } from 'vitest';
import { formatMail, withSnapshot, type LogLine, type NetworkRuntimeEngine } from '../src/index.js';
import { expectSameCaptures, GAP_TOLERANCE_MILLISECONDS, watchedEngine, withClientPortText, type JavaFrame, type JavaTables } from './javaRun.js';
import { MAIL_ACCOUNTS } from './mailAccounts.js';
import { clientText, emailServerText, isPolled, POLL_MILLISECONDS } from './programLines.js';
import { snapshotOf, type Scenario } from './scenario.js';
const scenarioDirectory = join(dirname(fileURLToPath(import.meta.url)), 'scenarios');
const CLIENT = '00000000-0000-0000-0000-000000000201';
const SERVER = '00000000-0000-0000-0000-000000000203';
type Program = 'server' | 'client';
interface JavaRun extends JavaTables {
frames: Record<string, JavaFrame[]>;
events: [number, Program, string][];
/** Each account's name and its messages: Email.toString, new, marked for deletion. */
accounts: [string, ...[string, string, string][]][];
}
type Act = (engine: NetworkRuntimeEngine) => void;
/** EmailRun's calls at their times: connecting takes 2.5 s, each message 1.5 s, from 500 ms on. */
function story(...parts: (readonly ['connect', number] | readonly ['send', ...string[]] | readonly ['wait', number])[]): [number, Act][] {
const acts: [number, Act][] = [];
let at = 500;
for (const [kind, ...rest] of parts) {
if (kind === 'connect') {
const port = rest[0] as number;
acts.push([at, (engine) => engine.simpleClient(CLIENT)!.connect('192.168.0.30', port)]);
at += 2_500;
} else if (kind === 'send') {
for (const text of rest as string[]) {
acts.push([at, (engine) => engine.simpleClient(CLIENT)!.send(text)]);
at += 1_500;
}
} else {
at += rest[0] as number;
}
}
return acts;
}
const STORIES: Record<string, [number, Act][]> = {
smtp: story(
['connect', 25],
[
'send',
'HELO 192.168.0.20',
'MAIL FROM: <bob@filius.de>',
'RCPT TO:<anna@filius.de>',
'DATA',
"From: Bob Beispiel <bob@filius.de>\nTo: <anna@filius.de>\nSubject: Gruß\n\nHallo Anna,\nwie geht's?\r\n.\r\n",
'QUIT',
],
),
smtpErrors: story(
['connect', 25],
[
'send',
'NOOP',
'EHLO',
'DATA',
'MAIL FROM: bob@filius.de',
'RCPT TO:<niemand@filius.de>',
'DATA',
'MAIL FROM:<bob@filius.de>',
'RCPT TO: anna',
'NOOP',
'DATA',
'From: <bob@filius.de>\nTo: <niemand@filius.de>\nSubject: Wer?\n\nIst da jemand?\n.\n',
'QUIT',
],
),
pop3: story(
['connect', 110],
['send', 'USER anna', 'PASS geheim', 'STAT', 'LIST', 'RETR 0', 'RETR 1', 'DELE 0', 'DELE 1', 'QUIT'],
['wait', 1_000],
['connect', 110],
['send', 'USER anna', 'PASS geheim', 'STAT', 'QUIT'],
),
pop3Errors: story(['connect', 110], ['send', 'NOOP', 'USER niemand', 'PASS x', 'USER anna', 'PASS falsch', 'PASS geheim', 'NOOP', 'USER bob', 'QUIT']),
};
/** A notice's date: Java's in its locale and the time it ran. */
const withoutDate = (text: string) => text.replace(/\nDate: [^\n]*\n/g, '\nDate: <date>\n');
type Lines = Record<Program, [number, string][]>;
function javaLines(java: JavaRun): Lines {
const lines: Lines = { server: [], client: [] };
for (const [time, program, text] of java.events) {
// Anwendung.starten and the server's refreshes tell their observers nothing (null), which the windows skip.
if (text === 'null' || text.startsWith('call ')) continue;
lines[program].push([time, withClientPortText(text)]);
}
return lines;
}
function engineLines(engine: NetworkRuntimeEngine): Lines {
const lines = <E>(log: readonly LogLine<E>[], text: (entry: E) => string): [number, string][] =>
log.map((line) => [line.timeMilliseconds, withClientPortText(text(line))]);
return { server: lines(engine.emailServer(SERVER)!.log.all, (entry) => emailServerText(entry)), client: lines(engine.simpleClient(CLIENT)!.log.all, clientText) };
}
describe('the e-mail server as Java FILIUS runs it', () => {
const scenario = JSON.parse(readFileSync(join(scenarioDirectory, 'mail-lan.scenario.json'), 'utf8')) as Scenario;
const snapshot = withSnapshot(snapshotOf(scenario), {
emailServersByNodeId: new Map([[SERVER, { domain: 'filius.de', accounts: MAIL_ACCOUNTS, isActive: true }]]),
simpleClientsByNodeId: new Map([[CLIENT, { targetAddress: '', targetPort: 25 }]]),
});
for (const [name, acts] of Object.entries(STORIES)) {
it(name, () => {
const java = JSON.parse(readFileSync(join(scenarioDirectory, `email-java-${name}.java.json`), 'utf8')) as JavaRun;
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 });
expectSameCaptures(engine, scenario, java.frames, { text: (text) => withoutDate(withClientPortText(text)), tables: java, cell: withoutDate });
const expected = javaLines(java);
const actual = engineLines(engine);
for (const program of ['server', 'client'] as const) {
expect(actual[program].map(([, text]) => text), `${program}'s lines`).toEqual(expected[program].map(([, text]) => text));
for (const [index, [javaTime, text]] of expected[program].entries()) {
const late = javaTime - actual[program][index]![0];
const most = (isPolled(text) ? POLL_MILLISECONDS : 0) + GAP_TOLERANCE_MILLISECONDS;
expect(late, `${program}'s line ${JSON.stringify(text.slice(0, 60))}`).toBeGreaterThanOrEqual(-GAP_TOLERANCE_MILLISECONDS);
expect(late, `${program}'s line ${JSON.stringify(text.slice(0, 60))}`).toBeLessThanOrEqual(most);
}
}
const accounts = engine.emailServer(SERVER)!.state.accounts.map((account) => [
account.username,
...account.mailbox.map((entry) => [withoutDate(formatMail(entry.mail)), String(entry.isNew), String(entry.isMarkedForDeletion)]),
]);
expect(accounts).toEqual(java.accounts.map(([username, ...mails]) => [username, ...mails.map(([text, isNew, deleted]) => [withoutDate(text), isNew, deleted])]));
});
}
});