import { afterEach, expect, test } from "bun:test"; import { createHash } from "node:crypto"; import { chmod, mkdir, mkdtemp, readFile, readdir, readlink, rm, symlink, writeFile, } from "node:fs/promises"; import { tmpdir } from "node:os"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; const installer = fileURLToPath(new URL("../install.sh", import.meta.url)); const directories: string[] = []; const configContents = '{\n // Keep user settings and comments.\n "theme": "dark"\n}\n'; const previousPlugin = 'export default { id: "previous", setup() { return "preserved"; } };\n'; const previousTarget = "../stanp-opencode/install.previous"; afterEach(async () => { await Promise.all( directories.splice(0).map((path) => rm(path, { recursive: true, force: true })), ); }); async function run(command: string[], cwd: string, env: NodeJS.ProcessEnv) { const child = Bun.spawn(command, { cwd, env, stdout: "pipe", stderr: "pipe", timeout: 20_000, }); const [exitCode, stdout, stderr] = await Promise.all([ child.exited, new Response(child.stdout).text(), new Response(child.stderr).text(), ]); return { exitCode, stdout, stderr }; } async function install(mode: "native" | "checksum" | "legacy") { const directory = await mkdtemp(join(tmpdir(), "stanp-install-")); directories.push(directory); const config = mode === "native" ? join(directory, "xdg", "opencode") : join(directory, "custom config with spaces"); const bin = join(directory, "bin"); const fixture = join(directory, "package"); await Promise.all([ mkdir(bin), mkdir(join(fixture, "dist"), { recursive: true }), mkdir(join(config, "plugins"), { recursive: true }), mkdir(join(config, "stanp-opencode", "install.previous"), { recursive: true }), ]); await Promise.all([ writeFile(join(config, "opencode.jsonc"), configContents), symlink(previousTarget, join(config, "plugins", "stanp-opencode")), writeFile(join(config, "stanp-opencode", "install.previous", "index.js"), previousPlugin), writeFile( join(config, "stanp-opencode", "install.previous", "package.json"), '{"name":"stanp-opencode","type":"module","main":"./index.js"}\n', ), writeFile(join(fixture, "index.js"), 'export { default } from "./dist/index.js";\n'), writeFile( join(fixture, "package.json"), JSON.stringify({ name: "stanp-opencode", version: "1.0.0", type: "module", main: "./dist/index.js", exports: "./dist/index.js", scripts: { postinstall: "bun ./postinstall.js" }, }), ), writeFile( join(fixture, "postinstall.js"), 'await Bun.write(process.env.POSTINSTALL_MARKER, "executed");\n', ), writeFile( join(fixture, "dist", "index.js"), 'await Bun.write(process.env.IMPORT_MARKER, "imported");\n' + (mode === "legacy" ? 'export default { id: "stanp-opencode", async server() { return {}; } };\n' : 'export default { id: "stanp-opencode", setup() { return "native-ready"; } };\n'), ), ]); const env = { ...Bun.env, PATH: `${bin}:${dirname(process.execPath)}:${Bun.env.PATH ?? "/usr/bin:/bin"}`, HOME: directory, XDG_CONFIG_HOME: join(directory, "xdg"), OPENCODE_CONFIG_DIR: mode === "native" ? "" : config, FIXTURE_DIR: directory, POSTINSTALL_MARKER: join(directory, "postinstall-ran"), IMPORT_MARKER: join(directory, "import-ran"), }; const packed = await run(["tar", "-czf", "stanp-opencode.tgz", "package"], directory, env); expect(packed, packed.stderr).toMatchObject({ exitCode: 0 }); const digest = mode === "checksum" ? "0".repeat(64) : createHash("sha256") .update(await readFile(join(directory, "stanp-opencode.tgz"))) .digest("hex"); await Promise.all([ writeFile(join(directory, "stanp-opencode.tgz.sha256"), `${digest} stanp-opencode.tgz\n`), writeFile( join(directory, "release.json"), JSON.stringify({ tag_name: "v1.0.0", assets: ["stanp-opencode.tgz", "stanp-opencode.tgz.sha256"].map((name) => ({ name, browser_download_url: `https://fixtures.invalid/${name}`, })), }), ), writeFile( join(bin, "curl"), `#!/usr/bin/env bash set -euo pipefail url="" output="" while (($#)); do case "$1" in -o|--output) output="$2"; shift 2 ;; https://*) url="$1"; shift ;; *) shift ;; esac done case "$url" in https://git.krtss.de/api/v1/repos/StanPonomarev/stanp-opencode/releases/latest) source=release.json ;; https://fixtures.invalid/stanp-opencode.tgz) source=stanp-opencode.tgz ;; https://fixtures.invalid/stanp-opencode.tgz.sha256) source=stanp-opencode.tgz.sha256 ;; *) printf 'Unexpected fixture URL: %s\\n' "$url" >&2; exit 1 ;; esac if [[ -n "$output" ]]; then cp "$FIXTURE_DIR/$source" "$output" else cat "$FIXTURE_DIR/$source" fi `, ), ]); await chmod(join(bin, "curl"), 0o755); const result = await run(["bash", installer], directory, env); return { directory, config, env, result }; } test("installs an importable native plugin under XDG without changing user configuration or running scripts", async () => { const { directory, config, env, result } = await install("native"); expect(result, result.stderr).toMatchObject({ exitCode: 0 }); // The generated package path is only known at runtime; exercise its module-loading boundary. const imported = await run( [ process.execPath, "-e", "const { default: plugin } = await import(process.argv[1]); console.log(JSON.stringify([plugin.id, plugin.setup()]));", join(config, "plugins", "stanp-opencode"), ], directory, env, ); expect(imported, imported.stderr).toMatchObject({ exitCode: 0, stdout: '["stanp-opencode","native-ready"]\n', }); expect(await readFile(join(config, "opencode.jsonc"), "utf8")).toBe(configContents); expect( await readFile(join(config, "stanp-opencode", "install.previous", "index.js"), "utf8"), ).toBe(previousPlugin); expect(await Bun.file(env.POSTINSTALL_MARKER).exists()).toBe(false); }, 60_000); for (const mode of ["checksum", "legacy"] as const) { test(`${mode} failure preserves the installed plugin in a custom path containing spaces`, async () => { const { directory, config, env, result } = await install(mode); expect(result.exitCode, result.stderr).not.toBe(0); expect(await readlink(join(config, "plugins", "stanp-opencode"))).toBe(previousTarget); expect(await readFile(join(config, "opencode.jsonc"), "utf8")).toBe(configContents); expect( await readFile(join(config, "stanp-opencode", "install.previous", "index.js"), "utf8"), ).toBe(previousPlugin); expect(await readdir(join(config, "stanp-opencode"))).toEqual(["install.previous"]); expect(await readdir(join(config, "plugins"))).toEqual(["stanp-opencode"]); // Load the preserved package from this test's runtime-selected configuration directory. const imported = await run( [ process.execPath, "-e", "const { default: plugin } = await import(process.argv[1]); console.log(plugin.setup());", join(config, "plugins", "stanp-opencode"), ], directory, env, ); expect(imported, imported.stderr).toMatchObject({ exitCode: 0, stdout: "preserved\n" }); expect(await Bun.file(env.POSTINSTALL_MARKER).exists()).toBe(false); expect(await Bun.file(env.IMPORT_MARKER).exists()).toBe(mode === "legacy"); }, 60_000); }