Files
rea/tests/installScript.test.ts
T

159 lines
4.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 only a pinned REA CLI with closed stdin", async () => {
const fixture = await createFixture();
const result = await runInstaller(fixture, ["--version", "0.3.0"]);
expect(result.stdout).toContain("REA 0.3.0 is installed");
expect(result.stdout).toContain("Run ");
expect(await readFile(fixture.npmLog, "utf8")).toContain(
"install --global --prefix",
);
expect(await readFile(fixture.reaLog, "utf8")).toBe("--version\n");
expect(await readdir(fixture.temporary)).toEqual([]);
});
it("accepts Node 25 without installing or replacing it", async () => {
const fixture = await createFixture();
const result = await runInstaller(fixture, ["--version", "0.3.0"], {
FAKE_NODE_VERSION: "25.1.0",
});
expect(result.stdout).toContain("Runtime: Node.js 25.1.0");
});
it("resolves and validates the latest REA release tag", async () => {
const fixture = await createFixture();
const result = await runInstaller(fixture);
expect(result.stdout).toContain("Version: 0.3.0");
});
it("prints a dry run without invoking npm", async () => {
const fixture = await createFixture();
const result = await runInstaller(fixture, [
"--version",
"0.3.0",
"--dry-run",
]);
expect(result.stdout).toContain("no changes made");
await expect(readFile(fixture.npmLog, "utf8")).rejects.toMatchObject({
code: "ENOENT",
});
});
it.each([
["unsupported Node", { FAKE_NODE_VERSION: "20.0.0" }, "unsupported"],
["npm failure", { FAKE_NPM_FAIL: "1" }, "npm package installation failed"],
["version mismatch", { FAKE_REA_VERSION: "9.9.9" }, "does not match"],
] as const)("fails closed on %s", async (_name, overrides, message) => {
const fixture = await createFixture();
await expect(
runInstaller(fixture, ["--version", "0.3.0"], overrides),
).rejects.toMatchObject({ stderr: expect.stringContaining(message) });
});
});
interface InstallerFixture {
readonly home: string;
readonly bin: string;
readonly temporary: string;
readonly npmLog: string;
readonly reaLog: 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 reaLog = join(root, "rea.log");
await Promise.all([mkdir(home), mkdir(bin), mkdir(temporary)]);
await executable(
join(bin, "uname"),
'#!/bin/sh\n[ "$1" = "-m" ] && echo x86_64 || echo Linux\n',
);
await executable(
join(bin, "node"),
`#!/bin/sh
if [ "$1" = "-p" ]; then printf '%s\n' "\${FAKE_NODE_VERSION:-24.18.0}"; else exec ${shellQuote(process.execPath)} "$@"; fi
`,
);
await executable(
join(bin, "npm"),
`#!/bin/sh
printf '%s\n' "$*" >> "$FAKE_NPM_LOG"
[ "\${FAKE_NPM_FAIL:-}" = "1" ] && exit 1
prefix="$HOME/.local"
while [ "$#" -gt 0 ]; do
if [ "$1" = "--prefix" ]; then shift; prefix="$1"; fi
shift
done
mkdir -p "$prefix/bin"
cp "$FAKE_REA_SOURCE" "$prefix/bin/rea"
chmod +x "$prefix/bin/rea"
`,
);
await executable(
join(bin, "curl"),
'#!/bin/sh\nprintf \'{"tag_name":"rea-agents-0.3.0"}\'\n',
);
await executable(
join(bin, "rea-source"),
`#!/bin/sh
printf '%s\n' "$*" >> "$FAKE_REA_LOG"
[ "$1" = "--version" ] && printf '%s\n' "\${FAKE_REA_VERSION:-0.3.0}"
`,
);
return { home, bin, temporary, npmLog, reaLog };
};
const executable = async (path: string, source: string): Promise<void> => {
await writeFile(path, source);
await chmod(path, 0o755);
};
const runInstaller = async (
fixture: InstallerFixture,
args: readonly string[] = [],
overrides: Readonly<Record<string, string>> = {},
): Promise<{ readonly stdout: string; readonly stderr: string }> =>
execFileAsync("/bin/bash", [join(process.cwd(), "install.sh"), ...args], {
env: {
...process.env,
HOME: fixture.home,
TMPDIR: fixture.temporary,
PATH: `${fixture.bin}:/usr/bin:/bin`,
FAKE_NPM_LOG: fixture.npmLog,
FAKE_REA_LOG: fixture.reaLog,
FAKE_REA_SOURCE: join(fixture.bin, "rea-source"),
...overrides,
},
});
const shellQuote = (value: string): string =>
`'${value.replaceAll("'", `'"'"'`)}'`;