Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 10 additions & 12 deletions packages/cli/test/artifact-flag-precedence.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,9 @@ import {
CLI,
childEnv,
E2E_SECRET_KEY,
portContentionError,
portDriftError,
KERNEL_ASSIGNED_PORT,
probeThroughChild,
randomPort,
readKernelAssignedPort,
TSX,
} from './helpers/serve-process.js';
import { defineStackSourceFromLiteral, linkSpec, writeDefineStackConfig } from './helpers/define-stack-fixture.js';
Expand Down Expand Up @@ -125,7 +124,7 @@ interface Live {
output: () => string;
}

function boot(argv: string[], cwd: string, port: string, env: Record<string, string | undefined>): Promise<Live> {
function boot(argv: string[], cwd: string, env: Record<string, string | undefined>): Promise<Live> {
return new Promise((resolveBoot, rejectBoot) => {
const child = spawn(TSX, [CLI, ...argv], {
cwd,
Expand All @@ -147,26 +146,26 @@ function boot(argv: string[], cwd: string, port: string, env: Record<string, str
groups.push(child);
let out = '';
let settled = false;
const settle = (err: Error | null) => {
const settle = (verdict: Error | number) => {
if (settled) return;
settled = true;
clearTimeout(timer);
if (err) rejectBoot(err);
else resolveBoot({ child, base: `http://localhost:${port}`, output: () => out });
if (verdict instanceof Error) rejectBoot(verdict);
else resolveBoot({ child, base: `http://localhost:${verdict}`, output: () => out });
};
const timer = setTimeout(
() => settle(new Error(`${argv[0]} never printed ${READY}\n--- output ---\n${out.slice(-4000)}`)),
BOOT_TIMEOUT_MS,
);
const onData = (d: unknown) => {
out += String(d);
if (READY.test(out)) settle(portDriftError(out, `os ${argv[0]}`, port));
// The child is the authority on the port it bound: a kernel-assigned one (#24342).
if (READY.test(out)) settle(readKernelAssignedPort(out, `os ${argv[0]}`));
};
child.stdout?.on('data', onData);
child.stderr?.on('data', onData);
child.on('exit', (code) =>
settle(portContentionError(out, `os ${argv[0]}`, port)
?? new Error(`os ${argv[0]} exited ${String(code)} before ${READY}\n--- output ---\n${out.slice(-4000)}`)),
settle(new Error(`os ${argv[0]} exited ${String(code)} before ${READY}\n--- output ---\n${out.slice(-4000)}`)),
);
});
}
Expand Down Expand Up @@ -250,10 +249,9 @@ interface Reading {
}

async function measure(argv: string[], cwd: string, env: Record<string, string | undefined> = {}): Promise<Reading> {
const port = randomPort();
let live: Live | undefined;
try {
live = await boot([...argv.slice(0, 1), '-p', port, ...argv.slice(1)], cwd, port, env);
live = await boot([...argv.slice(0, 1), '-p', KERNEL_ASSIGNED_PORT, ...argv.slice(1)], cwd, env);
const signIn = await http(live, 'POST', '/api/v1/auth/sign-in/email', '', { email: EMAIL, password: PASSWORD });
const token = signIn.body?.token;
if (signIn.status !== 200 || typeof token !== 'string') {
Expand Down
5 changes: 2 additions & 3 deletions packages/cli/test/dev-boot-output-order.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ import { fileURLToPath } from 'node:url';
import {
childEnv,
E2E_SECRET_KEY,
randomPort,
KERNEL_ASSIGNED_PORT,
requireBuiltCli,
RUN_JS_RESOLVES_FROM_DIST,
} from './helpers/serve-process.js';
Expand Down Expand Up @@ -115,10 +115,9 @@ function screenLines(raw: string): string[] {

/** Boot `os dev` under a pty until the banner and the block have both printed, then Ctrl+C it. */
function bootUnderPty(boot: number): Promise<string> {
const port = randomPort();
const command = [
`exec '${process.execPath}' '${CLI}' dev --no-watch`,
`-d 'file:boot${boot}.db' -p ${port}`,
`-d 'file:boot${boot}.db' -p ${KERNEL_ASSIGNED_PORT}`,
].join(' ');
return new Promise((resolveBoot, rejectBoot) => {
const child = spawn('script', ['-qfec', command, '/dev/null'], {
Expand Down
28 changes: 14 additions & 14 deletions packages/cli/test/helpers/serve-process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,8 +182,8 @@ function probeBind(want: number): number | null {

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

Expand Down Expand Up @@ -153,9 +152,9 @@ interface LiveStart {
output: () => string;
}

function bootStart(cwd: string, home: string, port: string, extra: string[] = []): Promise<LiveStart> {
function bootStart(cwd: string, home: string, extra: string[] = []): Promise<LiveStart> {
return new Promise((resolveBoot, rejectBoot) => {
const child = spawn(TSX, [CLI, 'start', '-p', port, '--home', home, '--auth-secret', E2E_SECRET_KEY, '--no-ui', ...extra], {
const child = spawn(TSX, [CLI, 'start', '-p', KERNEL_ASSIGNED_PORT, '--home', home, '--auth-secret', E2E_SECRET_KEY, '--no-ui', ...extra], {
cwd,
// `childEnv`, never a bare `...process.env` — see its header.
env: childEnv({ NO_COLOR: '1', OS_CLOUD_URL: 'off', OS_LOG_LEVEL: 'warn', OS_SECRET_KEY: E2E_SECRET_KEY }),
Expand All @@ -166,27 +165,26 @@ function bootStart(cwd: string, home: string, port: string, extra: string[] = []
groups.push(child);
let out = '';
let settled = false;
const settle = (err: Error | null) => {
const settle = (verdict: Error | number) => {
if (settled) return;
settled = true;
clearTimeout(timer);
if (err) rejectBoot(err);
else resolveBoot({ child, base: `http://localhost:${port}`, output: () => out });
if (verdict instanceof Error) rejectBoot(verdict);
else resolveBoot({ child, base: `http://localhost:${verdict}`, output: () => out });
};
const timer = setTimeout(
() => settle(new Error(`os start never printed ${READY}\n--- output ---\n${out.slice(-4000)}`)),
BOOT_TIMEOUT_MS,
);
const onData = (d: unknown) => {
out += String(d);
// The child is the authority on the port it bound.
if (READY.test(out)) settle(portDriftError(out, 'os start', port));
// The child is the authority on the port it bound: a kernel-assigned one (#24342).
if (READY.test(out)) settle(readKernelAssignedPort(out, 'os start'));
};
child.stdout?.on('data', onData);
child.stderr?.on('data', onData);
child.on('exit', (code) =>
settle(portContentionError(out, 'os start', port)
?? new Error(`os start exited ${String(code)} before ${READY}\n--- output ---\n${out.slice(-4000)}`)),
settle(new Error(`os start exited ${String(code)} before ${READY}\n--- output ---\n${out.slice(-4000)}`)),
);
});
}
Expand Down Expand Up @@ -320,24 +318,23 @@ beforeAll(async () => {
const hostArtifact = join(runtimeDir, 'host.json');
writeFileSync(hostArtifact, JSON.stringify(HOST_ARTIFACT, null, 2), 'utf8');
const home = join(runtimeDir, 'home');
const port = randomPort();

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

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

// ── boot 3: the CONTROL — the same file as the boot artifact ───────────
const controlDir = join(root, 'control');
mkdirSync(controlDir, { recursive: true });
const third = await bootStart(controlDir, join(controlDir, 'home'), port, ['--artifact', join(appDir, 'dist', 'objectstack.json')]);
const third = await bootStart(controlDir, join(controlDir, 'home'), ['--artifact', join(appDir, 'dist', 'objectstack.json')]);
phases.control = await probe(third, await authenticate(third));
await stopGroup(third.child);
}, 4 * BOOT_TIMEOUT_MS);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,9 @@
*
* The tsx source entry (`bin/run-dev.js`), as every `runServe()` caller spawns
* it, for `os start` and for `os package install` alike. It pins
* `NODE_ENV=development`, which lets `serve` auto-shift off a port taken
* between the probe and the bind, so the ready banner is read back
* (`portDriftError`) before any request is addressed to the port. The CLI runs
* `NODE_ENV=development`, where a taken port is a hop rather than an exit, so
* each boot takes a kernel-assigned port and reads it back off its own ready
* banner (`readKernelAssignedPort`) before any request is addressed to it. The CLI runs
* from `src/`, but every workspace package it loads — `@objectstack/runtime`
* and `@objectstack/cloud-connection` included — resolves through its
* `exports` to `dist/`: an ablation of either package's source reaches this
Expand All @@ -60,10 +60,9 @@ import {
CLI,
childEnv,
E2E_SECRET_KEY,
portContentionError,
portDriftError,
KERNEL_ASSIGNED_PORT,
probeThroughChild,
randomPort,
readKernelAssignedPort,
TSX,
} from './helpers/serve-process.js';

Expand Down Expand Up @@ -142,9 +141,9 @@ interface LiveStart {
output: () => string;
}

function bootStart(cwd: string, home: string, port: string, extra: string[] = []): Promise<LiveStart> {
function bootStart(cwd: string, home: string, extra: string[] = []): Promise<LiveStart> {
return new Promise((resolveBoot, rejectBoot) => {
const child = spawn(TSX, [CLI, 'start', '-p', port, '--home', home, '--auth-secret', E2E_SECRET_KEY, '--no-ui', ...extra], {
const child = spawn(TSX, [CLI, 'start', '-p', KERNEL_ASSIGNED_PORT, '--home', home, '--auth-secret', E2E_SECRET_KEY, '--no-ui', ...extra], {
cwd,
// `childEnv`, never a bare `...process.env` — see its header.
env: childEnv({ NO_COLOR: '1', OS_CLOUD_URL: 'off', OS_LOG_LEVEL: 'warn', OS_SECRET_KEY: E2E_SECRET_KEY }),
Expand All @@ -155,27 +154,26 @@ function bootStart(cwd: string, home: string, port: string, extra: string[] = []
groups.push(child);
let out = '';
let settled = false;
const settle = (err: Error | null) => {
const settle = (verdict: Error | number) => {
if (settled) return;
settled = true;
clearTimeout(timer);
if (err) rejectBoot(err);
else resolveBoot({ child, base: `http://localhost:${port}`, output: () => out });
if (verdict instanceof Error) rejectBoot(verdict);
else resolveBoot({ child, base: `http://localhost:${verdict}`, output: () => out });
};
const timer = setTimeout(
() => settle(new Error(`os start never printed ${READY}\n--- output ---\n${out.slice(-4000)}`)),
BOOT_TIMEOUT_MS,
);
const onData = (d: unknown) => {
out += String(d);
// The child is the authority on the port it bound.
if (READY.test(out)) settle(portDriftError(out, 'os start', port));
// The child is the authority on the port it bound: a kernel-assigned one (#24342).
if (READY.test(out)) settle(readKernelAssignedPort(out, 'os start'));
};
child.stdout?.on('data', onData);
child.stderr?.on('data', onData);
child.on('exit', (code) =>
settle(portContentionError(out, 'os start', port)
?? new Error(`os start exited ${String(code)} before ${READY}\n--- output ---\n${out.slice(-4000)}`)),
settle(new Error(`os start exited ${String(code)} before ${READY}\n--- output ---\n${out.slice(-4000)}`)),
);
});
}
Expand Down Expand Up @@ -327,10 +325,9 @@ beforeAll(async () => {
const runtimeDir = join(root, 'runtime');
mkdirSync(runtimeDir, { recursive: true });
const home = join(runtimeDir, 'home');
const port = randomPort();

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

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

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