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
+113
View File
@@ -0,0 +1,113 @@
// The DNS server's window in the room, on FILIUS' example email_komplett:
// its view (button, box, the three tables as /dns/hosts holds them), its
// actions, /dns/hosts written by another into the document and shown at
// once, and what a run leaves in the project (the file, whether it runs,
// recursive resolution, the records in its settings). The server's steps
// against Java's window are packages/core/test/dnsWindow.test.ts.
import { readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { describe, expect, it } from 'vitest';
import * as Y from 'yjs';
import { changeFiles, fileText, readProject, writeProject } from '@mdg-filius/doc';
import { openProjectArchive } from '@mdg-filius/fls';
import type { DnsServerView, HostMessage } from '@mdg-filius/protocol';
import { SimulationRoom, TICK_MILLISECONDS } from '../src/index.js';
const here = dirname(fileURLToPath(import.meta.url));
const corpus = join(here, '../../../corpus/java-examples');
function setUp() {
let ids = 0;
const doc = new Y.Doc();
writeProject(
doc,
openProjectArchive(new Uint8Array(readFileSync(join(corpus, 'email_komplett.fls'))), () => `00000000-0000-0000-0000-${(++ids).toString(16).padStart(12, '0')}`),
);
const id = readProject(doc).graph.nodes.find((node) => node.displayName === 'DNS-Server')!.id;
let now = 0;
const room = new SimulationRoom(doc, { now: () => now, seed: () => 5 });
let view: DnsServerView | null | undefined;
const errors: string[] = [];
const lines: HostMessage[] = [];
const client = room.connect((message: HostMessage) => {
if (message.kind === 'app') lines.push(message);
if (message.kind === 'view' && message.app === 'dnsServer') view = message.view as DnsServerView | null;
if (message.kind === 'error') errors.push(message.code);
});
const send = (fields: object) => room.receive(client, { v: 1, ...fields });
send({ kind: 'sub', topics: [`app:${id}/dnsServer`] });
send({ kind: 'start' });
const run = (milliseconds: number) => {
for (let elapsed = 0; elapsed < milliseconds; elapsed += TICK_MILLISECONDS) {
now += TICK_MILLISECONDS;
room.tick();
}
};
run(TICK_MILLISECONDS);
const act = (action: string, args: object = {}) => send({ kind: 'input', node: id, app: 'dnsServer', action, args, reqId: action });
return { doc, id, room, view: () => view, errors, lines, act, run, send };
}
describe("the DNS server's window in the room", () => {
it('shows the server and its tables, takes its actions, and shows what another writes into /dns/hosts at once', () => {
const { doc, id, view, act, run, errors, lines } = setUp();
// Java's DNS window has no lines: none are sent.
expect(lines).toEqual([]);
const hosts = () => fileText(doc, id, '/dns/hosts')!.toString();
expect(view()!.active).toBe(true);
const before = view()!.rows.A.length;
act('addRecord', { type: 'A', name: 'neu.schule.de', data: '10.0.0.1' });
act('addRecord', { type: 'MX', name: 'schule.de', data: 'mail.schule.de' });
act('addRecord', { type: 'NS', name: 'schule.de', data: 'ns.schule.de' });
expect(view()!.rows.A.at(-1)).toEqual(['neu.schule.de.', '10.0.0.1']);
expect([view()!.rows.MX.at(-1), view()!.rows.NS.at(-1)]).toEqual([
['schule.de.', 'mail.schule.de.'],
['schule.de.', 'ns.schule.de.'],
]);
// In the document's file at once.
expect(hosts().endsWith('neu.schule.de. A 3600 10.0.0.1\nschule.de. MX 3600 mail.schule.de.\nschule.de. NS 3600 ns.schule.de.\n')).toBe(true);
act('changeRecord', { type: 'A', row: before, column: 1, value: '10.0.0.2' });
expect(view()!.rows.A.at(-1)).toEqual(['neu.schule.de.', '10.0.0.2']);
act('removeRecord', { type: 'NS', row: 0 });
expect(view()!.rows.NS).toEqual([]);
act('setRecursive', { enabled: true });
act('setActive', { active: false });
expect([view()!.active, view()!.recursive]).toEqual([false, true]);
expect(errors).toEqual([]);
// The text editor writes the file: the table shows it.
changeFiles(doc, id, 'me', (files) => files.writeTextFile('/dns/hosts', `${files.textFile('/dns/hosts')}extern.schule.de. A 3600 10.0.0.9\n`));
run(TICK_MILLISECONDS);
expect(view()!.rows.A.at(-1)).toEqual(['extern.schule.de.', '10.0.0.9']);
});
it('leaves in the project what the run made of the server', () => {
const { doc, id, act, send } = setUp();
act('addRecord', { type: 'A', name: 'neu.schule.de', data: '10.0.0.1' });
act('setRecursive', { enabled: true });
act('setActive', { active: false });
send({ kind: 'stop' });
const project = readProject(doc);
expect(project.activePrograms.get(id)?.has('dnsServer') ?? false).toBe(false);
expect(project.dnsServerConfigurations.get(id)!.recursiveResolutionEnabled).toBe(true);
expect(project.dnsServerConfigurations.get(id)!.records.at(-1)).toEqual({ name: 'neu.schule.de', type: 'A', ttlSeconds: 3600, target: '10.0.0.1' });
expect(project.virtualFileSystems.get(id)!.textFile('/dns/hosts').endsWith('neu.schule.de. A 3600 10.0.0.1\n')).toBe(true);
});
it('refuses what the window never hands over', () => {
const { act, errors } = setUp();
act('setActive', { active: 'no' });
act('setRecursive', {});
act('addRecord', { type: 'TXT', name: 'a.de', data: 'x' });
act('addRecord', { type: 'A', name: '', data: '10.0.0.1' });
act('addRecord', { type: 'A', name: 'a.de', data: 5 });
act('removeRecord', { type: 'A', row: 1.5 });
act('removeRecord', { type: 'NS', row: 0 });
act('changeRecord', { type: 'A', row: 0, column: 2, value: 'x' });
act('changeRecord', { type: 'A', row: 0, column: 1, value: 'kein' });
act('changeRecord', { type: 'A', row: 0, column: 1, value: 3 });
act('restart');
expect(errors).toEqual(['badArgs', 'badArgs', 'badArgs', 'badArgs', 'badArgs', 'badArgs', 'badArgs', 'badArgs', 'badArgs', 'badArgs', 'unknownAction']);
});
});