Oregami
Repositories/oxedyne/daimond

oxedyne/daimond/dev/verify_worldports.mjs

19.4 KiB, 1 run

created by r2519314175:829, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1// verify_worldports.mjs — two worlds at once, and neither one can answer for the
2// other.
3//
4// WHAT THIS IS FOR. `dev/world.sh` numbers a world's app server and mock provider
5// off the world number, and until 2026-08-25 it did not number the third thing a
6// world reaches. Every world's `/api` was reverse-proxied to one fixed port, so
7// which account state a verifier read was decided by which OTHER lane happened to
8// be running something on it at that moment. `verify_attachfocus` and `verify_chatworkspace` went red
9// four runs out of six on a tree nobody had changed; `dev/pro.mjs` posted a signed
10// Pro licence event at a stranger's gateway; and `dev/reflux_brief.mjs` lost five
11// consecutive runs to a foreign 403 that left the model list empty. Three separate
12// symptoms, one cause, and each was answered where it was seen.
13//
14// A wrong answer with nothing saying it is wrong is the failure this file exists
15// against, so what it asserts is not "the ports differ". It is:
16//
17// 1. Two worlds ask for two different ports, and no world asks for the one a
18// hand-started server lands on when it was given no world at all.
19// 2. A world that HAS a gateway reaches its own and reads its own answer.
20// 3. A world that has NOT started one is REFUSED, in a sentence that names its
21// own port, and never reaches the neighbour that does have one.
22// 4. The refusal a caller reads says whose port it is, not merely that
23// something is absent.
24// 5. No two rows of the port register collide for any world 0..9.
25// 6. AND `world.sh --up` HANDS THE PORT TO THE SERVER IT STARTS. This one is
26// here because the fix shipped without it: the row was added, `--env`
27// exported it, and the `node dev/serve.mjs` line still passed only
28// DAIMOND_PORT -- so two worlds came up on 9711 and 9712 and both proxied to
29// 9002, exactly as before. An env block is what a caller reads; it is not
30// what the server was told. So this asks the server, through `/__world`.
31//
32// It starts its own stand-in on a port of its own. No gateway binary, no browser
33// and no mock provider: about ten seconds, and nothing to build.
34//
35// EACH CHECK IS PROVED AGAINST BROKEN CODE FIRST:
36//
37// node dev/verify_worldports.mjs --break register # 1 fails: one port for all worlds
38// node dev/verify_worldports.mjs --break shared # 2,3 fail: one shared port
39// node dev/verify_worldports.mjs --break quiet # 4 fails: the old sentence
40// node dev/verify_worldports.mjs --break collide # 5 fails: a duplicated row
41// node dev/verify_worldports.mjs --break handoff # 6 fails: --up drops the port
42// node dev/verify_worldports.mjs # and then, clean
43//
44// The register rows are 8480 + 2N (two app servers) and 9480 + 2N (their two
45// gateways); see dev/world.sh, which is where a port is claimed. Check 6 borrows
46// a whole spare WORLD instead -- the first of 21..30 whose ports are free -- so
47// that it drives the real `world.sh --up` rather than a second copy of it.
48
49import fs from 'node:fs';
50import net from 'node:net';
51import http from 'node:http';
52import path from 'node:path';
53import { spawn, spawnSync, execFileSync } from 'node:child_process';
54import { fileURLToPath } from 'node:url';
55
56const HERE = path.dirname(fileURLToPath(import.meta.url));
57const ROOT = path.join(HERE, '..');
58const WORLD = Math.max(0, Number(process.env.DAIMOND_PORT || 8777) - 8777);
59// Both spellings are in the tree -- `--break name` and `--break=name` -- and a
60// parser that quietly took the next argv entry read node's own path as a break
61// name on the very first run of this file.
62const BREAK = (() => {
63 const eq = process.argv.find(a => a.startsWith('--break='));
64 if (eq) return eq.slice(8);
65 const i = process.argv.indexOf('--break');
66 if (i < 0) return '';
67 const next = process.argv[i + 1];
68 return next && !next.startsWith('-') ? next : '';
69})();
70
71const APP_A = 8480 + 2 * WORLD, APP_B = 8481 + 2 * WORLD;
72const GW_A = 9480 + 2 * WORLD, GW_B = 9481 + 2 * WORLD;
73
74// The stand-in's own word for itself. A 401 alone would not distinguish "the
75// neighbour refused me" from "my own gateway refused me", and that ambiguity is
76// the whole fault -- so the body carries a name only this process writes.
77const MARK = `world-A-standin-${process.pid}`;
78
79const SCRATCH = process.env.DAIMOND_SCRATCH || path.join(process.env.HOME, '.cache/daimond');
80const OUT = path.join(SCRATCH, 'worldports');
81fs.mkdirSync(OUT, { recursive: true });
82
83let ok = 0, bad = 0;
84const check = (name, cond, detail = '') => {
85 if (cond) { ok++; console.log(` ok ${name}`); }
86 else { bad++; console.log(` FAIL ${name}${detail ? ' — ' + detail : ''}`); }
87};
88
89const free = (port) => new Promise((res) => {
90 const s = net.createServer();
91 s.once('error', () => res(false));
92 s.once('listening', () => s.close(() => res(true)));
93 s.listen(port, '127.0.0.1');
94});
95
96const waitFor = async (fn, ms = 15000) => {
97 const end = Date.now() + ms;
98 for (;;) {
99 try { if (await fn()) return true; } catch (e) { /* not up yet */ }
100 if (Date.now() > end) return false;
101 await new Promise(r => setTimeout(r, 150));
102 }
103};
104
105/// One world's environment, straight out of `dev/world.sh` rather than derived
106/// here a second time -- deriving it twice is the staleness the register exists
107/// against.
108///
109/// The three variables cleared below are the ones `world.sh` HONOURS when a
110/// caller has already set them, and clearing them is the whole point of this
111/// helper. `dev/run_all.sh:62` exports `DAIMOND_GW_PORT` for the entire suite,
112/// so inside a suite run every world answered with the suite's own port: checks
113/// 1 and 2 read 9709 twice and could not have read anything else. Check 2 passed
114/// on that -- 9709 is not 9002 -- which is worse than the failure beside it. A
115/// caller that means to pre-empt the register passes the variable in `extra`,
116/// where the check that asks for it can be read.
117function worldEnv(n, extra = {}, reg = path.join(HERE, 'world.sh')) {
118 const env = { ...process.env, ...extra };
119 for (const k of ['DAIMOND_GW_PORT', 'DAIMOND_IMAP_PORT', 'SMTPD_PORT']) {
120 if (!(k in extra)) delete env[k];
121 }
122 const txt = execFileSync('bash', [reg, String(n), '--env'],
123 { encoding: 'utf8', env });
124 const out = {};
125 for (const line of txt.split('\n')) {
126 const m = /^export ([A-Z_0-9]+)=(.*)$/.exec(line.trim());
127 if (m) out[m[1]] = m[2];
128 }
129 return out;
130}
131
132const procs = [];
133let standin = null;
134let spare = null;
135
136async function main() {
137 // ── 1. The register answers, and it answers differently per world ─────
138 //
139 // `--break register` is the fault this row is against: one gateway port for
140 // every world, written in the register rather than arrived at by accident.
141 let numbered = path.join(HERE, 'world.sh');
142 if (BREAK === 'register') {
143 numbered = path.join(OUT, 'world-register.sh');
144 const txt = fs.readFileSync(path.join(HERE, 'world.sh'), 'utf8');
145 const damaged = txt.replace('GW_PORT=${DAIMOND_GW_PORT:-$((9700 + N))}',
146 'GW_PORT=${DAIMOND_GW_PORT:-9700}');
147 if (damaged === txt) {
148 console.log(' break register: the GW_PORT line did not match, so this run would '
149 + 'prove nothing. Has world.sh\'s gateway row moved?');
150 process.exit(2);
151 }
152 fs.writeFileSync(numbered, damaged);
153 }
154 const w3 = worldEnv(3, {}, numbered), w4 = worldEnv(4, {}, numbered);
155 check('two worlds are given two different gateway ports',
156 !!w3.DAIMOND_GW_PORT && w3.DAIMOND_GW_PORT !== w4.DAIMOND_GW_PORT,
157 `${w3.DAIMOND_GW_PORT} / ${w4.DAIMOND_GW_PORT}`);
158
159 // 9002 is where a gateway started by hand with no world lands: it is the
160 // deployed `gateway/app.jdat` listen port and `dev/devgw.sh`'s own default. A
161 // world reaching it is a world reaching whatever got there first.
162 const nines = [];
163 for (let n = 0; n <= 12; n++) {
164 const p = worldEnv(n, {}, numbered).DAIMOND_GW_PORT;
165 if (String(p) === '9002') nines.push(n);
166 }
167 check('no world 0..12 is given :9002, the port a stray gateway lands on',
168 nines.length === 0, nines.length ? 'worlds ' + nines.join(', ') : '');
169
170 // A caller that has already chosen one has asked for a gateway and must keep it.
171 check('a caller that sets DAIMOND_GW_PORT itself keeps it',
172 worldEnv(3, { DAIMOND_GW_PORT: '9911' }).DAIMOND_GW_PORT === '9911',
173 worldEnv(3, { DAIMOND_GW_PORT: '9911' }).DAIMOND_GW_PORT);
174
175 // ── 2. The register does not collide with itself ──────────────────────
176 //
177 // Read out of `dev/world.sh`'s own table rather than restated here. A second
178 // copy of the register would be a second thing to keep in step, which is the
179 // failure the table was written to end.
180 let regPath = path.join(HERE, 'world.sh');
181 if (BREAK === 'collide') {
182 regPath = path.join(OUT, 'world-collide.sh');
183 const txt = fs.readFileSync(path.join(HERE, 'world.sh'), 'utf8');
184 fs.writeFileSync(regPath, txt.replace(
185 '# 9099 + N mock provider dev/world.sh',
186 '# 9099 + N mock provider dev/world.sh\n'
187 + '# 9100 + N a deliberately colliding row --break collide'));
188 }
189 const rows = [];
190 for (const line of fs.readFileSync(regPath, 'utf8').split('\n')) {
191 const m = /^#\s+(\d{4})(?:\s*\+\s*(\d*)N)?\s{2,}(\S.*?)\s*$/.exec(line);
192 if (!m) continue;
193 rows.push({ base: Number(m[1]), step: m[2] === undefined ? 0 : Number(m[2] || 1),
194 what: m[3].slice(0, 46) });
195 }
196 check('the port register is machine-readable at all', rows.length >= 12, `${rows.length} row(s)`);
197 const seen = new Map(); const clashes = [];
198 for (const r of rows) {
199 for (let n = 0; n <= 9; n++) {
200 const p = r.base + r.step * n;
201 const prev = seen.get(p);
202 if (prev && prev !== r.what) clashes.push(`:${p} ${prev} / ${r.what}`);
203 else seen.set(p, r.what);
204 }
205 }
206 // 0..9 because that is the band of world numbers the register's own rows are
207 // written for -- `dev/gate.sh` defaults to 9 and the lanes take 0..9. Higher
208 // numbers are borrowed rather than assigned, and the borrower checks the ports
209 // are free before it takes one.
210 check('no two register rows claim one port for any world 0..9',
211 clashes.length === 0, clashes.slice(0, 3).join(' | '));
212
213 // ── 3. Two worlds, side by side ───────────────────────────────────────
214 for (const [name, p] of [['A app', APP_A], ['B app', APP_B], ['A gw', GW_A], ['B gw', GW_B]]) {
215 if (!await free(p)) {
216 console.log(`SKIP verify_worldports — :${p} (${name}) is held by something else. `
217 + 'This file needs four ports of its own; see the 8480 + 2N and 9480 + 2N rows '
218 + 'in dev/world.sh, or run it in a world of its own.');
219 process.exit(0);
220 }
221 }
222
223 // World A's gateway. Nothing else on this machine answers with MARK, so an
224 // answer carrying it is proof of WHICH gateway was reached, not merely that
225 // one was.
226 standin = http.createServer((req, res) => {
227 res.writeHead(401, { 'content-type': 'application/json' });
228 res.end(JSON.stringify({ error: 'no session', standin: MARK, port: GW_A }));
229 });
230 await new Promise(r => standin.listen(GW_A, '127.0.0.1', r));
231
232 // `--break quiet` runs a COPY of dev/serve.mjs whose refusal is the sentence
233 // this file replaced: true, useless, and naming a port the run was never on.
234 let serveJs = 'dev/serve.mjs';
235 if (BREAK === 'quiet') {
236 const txt = fs.readFileSync(path.join(HERE, 'serve.mjs'), 'utf8');
237 const damaged = txt.replace(/res\.end\(JSON\.stringify\(\{ error: `No gateway[\s\S]*?\}\)\);/,
238 "res.end(JSON.stringify({ error: 'The gateway is not running on :9002. "
239 + "Start it, or use the browser-only features.' }));");
240 if (damaged === txt) {
241 console.log(' break quiet: the refusal anchor did not match, so this run would '
242 + 'prove nothing. Has serve.mjs\'s 502 body moved?');
243 process.exit(2);
244 }
245 const p = path.join(ROOT, 'dev/serve-quiet-break.mjs');
246 fs.writeFileSync(p, damaged);
247 procs.push({ path: p });
248 serveJs = 'dev/serve-quiet-break.mjs';
249 }
250
251 // `--break shared` is the tree as it stood: one gateway port for every world.
252 const gwForB = BREAK === 'shared' ? GW_A : GW_B;
253 for (const [port, gw, log] of [[APP_A, GW_A, 'a'], [APP_B, gwForB, 'b']]) {
254 const out = fs.openSync(path.join(OUT, `serve-${log}.log`), 'w');
255 const rec = { proc: spawn('node', [serveJs], {
256 cwd: ROOT, stdio: ['ignore', out, out],
257 env: { ...process.env, DAIMOND_PORT: String(port), DAIMOND_GW_PORT: String(gw) },
258 }) };
259 // `exitCode` is null until node has reaped the child, so a teardown reading
260 // it 400 ms after SIGTERM reports every ordinary stop as a process that
261 // would not die. The event is the fact.
262 rec.proc.on('exit', () => { rec.gone = true; });
263 procs.push(rec);
264 }
265 const up = await waitFor(async () =>
266 (await fetch(`http://localhost:${APP_A}/index.html`)).status < 500
267 && (await fetch(`http://localhost:${APP_B}/index.html`)).status < 500);
268 check('two dev servers are up at once', up, `:${APP_A} and :${APP_B}`);
269
270 const ask = async (port) => {
271 const r = await fetch(`http://localhost:${port}/api/account`, { method: 'POST' });
272 return { status: r.status, body: await r.text() };
273 };
274 const a = await ask(APP_A);
275 const b = await ask(APP_B);
276
277 check('world A reaches the gateway world A started',
278 a.status === 401 && a.body.includes(MARK), `${a.status} ${a.body.slice(0, 90)}`);
279
280 check('world B, which started none, is REFUSED rather than answered',
281 b.status === 502, `${b.status} ${b.body.slice(0, 90)}`);
282
283 // THE CHECK THIS FILE IS FOR. Before 2026-08-25 this is the line that went the
284 // other way, silently, four runs out of six.
285 check('and world B never reaches world A\'s gateway',
286 !b.body.includes(MARK) && !b.body.includes(String(GW_A)),
287 `${b.status} ${b.body.slice(0, 120)}`);
288
289 check('world B\'s refusal names world B\'s OWN port',
290 b.body.includes(String(GW_B)), b.body.slice(0, 120));
291
292 // The sentence a caller reads. A refusal that says only "not running" leaves a
293 // reader unable to tell an absent gateway from one they were never entitled to.
294 check('and says whose port it is, not merely that something is absent',
295 /this world's own/.test(b.body), b.body.slice(0, 160));
296
297 console.log(`\n the sentence a caller reads: ${JSON.parse(b.body).error}`);
298
299 // ── 4. The real `world.sh --up`, asked what it actually started ───────
300 //
301 // Everything above drives servers this file spawned itself with the right
302 // environment, which is why it could not have caught the handoff bug: the
303 // fault was in the one line this file was not exercising. So a whole spare
304 // world, brought up the way every lane brings one up.
305 let spareN = 21;
306 for (; spareN <= 30; spareN++) {
307 if (await free(8777 + spareN) && await free(9099 + spareN) && await free(9700 + spareN)) break;
308 }
309 if (spareN > 30) {
310 check('a spare world 21..30 was free for the handoff check', false,
311 'every one of worlds 21..30 has a port held; nothing was measured');
312 return;
313 }
314 let worldSh = path.join(HERE, 'world.sh');
315 if (BREAK === 'handoff') {
316 // IN dev/, not in the scratch root: world.sh resolves the app root from its
317 // own location, so a copy anywhere else serves the wrong tree and refuses
318 // before it can demonstrate anything. Removed in teardown.
319 worldSh = path.join(HERE, 'world-handoff-break.sh');
320 const txt = fs.readFileSync(path.join(HERE, 'world.sh'), 'utf8');
321 const damaged = txt.replace('DAIMOND_PORT=$PORT DAIMOND_GW_PORT=$GW_PORT \\\n\t\t\texec node dev/serve.mjs',
322 'DAIMOND_PORT=$PORT exec node dev/serve.mjs');
323 if (damaged === txt) {
324 console.log(' break handoff: the --up anchor did not match, so this run would '
325 + 'prove nothing. Has world.sh\'s serve.mjs line moved?');
326 process.exit(2);
327 }
328 // The identity check would catch the damage before the browser could, which
329 // is the point of it -- but then nothing downstream runs, and the check
330 // being proved is the one below. So the copy keeps the fault and drops the
331 // guard, and the guard has a check of its own further down.
332 fs.writeFileSync(worldSh, damaged.replace('\tgot_gw=$(identify', '\tgot_gw=$GW_PORT #$(identify'));
333 procs.push({ path: worldSh });
334 }
335 spare = { n: spareN, sh: worldSh };
336 const upRes = spawnSync('bash', [worldSh, String(spareN), '--up'], { encoding: 'utf8' });
337 if (upRes.status !== 0) {
338 check('world.sh --up brought a spare world up at all', false,
339 `world ${spareN}: ` + String(upRes.stderr || upRes.stdout || '').trim().split('\n').pop());
340 return;
341 }
342 const upOut = upRes.stdout;
343 const spareEnv = {};
344 for (const line of upOut.split('\n')) {
345 const m = /^export ([A-Z_0-9]+)=(.*)$/.exec(line.trim());
346 if (m) spareEnv[m[1]] = m[2];
347 }
348 const world = await (await fetch(`http://localhost:${spareEnv.DAIMOND_PORT}/__world`)).json();
349 check('world.sh --up hands the gateway port to the server it starts',
350 String(world.gateway) === String(spareEnv.DAIMOND_GW_PORT),
351 `world ${spareN}: the server proxies /api to :${world.gateway}, `
352 + `the env block says :${spareEnv.DAIMOND_GW_PORT}`);
353
354 // And the guard that would have caught it. `--break handoff` disables this one
355 // deliberately, so it is asserted on the REAL script, where the fault is absent
356 // and the guard must therefore be present.
357 check('and world.sh asks the server where it proxies rather than assuming',
358 /got_gw=\$\(identify/.test(fs.readFileSync(path.join(HERE, 'world.sh'), 'utf8')),
359 'verify_identity does not read /__world\'s gateway field');
360}
361
362// STOP WHAT WAS STARTED, AND SAY WHEN A KILL FAILED. An orphan holding a port for
363// hours is how this whole class of fault gets a second afternoon; a teardown that
364// swallows its own failure is how nobody finds out.
365async function teardown() {
366 // The world FIRST, and through the real `dev/world.sh` rather than through the
367 // copy a break may have made: `--down` reads pid files under the world's own
368 // scratch, which both copies compute identically, and the copy is about to be
369 // deleted. Deleting it first left world 21 running once.
370 if (spare) {
371 // `--down` promises not to report success while a port is held, and its
372 // exit status is the promise. Swallowing it is how a world outlives the run
373 // that started it.
374 const r = spawnSync('bash', [path.join(HERE, 'world.sh'), String(spare.n), '--down'],
375 { encoding: 'utf8' });
376 if (r.status !== 0) {
377 console.log(` note world ${spare.n} did NOT shut down cleanly: `
378 + String(r.stderr || r.stdout || '').trim());
379 }
380 }
381 for (const p of procs) {
382 if (p.path) { try { fs.unlinkSync(p.path); } catch (e) {
383 console.log(` note could not remove ${p.path}: ${e.message}`); } continue; }
384 if (!p.proc || p.gone) continue;
385 try { p.proc.kill('SIGTERM'); } catch (e) {
386 console.log(` note could not signal pid ${p.proc.pid}: ${e.message}`);
387 }
388 }
389 if (standin) await new Promise(r => standin.close(r));
390 await new Promise(r => setTimeout(r, 400));
391 for (const p of procs) {
392 if (!p.proc || p.gone) continue;
393 try { p.proc.kill('SIGKILL'); } catch (e) { /* already gone */ }
394 console.log(` note pid ${p.proc.pid} did not stop on SIGTERM and was killed`);
395 }
396 const held = [];
397 for (const port of [APP_A, APP_B, GW_A, GW_B]) if (!await free(port)) held.push(port);
398 if (held.length) {
399 console.log(` note STILL HELD after teardown: ${held.map(p => ':' + p).join(' ')}. `
400 + `Find the owner with ss -ltnp | grep -E ':(${held.join('|')}) '`);
401 }
402}
403
404try {
405 await main();
406} catch (e) {
407 check('the run completed', false, String((e && e.stack) || e));
408} finally {
409 await teardown();
410}
411
412console.log(`\n${ok} ok, ${bad} failed`);
413if (BREAK) {
414 console.log(bad ? `break '${BREAK}' correctly failed ${bad} check(s)`
415 : `break '${BREAK}': NOTHING FAILED, so the checks above prove nothing`);
416 process.exit(bad ? 0 : 1); // a break MUST fail something
417}
418process.exit(bad ? 1 : 0);