feat: F4.6g2 the DNS server window, its records being /dns/hosts

Oracle DnsWindowRun uses Java's DNS server window as a person does and
asks a computer's resolver after each step. The core's DNS server is a
program its window changes: on and off (its socket on port 53),
recursive resolution, and records that are /dns/hosts, read on every
query as Java's server reads them (the reader moves from fls to the
core). The snapshot carries every installed DNS server with whether it
runs. The room takes the window's actions, sends its view and writes
the server back after the run; the editor shows Java's window with its
three tabs. Not taken from Java: a cell edit spilling into records of
another type, an add losing what another program wrote into the file,
an empty mail domain, a removal hitting the first record of the name.
A table cell no longer takes the room's value over what is being typed
(the web server's too).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 20:36:17 +00:00
co-authored by Claude Opus 5.5
parent e31da62ae0
commit 065b42430d
27 changed files with 2850 additions and 158 deletions
+209
View File
@@ -0,0 +1,209 @@
// The DNS server as Java's window changes it, against Java's
// (dnsWindow.java.json, DnsWindowRun) on mail-lan's DNS server: Java's steps
// played on the engine's server — the tables, /dns/hosts, which names the
// first computer's resolver gets, the button and the box — and where Java
// errs, what the server does instead (apps/dnsServer.ts names each): an
// empty name refused, a cell changing its record only, a record added
// keeping what another program wrote into the file.
import { readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { describe, expect, it } from 'vitest';
import { NetworkRuntimeEngine, validDnsRecord, VirtualFileSystem, withSnapshot, type DnsRecordType, type Steps } from '../src/index.js';
import { snapshotOf, type Scenario } from './scenario.js';
interface JavaStep {
readonly step: string;
readonly button: string;
readonly recursive: boolean;
readonly a: readonly (readonly string[])[];
readonly mx: readonly (readonly string[])[];
readonly ns: readonly (readonly string[])[];
readonly file: string;
readonly dialogs: readonly string[];
readonly resolved?: Readonly<Record<string, string>>;
}
const here = dirname(fileURLToPath(import.meta.url));
const java = JSON.parse(readFileSync(join(here, 'dnsWindow.java.json'), 'utf8')) as { readonly steps: readonly JavaStep[] };
const scenario = JSON.parse(readFileSync(join(here, 'scenarios/mail-lan.scenario.json'), 'utf8')) as Scenario;
const CLIENT = '00000000-0000-0000-0000-000000000201';
const SERVER = '00000000-0000-0000-0000-000000000202';
function setUp() {
const engine = new NetworkRuntimeEngine(scenario.seed);
const files = new VirtualFileSystem();
engine.handle({ type: 'start', snapshot: withSnapshot(snapshotOf(scenario), { fileSystemsByNodeId: new Map([[SERVER, files]]) }), seed: scenario.seed, initialTimeMilliseconds: 0 });
const run = (milliseconds: number) => engine.handle({ type: 'advance', toMilliseconds: engine.state.currentTimeMilliseconds + milliseconds });
/** What the first computer's resolver gets for a name, as Java's oracle writes it. */
const resolve = (name: string): string => {
let answer = '(no answer)';
engine.startThread(
(function* (): Steps {
const result = yield* engine.resolveDnsAddressSteps(CLIENT, name);
answer = result.type === 'timedOut' ? 'TimeOutException' : String(result.address ?? null);
})(),
);
run(30_000);
return answer;
};
return { engine, server: engine.dnsServer(SERVER)!, files, run, resolve };
}
describe("the DNS server's window, against Java's", () => {
it("plays Java's steps: the same where Java is right, as named where it errs", () => {
const { server, files, resolve } = setUp();
const rows = (type: DnsRecordType) => server.rows(type).map(({ record }) => [record.domainName, record.rdata]);
const file = () => (files.contains('/dns/hosts') ? files.textFile('/dns/hosts') : undefined);
// Where Java's file holds what Java got wrong, the file as it should be (named below at each step).
const fixes: ((text: string) => string)[] = [];
const ours = (text: string) => fixes.reduce((each, fix) => fix(each), text);
const shown = (step: JavaStep) => {
const expected = {
button: step.button,
recursive: step.recursive,
a: ours(step.file) === step.file ? step.a : rowsOf(ours(step.file), 'A'),
mx: rowsOf(ours(step.file), 'MX'),
ns: step.ns,
file: ours(step.file),
};
const actual = { button: server.isActive ? 'Beenden' : 'Starten', recursive: server.recursiveResolutionEnabled, a: rows('A'), mx: rows('MX'), ns: rows('NS'), file: file() ?? ours(step.file) };
return { expected, actual };
};
const at = (name: string) => java.steps.find((step) => step.step === name)!;
const check = (name: string, resolved: Readonly<Record<string, string>> | undefined = at(name).resolved) => {
const { expected, actual } = shown(at(name));
expect(actual, name).toEqual(expected);
if (resolved !== undefined) expect(Object.fromEntries(Object.keys(resolved).map((each) => [each, resolve(each)])), name).toEqual(resolved);
};
check('open');
expect([server.addRecord('A', 'kein name!', '192.168.0.30'), server.addRecord('A', 'neu.schule.de', '192.168.0.300')]).toEqual([false, false]);
expect(at('A: add an invalid name').dialogs).toHaveLength(1);
check('A: add an invalid address');
expect(server.addRecord('A', 'neu.schule.de', '10.0.0.1')).toBe(true);
check('A: add neu.schule.de');
expect(server.addRecord('A', 'neu.schule.de', '10.0.0.2')).toBe(true);
check('A: add neu.schule.de again');
expect(server.addRecord('MX', 'schule.de', 'mail.schule.de')).toBe(true);
check('MX: add schule.de');
// An empty name is refused (Java makes ". MX 3600 x.").
expect(server.addRecord('MX', '', 'x')).toBe(false);
fixes.push((text) => text.replace('. MX 3600 x.\n', ''));
check('MX: add with no domain');
expect(server.addRecord('NS', 'schule.de', 'ns.schule.de')).toBe(true);
check('NS: add schule.de');
expect(server.changeRecord('A', 0, 0, 'web.filius.de')).toBe(true);
check("A: edit row 0's name to web.filius.de");
expect(server.changeRecord('A', 1, 1, '999.1.1.1')).toBe(false);
check("A: edit row 1's address to 999.1.1.1");
expect(server.changeRecord('A', 2, 1, '192.168.0.99')).toBe(true);
check("A: edit row 2's address");
// The MX record's server changes that record only (Java also writes it into the A record after it).
expect(server.changeRecord('MX', 0, 1, 'post.filius.de')).toBe(true);
fixes.push((text) => text.replace('mail.filius.de. A 3600 post.filius.de\n', 'mail.filius.de. A 3600 192.168.0.30\n'));
check("MX: edit row 0's server");
// Row 5 is mail.andere.de; Java's file loses mail.filius.de too, which its mistake had made invalid.
expect(server.removeRecord('A', 5)).toBe(true);
fixes.push((text) => text.replace('filius.de. MX 3600 post.filius.de.\n', 'filius.de. MX 3600 post.filius.de.\nmail.filius.de. A 3600 192.168.0.30\n'));
check('A: remove row 5');
server.setRecursiveResolutionEnabled(true);
check('recursive on');
server.setActive(false);
check('button once');
server.setActive(true);
check('button twice');
// Another program writes /dns/hosts: answered at once; a record added keeps it (Java's add loses it).
files.writeTextFile('/dns/hosts', `${files.textFile('/dns/hosts')}extern.schule.de. A 3600 10.0.0.9\n`);
check('file changed by another program');
expect(server.addRecord('A', 'danach.schule.de', '10.0.0.3')).toBe(true);
fixes.push((text) => text.replace('danach.schule.de.', 'extern.schule.de. A 3600 10.0.0.9\ndanach.schule.de.'));
check('A: add after the file changed', { ...at('A: add after the file changed').resolved, 'extern.schule.de': '10.0.0.9' });
expect(server.removeRecord('A', 0)).toBe(true);
check('A: remove row 0 after the file changed', { ...at('A: remove row 0 after the file changed').resolved, 'extern.schule.de': '10.0.0.9' });
});
});
/** The rows of a type the window shows for a file. */
function rowsOf(text: string, type: DnsRecordType): string[][] {
return text
.split('\n')
.filter((line) => line.split(' ')[1] === type)
.map((line) => [line.split(' ')[0]!, line.split(' ')[3]!]);
}
describe('the DNS server beyond the steps', () => {
it('removes the row chosen of two of one name (Java the first of the name), and changes the row of its type', () => {
const { server, files } = setUp();
server.addRecord('A', 'zwei.de', '10.0.0.1');
server.addRecord('A', 'zwei.de', '10.0.0.2');
const twins = () => server.rows('A').filter(({ record }) => record.domainName === 'zwei.de.');
expect(twins().map(({ record }) => record.rdata)).toEqual(['10.0.0.1', '10.0.0.2']);
const second = server.rows('A').findIndex(({ record }) => record.rdata === '10.0.0.2');
expect(server.removeRecord('A', second)).toBe(true);
expect(twins().map(({ record }) => record.rdata)).toEqual(['10.0.0.1']);
expect([server.removeRecord('A', 99), server.changeRecord('NS', 0, 0, 'x.de')]).toEqual([false, false]);
// A record's name and target in any form the file takes: the dots added, the TTL kept.
files.writeTextFile('/dns/hosts', 'alt.de. A 60 10.0.0.5\n');
expect(server.changeRecord('A', 0, 0, ' neu.de ')).toBe(true);
expect(files.textFile('/dns/hosts')).toBe('neu.de. A 60 10.0.0.5\n');
});
it("answers from its settings until it has a file, and its window's checks are Java's patterns without their errors", () => {
const engine = new NetworkRuntimeEngine(scenario.seed);
engine.handle({ type: 'start', snapshot: snapshotOf(scenario), seed: scenario.seed, initialTimeMilliseconds: 0 });
const server = engine.dnsServer(SERVER)!;
expect(server.rows('A').map(({ record }) => record.domainName)[0]).toBe('www.filius.de.');
expect(server.answering.map((record) => record.name)[0]).toBe('www.filius.de');
expect(engine.state.topologySnapshot.fileSystemsByNodeId.has(SERVER)).toBe(false);
server.addRecord('NS', 'schule.de', 'ns.schule.de');
expect(engine.state.topologySnapshot.fileSystemsByNodeId.get(SERVER)!.textFile('/dns/hosts').split('\n').at(-2)).toBe('schule.de. NS 3600 ns.schule.de.');
expect(
[
['A', 'a.de', '1.2.3.4'],
['A', '', '1.2.3.4'],
['A', 'a.de', ''],
['A', 'a.de', '1.2.3'],
['A', '9a.de', '1.2.3.4'],
['A', 'a.123', '1.2.3.4'],
['MX', 'a.de', 'mail.a.de'],
['MX', 'a.de', ''],
['NS', 'a.de', '1.2.3.4'],
// Java's pattern and IP.ipCheck both: each alone takes one of these.
['A', 'a.de', '1.2.3.4.'],
['A', 'a.de', '.1.2.3'],
['A', 'a.de', '01.2.3.4'],
].map(([type, name, data]) => validDnsRecord(type as DnsRecordType, name!, data!)),
).toEqual([true, false, false, false, true, false, true, false, false, false, false, true]);
// A folder named /dns/hosts is no file: the settings' records.
const files = engine.state.topologySnapshot.fileSystemsByNodeId.get(SERVER)!;
files.deleteItem('/dns/hosts');
files.createDirectory('/dns/hosts');
expect(server.records.map((record) => record.domainName)[0]).toBe('www.filius.de.');
});
it('runs a server switched off only once it is switched on, and an installed one switched off', () => {
const engine = new NetworkRuntimeEngine(scenario.seed);
const snapshot = snapshotOf(scenario);
const configuration = snapshot.dnsServerConfigurationsByNodeId.get(SERVER)!;
engine.handle({
type: 'start',
snapshot: withSnapshot(snapshot, { dnsServerConfigurationsByNodeId: new Map([[SERVER, { ...configuration, isActive: false }]]) }),
seed: scenario.seed,
initialTimeMilliseconds: 0,
});
const server = engine.dnsServer(SERVER)!;
expect([server.isActive, engine.state.dnsServerSocketIdsByNodeId.has(SERVER)]).toEqual([false, false]);
server.setActive(true);
server.setActive(true);
expect([server.isActive, engine.state.dnsServerSocketIdsByNodeId.has(SERVER)]).toEqual([true, true]);
engine.removeProgram(SERVER, 'dnsServer');
expect([engine.dnsServer(SERVER), engine.state.dnsServerSocketIdsByNodeId.has(SERVER)]).toEqual([undefined, false]);
engine.installProgram(SERVER, 'dnsServer', withSnapshot(snapshot, { dnsServerConfigurationsByNodeId: new Map([[SERVER, { ...configuration, isActive: false }]]) }));
expect(engine.dnsServer(SERVER)?.isActive).toBe(false);
});
});