Report installer preflight and cleanup failures

This commit is contained in:
Ryan Ward
2026-10-26 07:07:45 +00:00
parent 005aa8cdb8
commit 7ce0236825
4 changed files with 131 additions and 15 deletions
@@ -0,0 +1,100 @@
import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import * as fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, test } from "node:test";
import { fileURLToPath } from "node:url";
const project = fileURLToPath(new URL("../", import.meta.url));
const fixtures = [];
afterEach(async () => {
for (const root of fixtures.splice(0)) {
await fs.rm(root, { recursive: true, force: true });
}
});
async function fixture() {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "galaxy-install-preflight-"));
fixtures.push(root);
const app = path.join(root, "script/build-and-install-to-applications");
const lib = path.join(root, "script/lib");
const bin = path.join(root, "bin");
await fs.mkdir(path.join(app, "src"), { recursive: true });
await fs.mkdir(lib, { recursive: true });
await fs.mkdir(bin);
await fs.copyFile(path.join(project, "src/index.tsx"), path.join(app, "src/index.tsx"));
await fs.copyFile(path.join(project, "package.json"), path.join(app, "package.json"));
await fs.copyFile(path.join(project, "../lib/prepare-galaxy-build.mjs"), path.join(lib, "prepare-galaxy-build.mjs"));
await fs.symlink(path.join(project, "node_modules"), path.join(app, "node_modules"), "dir");
// A regression must never progress into a real Git, build, or install operation.
const gitSentinel = path.join(root, "unexpected-git-command");
await fs.writeFile(path.join(bin, "git"), "#!/bin/sh\ntouch unexpected-git-command\nexit 97\n", { mode: 0o755 });
const run = () => {
const result = spawnSync(process.execPath, [path.join(project, "node_modules/tsx/dist/cli.mjs"), path.join(app, "src/index.tsx")], {
cwd: root,
env: { ...process.env, PATH: `${bin}${path.delimiter}${process.env.PATH || ""}`, CI: "true", FORCE_COLOR: "0" },
encoding: "utf8",
timeout: 15000,
});
assert.equal(result.error, undefined);
assert.equal(result.status, 1);
assert.match(result.stdout, /Installation failed/);
return result;
};
return { root, lib, run, gitSentinel };
}
test("cache initialization errors are visible before any normal step starts", async () => {
const { root, run, gitSentinel } = await fixture();
await fs.mkdir(path.join(root, "target"));
await fs.writeFile(path.join(root, "target/galaxy-build-cache"), "not a directory");
const result = run();
assert.match(result.stderr, /Installation failed:.*EEXIST/);
assert.match(result.stderr, /galaxy-build-cache/);
await assert.rejects(fs.access(gitSentinel));
});
test("stale locks report the exact lock path and retain the lock for manual recovery", async () => {
const { root, run, gitSentinel } = await fixture();
const lock = path.join(root, "target/galaxy-build-cache/prepare.lock");
await fs.mkdir(path.dirname(lock), { recursive: true });
await fs.symlink(JSON.stringify({ pid: 99999999, start: "old process", token: "abandoned" }), lock);
const result = run();
assert.match(result.stderr, /Installation failed: Stale Galaxy build lock/);
assert.ok(result.stderr.includes(lock));
assert.ok((await fs.lstat(lock)).isSymbolicLink());
await assert.rejects(fs.access(gitSentinel));
});
test("failures without an active step still appear in the final output", async () => {
const { lib, run, gitSentinel } = await fixture();
await fs.writeFile(path.join(lib, "prepare-galaxy-build.mjs"), `
export async function prepareGalaxyBuild() {
throw new Error("fixture early failure");
}
`);
const result = run();
assert.match(result.stderr, /Installation failed: fixture early failure/);
assert.match(result.stdout, /Installation failed: fixture early failure/);
await assert.rejects(fs.access(gitSentinel));
});
test("cleanup failures are reported without hiding the original failure", async () => {
const { root, lib, run, gitSentinel } = await fixture();
await fs.writeFile(path.join(lib, "prepare-galaxy-build.mjs"), `
export async function prepareGalaxyBuild(root) {
return {
appPath: root + "/missing/Galaxy.app",
release: async () => { throw new Error("fixture cleanup failure"); },
};
}
`);
const result = run();
assert.match(result.stderr, /Installation failed: Built app not found/);
assert.match(result.stderr, /Failed to remove temporary build files: fixture cleanup failure/);
assert.match(result.stdout, /Installation failed:\s*Built app not found/);
await assert.rejects(fs.access(gitSentinel));
await assert.rejects(fs.access(path.join(root, "target")));
});