A runner that threw ETIMEDOUT landed in the catch and read as
unavailable, bypassing the split the previous commit added. The thrown
error is now normalized onto the `{ error }` path so the timeout check
covers both shapes; a test pins the thrown case.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PsmUAZ6aLUAFqenjJKSSRp
Signed-off-by: cliffhall <cliff@futurescale.com>
271 lines
9.6 KiB
JavaScript
271 lines
9.6 KiB
JavaScript
import { strict as assert } from "node:assert";
|
|
import test from "node:test";
|
|
|
|
import {
|
|
ptyCommand,
|
|
probeScriptVersion,
|
|
SCRIPT_PROBE_TIMEOUT_MS,
|
|
resolvePtyWrapper,
|
|
scriptFlavorFor,
|
|
shellQuote,
|
|
} from "./pty.mjs";
|
|
|
|
// The flavors are not interchangeable: BSD `script` takes argv after the
|
|
// typescript file, util-linux takes ONE shell string before it, and busybox
|
|
// takes that string without `-e`. Getting it wrong is not a subtle failure —
|
|
// `script` refuses to start, and the smoke reports it as a TUI crash.
|
|
test("scriptFlavorFor prefers the version probe over the platform", () => {
|
|
assert.equal(
|
|
scriptFlavorFor({
|
|
platform: "linux",
|
|
versionOutput: "script from util-linux 2.39.3",
|
|
}),
|
|
"util-linux",
|
|
);
|
|
assert.equal(
|
|
scriptFlavorFor({
|
|
platform: "linux",
|
|
versionOutput: "BusyBox v1.36.1 (2024-01-01) multi-call binary.",
|
|
}),
|
|
"busybox",
|
|
);
|
|
// Alpine is the reason the probe exists at all: "linux" does not imply
|
|
// util-linux, and busybox's `script` rejects `-e`.
|
|
assert.notEqual(
|
|
scriptFlavorFor({ platform: "linux", versionOutput: "BusyBox v1.36.1" }),
|
|
"util-linux",
|
|
);
|
|
});
|
|
|
|
test("scriptFlavorFor falls back to the platform when the probe says nothing", () => {
|
|
// BSD `script` has no --version, so failing the probe is itself the signal.
|
|
assert.equal(scriptFlavorFor({ platform: "darwin" }), "bsd");
|
|
assert.equal(scriptFlavorFor({ platform: "freebsd" }), "bsd");
|
|
// NOT every BSD: NetBSD takes the command through `-c`, and nobody has run
|
|
// either it or OpenBSD here. Guessing builds an invocation that fails at
|
|
// `script` startup, which this smoke would then report as a TUI crash.
|
|
assert.equal(scriptFlavorFor({ platform: "netbsd" }), null);
|
|
assert.equal(scriptFlavorFor({ platform: "openbsd" }), null);
|
|
// An unidentifiable `script` on linux is overwhelmingly util-linux. Guess it:
|
|
// a wrong guess fails loudly with `script`'s own usage error in the captured
|
|
// output, whereas skipping is the silence this issue exists to stop.
|
|
assert.equal(scriptFlavorFor({ platform: "linux" }), "util-linux");
|
|
});
|
|
|
|
test("scriptFlavorFor returns null where there is no verified invocation", () => {
|
|
assert.equal(scriptFlavorFor({ platform: "win32" }), null);
|
|
assert.equal(
|
|
scriptFlavorFor({ platform: "win32", versionOutput: "not a thing" }),
|
|
null,
|
|
);
|
|
// Unsupported is the default, not a win32 special case.
|
|
assert.equal(scriptFlavorFor({ platform: "aix" }), null);
|
|
assert.equal(scriptFlavorFor({ platform: "sunos" }), null);
|
|
});
|
|
|
|
test("ptyCommand builds the BSD argv form", () => {
|
|
assert.deepEqual(
|
|
ptyCommand({
|
|
command: "/bin/node",
|
|
args: ["app.js", "--x"],
|
|
flavor: "bsd",
|
|
}),
|
|
{
|
|
command: "script",
|
|
args: ["-q", "/dev/null", "/bin/node", "app.js", "--x"],
|
|
},
|
|
);
|
|
});
|
|
|
|
test("ptyCommand builds the util-linux shell-string form with -e", () => {
|
|
// -e is what makes `script` exit with the child's status instead of its own.
|
|
assert.deepEqual(
|
|
ptyCommand({
|
|
command: "/bin/node",
|
|
args: ["app.js"],
|
|
flavor: "util-linux",
|
|
}),
|
|
{
|
|
command: "script",
|
|
args: ["-qec", "'/bin/node' 'app.js'", "/dev/null"],
|
|
},
|
|
);
|
|
});
|
|
|
|
test("ptyCommand omits -e for busybox, which has no such flag", () => {
|
|
const { args } = ptyCommand({
|
|
command: "node",
|
|
args: ["app.js"],
|
|
flavor: "busybox",
|
|
});
|
|
assert.equal(args[0], "-qc");
|
|
assert.deepEqual(args, ["-qc", "'node' 'app.js'", "/dev/null"]);
|
|
});
|
|
|
|
test("ptyCommand quotes arguments for the shell-string flavors", () => {
|
|
// The smoke passes an mkdtemp path; a space in it would otherwise split into
|
|
// two arguments inside the shell `script -c` runs.
|
|
const { args } = ptyCommand({
|
|
command: "/usr/local/my node/bin/node",
|
|
args: ["/tmp/a dir/catalog.json", "it's-fine"],
|
|
flavor: "util-linux",
|
|
});
|
|
assert.equal(
|
|
args[1],
|
|
`'/usr/local/my node/bin/node' '/tmp/a dir/catalog.json' 'it'\\''s-fine'`,
|
|
);
|
|
});
|
|
|
|
test("shellQuote survives an embedded single quote", () => {
|
|
assert.equal(shellQuote("it's"), `'it'\\''s'`);
|
|
assert.equal(shellQuote("plain"), "'plain'");
|
|
});
|
|
|
|
test("probeScriptVersion combines both streams and reads a runner failure as unavailable", () => {
|
|
assert.deepEqual(
|
|
probeScriptVersion(() => ({ stdout: "out ", stderr: "err" })),
|
|
{ available: true, output: "out err" },
|
|
);
|
|
// A non-zero exit is NOT unavailability. BSD `script` has no `--version` and
|
|
// answers with `illegal option -- -` plus its usage on stderr — which is the
|
|
// very evidence the flavor fallback reads, so it must survive the probe.
|
|
assert.deepEqual(
|
|
probeScriptVersion(() => ({
|
|
status: 1,
|
|
stdout: "",
|
|
stderr: "script: illegal option -- -\nusage: script [-aeFkpqr] ...",
|
|
})),
|
|
{
|
|
available: true,
|
|
output: "script: illegal option -- -\nusage: script [-aeFkpqr] ...",
|
|
},
|
|
);
|
|
// A runner that throws outright (injected, or a future spawn shape).
|
|
assert.deepEqual(
|
|
probeScriptVersion(() => {
|
|
throw new Error("ENOENT");
|
|
}),
|
|
{ available: false, output: "" },
|
|
);
|
|
// Told nothing at all is not evidence of a working `script`.
|
|
assert.deepEqual(
|
|
probeScriptVersion(() => undefined),
|
|
{
|
|
available: false,
|
|
output: "",
|
|
},
|
|
);
|
|
});
|
|
|
|
// `spawnSync` REPORTS ENOENT in `result.error` — it does not throw — so a
|
|
// try/catch alone reads a missing binary as "ran, printed nothing". On linux
|
|
// that empty output falls through to the util-linux guess, and `smoke:tui`
|
|
// hard-fails spawning a binary that does not exist instead of skipping.
|
|
test("probeScriptVersion treats result.error as unavailable, not as empty output", () => {
|
|
const enoent = Object.assign(new Error("spawnSync script ENOENT"), {
|
|
code: "ENOENT",
|
|
});
|
|
assert.deepEqual(
|
|
probeScriptVersion(() => ({ error: enoent, stdout: "", stderr: "" })),
|
|
{ available: false, output: "" },
|
|
);
|
|
// An `error` with no recognisable code is still "could not run it".
|
|
assert.equal(
|
|
probeScriptVersion(() => ({ error: new Error("EACCES") })).available,
|
|
false,
|
|
);
|
|
});
|
|
|
|
// The one `error` that must NOT read as unavailable (#2333). `script(1)` was
|
|
// found and started; the machine did not let it finish. Folded into
|
|
// `available: false` it becomes a `smoke:tui` SKIP — exit 0, blaming PATH — on
|
|
// exactly the loaded machine the gate is supposed to be believable on.
|
|
test("probeScriptVersion throws on a timed-out probe instead of reporting no script(1)", () => {
|
|
const timedOut = Object.assign(new Error("spawnSync script ETIMEDOUT"), {
|
|
code: "ETIMEDOUT",
|
|
});
|
|
assert.throws(
|
|
() =>
|
|
probeScriptVersion(() => ({ error: timedOut, stdout: "", stderr: "" })),
|
|
new RegExp(`did not exit within ${SCRIPT_PROBE_TIMEOUT_MS}ms`),
|
|
);
|
|
// A runner that THROWS the same error is normalized onto the same path — the
|
|
// `catch` must not quietly turn a thrown ETIMEDOUT back into "unavailable".
|
|
assert.throws(
|
|
() =>
|
|
probeScriptVersion(() => {
|
|
throw timedOut;
|
|
}),
|
|
/did not exit within/,
|
|
);
|
|
// And it reaches the caller: resolvePtyWrapper must not turn it into a skip.
|
|
assert.throws(
|
|
() =>
|
|
resolvePtyWrapper({
|
|
platform: "darwin",
|
|
probe: () => probeScriptVersion(() => ({ error: timedOut })),
|
|
}),
|
|
/did not exit within/,
|
|
);
|
|
});
|
|
|
|
test("resolvePtyWrapper reports a missing script(1) rather than guessing", () => {
|
|
// linux would otherwise be classified util-linux off the empty output.
|
|
const r = resolvePtyWrapper({
|
|
platform: "linux",
|
|
probe: () => ({ available: false, output: "" }),
|
|
});
|
|
assert.equal(r.ok, false);
|
|
assert.match(r.reason, /on PATH/);
|
|
});
|
|
|
|
test("resolvePtyWrapper wraps with the resolved flavor, or reports why not", () => {
|
|
const wrapper = resolvePtyWrapper({
|
|
platform: "linux",
|
|
probe: () => ({ available: true, output: "script from util-linux 2.39.3" }),
|
|
});
|
|
assert.equal(wrapper.ok, true);
|
|
assert.equal(wrapper.flavor, "util-linux");
|
|
assert.deepEqual(wrapper.wrap({ command: "node", args: ["a.js"] }), {
|
|
command: "script",
|
|
args: ["-qec", "'node' 'a.js'", "/dev/null"],
|
|
});
|
|
// `args` is optional at the call site.
|
|
assert.deepEqual(wrapper.wrap({ command: "node" }).args[1], "'node'");
|
|
|
|
// A present `script` on a platform whose invocation nobody verified is a
|
|
// DIFFERENT failure from a missing one, and the reason must say which.
|
|
const unsupported = resolvePtyWrapper({
|
|
platform: "win32",
|
|
probe: () => ({ available: true, output: "" }),
|
|
});
|
|
assert.equal(unsupported.ok, false);
|
|
assert.match(unsupported.reason, /no verified .* invocation for win32/);
|
|
});
|
|
|
|
// The platforms this repo claims a verified `script(1)` invocation for. Any
|
|
// other — win32, aix, sunos, netbsd, openbsd — resolves to null BY DESIGN and
|
|
// makes `smoke:tui` skip, so asserting "supported unless Windows" would fail
|
|
// `test:scripts` on a machine where the smoke is behaving exactly as intended.
|
|
const SUPPORTED_PLATFORMS = new Set(["darwin", "freebsd", "linux"]);
|
|
|
|
test("resolvePtyWrapper resolves on the platforms we claim to support", () => {
|
|
// Not a tautology: it runs the real probe against the real machine, so a
|
|
// `script` that has vanished from PATH surfaces here rather than as an opaque
|
|
// smoke failure.
|
|
const wrapper = resolvePtyWrapper();
|
|
if (SUPPORTED_PLATFORMS.has(process.platform)) {
|
|
assert.equal(
|
|
wrapper.ok,
|
|
true,
|
|
`no PTY wrapper on ${process.platform}: ${wrapper.reason ?? ""}`,
|
|
);
|
|
assert.equal(wrapper.wrap({ command: "node" }).command, "script");
|
|
} else {
|
|
assert.equal(
|
|
wrapper.ok,
|
|
false,
|
|
`${process.platform} is not in SUPPORTED_PLATFORMS but resolved a wrapper`,
|
|
);
|
|
}
|
|
});
|