Report installer preflight and cleanup failures
This commit is contained in:
@@ -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")));
|
||||
});
|
||||
Reference in New Issue
Block a user