import { afterEach, expect, test } from "bun:test"; import { chmod, mkdir, mkdtemp, rm, symlink, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { Agent, Model, Plugin, Provider } from "@opencode/plugin"; import type { AgentEditor } from "@opencode/plugin/promise/agent"; import type { DeepMutable } from "@opencode/plugin/promise/types"; import type { Info, ToolContext, ToolEditor } from "@opencode/plugin/promise/tool"; import type { JsonValue, ModelInfo, SessionGetInput } from "@opencode/client"; import type { SessionContext } from "@opencode/plugin/promise/session"; import type { SkillEditor } from "@opencode/plugin/promise/skill"; import type { Skill } from "@opencode/schema/skill"; import { z } from "zod"; import { gptAgents, settings, workerNames } from "../src/config.js"; import { basePlugin as plugin } from "../src/index.js"; import { playwrightMcpCommand } from "../src/playwright.js"; import { bindingKey } from "../src/multi-work.js"; const providerID = Provider.ID.make("test"); function model(id: string): ModelInfo { return Model.Info.default(providerID, Model.ID.make(id)); } function reasoningModel(id: string): ModelInfo { return { ...model(id), variants: ["low", "medium", "high"].map((effort) => ({ id: Model.VariantID.make(effort), settings: { reasoningEffort: effort }, })), }; } function providerModel(provider: string, id: string): ModelInfo { return { ...reasoningModel(id), providerID: Provider.ID.make(provider), }; } const allModels = ["gpt-6-luna", "gpt-6.1-sol", "gpt-6-astra"].map(reasoningModel); type CustomAssignment = { model: string; effort: "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max"; }; const customAssignments = (provider = "test") => { const assignment = (name: (typeof workerNames)[number]): CustomAssignment => ({ ...gptAgents[name], model: `${provider}/${gptAgents[name].model}`, }); return { "fast-lo": assignment("fast-lo"), "fast-hi": assignment("fast-hi"), "good-lo": assignment("good-lo"), "good-hi": assignment("good-hi"), "deep-lo": assignment("deep-lo"), "deep-hi": assignment("deep-hi"), }; }; const cleanups: Plugin.Cleanup[] = []; afterEach(async () => { for (const cleanup of cleanups.splice(0)) await cleanup(); }); type ModelContext = Pick; type HookEvent = ModelContext | Pick; type FixtureSetup = ( context: Pick, ) => Plugin.Cleanup | void | Promise; async function host( models: ModelInfo[] = allModels, options: Plugin.Context["options"] = { provider: "test" }, configuredMcp: Record< string, { type: "local"; command: string[]; codemode?: boolean; disabled?: boolean; timeout?: { startup?: number; catalog?: number; execution?: number }; } > = {}, configuredMcpTools: string[] = [], directory = "/test", workspaceID?: string, ) { const agents = new Map>(); const build: DeepMutable = Agent.Info.default(Agent.ID.make("build")); build.permissions.push({ action: "shell", resource: "git push *", effect: "ask" }); agents.set("build", build); const originalBuild = structuredClone(build); const sessionHooks: Record Promise | void)[]> = {}; let before!: (event: { tool: string; agent: string; input: unknown }) => Promise | void; let parent = "build"; let child = "fast-lo"; let childDirectory = directory; let defaultAgent = "build"; const registration = { async dispose() {} }; let transform!: (editor: AgentEditor) => void; let reloaded = Promise.withResolvers(); let events!: ReadableStreamDefaultController<{ type: string; location?: { directory: string; workspaceID?: string }; }>; const stream = new ReadableStream<{ type: string; location?: { directory: string; workspaceID?: string }; }>({ start(controller) { events = controller; }, }); const storage = new Map(); const tools = new Map(); const skills = new Map(); const mcpTools = new Set(configuredMcpTools); const mcpServers = new Map(Object.entries(structuredClone(configuredMcp))); let mcpTransform!: (editor: { get(name: string): (typeof configuredMcp)[string] | undefined; set(name: string, server: (typeof configuredMcp)[string]): void; update(name: string, update: (server: (typeof configuredMcp)[string]) => void): void; }) => void; const toolTransforms: ((editor: ToolEditor) => void)[] = []; const editMcp = () => ({ get: (name: string) => mcpServers.get(name), set(name: string, server: (typeof configuredMcp)[string]) { mcpServers.set(name, structuredClone(server)); }, update(name: string, update: (server: (typeof configuredMcp)[string]) => void) { const server = mcpServers.get(name); if (server) update(server); }, }); const editTools = (): ToolEditor => ({ get(id) { const tool = tools.get(id); return tool ? { ...tool, id } : undefined; }, list: () => [ ...[...tools].map(([id, tool]) => ({ ...tool, id })), ...[...mcpTools].map((id) => ({ id, name: id, description: id, input: z.object({}), options: { codemode: false }, async execute() { return { content: "" }; }, })), ], namespace() {}, add(tool) { tools.set(tool.name, tool); }, update(id, update) { const tool = tools.get(id); if (tool) update(tool); }, remove(id) { tools.delete(id); }, }); // SAFETY: This test host implements the native APIs used by setup; unrelated context // services are deliberately absent so unexpected new dependencies fail the test. const context = { location: { directory, workspaceID }, options, model: { async list() { return { data: models }; }, }, event: { subscribe({ signal }: { signal: AbortSignal }) { signal.addEventListener("abort", () => events.close(), { once: true }); return stream; }, }, storage: { async get(key: string) { return storage.get(key); }, async set(key: string, value: JsonValue) { storage.set(key, value); }, async remove(key: string) { storage.delete(key); }, }, integration: { connection: { async active() { return undefined; }, async resolve() { return undefined; }, }, }, rpc: { async register() { return { ...registration, events: { async emit() {} } }; }, }, mcp: { async transform(callback: typeof mcpTransform) { mcpTransform = callback; callback(editMcp()); return registration; }, }, skill: { async transform(callback: (editor: SkillEditor) => void) { callback({ list: () => [...skills.values()], get: (id) => skills.get(id), add: (skill) => { skills.set(skill.id, skill); }, update(id, update) { const skill = skills.get(id); if (skill) update(skill); }, remove: (id) => { skills.delete(id); }, }); return registration; }, }, agent: { async list() { return { data: [...agents.values()] }; }, async get({ agentID }: { agentID: string }) { return { data: agents.get(agentID) }; }, async transform(callback: (editor: AgentEditor) => void) { transform = callback; callback({ get: (id) => agents.get(id), list: () => [...agents.values()], default(id) { defaultAgent = id ?? "build"; }, remove(id) { agents.delete(id); }, update(id, update) { const agent = agents.get(id) ?? Agent.Info.default(Agent.ID.make(id)); update(agent); agents.set(id, agent); }, }); return registration; }, async reload() { agents.clear(); agents.set("build", build); await this.transform(transform); reloaded.resolve(); }, }, session: { async hook(name: string, callback: (event: HookEvent) => Promise | void) { (sessionHooks[name] ??= []).push(callback); return registration; }, async get({ sessionID }: { sessionID: string }) { return sessionID === "child" ? { agent: child, projectID: "project-fixture", parentID: "parent", location: { directory: childDirectory }, } : { agent: parent, projectID: "project-fixture", location: { directory } }; }, }, tool: { async transform(callback: (editor: ToolEditor) => void) { toolTransforms.push(callback); callback(editTools()); return registration; }, async hook(_name: string, callback: typeof before) { before = callback; return registration; }, }, }; // SAFETY: Exercise the real setup with the native operations implemented by this // partial host; absent services intentionally fail instead of becoming inert mocks. const setup = plugin.setup as FixtureSetup; const cleanup = await setup(context); if (cleanup) cleanups.push(cleanup); return { agents, skills, build, mcpServers, defaultAgent, originalBuild, async refresh(next: ModelInfo[]) { models = next; reloaded = Promise.withResolvers(); events.enqueue({ type: "model.updated" }); await reloaded.promise; }, publishModels(next: ModelInfo[], location?: { directory: string; workspaceID?: string }) { models = next; if (location) events.enqueue({ type: "model.updated", location }); else events.enqueue({ type: "model.updated" }); }, async waitForReload() { reloaded = Promise.withResolvers(); await reloaded.promise; }, async changeBuildPermissions(permissions: Agent.Info["permissions"]) { build.permissions = [...permissions]; reloaded = Promise.withResolvers(); events.enqueue({ type: "agent.updated" }); await reloaded.promise; }, changeBuildPermissionsElsewhere(permissions: Agent.Info["permissions"]) { build.permissions = [...permissions]; events.enqueue({ type: "agent.updated", location: { directory: "/other" } }); }, async changeCodegraph( server: | { type: "local"; command: string[]; codemode?: boolean; disabled?: boolean } | undefined, toolNames: string[], ) { if (server) mcpServers.set("codegraph", server); else mcpServers.delete("codegraph"); mcpTools.clear(); for (const name of toolNames) mcpTools.add(name); reloaded = Promise.withResolvers(); mcpTransform(editMcp()); for (const callback of toolTransforms) callback(editTools()); await reloaded.promise; }, async todo(name: string, sessionID: string, input: JsonValue = {}) { // SAFETY: Todo executors use only sessionID and agent from the native context. return tools .get(name)! .execute(input, { sessionID, agent: Agent.ID.make("build") } as ToolContext); }, async delegate(caller: string, target: string) { await before({ tool: "subagent", agent: caller, input: { agent: target } }); }, async command(caller: string, target: string) { parent = caller; child = target; for (const hook of sessionHooks.prompt ?? []) await hook({ sessionID: "child" }); }, bindCoordinate(ready: boolean, moved = true, local = false) { childDirectory = local ? directory : moved ? `${directory}/worktree` : directory; storage.set(bindingKey("child"), { owner: "parent", child: "child", projectID: "project-fixture", ready, root: { directory, workspaceID: "w1", repo: `${directory}/.git` }, directory: local ? directory : `${directory}/worktree`, workspaceID: local ? "w1" : "w2", branch: local ? "root" : "multi-work/test", }); }, async tools(caller: string) { const event: ModelContext = { agent: Agent.ID.make(caller), tools: { subagent: { description: "Native subagents", input: { type: "object", properties: { agent: { type: "string" }, prompt: { type: "string" } }, }, }, }, }; for (const hook of sessionHooks.context ?? []) await hook(event); return event.tools; }, }; } test("orchestration inherits deep-lo and falls back to good-lo with available worker sets", async () => { const full = await host(); expect(full.agents.get("good-lo")?.model).toMatchObject({ id: "gpt-6.1-sol", variant: "low", }); expect(full.agents.get("good-hi")?.model).toMatchObject({ id: "gpt-6.1-sol", variant: "high", }); expect(full.agents.has("deep-hi")).toBe(true); expect(String(full.agents.get("coordinate")?.model?.id)).toBe("gpt-6-astra"); expect(String(full.agents.get("coordinate")?.model?.variant)).toBe("low"); const reduced = await host(allModels.slice(0, 2)); expect(reduced.agents.has("deep-hi")).toBe(false); expect(String(reduced.agents.get("coordinate")?.model?.id)).toBe("gpt-6.1-sol"); expect(String(reduced.agents.get("coordinate")?.model?.variant)).toBe("low"); expect(reduced.agents.get("coordinate")?.system).toContain("Deep agents are unavailable"); expect((await host([])).agents.has("coordinate")).toBe(false); expect((await host(allModels.slice(1))).agents.has("coordinate")).toBe(false); const customAgents = { ...customAssignments(), "good-lo": { model: "test/custom-good", effort: "high" as const, }, "deep-lo": { model: "test/custom-deep", effort: "medium" as const, }, }; const custom = await host( [...allModels, reasoningModel("custom-good"), reasoningModel("custom-deep")], { mode: "custom", agents: customAgents, }, ); expect(custom.agents.get("coordinate")?.model).toMatchObject({ id: "custom-deep", variant: "medium", }); const customFallback = await host([...allModels, reasoningModel("custom-good")], { mode: "custom", agents: customAgents, }); expect(customFallback.agents.get("coordinate")?.model).toMatchObject({ id: "custom-good", variant: "high", }); expect(customFallback.agents.has("deep-lo")).toBe(false); expect( ( await host(allModels, { mode: "custom", agents: customAgents, }) ).agents.has("coordinate"), ).toBe(false); }); test("custom deep assignments are optional and a partial deep tier falls back to good-lo", async () => { for (const omitted of [["deep-lo", "deep-hi"], ["deep-lo"], ["deep-hi"]] as const) { const agents: Partial> = customAssignments(); for (const name of omitted) delete agents[name]; const result = await host(allModels, { mode: "custom", agents }); expect(result.agents.get("coordinate")?.model).toMatchObject({ id: "gpt-6.1-sol", variant: "low", }); expect(result.agents.get("coordinate")?.system).toContain("Deep agents are unavailable"); for (const name of ["fast-lo", "fast-hi", "good-lo", "good-hi"]) expect(result.agents.has(name)).toBe(true); expect(result.agents.has("deep-lo")).toBe(false); expect(result.agents.has("deep-hi")).toBe(false); expect((await result.tools("coordinate")).subagent?.input.properties).toMatchObject({ agent: { type: "string", enum: ["fast-lo", "fast-hi", "good-lo", "good-hi"] }, }); } }); test("registers the embedded anti-overengineering skill and makes it available to coordinate", async () => { const result = await host(); const skill = result.skills.get("anti-overengineering")!; expect(String(skill.name)).toBe("anti-overengineering"); expect(skill.content).toContain("## Correctness floor"); expect(skill.content).toContain("## Final subtraction pass"); expect(await Bun.file(skill.path).exists()).toBe(true); const coordinate = result.agents.get("coordinate")!; expect(coordinate.permissions).toContainEqual({ action: "skill", resource: "*", effect: "allow", }); expect(coordinate.permissions.some((rule) => rule.action === "shell")).toBe(false); for (const models of [allModels, allModels.slice(0, 2)]) { const prompt = (await host(models)).agents.get("coordinate")?.system; expect(prompt).toContain("Audit subagent review"); expect(prompt).toContain("reject overkill or"); expect(prompt).toContain("BEFORE delegating it"); expect(prompt).toContain("Ordinary in-scope bugfixes need no extra approval"); expect(prompt).toContain("Never waive correctness, security or data integrity"); } }); test("custom assignments resolve duplicate model IDs by provider and refresh provider-aware fallback", async () => { const agents = customAssignments("alpha"); agents["fast-lo"] = { ...agents["fast-lo"], model: "beta/shared" }; agents["fast-hi"] = { ...agents["fast-hi"], model: "alpha/shared" }; agents["deep-lo"] = { ...agents["deep-lo"], model: "beta/deep" }; agents["deep-hi"] = { ...agents["deep-hi"], model: "beta/deep" }; const base = [ providerModel("alpha", "shared"), providerModel("beta", "shared"), providerModel("alpha", "gpt-6.1-sol"), providerModel("alpha", "gpt-6-astra"), ]; const result = await host(base, { mode: "custom", agents }); expect(String(result.agents.get("fast-lo")?.model?.providerID)).toBe("beta"); expect(String(result.agents.get("fast-hi")?.model?.providerID)).toBe("alpha"); expect(String(result.agents.get("coordinate")?.model?.id)).toBe("gpt-6.1-sol"); expect(result.agents.has("deep-lo")).toBe(false); await result.refresh([...base, providerModel("beta", "deep")]); expect(result.agents.get("coordinate")?.model).toMatchObject({ providerID: "beta", id: "deep", }); expect(result.agents.has("deep-lo")).toBe(true); const nested = customAssignments("alpha"); nested["fast-lo"] = { ...nested["fast-lo"], model: "alpha/group/shared" }; const nestedResult = await host( [...base, providerModel("alpha", "gpt-6-luna"), providerModel("alpha", "group/shared")], { mode: "custom", agents: nested }, ); expect(String(nestedResult.agents.get("fast-lo")?.model?.id)).toBe("group/shared"); }); test("custom configuration requires qualified models and rejects obsolete fields", () => { expect(() => settings.parse({ mode: "custom", agents: customAssignments() })).not.toThrow(); for (const omitted of [["deep-lo", "deep-hi"], ["deep-lo"], ["deep-hi"]] as const) { const agents: Partial> = customAssignments(); for (const name of omitted) delete agents[name]; expect(() => settings.parse({ mode: "custom", agents })).not.toThrow(); } for (const name of ["fast-lo", "fast-hi", "good-lo", "good-hi"] as const) { const agents: Partial> = customAssignments(); delete agents[name]; expect(() => settings.parse({ mode: "custom", agents })).toThrow(); } for (const name of ["deep-lo", "deep-hi"] as const) expect(() => settings.parse({ mode: "custom", agents: { ...customAssignments(), [name]: { model: "test/model" } }, }), ).toThrow(); for (const model of ["bare", "/model", "provider/"]) expect(() => settings.parse({ mode: "custom", agents: { ...customAssignments(), "fast-lo": { model, effort: "low" }, }, }), ).toThrow(); const nested = customAssignments(); nested["fast-lo"] = { model: "provider/group/model", effort: "low" }; expect(() => settings.parse({ mode: "custom", agents: nested })).not.toThrow(); expect(() => settings.parse({ mode: "custom", provider: "test", agents: customAssignments() }), ).toThrow(); expect(() => settings.parse({ mode: "custom", lunaPriority: false, agents: customAssignments() }), ).toThrow(); expect(() => settings.parse({ mode: "custom", agents: { ...customAssignments(), "fast-lo": { ...customAssignments()["fast-lo"], provider: "test" }, }, }), ).toThrow(); expect(() => settings.parse({ mode: "custom", agents: { ...customAssignments(), "fast-lo": { ...customAssignments()["fast-lo"], lunaPriority: true }, }, }), ).toThrow(); expect(() => settings.parse({ mode: "custom", agents: { ...customAssignments(), coordinate: customAssignments()["deep-lo"] }, }), ).not.toThrow(); for (const obsolete of [{ provider: "test" }, { lunaPriority: true }]) expect(() => settings.parse({ mode: "custom", agents: { ...customAssignments(), coordinate: { ...customAssignments()["deep-lo"], ...obsolete, }, }, }), ).toThrow(); }); test("worker inheritance and coordinator isolation preserve build and default", async () => { const result = await host(); expect(result.defaultAgent).toBe("build"); expect(result.agents.get("build")).toEqual(result.originalBuild); const worker = result.agents.get("fast-lo")!; expect(worker.permissions).toContainEqual({ action: "shell", resource: "git push *", effect: "ask", }); expect(worker.permissions.at(-1)).toEqual({ action: "subagent", resource: "*", effect: "deny", }); const coordinate = result.agents.get("coordinate")!; expect(coordinate.permissions[0]).toEqual({ action: "*", resource: "*", effect: "deny" }); expect(coordinate.permissions.some((rule) => rule.action === "shell")).toBe(false); expect(coordinate.permissions).toContainEqual({ action: "read", resource: "*.env", effect: "ask", }); expect(coordinate.permissions).toContainEqual({ action: "subagent", resource: "fast-lo", effect: "allow", }); }); test("coordinator inherits only codegraph MCP permissions when that server exists", async () => { const absent = await host(); expect( absent.agents .get("coordinate") ?.permissions.some((rule) => rule.action.startsWith("codegraph_")), ).toBe(false); expect(absent.mcpServers.has("codegraph")).toBe(false); const result = await host( allModels, { provider: "test" }, { codegraph: { type: "local", command: ["codegraph"], codemode: true }, docs: { type: "local", command: ["docs"], codemode: true }, }, ["codegraph_codegraph_search", "codegraph_future_query", "docs_search"], ); expect(result.mcpServers.get("codegraph")?.codemode).toBe(false); expect(result.mcpServers.get("docs")?.codemode).toBe(true); let coordinate = result.agents.get("coordinate")!; expect(coordinate.permissions).toContainEqual({ action: "codegraph_codegraph_search", resource: "*", effect: "allow", }); expect(coordinate.permissions.some((rule) => rule.action.startsWith("docs_"))).toBe(false); expect(coordinate.permissions.some((rule) => rule.action === "shell")).toBe(false); expect(coordinate.permissions.some((rule) => rule.action === "execute")).toBe(false); await result.changeBuildPermissions([ { action: "*", resource: "*", effect: "allow" }, { action: "code*", resource: "*", effect: "ask" }, { action: "*search", resource: "*", effect: "deny" }, { action: "docs_*", resource: "*", effect: "allow" }, { action: "shell", resource: "*", effect: "allow" }, ]); coordinate = result.agents.get("coordinate")!; expect(coordinate.permissions).toContainEqual({ action: "codegraph_future_query", resource: "*", effect: "ask", }); expect(coordinate.permissions).toContainEqual({ action: "codegraph_codegraph_search", resource: "*", effect: "deny", }); expect(coordinate.permissions.some((rule) => rule.action === "docs_*")).toBe(false); expect(coordinate.permissions.some((rule) => rule.action === "shell")).toBe(false); await result.changeCodegraph(undefined, []); coordinate = result.agents.get("coordinate")!; expect(coordinate.permissions.some((rule) => rule.action.startsWith("codegraph_"))).toBe(false); await result.changeCodegraph({ type: "local", command: ["codegraph"], codemode: true }, [ "codegraph_added_later", ]); expect(result.mcpServers.get("codegraph")?.codemode).toBe(false); expect(result.agents.get("coordinate")?.permissions).toContainEqual({ action: "codegraph_added_later", resource: "*", effect: "ask", }); }); test("playwright MCP prefers direct MCP, uses npx for Playwright, and preserves fallback behavior", async () => { const originalPath = process.env.PATH; const root = await mkdtemp(join(tmpdir(), "stanp-playwright-")); try { process.env.PATH = ""; const packageRoot = join(root, "local", "node_modules", "@playwright", "mcp"); const executable = join(packageRoot, "cli.js"); const localProject = join(root, "local", "project"); const globalProject = join(root, "global-project"); await mkdir(localProject, { recursive: true }); await mkdir(globalProject); await mkdir(packageRoot, { recursive: true }); await writeFile( join(packageRoot, "package.json"), JSON.stringify({ name: "@playwright/mcp", bin: { "playwright-mcp": "cli.js" } }), ); await writeFile(executable, "#!/usr/bin/env node\n"); await chmod(executable, 0o755); const local = await host( allModels, { provider: "test", playwright: true }, {}, [], localProject, ); expect(local.mcpServers.get("playwright")).toEqual({ type: "local", command: [executable], timeout: { startup: 300_000 }, }); const globalPackage = join(root, "global", "@playwright", "mcp"); const globalExecutable = join(globalPackage, "cli.js"); const globalBin = join(root, "bin"); const pathExecutable = join(globalBin, "playwright-mcp"); await mkdir(globalPackage, { recursive: true }); await mkdir(globalBin, { recursive: true }); await writeFile( join(globalPackage, "package.json"), JSON.stringify({ name: "@playwright/mcp", bin: { "playwright-mcp": "cli.js" } }), ); await writeFile(globalExecutable, "#!/usr/bin/env node\n"); await chmod(globalExecutable, 0o755); await symlink(globalExecutable, pathExecutable); expect(await playwrightMcpCommand(globalProject, globalBin)).toEqual([pathExecutable]); const ordinaryPackage = join(root, "ordinary-project", "node_modules", "playwright"); const ordinaryProject = join(root, "ordinary-project", "project"); const ordinaryExecutable = join(ordinaryPackage, "cli.js"); await mkdir(ordinaryPackage, { recursive: true }); await mkdir(ordinaryProject); await writeFile( join(ordinaryPackage, "package.json"), JSON.stringify({ name: "playwright", bin: { playwright: "cli.js" } }), ); await writeFile(ordinaryExecutable, "#!/usr/bin/env node\n"); await chmod(ordinaryExecutable, 0o755); const ordinary = await host( allModels, { provider: "test", playwright: true }, {}, [], ordinaryProject, ); expect(ordinary.mcpServers.get("playwright")?.command).toEqual([ "npx", "-y", "@playwright/mcp@latest", ]); const testPackage = join(root, "test-project", "node_modules", "@playwright", "test"); const testDependency = join(testPackage, "node_modules", "playwright"); const testProject = join(root, "test-project", "project"); await mkdir(testDependency, { recursive: true }); await mkdir(testProject); await writeFile( join(testPackage, "package.json"), JSON.stringify({ name: "@playwright/test", bin: { playwright: "cli.js" }, dependencies: { playwright: "1.0.0" }, }), ); await writeFile(join(testPackage, "cli.js"), "#!/usr/bin/env node\n"); await chmod(join(testPackage, "cli.js"), 0o755); await writeFile( join(testDependency, "package.json"), JSON.stringify({ name: "playwright", bin: { playwright: "cli.js" } }), ); await writeFile(join(testDependency, "cli.js"), "#!/usr/bin/env node\n"); await chmod(join(testDependency, "cli.js"), 0o755); expect(await playwrightMcpCommand(testProject, "")).toEqual([ "npx", "-y", "@playwright/mcp@latest", ]); const pathPackage = join(root, "path-playwright", "playwright"); const pathBin = join(root, "playwright-bin"); const pathPlaywright = join(pathBin, "playwright"); await mkdir(pathPackage, { recursive: true }); await mkdir(pathBin); await writeFile( join(pathPackage, "package.json"), JSON.stringify({ name: "playwright", bin: { playwright: "cli.js" } }), ); await writeFile(join(pathPackage, "cli.js"), "#!/usr/bin/env node\n"); await chmod(join(pathPackage, "cli.js"), 0o755); await symlink(join(pathPackage, "cli.js"), pathPlaywright); expect(await playwrightMcpCommand(globalProject, pathBin)).toEqual([ "npx", "-y", "@playwright/mcp@latest", ]); const ordinaryBin = join(root, "ordinary-bin"); const fakePlaywright = join(ordinaryBin, "playwright"); await mkdir(ordinaryBin); await writeFile(fakePlaywright, "#!/usr/bin/env node\n"); await chmod(fakePlaywright, 0o755); expect((await playwrightMcpCommand(globalProject, ordinaryBin))[0]).toBe("docker"); const fallback = await host(allModels, { provider: "test", playwright: true }); expect(fallback.mcpServers.get("playwright")).toEqual({ type: "local", command: ["docker", "run", "-i", "--rm", "--init", "mcr.microsoft.com/playwright/mcp"], timeout: { startup: 300_000 }, }); const existing = await host( allModels, { provider: "test" }, { playwright: { type: "local", command: ["custom-playwright-mcp"], disabled: true, timeout: { startup: 12_345 }, }, docs: { type: "local", command: ["docs"], codemode: true }, }, ); expect(existing.mcpServers.get("playwright")).toEqual({ type: "local", command: ["custom-playwright-mcp"], disabled: true, timeout: { startup: 12_345 }, }); expect(existing.mcpServers.get("docs")?.codemode).toBe(true); const disabled = await host(); expect(disabled.mcpServers.has("playwright")).toBe(false); } finally { if (originalPath === undefined) delete process.env.PATH; else process.env.PATH = originalPath; await rm(root, { recursive: true, force: true }); } }); test("manual calls and command-created child sessions cannot bypass delegation restrictions", async () => { const result = await host(allModels.slice(0, 2)); for (const invoke of [result.delegate, result.command]) { await expect(invoke("build", "coordinate")).rejects.toThrow("cannot run as a subagent"); await expect(invoke("fast-hi", "good-lo")).rejects.toThrow("Workers cannot delegate"); await expect(invoke("coordinate", "deep-lo")).rejects.toThrow( "only to its available workers", ); await invoke("coordinate", "good-hi"); } const tools = await result.tools("coordinate"); expect(tools.subagent?.input.properties).toEqual({ agent: { type: "string", enum: ["fast-lo", "fast-hi", "good-lo", "good-hi"] }, prompt: { type: "string" }, }); expect(await result.tools("fast-lo")).toEqual({}); expect((await result.tools("build")).subagent?.description).toBe("Native subagents"); }); test("coordinate prompt admission requires owned ready placement; local and moved bindings work", async () => { const result = await host(); await expect(result.command("multi-work", "coordinate")).rejects.toThrow( "ready multi-work binding", ); result.bindCoordinate(false); await expect(result.command("multi-work", "coordinate")).rejects.toThrow( "ready multi-work binding", ); result.bindCoordinate(true, false); await expect(result.command("multi-work", "coordinate")).rejects.toThrow( "ready multi-work binding", ); result.bindCoordinate(true); await result.command("multi-work", "coordinate"); result.bindCoordinate(true, true, true); await result.command("multi-work", "coordinate"); await expect(result.command("build", "coordinate")).rejects.toThrow("ready multi-work binding"); result.bindCoordinate(true); await expect(result.command("multi-work", "fast-lo")).rejects.toThrow("local fast-lo"); for (const invoke of [result.delegate, result.command]) await expect(invoke("build", "multi-work")).rejects.toThrow("primary only"); }); test("reasoning settings and Luna priority use native advertised or official support", async () => { const luna = { ...providerModel("openai", "gpt-6-luna"), package: "@opencode/ai/providers/openai", settings: { baseURL: "https://api.openai.com/v1" }, }; const openaiModels = [ luna, providerModel("openai", "gpt-6.1-sol"), providerModel("openai", "gpt-6-astra"), ]; const ordinary = await host([providerModel("openai", "gpt-6-luna"), ...openaiModels.slice(1)], { provider: "openai", }); expect(ordinary.agents.get("fast-lo")?.request.settings).toEqual({}); expect(String(ordinary.agents.get("fast-lo")?.model?.variant)).toBe("low"); const official = await host(openaiModels, { provider: "openai" }); expect(official.agents.get("fast-hi")?.request.settings).toEqual({ serviceTier: "priority" }); const proxy = await host( [{ ...luna, settings: { baseURL: "https://proxy.example/v1" } }, ...openaiModels.slice(1)], { provider: "openai" }, ); expect(proxy.agents.get("fast-lo")?.request.settings).toEqual({}); const advertised = await host( [ { ...luna, package: "custom", settings: { serviceTier: "fast" } }, ...openaiModels.slice(1), ], { provider: "openai" }, ); expect(advertised.agents.get("fast-lo")?.request.settings.serviceTier).toBe("fast"); const sdkAlias = await host( [{ ...luna, package: "@ai-sdk/openai" }, ...openaiModels.slice(1)], { provider: "openai" }, ); expect(sdkAlias.agents.get("fast-lo")?.request.settings.serviceTier).toBe("priority"); const disabled = await host(openaiModels, { provider: "openai", lunaPriority: false, }); expect(disabled.agents.get("fast-lo")?.request.settings).toEqual({}); const custom = customAssignments("openai"); const customResult = await host(openaiModels, { mode: "custom", agents: custom, }); expect(customResult.agents.get("fast-lo")?.request.settings.serviceTier).toBeUndefined(); expect(customResult.agents.get("fast-hi")?.request.settings.serviceTier).toBeUndefined(); const other = await host(allModels, { provider: "test" }); expect(other.agents.get("fast-lo")?.request.settings.serviceTier).toBeUndefined(); expect(() => settings.parse({ mode: "gpt", provider: "test", lunaPriority: true })).toThrow(); }); test("coordinator uses inherited effort and ignores the legacy custom assignment", async () => { for (const options of [{ provider: "test" }, { mode: "gpt", provider: "test" }] as const) { const result = await host(allModels, options); expect(result.agents.get("coordinate")?.model).toMatchObject({ id: "gpt-6-astra", variant: "low", }); expect(result.agents.get("coordinate")?.request.settings.reasoningEffort).toBeUndefined(); } const fallback = await host(allModels.slice(0, 2)); expect(fallback.agents.get("coordinate")?.model).toMatchObject({ id: "gpt-6.1-sol", variant: "low", }); expect(fallback.agents.get("coordinate")?.request.settings.reasoningEffort).toBeUndefined(); const custom = await host([...allModels, model("local")], { mode: "custom", agents: { ...customAssignments(), coordinate: { model: "test/local", effort: "xhigh" }, }, }); expect(custom.agents.get("coordinate")?.model).toMatchObject({ id: "gpt-6-astra", variant: "low", }); expect(custom.agents.get("coordinate")?.request.settings.reasoningEffort).toBeUndefined(); }); test("the current host model API filters discovery and reloads changed availability", async () => { const disabled = { ...reasoningModel("gpt-6-luna"), enabled: false }; const other = { ...reasoningModel("gpt-6.1-sol"), providerID: Provider.ID.make("other") }; expect((await host([disabled, other, ...allModels.slice(1)])).agents.has("coordinate")).toBe( false, ); const result = await host([]); await result.refresh(allModels); expect(String(result.agents.get("coordinate")?.model?.id)).toBe("gpt-6-astra"); expect(String(result.agents.get("coordinate")?.model?.variant)).toBe("low"); expect(result.agents.get("coordinate")?.request.settings.reasoningEffort).toBeUndefined(); await result.refresh(allModels.slice(0, 2)); expect(result.agents.has("deep-lo")).toBe(false); expect(String(result.agents.get("coordinate")?.model?.id)).toBe("gpt-6.1-sol"); expect(String(result.agents.get("coordinate")?.model?.variant)).toBe("low"); expect(result.agents.get("coordinate")?.request.settings.reasoningEffort).toBeUndefined(); await result.refresh([]); expect(result.agents.has("coordinate")).toBe(false); }); test("late build permission overrides remain effective for workers and coordinator", async () => { const result = await host(); await result.changeBuildPermissions([ ...result.build.permissions, { action: "read", resource: "*.env", effect: "deny" }, { action: "shell", resource: "git push *", effect: "deny" }, ]); expect(result.agents.get("fast-lo")?.permissions).toContainEqual({ action: "shell", resource: "git push *", effect: "deny", }); expect( result.agents .get("coordinate") ?.permissions.filter((rule) => rule.action === "read" && rule.resource === "*.env") .at(-1)?.effect, ).toBe("deny"); }); test("events from another location do not reload this location's agents", async () => { const result = await host(); result.changeBuildPermissionsElsewhere([ ...result.build.permissions, { action: "read", resource: "*.env", effect: "deny" }, ]); await result.refresh(allModels); expect( result.agents .get("coordinate") ?.permissions.filter((rule) => rule.action === "read" && rule.resource === "*.env") .at(-1)?.effect, ).toBe("ask"); }); test("model updates require an exact workspace match within the same directory", async () => { const result = await host([], { provider: "test" }, {}, [], "/test", "workspace-a"); result.publishModels(allModels, { directory: "/test" }); await Bun.sleep(0); expect(result.agents.has("coordinate")).toBe(false); result.publishModels(allModels, { directory: "/test", workspaceID: "workspace-b" }); await Bun.sleep(0); expect(result.agents.has("coordinate")).toBe(false); const reloaded = result.waitForReload(); result.publishModels(allModels, { directory: "/test", workspaceID: "workspace-a" }); await reloaded; expect(result.agents.has("coordinate")).toBe(true); }); test("todo lists persist within one session without leaking to another", async () => { const result = await host(); const todos = [{ id: "1", content: "Implement", status: "in_progress", priority: "high" }]; await result.todo("todowrite", "one", { todos }); expect((await result.todo("todoread", "one")).content).toBe(JSON.stringify(todos)); expect((await result.todo("todoread", "two")).content).toBe("[]"); result.build.permissions.push({ action: "todowrite", resource: "*", effect: "ask" }); await expect(result.todo("todowrite", "one", { todos: [] })).rejects.toThrow("cannot request"); expect((await result.todo("todoread", "one")).content).toBe(JSON.stringify(todos)); result.build.permissions.push({ action: "todoread", resource: "*", effect: "deny" }); await expect(result.todo("todoread", "one")).rejects.toThrow("(deny)"); });