297 lines
12 KiB
TypeScript
297 lines
12 KiB
TypeScript
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 jsonConfig = { permissions: { shell: "ask" }, plugins: [{ package: "existing-plugin" }] };
|
|
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",
|
|
configured = false,
|
|
playwright = false,
|
|
) {
|
|
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),
|
|
writeFile(
|
|
join(config, "opencode.json"),
|
|
JSON.stringify(
|
|
configured
|
|
? {
|
|
...jsonConfig,
|
|
plugins: [
|
|
...jsonConfig.plugins,
|
|
{
|
|
package: "./plugins/stanp-opencode",
|
|
options: {
|
|
mode: "gpt",
|
|
provider: "existing-provider",
|
|
lunaPriority: false,
|
|
},
|
|
},
|
|
],
|
|
}
|
|
: jsonConfig,
|
|
),
|
|
),
|
|
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", "./tui": "./dist/tui.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'),
|
|
),
|
|
writeFile(
|
|
join(fixture, "dist", "tui.js"),
|
|
'export default { id: "stanp-opencode.quota-tui", setup() { return "tui-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, id) => ({
|
|
id: id + 1,
|
|
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?asset_id=1) source=stanp-opencode.tgz ;;
|
|
https://fixtures.invalid/stanp-opencode.tgz.sha256?asset_id=2) 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
|
|
`,
|
|
),
|
|
writeFile(
|
|
join(bin, "sha256sum"),
|
|
`#!/usr/bin/env bash
|
|
if [[ "\${1:-}" == -* ]]; then
|
|
printf 'usage: sha256sum [-bctwz] [files ...]\n' >&2
|
|
exit 1
|
|
fi
|
|
PATH="\${PATH#*:}"
|
|
export PATH
|
|
exec sha256sum "$@"
|
|
`,
|
|
),
|
|
]);
|
|
await Promise.all([chmod(join(bin, "curl"), 0o755), chmod(join(bin, "sha256sum"), 0o755)]);
|
|
const result = await run(
|
|
["bash", installer, ...(playwright ? ["--playwright"] : [])],
|
|
directory,
|
|
env,
|
|
);
|
|
return { directory, config, env, result };
|
|
}
|
|
|
|
test("installs an importable native plugin and default config under XDG without 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(JSON.parse(await readFile(join(config, "opencode.json"), "utf8"))).toEqual({
|
|
$schema: "https://opencode.ai/config.json",
|
|
plugins: [
|
|
{ package: "existing-plugin" },
|
|
{
|
|
package: "./plugins/stanp-opencode",
|
|
options: { mode: "gpt", provider: "openai", lunaPriority: true },
|
|
},
|
|
],
|
|
permissions: { shell: "ask" },
|
|
});
|
|
expect(await Bun.file(join(config, "plugins", "stanp-opencode", "package.json")).exists()).toBe(
|
|
true,
|
|
);
|
|
expect(await Bun.file(join(config, "plugins", "stanp-opencode", "tui.js")).exists()).toBe(true);
|
|
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);
|
|
|
|
test("preserves an existing stanp-opencode configuration", async () => {
|
|
const { config, result } = await install("native", true);
|
|
expect(result, result.stderr).toMatchObject({ exitCode: 0 });
|
|
const installed = JSON.parse(await readFile(join(config, "opencode.json"), "utf8"));
|
|
expect(installed.plugins).toEqual([
|
|
{ package: "existing-plugin" },
|
|
{
|
|
package: "./plugins/stanp-opencode",
|
|
options: {
|
|
mode: "gpt",
|
|
provider: "existing-provider",
|
|
lunaPriority: false,
|
|
},
|
|
},
|
|
]);
|
|
expect(installed.permissions).toEqual(jsonConfig.permissions);
|
|
}, 60_000);
|
|
|
|
test("enables Playwright only when requested", async () => {
|
|
const { config, result } = await install("native", false, true);
|
|
expect(result, result.stderr).toMatchObject({ exitCode: 0 });
|
|
const installed = JSON.parse(await readFile(join(config, "opencode.json"), "utf8"));
|
|
expect(installed.plugins.at(-1).options.playwright).toBe(true);
|
|
}, 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(JSON.parse(await readFile(join(config, "opencode.json"), "utf8"))).toEqual(
|
|
jsonConfig,
|
|
);
|
|
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);
|
|
}
|