fix(scripts): address Copilot review round 2 on #2147
Two findings, both of which quietly undo the PR.
1. `SMOKE_TUI_SURVIVE_MS` was unvalidated. `??` guards only nullish, so
an env var set to the empty string reaches `Number("")` -> 0 and a typo
reaches NaN; `setTimeout` fires immediately on both. The survival check
would then resolve before the child could possibly have died, printing
"still running 0ms later" and asserting nothing — the exact first-paint
false green this PR removes, restored by an empty environment variable,
on a green run.
Fixed at the module rather than only at the env-parse site, so every
caller is covered: `normalizeMs` (same shape as child-cleanup.mjs's
`normalizeGraceMs`, matching an existing policy rather than inventing a
second one) applied to all four durations before anything reads them,
which also stops the messages interpolating "NaNms". Defaults moved to
an exported DEFAULTS so the smoke and the module cannot drift.
`surviveMs: 0` is deliberately NOT honored as "skip the wait": no
caller wants that, and it is indistinguishable from the typo.
2. (suppressed comment) `spawnSync` REPORTS ENOENT in `result.error` — it
does not throw — so `probeScriptVersion`'s try/catch read a missing
binary as "ran, printed nothing". On linux that empty output fell
through to the util-linux guess, and the smoke then hard-failed
spawning a binary that does not exist instead of taking the documented
skip.
`probeScriptVersion` now returns `{ available, output }`, and
`resolvePtyWrapper` returns `{ ok: true, flavor, wrap }` or
`{ ok: false, reason }` — the two unavailability causes deserve
different skip messages, since "no script(1) on PATH" and "nobody has
verified this platform's invocation" send the reader elsewhere. A
non-zero exit is explicitly not unavailability: BSD `script` has no
`--version` and answers with its usage, which is the evidence the
flavor fallback reads.
Tests for both, since each finding lives entirely in a branch no happy
path reaches: the normalizeMs table (empty string, typo, undefined, -1,
Infinity, a numeric string, 0), an end-to-end run proving a
`surviveMs: Number("")` paint-then-die stub still fails, the
`result.error` ENOENT and timeout shapes, and both `resolvePtyWrapper`
failure reasons.
Verified: `SMOKE_TUI_SURVIVE_MS="" npm run smoke:tui` reports "still
running 2000ms later"; with PATH blanked, resolvePtyWrapper returns
`{ok: false, reason: "no `script(1)` on PATH"}` rather than guessing.
Signed-off-by: Cliff Hall <cliff@futurescale.com>
Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
bd7297da88
commit
b7a73e7da3
1 changed files with 225 additions and 40 deletions
|
|
@ -109,39 +109,71 @@ export function ptyCommand({ command, args = [], flavor }) {
|
|||
}
|
||||
|
||||
/**
|
||||
* Probe the local `script(1)` for its flavor.
|
||||
* Probe the local `script(1)`: is it there, and what does it say about itself?
|
||||
*
|
||||
* @param {(cmd: string, args: string[]) => { stdout?: string, stderr?: string }} [runner]
|
||||
* **`spawnSync` does not throw on ENOENT** — it *returns* `{ error }` with
|
||||
* empty output. So a `try`/`catch` alone cannot tell "no `script` on this box"
|
||||
* from "a `script` that printed nothing", and collapsing both to `""` is
|
||||
* actively harmful: on linux the empty output falls through to the
|
||||
* `util-linux` guess, and `smoke:tui` then hard-fails spawning a binary that
|
||||
* does not exist, instead of taking the documented skip. Hence the `available`
|
||||
* flag rather than a bare string. The `catch` stays for a runner that really
|
||||
* does throw (an injected one, or a future spawn shape).
|
||||
*
|
||||
* A non-zero exit is NOT unavailability: BSD `script` has no `--version` and
|
||||
* answers `illegal option -- -` plus its usage on stderr — which is exactly the
|
||||
* evidence `scriptFlavorFor` reads. Only `error` means "could not run it".
|
||||
*
|
||||
* @param {(cmd: string, args: string[]) => { stdout?: string, stderr?: string, error?: Error }} [runner]
|
||||
* Injected for tests; defaults to a real `spawnSync`.
|
||||
* @returns {string} Combined output, or "" if `script` could not be run at all.
|
||||
* @returns {{ available: boolean, output: string }}
|
||||
*/
|
||||
export function probeScriptVersion(
|
||||
runner = (cmd, args) =>
|
||||
spawnSync(cmd, args, { encoding: "utf8", timeout: 5000 }),
|
||||
) {
|
||||
try {
|
||||
const r = runner("script", ["--version"]) ?? {};
|
||||
return `${r.stdout ?? ""}${r.stderr ?? ""}`;
|
||||
const r = runner("script", ["--version"]);
|
||||
// `error` covers ENOENT, EACCES and the timeout; a missing result at all
|
||||
// means the runner told us nothing, which is not evidence of a working one.
|
||||
if (!r || r.error) return { available: false, output: "" };
|
||||
return { available: true, output: `${r.stdout ?? ""}${r.stderr ?? ""}` };
|
||||
} catch {
|
||||
return "";
|
||||
return { available: false, output: "" };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a PTY wrapper for this machine, or null if none is available.
|
||||
* Resolve a PTY wrapper for this machine, or say why there isn't one.
|
||||
*
|
||||
* The two ways it can be unavailable are worth telling apart in the skip
|
||||
* message — "you have no `script(1)`" and "nobody has verified this platform's
|
||||
* invocation" send the reader somewhere different — so the failure carries a
|
||||
* `reason` rather than being a bare `null` the caller has to narrate.
|
||||
*
|
||||
* @param {object} [opts]
|
||||
* @param {string} [opts.platform]
|
||||
* @param {() => string} [opts.probe]
|
||||
* @returns {{ flavor: string, wrap: (spec: { command: string, args?: string[] }) => { command: string, args: string[] } } | null}
|
||||
* @param {() => { available: boolean, output: string }} [opts.probe]
|
||||
* @returns {{ ok: true, flavor: string, wrap: (spec: { command: string, args?: string[] }) => { command: string, args: string[] } }
|
||||
* | { ok: false, reason: string }}
|
||||
*/
|
||||
export function resolvePtyWrapper({
|
||||
platform = process.platform,
|
||||
probe = probeScriptVersion,
|
||||
} = {}) {
|
||||
const flavor = scriptFlavorFor({ platform, versionOutput: probe() });
|
||||
if (!flavor) return null;
|
||||
const { available, output } = probe();
|
||||
if (!available) {
|
||||
return { ok: false, reason: "no `script(1)` on PATH" };
|
||||
}
|
||||
const flavor = scriptFlavorFor({ platform, versionOutput: output });
|
||||
if (!flavor) {
|
||||
return {
|
||||
ok: false,
|
||||
reason: `no verified \`script(1)\` invocation for ${platform}`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
flavor,
|
||||
wrap: ({ command, args = [] }) => ptyCommand({ command, args, flavor }),
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue