Skip to content

Commit 4fb8873

Browse files
committed
test(cli): every bespoke CLI spawner takes a kernel-assigned port and reads it back
Slice 2 of #24342. The 23 development-column files that spawned the CLI themselves on a released port now spawn on KERNEL_ASSIGNED_PORT and read the bound port back off the child's own output; each boot builds its base URL from its own read-back. The dead local drift checks go with them. The two draws that are an arm's subject stay ledgered with their real reasons. Claude-Session: https://claude.ai/code/session_01SYEJqDL3NXxrJRBVqaMu9T Co-authored-by: Claude <noreply@anthropic.com>
1 parent cf16a0d commit 4fb8873

25 files changed

Lines changed: 349 additions & 572 deletions

‎packages/cli/test/artifact-flag-precedence.integration.test.ts‎

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -81,10 +81,9 @@ import {
8181
CLI,
8282
childEnv,
8383
E2E_SECRET_KEY,
84-
portContentionError,
85-
portDriftError,
84+
KERNEL_ASSIGNED_PORT,
8685
probeThroughChild,
87-
randomPort,
86+
readKernelAssignedPort,
8887
TSX,
8988
} from './helpers/serve-process.js';
9089
import { defineStackSourceFromLiteral, linkSpec, writeDefineStackConfig } from './helpers/define-stack-fixture.js';
@@ -125,7 +124,7 @@ interface Live {
125124
output: () => string;
126125
}
127126

128-
function boot(argv: string[], cwd: string, port: string, env: Record<string, string | undefined>): Promise<Live> {
127+
function boot(argv: string[], cwd: string, env: Record<string, string | undefined>): Promise<Live> {
129128
return new Promise((resolveBoot, rejectBoot) => {
130129
const child = spawn(TSX, [CLI, ...argv], {
131130
cwd,
@@ -147,26 +146,26 @@ function boot(argv: string[], cwd: string, port: string, env: Record<string, str
147146
groups.push(child);
148147
let out = '';
149148
let settled = false;
150-
const settle = (err: Error | null) => {
149+
const settle = (verdict: Error | number) => {
151150
if (settled) return;
152151
settled = true;
153152
clearTimeout(timer);
154-
if (err) rejectBoot(err);
155-
else resolveBoot({ child, base: `http://localhost:${port}`, output: () => out });
153+
if (verdict instanceof Error) rejectBoot(verdict);
154+
else resolveBoot({ child, base: `http://localhost:${verdict}`, output: () => out });
156155
};
157156
const timer = setTimeout(
158157
() => settle(new Error(`${argv[0]} never printed ${READY}\n--- output ---\n${out.slice(-4000)}`)),
159158
BOOT_TIMEOUT_MS,
160159
);
161160
const onData = (d: unknown) => {
162161
out += String(d);
163-
if (READY.test(out)) settle(portDriftError(out, `os ${argv[0]}`, port));
162+
// The child is the authority on the port it bound: a kernel-assigned one (#24342).
163+
if (READY.test(out)) settle(readKernelAssignedPort(out, `os ${argv[0]}`));
164164
};
165165
child.stdout?.on('data', onData);
166166
child.stderr?.on('data', onData);
167167
child.on('exit', (code) =>
168-
settle(portContentionError(out, `os ${argv[0]}`, port)
169-
?? new Error(`os ${argv[0]} exited ${String(code)} before ${READY}\n--- output ---\n${out.slice(-4000)}`)),
168+
settle(new Error(`os ${argv[0]} exited ${String(code)} before ${READY}\n--- output ---\n${out.slice(-4000)}`)),
170169
);
171170
});
172171
}
@@ -250,10 +249,9 @@ interface Reading {
250249
}
251250

252251
async function measure(argv: string[], cwd: string, env: Record<string, string | undefined> = {}): Promise<Reading> {
253-
const port = randomPort();
254252
let live: Live | undefined;
255253
try {
256-
live = await boot([...argv.slice(0, 1), '-p', port, ...argv.slice(1)], cwd, port, env);
254+
live = await boot([...argv.slice(0, 1), '-p', KERNEL_ASSIGNED_PORT, ...argv.slice(1)], cwd, env);
257255
const signIn = await http(live, 'POST', '/api/v1/auth/sign-in/email', '', { email: EMAIL, password: PASSWORD });
258256
const token = signIn.body?.token;
259257
if (signIn.status !== 200 || typeof token !== 'string') {

‎packages/cli/test/dev-boot-output-order.integration.test.ts‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ import { fileURLToPath } from 'node:url';
5858
import {
5959
childEnv,
6060
E2E_SECRET_KEY,
61-
randomPort,
61+
KERNEL_ASSIGNED_PORT,
6262
requireBuiltCli,
6363
RUN_JS_RESOLVES_FROM_DIST,
6464
} from './helpers/serve-process.js';
@@ -115,10 +115,9 @@ function screenLines(raw: string): string[] {
115115

116116
/** Boot `os dev` under a pty until the banner and the block have both printed, then Ctrl+C it. */
117117
function bootUnderPty(boot: number): Promise<string> {
118-
const port = randomPort();
119118
const command = [
120119
`exec '${process.execPath}' '${CLI}' dev --no-watch`,
121-
`-d 'file:boot${boot}.db' -p ${port}`,
120+
`-d 'file:boot${boot}.db' -p ${KERNEL_ASSIGNED_PORT}`,
122121
].join(' ');
123122
return new Promise((resolveBoot, rejectBoot) => {
124123
const child = spawn('script', ['-qfec', command, '/dev/null'], {

‎packages/cli/test/helpers/serve-process.ts‎

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -182,8 +182,8 @@ function probeBind(want: number): number | null {
182182

183183
/**
184184
* The released-port draw — a real bind probe, not a blind `Math.random()`
185-
* (#12441) — for the spawns that still take a number. ⛔ NOT for a spawn that
186-
* can reach production posture: those take {@link KERNEL_ASSIGNED_PORT}.
185+
* (#12441) — for the few sites that still take a number. ⛔ NOT for a CLI
186+
* spawn, in either posture: those take {@link KERNEL_ASSIGNED_PORT}.
187187
*
188188
* Listen on `0.0.0.0:0`, read the port the kernel assigned, close the listener,
189189
* return the port for the caller to hand to `os serve`.
@@ -224,21 +224,20 @@ function probeBind(want: number): number | null {
224224
* development-posture child hops instead, and `portDriftError()` reds the hop
225225
* just as surely, so that column closes the same way: `runServe()` hands its
226226
* child a kernel-assigned port whenever the caller passes no `--port`, and
227-
* returns the port the child bound (#24342).
227+
* returns the port the child bound; every bespoke spawner reads its own
228+
* kernel-assigned port back the same way (#24342).
228229
* `released-port-production-spawn.pin.test.ts` holds both: a file that spawns
229230
* the CLI may call this or `randomPort()` only as often as its ledger entry
230231
* there allows — zero without one.
231232
*
232233
* What may still call it, each with the race still real and unclosed there:
233234
*
234-
* • the development-column spawners slice 2 of #24342 has yet to convert —
235-
* the files that spawn `CLI` themselves (the `package-install-local-*`
236-
* `os start` family among them) and the four `bin/run.js --dev` / `os dev`
237-
* files, each ledgered in the pin with its exact count. A lost race there
238-
* is a hop to the next port, and `portDriftError()` below turns it into a
239-
* red on a correct tree;
240235
* • instruments that need a number NOTHING is listening on (a closed-port
241-
* probe), or that measure this draw itself (`serve-port-bind-probe`).
236+
* probe), or that measure this draw itself (`serve-port-bind-probe`);
237+
* • a CLI spawn whose SUBJECT is a non-zero request — a boot that must bind
238+
* exactly the port it asked for, which `--port 0` cannot ask. The pin's
239+
* ledger names each such file with its reason and exact count; a lost
240+
* race there is the residual this draw narrows and cannot close.
242241
*
243242
* `portContentionError()` below is what makes a residual loss SAY it is a port
244243
* race instead of `serve exited 1 before "Server is ready"`.
@@ -318,8 +317,8 @@ export function holdPort(): Promise<{ port: number; release: () => Promise<void>
318317
* ⚠️ The name is kept — and is now a slight misnomer, which is cheaper than the
319318
* alternative. It is `String(reservePort())` and nothing else; the draw, the
320319
* guarantees, the residual race and the callers it may still serve
321-
* (instruments, and the development-column files the pin's ledger names until
322-
* slice 2 of #24342 converts them — never another CLI spawn) are all
320+
* (instruments, and the few CLI spawns the pin's ledger names with their
321+
* reason — never another CLI spawn) are all
323322
* documented on `reservePort()` above, and there is no second mechanism hiding
324323
* behind this name.
325324
*/
@@ -599,8 +598,9 @@ export function boundPortFromBanner(output: string): BannerPortReadback {
599598
* the port between this harness's bind probe and the child's `listen()`. The
600599
* production column would die loudly, which is why it no longer draws (it takes
601600
* {@link KERNEL_ASSIGNED_PORT}, #24295); the development column drifts and gets
602-
* this. `runServe()` without a `--port` no longer draws either (#24342); the
603-
* spawners still ledgered in the pin do, and this is what makes their race a red.
601+
* this. `runServe()` without a `--port` no longer draws either, nor does any
602+
* bespoke spawner (#24342): what reaches this now is an EXPLICIT port — one held
603+
* on purpose to force the drift, or a draw the pin's ledger names.
604604
*
605605
* ## Why `unreadable` is an ERROR and not a skip
606606
*

‎packages/cli/test/package-install-local-boot-steps.integration.test.ts‎

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,9 @@ import {
5555
CLI,
5656
childEnv,
5757
E2E_SECRET_KEY,
58-
portContentionError,
59-
portDriftError,
58+
KERNEL_ASSIGNED_PORT,
6059
probeThroughChild,
61-
randomPort,
60+
readKernelAssignedPort,
6261
TSX,
6362
} from './helpers/serve-process.js';
6463

@@ -153,9 +152,9 @@ interface LiveStart {
153152
output: () => string;
154153
}
155154

156-
function bootStart(cwd: string, home: string, port: string, extra: string[] = []): Promise<LiveStart> {
155+
function bootStart(cwd: string, home: string, extra: string[] = []): Promise<LiveStart> {
157156
return new Promise((resolveBoot, rejectBoot) => {
158-
const child = spawn(TSX, [CLI, 'start', '-p', port, '--home', home, '--auth-secret', E2E_SECRET_KEY, '--no-ui', ...extra], {
157+
const child = spawn(TSX, [CLI, 'start', '-p', KERNEL_ASSIGNED_PORT, '--home', home, '--auth-secret', E2E_SECRET_KEY, '--no-ui', ...extra], {
159158
cwd,
160159
// `childEnv`, never a bare `...process.env` — see its header.
161160
env: childEnv({ NO_COLOR: '1', OS_CLOUD_URL: 'off', OS_LOG_LEVEL: 'warn', OS_SECRET_KEY: E2E_SECRET_KEY }),
@@ -166,27 +165,26 @@ function bootStart(cwd: string, home: string, port: string, extra: string[] = []
166165
groups.push(child);
167166
let out = '';
168167
let settled = false;
169-
const settle = (err: Error | null) => {
168+
const settle = (verdict: Error | number) => {
170169
if (settled) return;
171170
settled = true;
172171
clearTimeout(timer);
173-
if (err) rejectBoot(err);
174-
else resolveBoot({ child, base: `http://localhost:${port}`, output: () => out });
172+
if (verdict instanceof Error) rejectBoot(verdict);
173+
else resolveBoot({ child, base: `http://localhost:${verdict}`, output: () => out });
175174
};
176175
const timer = setTimeout(
177176
() => settle(new Error(`os start never printed ${READY}\n--- output ---\n${out.slice(-4000)}`)),
178177
BOOT_TIMEOUT_MS,
179178
);
180179
const onData = (d: unknown) => {
181180
out += String(d);
182-
// The child is the authority on the port it bound.
183-
if (READY.test(out)) settle(portDriftError(out, 'os start', port));
181+
// The child is the authority on the port it bound: a kernel-assigned one (#24342).
182+
if (READY.test(out)) settle(readKernelAssignedPort(out, 'os start'));
184183
};
185184
child.stdout?.on('data', onData);
186185
child.stderr?.on('data', onData);
187186
child.on('exit', (code) =>
188-
settle(portContentionError(out, 'os start', port)
189-
?? new Error(`os start exited ${String(code)} before ${READY}\n--- output ---\n${out.slice(-4000)}`)),
187+
settle(new Error(`os start exited ${String(code)} before ${READY}\n--- output ---\n${out.slice(-4000)}`)),
190188
);
191189
});
192190
}
@@ -320,24 +318,23 @@ beforeAll(async () => {
320318
const hostArtifact = join(runtimeDir, 'host.json');
321319
writeFileSync(hostArtifact, JSON.stringify(HOST_ARTIFACT, null, 2), 'utf8');
322320
const home = join(runtimeDir, 'home');
323-
const port = randomPort();
324321

325322
// ── boot 1: the host, hot install, probe ───────────────────────────────
326-
const first = await bootStart(runtimeDir, home, port, ['--artifact', hostArtifact]);
323+
const first = await bootStart(runtimeDir, home, ['--artifact', hostArtifact]);
327324
const token = await authenticate(first);
328325
installs.push(await packageInstall(appDir, first));
329326
phases.install = await probe(first, token);
330327
await stopGroup(first.child);
331328

332329
// ── boot 2: same host, home and cwd — the ledger rehydrates on kernel:ready ──
333-
const second = await bootStart(runtimeDir, home, port, ['--artifact', hostArtifact]);
330+
const second = await bootStart(runtimeDir, home, ['--artifact', hostArtifact]);
334331
phases.restart = await probe(second, await authenticate(second));
335332
await stopGroup(second.child);
336333

337334
// ── boot 3: the CONTROL — the same file as the boot artifact ───────────
338335
const controlDir = join(root, 'control');
339336
mkdirSync(controlDir, { recursive: true });
340-
const third = await bootStart(controlDir, join(controlDir, 'home'), port, ['--artifact', join(appDir, 'dist', 'objectstack.json')]);
337+
const third = await bootStart(controlDir, join(controlDir, 'home'), ['--artifact', join(appDir, 'dist', 'objectstack.json')]);
341338
phases.control = await probe(third, await authenticate(third));
342339
await stopGroup(third.child);
343340
}, 4 * BOOT_TIMEOUT_MS);

‎packages/cli/test/package-install-local-handlers.integration.test.ts‎

Lines changed: 16 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,9 @@
4040
*
4141
* The tsx source entry (`bin/run-dev.js`), as every `runServe()` caller spawns
4242
* it, for `os start` and for `os package install` alike. It pins
43-
* `NODE_ENV=development`, which lets `serve` auto-shift off a port taken
44-
* between the probe and the bind, so the ready banner is read back
45-
* (`portDriftError`) before any request is addressed to the port. The CLI runs
43+
* `NODE_ENV=development`, where a taken port is a hop rather than an exit, so
44+
* each boot takes a kernel-assigned port and reads it back off its own ready
45+
* banner (`readKernelAssignedPort`) before any request is addressed to it. The CLI runs
4646
* from `src/`, but every workspace package it loads — `@objectstack/runtime`
4747
* and `@objectstack/cloud-connection` included — resolves through its
4848
* `exports` to `dist/`: an ablation of either package's source reaches this
@@ -60,10 +60,9 @@ import {
6060
CLI,
6161
childEnv,
6262
E2E_SECRET_KEY,
63-
portContentionError,
64-
portDriftError,
63+
KERNEL_ASSIGNED_PORT,
6564
probeThroughChild,
66-
randomPort,
65+
readKernelAssignedPort,
6766
TSX,
6867
} from './helpers/serve-process.js';
6968

@@ -142,9 +141,9 @@ interface LiveStart {
142141
output: () => string;
143142
}
144143

145-
function bootStart(cwd: string, home: string, port: string, extra: string[] = []): Promise<LiveStart> {
144+
function bootStart(cwd: string, home: string, extra: string[] = []): Promise<LiveStart> {
146145
return new Promise((resolveBoot, rejectBoot) => {
147-
const child = spawn(TSX, [CLI, 'start', '-p', port, '--home', home, '--auth-secret', E2E_SECRET_KEY, '--no-ui', ...extra], {
146+
const child = spawn(TSX, [CLI, 'start', '-p', KERNEL_ASSIGNED_PORT, '--home', home, '--auth-secret', E2E_SECRET_KEY, '--no-ui', ...extra], {
148147
cwd,
149148
// `childEnv`, never a bare `...process.env` — see its header.
150149
env: childEnv({ NO_COLOR: '1', OS_CLOUD_URL: 'off', OS_LOG_LEVEL: 'warn', OS_SECRET_KEY: E2E_SECRET_KEY }),
@@ -155,27 +154,26 @@ function bootStart(cwd: string, home: string, port: string, extra: string[] = []
155154
groups.push(child);
156155
let out = '';
157156
let settled = false;
158-
const settle = (err: Error | null) => {
157+
const settle = (verdict: Error | number) => {
159158
if (settled) return;
160159
settled = true;
161160
clearTimeout(timer);
162-
if (err) rejectBoot(err);
163-
else resolveBoot({ child, base: `http://localhost:${port}`, output: () => out });
161+
if (verdict instanceof Error) rejectBoot(verdict);
162+
else resolveBoot({ child, base: `http://localhost:${verdict}`, output: () => out });
164163
};
165164
const timer = setTimeout(
166165
() => settle(new Error(`os start never printed ${READY}\n--- output ---\n${out.slice(-4000)}`)),
167166
BOOT_TIMEOUT_MS,
168167
);
169168
const onData = (d: unknown) => {
170169
out += String(d);
171-
// The child is the authority on the port it bound.
172-
if (READY.test(out)) settle(portDriftError(out, 'os start', port));
170+
// The child is the authority on the port it bound: a kernel-assigned one (#24342).
171+
if (READY.test(out)) settle(readKernelAssignedPort(out, 'os start'));
173172
};
174173
child.stdout?.on('data', onData);
175174
child.stderr?.on('data', onData);
176175
child.on('exit', (code) =>
177-
settle(portContentionError(out, 'os start', port)
178-
?? new Error(`os start exited ${String(code)} before ${READY}\n--- output ---\n${out.slice(-4000)}`)),
176+
settle(new Error(`os start exited ${String(code)} before ${READY}\n--- output ---\n${out.slice(-4000)}`)),
179177
);
180178
});
181179
}
@@ -327,10 +325,9 @@ beforeAll(async () => {
327325
const runtimeDir = join(root, 'runtime');
328326
mkdirSync(runtimeDir, { recursive: true });
329327
const home = join(runtimeDir, 'home');
330-
const port = randomPort();
331328

332329
// ── boot 1: empty `os start`, install, probe, reinstall, probe ─────────
333-
const first = await bootStart(runtimeDir, home, port);
330+
const first = await bootStart(runtimeDir, home);
334331
const token = await authenticate(first);
335332
installs.push(await packageInstall(appDir, first));
336333
phases.install = await probe(first, token);
@@ -339,14 +336,14 @@ beforeAll(async () => {
339336
await stopGroup(first.child);
340337

341338
// ── boot 2: same home and cwd — the ledger rehydrates on kernel:ready ──
342-
const second = await bootStart(runtimeDir, home, port);
339+
const second = await bootStart(runtimeDir, home);
343340
phases.restart = await probe(second, await authenticate(second));
344341
await stopGroup(second.child);
345342

346343
// ── boot 3: the CONTROL — the same file as the boot artifact ───────────
347344
const controlDir = join(root, 'control');
348345
mkdirSync(controlDir, { recursive: true });
349-
const third = await bootStart(controlDir, join(controlDir, 'home'), port, ['--artifact', join(appDir, 'dist', 'objectstack.json')]);
346+
const third = await bootStart(controlDir, join(controlDir, 'home'), ['--artifact', join(appDir, 'dist', 'objectstack.json')]);
350347
phases.control = await probe(third, await authenticate(third));
351348
await stopGroup(third.child);
352349
}, 4 * BOOT_TIMEOUT_MS);

0 commit comments

Comments
 (0)