+51
-27
@@ -3,6 +3,7 @@ import { chmod, mkdir, mkdtemp, readFile, readdir, rm, writeFile } from "node:fs
|
||||
import { tmpdir } from "node:os";
|
||||
import { dirname, join } from "node:path";
|
||||
import { createHash } from "node:crypto";
|
||||
import { z } from "zod";
|
||||
import { install } from "../scripts/install.js";
|
||||
|
||||
const directories: string[] = [];
|
||||
@@ -34,6 +35,15 @@ const empty = {
|
||||
"deep-hi": "",
|
||||
},
|
||||
};
|
||||
const pluginEntry = z
|
||||
.object({ package: z.string(), options: z.record(z.string(), z.json()).optional() })
|
||||
.loose();
|
||||
const configFile = z
|
||||
.object({ plugins: z.array(z.json()), experimental: z.json().optional() })
|
||||
.loose();
|
||||
const manifestFile = z.object({ exports: z.record(z.string(), z.json()) }).loose();
|
||||
const entries = (plugins: z.infer<typeof configFile>["plugins"]) =>
|
||||
z.array(pluginEntry).parse(plugins);
|
||||
const helper = new URL("../scripts/install.ts", import.meta.url);
|
||||
async function run(
|
||||
command: string[],
|
||||
@@ -50,8 +60,8 @@ async function run(
|
||||
timeout: 30_000,
|
||||
});
|
||||
if (input !== undefined && process.stdin) {
|
||||
process.stdin.write(input);
|
||||
process.stdin.end();
|
||||
void process.stdin.write(input);
|
||||
void process.stdin.end();
|
||||
}
|
||||
const [exitCode, stdout, stderr] = await Promise.all([
|
||||
process.exited,
|
||||
@@ -83,7 +93,9 @@ async function fixture() {
|
||||
format: "esm",
|
||||
});
|
||||
expect(built.success).toBe(true);
|
||||
await Bun.write(join(source, "dist", "install.js"), built.outputs[0]!);
|
||||
const [output] = built.outputs;
|
||||
if (output === undefined) throw new Error("Missing installer build output");
|
||||
await Bun.write(join(source, "dist", "install.js"), output);
|
||||
await writeFile(join(source, "package.json"), '{"type":"module"}');
|
||||
await writeFile(
|
||||
join(config, "opencode.json"),
|
||||
@@ -96,7 +108,7 @@ async function fixture() {
|
||||
return { directory, source, config };
|
||||
}
|
||||
async function contents(config: string) {
|
||||
return JSON.parse(await readFile(join(config, "opencode.json"), "utf8"));
|
||||
return configFile.parse(JSON.parse(await readFile(join(config, "opencode.json"), "utf8")));
|
||||
}
|
||||
|
||||
for (const selection of ["quota", "agents,multi-work,quota"]) {
|
||||
@@ -106,14 +118,16 @@ for (const selection of ["quota", "agents,multi-work,quota"]) {
|
||||
const result = await contents(f.config);
|
||||
expect(result).toMatchObject(unrelated);
|
||||
expect(result.experimental === undefined).toBe(selection === "quota");
|
||||
expect(result.plugins.map((entry: { package: string }) => entry.package)).toEqual([
|
||||
expect(entries(result.plugins).map((entry) => entry.package)).toEqual([
|
||||
"before",
|
||||
...selection.split(",").map((name) => `./plugins/stanp-opencode/${name}`),
|
||||
]);
|
||||
const target = join(f.config, "plugins", "stanp-opencode");
|
||||
expect((await readdir(target)).sort()).toEqual(selection.split(",").sort());
|
||||
expect((await readdir(target)).toSorted()).toEqual(selection.split(",").toSorted());
|
||||
for (const name of selection.split(",")) {
|
||||
const manifest = await Bun.file(join(target, name, "package.json")).json();
|
||||
const manifest = manifestFile.parse(
|
||||
await Bun.file(join(target, name, "package.json")).json(),
|
||||
);
|
||||
expect(manifest.exports["./tui"] !== undefined).toBe(name === "quota");
|
||||
expect(await Bun.file(join(target, name, "tui.js")).exists()).toBe(name === "quota");
|
||||
}
|
||||
@@ -127,18 +141,23 @@ test("upgrades retain selection and options; explicit selection replaces only ow
|
||||
const f = await fixture();
|
||||
await install(f.source, f.config, "agents,quota");
|
||||
const current = await contents(f.config);
|
||||
current.plugins[1].options = {
|
||||
agents: { ...empty.agents, "fast-lo": "openai/gpt-6-luna#low" },
|
||||
};
|
||||
current.plugins.push({ package: "after", options: { untouched: 1 } });
|
||||
await writeFile(join(f.config, "opencode.json"), JSON.stringify(current));
|
||||
const [before, agents, quota] = entries(current.plugins);
|
||||
if (!before || !agents || !quota) throw new Error("Missing installed plugins");
|
||||
const after = { package: "after", options: { untouched: 1 } };
|
||||
const plugins = [
|
||||
before,
|
||||
{ ...agents, options: { agents: { ...empty.agents, "fast-lo": "openai/gpt-6-luna#low" } } },
|
||||
quota,
|
||||
after,
|
||||
];
|
||||
await writeFile(join(f.config, "opencode.json"), JSON.stringify({ ...current, plugins }));
|
||||
await install(f.source, f.config);
|
||||
expect((await contents(f.config)).plugins).toEqual(current.plugins);
|
||||
expect(entries((await contents(f.config)).plugins)).toEqual(plugins);
|
||||
await install(f.source, f.config, "quota");
|
||||
expect((await contents(f.config)).plugins).toEqual([
|
||||
current.plugins[0],
|
||||
expect(entries((await contents(f.config)).plugins)).toEqual([
|
||||
before,
|
||||
{ package: "./plugins/stanp-opencode/quota" },
|
||||
current.plugins[3],
|
||||
after,
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -152,14 +171,17 @@ test("installer writes empty option skeletons and subagent_depth, fills only mis
|
||||
{ package: "./plugins/stanp-opencode/quota" },
|
||||
]);
|
||||
expect(fresh.experimental).toEqual({ subagent_depth: 2 });
|
||||
fresh.plugins[1].options = {
|
||||
agents: { "fast-lo": "openai/gpt-6-luna#low" },
|
||||
extra: 1,
|
||||
};
|
||||
await writeFile(join(f.config, "opencode.json"), JSON.stringify(fresh));
|
||||
const [before, agents, ...rest] = entries(fresh.plugins);
|
||||
if (!before || !agents) throw new Error("Missing installed plugins");
|
||||
const plugins = [
|
||||
before,
|
||||
{ ...agents, options: { agents: { "fast-lo": "openai/gpt-6-luna#low" }, extra: 1 } },
|
||||
...rest,
|
||||
];
|
||||
await writeFile(join(f.config, "opencode.json"), JSON.stringify({ ...fresh, plugins }));
|
||||
await install(f.source, f.config);
|
||||
const filled = await contents(f.config);
|
||||
expect(filled.plugins[1].options).toEqual({
|
||||
expect(entries(filled.plugins)[1]?.options).toEqual({
|
||||
agents: { ...empty.agents, "fast-lo": "openai/gpt-6-luna#low" },
|
||||
extra: 1,
|
||||
});
|
||||
@@ -169,7 +191,7 @@ test("installer writes empty option skeletons and subagent_depth, fills only mis
|
||||
for (const [depth, expected] of [
|
||||
[1, 2],
|
||||
[3, 3],
|
||||
]) {
|
||||
] as const) {
|
||||
await writeFile(
|
||||
join(f.config, "opencode.json"),
|
||||
JSON.stringify({
|
||||
@@ -214,12 +236,14 @@ test("replaces the legacy monolith without duplicates and retains Agents options
|
||||
await writeFile(join(f.config, "plugins", "stanp-opencode.js"), "legacy");
|
||||
await install(f.source, f.config);
|
||||
const result = await contents(f.config);
|
||||
expect(result.plugins.map((entry: { package: string }) => entry.package)).toEqual([
|
||||
expect(entries(result.plugins).map((entry) => entry.package)).toEqual([
|
||||
"before",
|
||||
...names.map((name) => `./plugins/stanp-opencode/${name}`),
|
||||
"after",
|
||||
]);
|
||||
expect(result.plugins[1].options.agents["fast-lo"]).toBe("openai/gpt-6-luna#low");
|
||||
expect(result.plugins[1]).toMatchObject({
|
||||
options: { agents: { "fast-lo": "openai/gpt-6-luna#low" } },
|
||||
});
|
||||
expect(await Bun.file(join(f.config, "plugins", "stanp-opencode.js")).exists()).toBe(false);
|
||||
expect(result).toMatchObject(unrelated);
|
||||
});
|
||||
@@ -283,7 +307,7 @@ test("local shell installer builds its checkout and upgrades mixed entries witho
|
||||
await chmod(join(bin, "bun"), 0o755);
|
||||
const env = {
|
||||
...Bun.env,
|
||||
PATH: `${bin}:${dirname(process.execPath)}:${Bun.env.PATH}`,
|
||||
PATH: `${bin}:${dirname(process.execPath)}:${Bun.env["PATH"]}`,
|
||||
REAL_BUN: process.execPath,
|
||||
CALLS: join(f.directory, "calls"),
|
||||
OPENCODE_CONFIG_DIR: f.config,
|
||||
@@ -357,7 +381,7 @@ for (const validChecksum of [true, false])
|
||||
f.directory,
|
||||
{
|
||||
...Bun.env,
|
||||
PATH: `${bin}:${dirname(process.execPath)}:${Bun.env.PATH}`,
|
||||
PATH: `${bin}:${dirname(process.execPath)}:${Bun.env["PATH"]}`,
|
||||
ARCHIVE: archive,
|
||||
CHECKSUM: checksum,
|
||||
RELEASE: release,
|
||||
|
||||
Reference in New Issue
Block a user