rea/tests/installScript.test.ts

200 lines
6.8 KiB
TypeScript

import { execFile } from "node:child_process";
import {
chmod,
mkdtemp,
mkdir,
readFile,
readdir,
rm,
writeFile,
} from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { promisify } from "node:util";
import { afterEach, describe, expect, it } from "vitest";
const execFileAsync = promisify(execFile);
const roots: string[] = [];
afterEach(async () => {
await Promise.all(
roots.splice(0).map((root) => rm(root, { recursive: true, force: true })),
);
});
describe("curl installer scenarios", () => {
it("installs a pinned release with closed stdin and cleans temporary files", async () => {
const fixture = await createFixture();
const result = await runInstaller(fixture, { REA_VERSION: "0.3.0" });
expect(result.stdout).toContain("REA 0.3.0 is installed and ready");
expect(await readFile(fixture.npmLog, "utf8")).toContain(
"rea-agents@0.3.0",
);
expect(await readdir(fixture.temporary)).toEqual([]);
});
it("resolves and validates the latest rea-agents release tag", async () => {
const fixture = await createFixture();
const result = await runInstaller(fixture);
expect(result.stdout).toContain("Installing rea-agents@0.3.0");
});
it("bootstraps checksum-verified Node 24 into the user prefix", async () => {
const fixture = await createFixture();
const result = await runInstaller(fixture, {
REA_VERSION: "0.3.0",
FAKE_OLD_NODE: "1",
FAKE_NODE_ARCHIVE: fixture.nodeArchive,
FAKE_NODE_SUMS: fixture.nodeSums,
});
expect(result.stdout).toContain("REA 0.3.0 is installed and ready");
expect(
await readFile(
join(fixture.home, ".local/share/rea/node/bin/node"),
"utf8",
),
).toContain("24.18.0");
});
it("reports fresh Hopper activation as partial success", async () => {
const fixture = await createFixture();
const result = await runInstaller(fixture, {
REA_VERSION: "0.3.0",
FAKE_SETUP_ACTIONS: '["installed_hopper"]',
});
expect(result.stdout).toContain("One user action remains");
expect(result.stdout).toContain("open Hopper");
});
it.each([
[
"injected version",
{ REA_VERSION: "0.3.0;touch /tmp/pwned" },
"exact semantic version",
],
[
"npm failure",
{ REA_VERSION: "0.3.0", FAKE_NPM_FAIL: "1" },
"npm package installation failed",
],
[
"version mismatch",
{ REA_VERSION: "0.3.0", FAKE_REA_VERSION: "9.9.9" },
"does not match",
],
[
"malformed setup",
{ REA_VERSION: "0.3.0", FAKE_SETUP_JSON: "not-json" },
"malformed output",
],
[
"unhealthy doctor",
{ REA_VERSION: "0.3.0", FAKE_DOCTOR_HEALTHY: "false" },
"unhealthy required component",
],
] as const)("fails closed on %s", async (_name, overrides, message) => {
const fixture = await createFixture();
await expect(runInstaller(fixture, overrides)).rejects.toMatchObject({
stderr: expect.stringContaining(message),
});
});
it("is safe to rerun", async () => {
const fixture = await createFixture();
await runInstaller(fixture, { REA_VERSION: "0.3.0" });
await runInstaller(fixture, { REA_VERSION: "0.3.0" });
expect(
(await readFile(fixture.npmLog, "utf8")).trim().split("\n"),
).toHaveLength(2);
expect(await readdir(fixture.temporary)).toEqual([]);
});
});
interface InstallerFixture {
readonly home: string;
readonly bin: string;
readonly temporary: string;
readonly npmLog: string;
readonly nodeArchive: string;
readonly nodeSums: string;
}
const createFixture = async (): Promise<InstallerFixture> => {
const root = await mkdtemp(join(tmpdir(), "rea-install-test-"));
roots.push(root);
const home = join(root, "home");
const bin = join(root, "bin");
const temporary = join(root, "tmp");
const npmLog = join(root, "npm.log");
const nodeFixture = join(root, "node-v24.18.0-linux-x64");
const nodeArchive = `${nodeFixture}.tar.xz`;
const nodeSums = join(root, "SHASUMS256.txt");
await Promise.all([mkdir(home), mkdir(bin), mkdir(temporary)]);
const realNode = process.execPath;
await mkdir(join(nodeFixture, "bin"), { recursive: true });
await executable(
join(nodeFixture, "bin/node"),
`#!/bin/sh\nif [ "$1" = "-p" ]; then echo 24.18.0; else exec ${shellQuote(realNode)} "$@"; fi\n`,
);
await executable(
join(nodeFixture, "bin/npm"),
'#!/bin/sh\nif [ "$1" = "--version" ]; then echo 11.16.0; exit 0; fi\nprintf "%s\\n" "$*" >> "$FAKE_NPM_LOG"\nexit 0\n',
);
await execFileAsync("tar", [
"-cJf",
nodeArchive,
"-C",
root,
"node-v24.18.0-linux-x64",
]);
const checksum = (
await execFileAsync("sha256sum", [nodeArchive])
).stdout.split(" ")[0];
await writeFile(nodeSums, `${checksum} node-v24.18.0-linux-x64.tar.xz\n`);
await executable(
join(bin, "uname"),
'#!/bin/sh\n[ "$1" = "-m" ] && echo x86_64 || echo Linux\n',
);
await executable(
join(bin, "node"),
`#!/bin/sh\nif [ "$1" = "-p" ]; then [ "\${FAKE_OLD_NODE:-}" = "1" ] && echo 20.0.0 || echo 24.18.0; else exec ${shellQuote(realNode)} "$@"; fi\n`,
);
await executable(
join(bin, "npm"),
'#!/bin/sh\nif [ "$1" = "--version" ]; then echo 11.16.0; exit 0; fi\nprintf "%s\\n" "$*" >> "$FAKE_NPM_LOG"\n[ "${FAKE_NPM_FAIL:-}" = "1" ] && exit 1\nexit 0\n',
);
await executable(
join(bin, "curl"),
'#!/bin/sh\nout=""\nurl=""\nwhile [ "$#" -gt 0 ]; do\n if [ "$1" = "-o" ]; then shift; out="$1"; else url="$1"; fi\n shift\ndone\ncase "$url" in\n *node-v24.18.0-linux-x64.tar.xz) cp "$FAKE_NODE_ARCHIVE" "$out" ;;\n *SHASUMS256.txt) cp "$FAKE_NODE_SUMS" "$out" ;;\n *) printf \'{"tag_name":"rea-agents-0.3.0"}\' ;;\nesac\n',
);
await executable(
join(bin, "rea"),
'#!/bin/sh\ncase "$1" in\n--version) printf "%s\\n" "${FAKE_REA_VERSION:-0.3.0}" ;;\nsetup) if [ -n "${FAKE_SETUP_JSON:-}" ]; then printf "%s\\n" "$FAKE_SETUP_JSON"; else printf \'{"status":"ready","actions":%s}\\n\' "${FAKE_SETUP_ACTIONS:-[]}"; fi ;;\ndoctor) printf \'{"healthy":%s,"checks":[]}\\n\' "${FAKE_DOCTOR_HEALTHY:-true}" ;;\nesac\n',
);
return { home, bin, temporary, npmLog, nodeArchive, nodeSums };
};
const executable = async (path: string, source: string): Promise<void> => {
await writeFile(path, source);
await chmod(path, 0o755);
};
const runInstaller = async (
fixture: InstallerFixture,
overrides: Readonly<Record<string, string>> = {},
): Promise<{ readonly stdout: string; readonly stderr: string }> =>
execFileAsync("/bin/bash", [join(process.cwd(), "install.sh")], {
env: {
...process.env,
HOME: fixture.home,
TMPDIR: fixture.temporary,
PATH: `${fixture.bin}:${process.env.PATH ?? ""}`,
FAKE_NPM_LOG: fixture.npmLog,
...overrides,
},
});
const shellQuote = (value: string): string =>
`'${value.replaceAll("'", `'"'"'`)}'`;