157 lines
4.8 KiB
TypeScript
157 lines
4.8 KiB
TypeScript
import { chmod, mkdtemp, rm, symlink, writeFile } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { delimiter, join } from "node:path";
|
|
|
|
import { afterEach, describe, expect, it } from "vitest";
|
|
|
|
import { inspectRuntimeExecutables } from "../src/application/RuntimeExecutableDiagnostics.js";
|
|
|
|
const roots: string[] = [];
|
|
|
|
afterEach(async () => {
|
|
await Promise.all(
|
|
roots.splice(0).map((root) => rm(root, { recursive: true, force: true })),
|
|
);
|
|
});
|
|
|
|
describe("runtime executable diagnostics", () => {
|
|
it("preserves lexical and canonical identities for duplicate healthy runtimes", async () => {
|
|
const first = await temporaryRoot();
|
|
const second = await temporaryRoot();
|
|
await symlink(process.execPath, join(first, "node"));
|
|
await symlink(process.execPath, join(second, "node"));
|
|
|
|
const inventory = await inspectRuntimeExecutables({
|
|
platform: process.platform,
|
|
path: [first, second].join(delimiter),
|
|
launcherNode: process.execPath,
|
|
});
|
|
const pathNodes = inventory.candidates.filter(
|
|
({ tool, path_index: pathIndex }) =>
|
|
tool === "node" && pathIndex !== null,
|
|
);
|
|
|
|
expect(pathNodes).toHaveLength(2);
|
|
expect(pathNodes.map(({ selection }) => selection)).toEqual([
|
|
"path-primary",
|
|
"path-shadowed",
|
|
]);
|
|
expect(new Set(pathNodes.map(({ canonical_path: path }) => path))).toEqual(
|
|
new Set([process.execPath]),
|
|
);
|
|
expect(pathNodes.every(({ healthy }) => healthy)).toBe(true);
|
|
});
|
|
|
|
it("distinguishes a healthy primary runtime from a broken shadowed candidate", async () => {
|
|
const healthy = await temporaryRoot();
|
|
const broken = await temporaryRoot();
|
|
await symlink(process.execPath, join(healthy, "node"));
|
|
await executable(
|
|
join(broken, "node"),
|
|
"#!/bin/sh\necho broken-runtime >&2\nexit 17\n",
|
|
);
|
|
|
|
const inventory = await inspectRuntimeExecutables({
|
|
platform: process.platform,
|
|
path: [healthy, broken].join(delimiter),
|
|
launcherNode: process.execPath,
|
|
});
|
|
const pathNodes = inventory.candidates.filter(
|
|
({ tool, path_index: pathIndex }) =>
|
|
tool === "node" && pathIndex !== null,
|
|
);
|
|
|
|
expect(pathNodes[0]).toMatchObject({
|
|
healthy: true,
|
|
selection: "path-primary",
|
|
});
|
|
expect(pathNodes[1]).toMatchObject({
|
|
healthy: false,
|
|
selection: "path-shadowed",
|
|
failure: {
|
|
code: "runtime_nonzero_exit",
|
|
exit_code: 17,
|
|
stderr: "broken-runtime",
|
|
},
|
|
});
|
|
});
|
|
|
|
it("classifies a macOS dynamic-loader failure and names the dependency", async () => {
|
|
const root = await temporaryRoot();
|
|
await executable(
|
|
join(root, "node"),
|
|
"#!/bin/sh\necho 'dyld: Library not loaded: /opt/homebrew/opt/simdjson/lib/libsimdjson.29.dylib' >&2\nexit 134\n",
|
|
);
|
|
|
|
const inventory = await inspectRuntimeExecutables({
|
|
platform: "darwin",
|
|
path: root,
|
|
launcherNode: process.execPath,
|
|
});
|
|
const candidate = inventory.candidates.find(
|
|
({ tool, path_index: pathIndex }) => tool === "node" && pathIndex === 0,
|
|
);
|
|
|
|
expect(candidate).toMatchObject({
|
|
healthy: false,
|
|
failure: {
|
|
code: "runtime_dynamic_library_missing",
|
|
dependency: "/opt/homebrew/opt/simdjson/lib/libsimdjson.29.dylib",
|
|
},
|
|
});
|
|
});
|
|
|
|
it("probes env shebangs under the diagnosed PATH", async () => {
|
|
const root = await temporaryRoot();
|
|
await symlink(process.execPath, join(root, "node"));
|
|
await executable(
|
|
join(root, "npm"),
|
|
"#!/usr/bin/env node\nconsole.log('npm-under-effective-path')\n",
|
|
);
|
|
|
|
const inventory = await inspectRuntimeExecutables({
|
|
platform: process.platform,
|
|
path: root,
|
|
launcherNode: process.execPath,
|
|
});
|
|
const npm = inventory.candidates.find(({ tool }) => tool === "npm");
|
|
|
|
expect(npm).toMatchObject({
|
|
healthy: true,
|
|
version: "npm-under-effective-path",
|
|
selection: "path-primary",
|
|
});
|
|
});
|
|
|
|
it("classifies a bounded probe timeout before its kill signal", async () => {
|
|
const root = await temporaryRoot();
|
|
await executable(join(root, "node"), "#!/bin/sh\nwhile :; do :; done\n");
|
|
|
|
const inventory = await inspectRuntimeExecutables({
|
|
platform: process.platform,
|
|
path: root,
|
|
launcherNode: process.execPath,
|
|
timeoutMs: 20,
|
|
});
|
|
const candidate = inventory.candidates.find(
|
|
({ tool, path_index: pathIndex }) => tool === "node" && pathIndex === 0,
|
|
);
|
|
|
|
expect(candidate).toMatchObject({
|
|
healthy: false,
|
|
failure: { code: "runtime_timeout", signal: "SIGTERM" },
|
|
});
|
|
});
|
|
});
|
|
|
|
const temporaryRoot = async (): Promise<string> => {
|
|
const root = await mkdtemp(join(tmpdir(), "rea-runtime-diagnostics-"));
|
|
roots.push(root);
|
|
return root;
|
|
};
|
|
|
|
const executable = async (path: string, source: string): Promise<void> => {
|
|
await writeFile(path, source);
|
|
await chmod(path, 0o700);
|
|
};
|