187 lines
7.0 KiB
TypeScript
187 lines
7.0 KiB
TypeScript
import { afterAll, beforeAll, expect, test } from "bun:test";
|
|
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { createOpencodeClient } from "@opencode-ai/sdk";
|
|
import type { Config, PluginOptions } from "@opencode-ai/plugin";
|
|
import plugin from "../src/index.js";
|
|
import { z } from "zod";
|
|
import { gptAgents } from "../src/config.js";
|
|
|
|
let directory: string;
|
|
const originalModels = process.env.OPENCODE_MODELS_PATH;
|
|
const originalAuth = process.env.OPENCODE_AUTH_CONTENT;
|
|
|
|
beforeAll(async () => {
|
|
directory = await mkdtemp(join(tmpdir(), "stanp-tests-"));
|
|
const path = join(directory, "models.json");
|
|
await writeFile(
|
|
path,
|
|
JSON.stringify({
|
|
test: {
|
|
id: "test",
|
|
env: [],
|
|
npm: "@ai-sdk/openai-compatible",
|
|
models: Object.fromEntries(
|
|
["gpt-5.6-luna", "gpt-5.6-sol", "gpt-6-astra"].map((id) => [id, { id }]),
|
|
),
|
|
},
|
|
}),
|
|
);
|
|
process.env.OPENCODE_MODELS_PATH = path;
|
|
process.env.OPENCODE_AUTH_CONTENT = "{}";
|
|
});
|
|
|
|
afterAll(async () => {
|
|
if (originalModels === undefined) delete process.env.OPENCODE_MODELS_PATH;
|
|
else process.env.OPENCODE_MODELS_PATH = originalModels;
|
|
if (originalAuth === undefined) delete process.env.OPENCODE_AUTH_CONTENT;
|
|
else process.env.OPENCODE_AUTH_CONTENT = originalAuth;
|
|
await rm(directory, { recursive: true, force: true });
|
|
});
|
|
|
|
async function hooks(caller = "build", options: PluginOptions = { provider: "test" }) {
|
|
return plugin.server(
|
|
{
|
|
client: createOpencodeClient({
|
|
baseUrl: "http://localhost:1",
|
|
fetch: async (request) =>
|
|
Response.json(
|
|
request.url.endsWith("/user")
|
|
? { info: { role: "user", agent: caller }, parts: [] }
|
|
: [
|
|
{
|
|
info: {
|
|
role: "assistant",
|
|
mode: "general",
|
|
parentID: "user",
|
|
},
|
|
parts: [],
|
|
},
|
|
],
|
|
),
|
|
}),
|
|
project: { id: "test", worktree: directory, time: { created: 0 } },
|
|
directory,
|
|
worktree: directory,
|
|
serverUrl: new URL("http://localhost:1"),
|
|
experimental_workspace: { register() {} },
|
|
$: Bun.$,
|
|
},
|
|
options,
|
|
);
|
|
}
|
|
|
|
test("registration is atomic; only missing Astra degrades to four workers", async () => {
|
|
const full: Config = { provider: { test: {} } };
|
|
await (await hooks()).config(full);
|
|
// The host's /agent endpoint rejects undefined options rather than JSON-stringifying them.
|
|
z.json().parse(full.agent);
|
|
expect(Object.keys(full.agent ?? {})).toEqual([
|
|
"fast-lo",
|
|
"fast-hi",
|
|
"good-lo",
|
|
"good-hi",
|
|
"deep-lo",
|
|
"deep-hi",
|
|
"coordinate",
|
|
]);
|
|
|
|
const reduced: Config = { provider: { test: { blacklist: ["gpt-6-astra"] } } };
|
|
await (await hooks()).config(reduced);
|
|
expect(Object.keys(reduced.agent ?? {})).toEqual([
|
|
"fast-lo",
|
|
"fast-hi",
|
|
"good-lo",
|
|
"good-hi",
|
|
"coordinate",
|
|
]);
|
|
expect(reduced.agent?.coordinate?.model).toBe("test/gpt-5.6-sol");
|
|
|
|
for (const missing of ["gpt-5.6-luna", "gpt-5.6-sol"]) {
|
|
const config: Config = { provider: { test: { blacklist: [missing] } } };
|
|
await (await hooks()).config(config);
|
|
expect(config.agent).toBeUndefined();
|
|
}
|
|
});
|
|
|
|
test("disconnected and explicitly disabled providers register nothing", async () => {
|
|
const disconnected: Config = {};
|
|
await (await hooks()).config(disconnected);
|
|
expect(disconnected.agent).toBeUndefined();
|
|
|
|
const disabled: Config = { provider: { test: {} }, disabled_providers: ["test"] };
|
|
await (await hooks()).config(disabled);
|
|
expect(disabled.agent).toBeUndefined();
|
|
});
|
|
|
|
test("manual task targets cannot bypass coordinate's primary-only restriction", async () => {
|
|
const active = await hooks();
|
|
await active.config({ provider: { test: {} } });
|
|
await expect(
|
|
active["tool.execute.before"](
|
|
{ tool: "task", sessionID: "test", callID: "test" },
|
|
{ args: { subagent_type: "coordinate", prompt: "implement" } },
|
|
),
|
|
).rejects.toThrow("cannot be delegated to");
|
|
});
|
|
|
|
test("subtask bypass uses the parent user agent, not the synthetic assistant target", async () => {
|
|
const coordinate = await hooks("coordinate");
|
|
await coordinate.config({ provider: { test: { blacklist: ["gpt-6-astra"] } } });
|
|
for (const target of ["general", "deep-lo"]) {
|
|
await expect(
|
|
coordinate["tool.execute.before"](
|
|
{ tool: "task", sessionID: "test", callID: "test" },
|
|
{ args: { subagent_type: target } },
|
|
),
|
|
).rejects.toThrow("only to its available workers");
|
|
}
|
|
const worker = await hooks("fast-hi");
|
|
await worker.config({ provider: { test: {} } });
|
|
await expect(
|
|
worker["tool.execute.before"](
|
|
{ tool: "task", sessionID: "test", callID: "test" },
|
|
{ args: { subagent_type: "good-lo" } },
|
|
),
|
|
).rejects.toThrow("Workers cannot delegate");
|
|
});
|
|
|
|
test("custom assignments do not silently take the GPT fallback", async () => {
|
|
const custom = await hooks("build", {
|
|
mode: "custom",
|
|
provider: "test",
|
|
agents: { ...gptAgents, coordinate: { model: "local", effort: "low" } },
|
|
});
|
|
const config: Config = { provider: { test: { models: { local: { name: "Local" } } } } };
|
|
await custom.config(config);
|
|
expect(config.agent?.coordinate?.model).toBe("test/local");
|
|
expect(config.agent?.coordinate?.options).toEqual({ reasoningEffort: "low" });
|
|
|
|
const missing: Config = {
|
|
provider: { test: { blacklist: ["gpt-6-astra"], models: { local: {} } } },
|
|
};
|
|
await custom.config(missing);
|
|
expect(missing.agent).toBeUndefined();
|
|
});
|
|
|
|
test("tier selection requires evidence and preserves the advertised fast tier", async () => {
|
|
const namedFast = { name: "Luna", variants: { fast: { reasoningEffort: "low" } } };
|
|
const ordinary: Config = { provider: { test: { models: { "gpt-5.6-luna": namedFast } } } };
|
|
await (await hooks()).config(ordinary);
|
|
expect(ordinary.agent?.["fast-lo"]?.options).toEqual({ reasoningEffort: "low" });
|
|
|
|
const advertised: Config = {
|
|
provider: {
|
|
test: {
|
|
models: { "gpt-5.6-luna": { options: { serviceTier: "fast" } } },
|
|
},
|
|
},
|
|
};
|
|
await (await hooks()).config(advertised);
|
|
expect(advertised.agent?.["fast-lo"]?.options).toEqual({
|
|
reasoningEffort: "low",
|
|
serviceTier: "fast",
|
|
});
|
|
});
|