@@ -97,7 +97,10 @@ test("reads credentials file and sends OAuth usage request", async () => {
|
||||
changed++;
|
||||
},
|
||||
async (input, init) => {
|
||||
calls.push({ url: String(input), headers: new Headers(init?.headers) });
|
||||
calls.push({
|
||||
url: input instanceof Request ? input.url : String(input),
|
||||
headers: new Headers(init?.headers),
|
||||
});
|
||||
return Response.json(payload);
|
||||
},
|
||||
60_000,
|
||||
@@ -105,10 +108,12 @@ test("reads credentials file and sends OAuth usage request", async () => {
|
||||
);
|
||||
await tracker.init();
|
||||
expect(calls).toHaveLength(1);
|
||||
expect(calls[0]!.url).toBe("https://api.anthropic.com/api/oauth/usage");
|
||||
expect(calls[0]!.headers.get("authorization")).toBe("Bearer token-123");
|
||||
expect(calls[0]!.headers.get("anthropic-beta")).toBe("oauth-2025-04-20");
|
||||
expect(calls[0]!.headers.get("accept")).toBe("application/json");
|
||||
const [call] = calls;
|
||||
if (!call) throw new Error("Missing usage request");
|
||||
expect(call.url).toBe("https://api.anthropic.com/api/oauth/usage");
|
||||
expect(call.headers.get("authorization")).toBe("Bearer token-123");
|
||||
expect(call.headers.get("anthropic-beta")).toBe("oauth-2025-04-20");
|
||||
expect(call.headers.get("accept")).toBe("application/json");
|
||||
expect(tracker.status()?.plan).toBe("max");
|
||||
expect(tracker.status()?.windows[0]?.remaining).toBe(87.5);
|
||||
expect(changed).toBe(1);
|
||||
|
||||
+5
-5
@@ -10,13 +10,13 @@ export default Plugin.define({
|
||||
const providerID = Provider.ID.make("fixture");
|
||||
yield* context.provider.transform((editor) =>
|
||||
editor.add({
|
||||
info: {
|
||||
...Provider.Info.empty(providerID),
|
||||
// Copy own fields like the former spread; the editor accepts plain info.
|
||||
info: Object.assign({}, Provider.Info.empty(providerID), {
|
||||
name: "Isolated fixture",
|
||||
activation: "enabled",
|
||||
activation: "enabled" as const,
|
||||
package: "@opencode/ai/providers/openai-compatible",
|
||||
settings: { baseURL: process.env.FIXTURE_MODEL_URL! },
|
||||
},
|
||||
settings: { baseURL: process.env["FIXTURE_MODEL_URL"] ?? "" },
|
||||
}),
|
||||
models: ["gpt-6-luna", "gpt-6.1-sol", "gpt-6-astra"].map((id) =>
|
||||
Model.Info.default(providerID, Model.ID.make(id)),
|
||||
),
|
||||
|
||||
Vendored
+2
-2
@@ -37,7 +37,7 @@ export default Plugin.define({
|
||||
const child = yield* context.session.get({ sessionID: event.sessionID });
|
||||
yield* Effect.promise(() =>
|
||||
appendFile(
|
||||
process.env.FIXTURE_TRACE!,
|
||||
process.env["FIXTURE_TRACE"] ?? "",
|
||||
JSON.stringify({
|
||||
id: child.id,
|
||||
parent: child.parentID,
|
||||
@@ -59,7 +59,7 @@ export default Plugin.define({
|
||||
description: "native host proof",
|
||||
prompt: "Reply with FIXTURE_COMPLETE",
|
||||
background: input.background,
|
||||
...(input.child
|
||||
...(input.child !== undefined && input.child !== ""
|
||||
? { sessionID: input.child }
|
||||
: {
|
||||
worktree: {
|
||||
|
||||
+8
-7
@@ -45,9 +45,9 @@ async function fixture() {
|
||||
let childWorkspaceIDOverride: string | undefined;
|
||||
let childPathOverride: string | undefined;
|
||||
let repoOverride: string | undefined;
|
||||
const run = async (args: string[], cwd: string, signal: AbortSignal) => {
|
||||
const run = async (args: string[], cwd: string, abort: AbortSignal) => {
|
||||
calls.push(args);
|
||||
if (args[0] !== "herdr") return command(args, cwd, signal);
|
||||
if (args[0] !== "herdr") return command(args, cwd, abort);
|
||||
if (args[1] === "workspace")
|
||||
return JSON.stringify({
|
||||
result: {
|
||||
@@ -57,7 +57,7 @@ async function fixture() {
|
||||
},
|
||||
});
|
||||
if (args[2] === "list") {
|
||||
const id = args[4]!;
|
||||
const id = args[4] ?? "";
|
||||
const path = id === "w2" ? destination : root;
|
||||
const sourceWorkspaceID = sourceWorkspaceIDOverride ?? (id === "w2" ? "w1" : id);
|
||||
const sourcePath = sourcePathOverride ?? root;
|
||||
@@ -93,10 +93,10 @@ async function fixture() {
|
||||
expect(args[2]).toBe("create");
|
||||
expect(args.slice(3, 5)).toEqual(["--cwd", root]);
|
||||
expect(args.at(-1)).toBe("--no-focus");
|
||||
branch = args[args.indexOf("--branch") + 1]!;
|
||||
branch = args[args.indexOf("--branch") + 1] ?? "";
|
||||
const labelIndex = args.indexOf("--label");
|
||||
label = labelIndex === -1 ? undefined : args[labelIndex + 1];
|
||||
const base = args[args.indexOf("--base") + 1]!;
|
||||
const base = args[args.indexOf("--base") + 1] ?? "";
|
||||
// Real Git worktree behavior, but only in an isolated temporary repository.
|
||||
await command(["git", "worktree", "add", "-b", branch, destination, base], root, signal);
|
||||
return JSON.stringify({
|
||||
@@ -144,14 +144,15 @@ test("create verifies a linked workspace whose inventory source remains the pare
|
||||
const f = await fixture();
|
||||
const root = await f.herdr.root(f.root, f.signal);
|
||||
expect(root?.workspaceID).toBe("w1");
|
||||
const destination = await f.herdr.create(root!, "other", "multi-work/fixture", f.signal);
|
||||
if (!root) throw new Error("Missing Herdr root");
|
||||
const destination = await f.herdr.create(root, "other", "multi-work/fixture", f.signal);
|
||||
expect(destination.directory).toBe(f.destination);
|
||||
expect(destination.branch).toStartWith("multi-work/");
|
||||
expect(await command(["git", "branch", "--show-current"], f.root, f.signal)).toBe("main");
|
||||
await f.herdr.verify(
|
||||
destination.directory,
|
||||
destination.workspaceID,
|
||||
root!,
|
||||
root,
|
||||
destination.branch,
|
||||
f.signal,
|
||||
);
|
||||
|
||||
+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,
|
||||
|
||||
+29
-20
@@ -70,6 +70,19 @@ interface FixtureSession {
|
||||
permissions: Agent.Info["permissions"];
|
||||
}
|
||||
|
||||
const defaultInput = {
|
||||
agent: "coordinate",
|
||||
description: "test",
|
||||
prompt: "work",
|
||||
worktree: { base: "main" },
|
||||
};
|
||||
|
||||
function required<T>(map: Map<string, T>, key: string) {
|
||||
const value = map.get(key);
|
||||
if (value === undefined) throw new Error(`Missing fixture entry ${key}`);
|
||||
return value;
|
||||
}
|
||||
|
||||
function fixture() {
|
||||
const herdr = new FixtureHerdr();
|
||||
const storage = new Map<string, Parameters<Context["storage"]["set"]>[1]>();
|
||||
@@ -112,7 +125,7 @@ function fixture() {
|
||||
})
|
||||
.parse(raw);
|
||||
const id = Session.ID.make(input.sessionID ?? `ses_child${++serial}`);
|
||||
if (!input.sessionID)
|
||||
if (input.sessionID === undefined || input.sessionID === "")
|
||||
sessions.set(id, {
|
||||
...parent,
|
||||
id,
|
||||
@@ -122,7 +135,7 @@ function fixture() {
|
||||
});
|
||||
order.push("create");
|
||||
yield* call.progress({ sessionID: id, status: "running" });
|
||||
order.push(`prompt:${sessions.get(id)!.location.directory}`);
|
||||
order.push(`prompt:${required(sessions, id).location.directory}`);
|
||||
prompts++;
|
||||
entered.resolve();
|
||||
if (hold)
|
||||
@@ -135,7 +148,10 @@ function fixture() {
|
||||
);
|
||||
return {
|
||||
content: "native result",
|
||||
metadata: { sessionID: id, status: input.background ? "running" : "completed" },
|
||||
metadata: {
|
||||
sessionID: id,
|
||||
status: input.background === true ? "running" : "completed",
|
||||
},
|
||||
};
|
||||
}),
|
||||
};
|
||||
@@ -144,7 +160,7 @@ function fixture() {
|
||||
[];
|
||||
const hooks: ((event: SessionContext) => Effect.Effect<void>)[] = [];
|
||||
let prompt!: (event: Pick<SessionContext, "sessionID">) => Effect.Effect<void>;
|
||||
let transform!: (editor: AgentEditor) => void;
|
||||
let transform: ((editor: AgentEditor) => void) | undefined;
|
||||
const registration = Effect.succeed({ dispose: Effect.void });
|
||||
const editAgents = (): AgentEditor => ({
|
||||
get: (id) => agents.get(id),
|
||||
@@ -185,16 +201,16 @@ function fixture() {
|
||||
}),
|
||||
},
|
||||
agent: {
|
||||
transform: (callback: typeof transform) =>
|
||||
transform: (callback: (editor: AgentEditor) => void) =>
|
||||
Effect.sync(() => {
|
||||
transform = callback;
|
||||
callback(editAgents());
|
||||
}).pipe(Effect.andThen(registration)),
|
||||
reload: () =>
|
||||
Effect.sync(() => {
|
||||
if (transform) transform(editAgents());
|
||||
transform?.(editAgents());
|
||||
}),
|
||||
get: () => Effect.succeed({ data: agents.get("multi-work")! }),
|
||||
get: () => Effect.succeed({ data: required(agents, "multi-work") }),
|
||||
list: () =>
|
||||
Effect.sync(() => {
|
||||
agentReads++;
|
||||
@@ -204,14 +220,15 @@ function fixture() {
|
||||
},
|
||||
session: {
|
||||
get: ({ sessionID }: { sessionID: string }) =>
|
||||
Effect.sync(() => sessions.get(sessionID)!),
|
||||
Effect.sync(() => required(sessions, sessionID)),
|
||||
move: ({ sessionID, directory }: { sessionID: string; directory: string }) =>
|
||||
Effect.sync(() => {
|
||||
order.push("move-request");
|
||||
if (!delayMove) {
|
||||
sessions.get(sessionID)!.location.directory = directory;
|
||||
required(sessions, sessionID).location.directory = directory;
|
||||
order.push("moved");
|
||||
if (crossProject) sessions.get(sessionID)!.projectID = "another-project";
|
||||
if (crossProject)
|
||||
required(sessions, sessionID).projectID = "another-project";
|
||||
}
|
||||
entered.resolve();
|
||||
}),
|
||||
@@ -272,15 +289,7 @@ function fixture() {
|
||||
Agent.Info.default(Agent.ID.make("build")).permissions,
|
||||
herdr,
|
||||
),
|
||||
invoke: (
|
||||
input = {
|
||||
agent: "coordinate",
|
||||
description: "test",
|
||||
prompt: "work",
|
||||
worktree: { base: "main" },
|
||||
},
|
||||
caller = call,
|
||||
) => tool.execute(input, caller),
|
||||
invoke: (input = defaultInput, caller = call) => tool.execute(input, caller),
|
||||
execute: (input: z.infer<typeof request>, caller = call) => tool.execute(input, caller),
|
||||
hold() {
|
||||
hold = true;
|
||||
@@ -381,7 +390,7 @@ test("root-local resume rejects location, binding, and live-root changes", async
|
||||
prompt: "work",
|
||||
});
|
||||
if (tamper === "session")
|
||||
f.sessions.get("ses_child1")!.location.directory = "/other";
|
||||
required(f.sessions, "ses_child1").location.directory = "/other";
|
||||
else if (tamper === "live-root") f.herdr.rootWorkspaceID = "foreign";
|
||||
else {
|
||||
const binding = bindingSchema.parse(
|
||||
|
||||
+13
-4
@@ -3,6 +3,7 @@ import { mkdir, mkdtemp, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { Host } from "@opencode/plugin/host";
|
||||
import { z } from "zod";
|
||||
import { build } from "../scripts/build.js";
|
||||
import { install } from "../scripts/install.js";
|
||||
|
||||
@@ -23,13 +24,19 @@ test("packed package has independent native plugins and quota-only TUI discovery
|
||||
});
|
||||
expect(await unpack.exited).toBe(0);
|
||||
const source = join(directory, "package");
|
||||
const manifest = await Bun.file(join(source, "package.json")).json();
|
||||
const manifest = z
|
||||
.object({ exports: z.record(z.string(), z.object({ import: z.string() }).loose()) })
|
||||
.loose()
|
||||
.parse(await Bun.file(join(source, "package.json")).json());
|
||||
const entrypoint = (name: string) =>
|
||||
join(source, z.string().parse(manifest.exports[name]?.import));
|
||||
expect(manifest.exports["."]).toBeUndefined();
|
||||
expect(await Bun.file(join(source, "index.js")).exists()).toBe(false);
|
||||
expect(await Bun.file(join(source, "dist", "index.js")).exists()).toBe(false);
|
||||
for (const name of ["agents", "multi-work", "quota"]) {
|
||||
const entrypoint = join(source, manifest.exports[`./${name}`].import);
|
||||
const { default: plugin } = await import(entrypoint);
|
||||
const { default: plugin } = z
|
||||
.object({ default: z.object({ id: z.string() }).loose() })
|
||||
.parse(await import(entrypoint(`./${name}`)));
|
||||
expect(plugin.id).toBe(`stanp-opencode.${name}`);
|
||||
}
|
||||
const config = join(directory, "config");
|
||||
@@ -42,7 +49,9 @@ test("packed package has independent native plugins and quota-only TUI discovery
|
||||
expect(entries.server).toBeDefined();
|
||||
expect(entries.tui !== undefined).toBe(name === "quota");
|
||||
}
|
||||
const { QuotaRpc } = await import(join(source, manifest.exports["./quota-rpc"].import));
|
||||
const { QuotaRpc } = z
|
||||
.object({ QuotaRpc: z.object({ id: z.string() }).loose() })
|
||||
.parse(await import(entrypoint("./quota-rpc")));
|
||||
expect(QuotaRpc.id).toBe("stanp-opencode.quota");
|
||||
} finally {
|
||||
await rm(directory, { recursive: true, force: true });
|
||||
|
||||
+24
-22
@@ -31,24 +31,29 @@ const agents = {
|
||||
"deep-lo": "test/gpt-6-astra#low",
|
||||
"deep-hi": "test/gpt-6-astra#high",
|
||||
};
|
||||
const defaultOptions = { agents };
|
||||
const defaultLocation = { directory: "/fixture" };
|
||||
async function flush() {
|
||||
await Bun.sleep(10);
|
||||
}
|
||||
async function host(
|
||||
catalog: ModelInfo[] = models,
|
||||
options: Plugin.Context["options"] = { agents },
|
||||
options: Plugin.Context["options"] = defaultOptions,
|
||||
existingSkill?: Skill.Info,
|
||||
) {
|
||||
const agents = new Map<string, DeepMutable<Agent.Info>>();
|
||||
const registry = new Map<string, DeepMutable<Agent.Info>>();
|
||||
let build: DeepMutable<Agent.Info> = Agent.Info.default(Agent.ID.make("build"));
|
||||
const skills = new Map<string, Skill.Info>(
|
||||
existingSkill ? [[existingSkill.id, existingSkill]] : [],
|
||||
);
|
||||
let transform: (editor: AgentEditor) => void;
|
||||
let transform: ((editor: AgentEditor) => void) | undefined;
|
||||
let contextHook: (event: Pick<SessionContext, "agent" | "tools">) => Promise<void> | void;
|
||||
let promptHook: (event: { sessionID: string }) => Promise<void> | void;
|
||||
let before: (event: { tool: string; agent: string; input: unknown }) => Promise<void> | void;
|
||||
let parent = "build";
|
||||
let child = "fast-lo";
|
||||
let tools: string[] = [];
|
||||
let toolTransform: (editor: ToolEditor) => void;
|
||||
let toolTransform: ((editor: ToolEditor) => void) | undefined;
|
||||
let permissions: () => Promise<Agent.Info["permissions"]>;
|
||||
let controller: ReadableStreamDefaultController<unknown>;
|
||||
const stream = new ReadableStream({
|
||||
@@ -58,20 +63,20 @@ async function host(
|
||||
});
|
||||
const registration = { async dispose() {} };
|
||||
const replay = () => {
|
||||
agents.clear();
|
||||
agents.set("build", structuredClone(build));
|
||||
registry.clear();
|
||||
registry.set("build", structuredClone(build));
|
||||
transform?.({
|
||||
get: (id) => agents.get(id),
|
||||
list: () => [...agents.values()],
|
||||
get: (id) => registry.get(id),
|
||||
list: () => [...registry.values()],
|
||||
default() {},
|
||||
remove: (id) => {
|
||||
agents.delete(id);
|
||||
registry.delete(id);
|
||||
},
|
||||
update(id, update) {
|
||||
const draft =
|
||||
agents.get(id) ?? structuredClone(Agent.Info.default(Agent.ID.make(id)));
|
||||
registry.get(id) ?? structuredClone(Agent.Info.default(Agent.ID.make(id)));
|
||||
update(draft);
|
||||
agents.set(id, draft);
|
||||
registry.set(id, draft);
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -110,7 +115,7 @@ async function host(
|
||||
},
|
||||
agent: {
|
||||
async list() {
|
||||
return { data: [...agents.values()] };
|
||||
return { data: [...registry.values()] };
|
||||
},
|
||||
async reload() {
|
||||
replay();
|
||||
@@ -182,11 +187,8 @@ async function host(
|
||||
replay();
|
||||
const cleanup = await setup(context);
|
||||
if (cleanup) cleanups.push(cleanup);
|
||||
const flush = async () => {
|
||||
await Bun.sleep(10);
|
||||
};
|
||||
return {
|
||||
agents,
|
||||
agents: registry,
|
||||
skills,
|
||||
permissions: () => permissions(),
|
||||
async configure(rules: Agent.Info["permissions"]) {
|
||||
@@ -195,7 +197,7 @@ async function host(
|
||||
controller.enqueue({ type: "agent.updated" });
|
||||
await flush();
|
||||
},
|
||||
async models(next: ModelInfo[], location = { directory: "/fixture" }) {
|
||||
async models(next: ModelInfo[], location = defaultLocation) {
|
||||
catalog = next;
|
||||
controller.enqueue({ type: "model.updated", location });
|
||||
await flush();
|
||||
@@ -205,10 +207,10 @@ async function host(
|
||||
replayTools();
|
||||
await flush();
|
||||
},
|
||||
async delegate(agent: string, target: string) {
|
||||
delegate: async (agent: string, target: string) => {
|
||||
await before({ tool: "subagent", agent, input: { agent: target } });
|
||||
},
|
||||
async command(owner: string, target: string) {
|
||||
command: async (owner: string, target: string) => {
|
||||
parent = owner;
|
||||
child = target;
|
||||
await promptHook({ sessionID: "child" });
|
||||
@@ -231,7 +233,7 @@ async function host(
|
||||
|
||||
test("worker availability, deep fallback, and model refresh remain provider aware", async () => {
|
||||
const h = await host();
|
||||
expect([...h.skills.keys()].sort()).toEqual(["anti-overengineering", "task-planning"]);
|
||||
expect([...h.skills.keys()].toSorted()).toEqual(["anti-overengineering", "task-planning"]);
|
||||
expect(String(h.agents.get("coordinate")?.model?.id)).toBe("gpt-6-astra");
|
||||
await h.models(models.slice(0, 2));
|
||||
expect(String(h.agents.get("coordinate")?.model?.id)).toBe("gpt-6.1-sol");
|
||||
@@ -256,7 +258,7 @@ test("assignments are provider-qualified with optional deep pair and variant; pa
|
||||
const shallow = Object.fromEntries(
|
||||
Object.entries(agents).filter(([name]) => !name.startsWith("deep-")),
|
||||
);
|
||||
const duplicate = models.map((model) => ({ ...model, providerID: Provider.ID.make("other") }));
|
||||
const duplicate = models.map((entry) => ({ ...entry, providerID: Provider.ID.make("other") }));
|
||||
const h = await host([...duplicate, ...models], { agents: shallow });
|
||||
expect(String(h.agents.get("coordinate")?.model?.providerID)).toBe("test");
|
||||
expect(String(h.agents.get("coordinate")?.model?.id)).toBe("gpt-6.1-sol");
|
||||
@@ -295,6 +297,6 @@ test("native tool and command paths retain core delegation restrictions", async
|
||||
expect(
|
||||
z
|
||||
.object({ agent: z.object({ type: z.string(), enum: z.array(z.string()) }) })
|
||||
.parse((await h.request("coordinate")).subagent?.input.properties).agent,
|
||||
.parse((await h.request("coordinate"))["subagent"]?.input["properties"]).agent,
|
||||
).toEqual({ type: "string", enum: ["fast-lo", "fast-hi", "good-lo", "good-hi"] });
|
||||
});
|
||||
|
||||
@@ -18,9 +18,7 @@ test("Quota loads independently and retains its RPC ID and nullable status", asy
|
||||
location: { directory: "/fixture" },
|
||||
integration: {
|
||||
connection: {
|
||||
async active() {
|
||||
return undefined;
|
||||
},
|
||||
async active() {},
|
||||
},
|
||||
},
|
||||
event: {
|
||||
@@ -56,7 +54,7 @@ test("Quota loads independently and retains its RPC ID and nullable status", asy
|
||||
) => ReturnType<typeof quota.setup>;
|
||||
const cleanup = await setup(context);
|
||||
expect(quota.id).toBe("stanp-opencode.quota");
|
||||
expect(await status!()).toBeNull();
|
||||
expect(await status?.()).toBeNull();
|
||||
expect(modelHook).toBe(true);
|
||||
if (cleanup) await cleanup();
|
||||
expect(disposed).toBe(true);
|
||||
|
||||
+2
-2
@@ -186,8 +186,8 @@ function fixture(
|
||||
changes: () => changes,
|
||||
headers,
|
||||
stored,
|
||||
switch(credential: typeof current) {
|
||||
current = credential;
|
||||
switch(next: typeof current) {
|
||||
current = next;
|
||||
},
|
||||
logout: () => {
|
||||
current = undefined;
|
||||
|
||||
Reference in New Issue
Block a user