import { test, expect } from "bun:test" import { tmpdir } from "../fixture/fixture" import { Instance } from "../../src/project/instance" import { Agent } from "../../src/agent/agent" import { PermissionNext } from "../../src/permission/next" // Helper to evaluate permission for a tool with wildcard pattern function evalPerm(agent: Agent.Info | undefined, permission: string): PermissionNext.Action | undefined { if (!agent) return undefined return PermissionNext.evaluate(permission, "*", agent.permission).action } test("returns default native agents when no config", async () => { await using tmp = await tmpdir() await Instance.provide({ directory: tmp.path, fn: async () => { const agents = await Agent.list() const names = agents.map((a) => a.name) expect(names).toContain("build") expect(names).toContain("plan") expect(names).toContain("general") expect(names).toContain("explore") expect(names).toContain("compaction") expect(names).toContain("title") expect(names).toContain("summary") }, }) }) test("build agent has correct default properties", async () => { await using tmp = await tmpdir() await Instance.provide({ directory: tmp.path, fn: async () => { const build = await Agent.get("build") expect(build).toBeDefined() expect(build?.mode).toBe("primary") expect(build?.native).toBe(true) expect(evalPerm(build, "edit")).toBe("allow") expect(evalPerm(build, "bash")).toBe("allow") }, }) }) test("plan agent denies edits except .opencode/plan/*", async () => { await using tmp = await tmpdir() await Instance.provide({ directory: tmp.path, fn: async () => { const plan = await Agent.get("plan") expect(plan).toBeDefined() // Wildcard is denied expect(evalPerm(plan, "edit")).toBe("deny") // But specific path is allowed expect(PermissionNext.evaluate("edit", ".opencode/plan/foo.md", plan!.permission).action).toBe("allow") }, }) }) test("explore agent denies edit and write", async () => { await using tmp = await tmpdir() await Instance.provide({ directory: tmp.path, fn: async () => { const explore = await Agent.get("explore") expect(explore).toBeDefined() expect(explore?.mode).toBe("subagent") expect(evalPerm(explore, "edit")).toBe("deny") expect(evalPerm(explore, "write")).toBe("deny") expect(evalPerm(explore, "todoread")).toBe("deny") expect(evalPerm(explore, "todowrite")).toBe("deny") }, }) }) test("general agent denies todo tools", async () => { await using tmp = await tmpdir() await Instance.provide({ directory: tmp.path, fn: async () => { const general = await Agent.get("general") expect(general).toBeDefined() expect(general?.mode).toBe("subagent") expect(general?.hidden).toBeUndefined() expect(evalPerm(general, "todoread")).toBe("deny") expect(evalPerm(general, "todowrite")).toBe("deny") }, }) }) test("compaction agent denies all permissions", async () => { await using tmp = await tmpdir() await Instance.provide({ directory: tmp.path, fn: async () => { const compaction = await Agent.get("compaction") expect(compaction).toBeDefined() expect(compaction?.hidden).toBe(true) expect(evalPerm(compaction, "bash")).toBe("deny") expect(evalPerm(compaction, "edit")).toBe("deny") expect(evalPerm(compaction, "read")).toBe("deny") }, }) }) test("custom agent from config creates new agent", async () => { await using tmp = await tmpdir({ config: { agent: { my_custom_agent: { model: "openai/gpt-4", description: "My custom agent", temperature: 0.5, top_p: 0.9, }, }, }, }) await Instance.provide({ directory: tmp.path, fn: async () => { const custom = await Agent.get("my_custom_agent") expect(custom).toBeDefined() expect(custom?.model?.providerID).toBe("openai") expect(custom?.model?.modelID).toBe("gpt-4") expect(custom?.description).toBe("My custom agent") expect(custom?.temperature).toBe(0.5) expect(custom?.topP).toBe(0.9) expect(custom?.native).toBe(false) expect(custom?.mode).toBe("all") }, }) }) test("custom agent config overrides native agent properties", async () => { await using tmp = await tmpdir({ config: { agent: { build: { model: "anthropic/claude-3", description: "Custom build agent", temperature: 0.7, color: "#FF0000", }, }, }, }) await Instance.provide({ directory: tmp.path, fn: async () => { const build = await Agent.get("build") expect(build).toBeDefined() expect(build?.model?.providerID).toBe("anthropic") expect(build?.model?.modelID).toBe("claude-3") expect(build?.description).toBe("Custom build agent") expect(build?.temperature).toBe(0.7) expect(build?.color).toBe("#FF0000") expect(build?.native).toBe(true) }, }) }) test("agent disable removes agent from list", async () => { await using tmp = await tmpdir({ config: { agent: { explore: { disable: true }, }, }, }) await Instance.provide({ directory: tmp.path, fn: async () => { const explore = await Agent.get("explore") expect(explore).toBeUndefined() const agents = await Agent.list() const names = agents.map((a) => a.name) expect(names).not.toContain("explore") }, }) }) test("agent permission config merges with defaults", async () => { await using tmp = await tmpdir({ config: { agent: { build: { permission: { bash: { "rm -rf *": "deny", }, }, }, }, }, }) await Instance.provide({ directory: tmp.path, fn: async () => { const build = await Agent.get("build") expect(build).toBeDefined() // Specific pattern is denied expect(PermissionNext.evaluate("bash", "rm -rf *", build!.permission).action).toBe("deny") // Edit still allowed expect(evalPerm(build, "edit")).toBe("allow") }, }) }) test("global permission config applies to all agents", async () => { await using tmp = await tmpdir({ config: { permission: { bash: "deny", }, }, }) await Instance.provide({ directory: tmp.path, fn: async () => { const build = await Agent.get("build") expect(build).toBeDefined() expect(evalPerm(build, "bash")).toBe("deny") }, }) }) test("agent steps/maxSteps config sets steps property", async () => { await using tmp = await tmpdir({ config: { agent: { build: { steps: 50 }, plan: { maxSteps: 100 }, }, }, }) await Instance.provide({ directory: tmp.path, fn: async () => { const build = await Agent.get("build") const plan = await Agent.get("plan") expect(build?.steps).toBe(50) expect(plan?.steps).toBe(100) }, }) }) test("agent mode can be overridden", async () => { await using tmp = await tmpdir({ config: { agent: { explore: { mode: "primary" }, }, }, }) await Instance.provide({ directory: tmp.path, fn: async () => { const explore = await Agent.get("explore") expect(explore?.mode).toBe("primary") }, }) }) test("agent name can be overridden", async () => { await using tmp = await tmpdir({ config: { agent: { build: { name: "Builder" }, }, }, }) await Instance.provide({ directory: tmp.path, fn: async () => { const build = await Agent.get("build") expect(build?.name).toBe("Builder") }, }) }) test("agent prompt can be set from config", async () => { await using tmp = await tmpdir({ config: { agent: { build: { prompt: "Custom system prompt" }, }, }, }) await Instance.provide({ directory: tmp.path, fn: async () => { const build = await Agent.get("build") expect(build?.prompt).toBe("Custom system prompt") }, }) }) test("unknown agent properties are placed into options", async () => { await using tmp = await tmpdir({ config: { agent: { build: { random_property: "hello", another_random: 123, }, }, }, }) await Instance.provide({ directory: tmp.path, fn: async () => { const build = await Agent.get("build") expect(build?.options.random_property).toBe("hello") expect(build?.options.another_random).toBe(123) }, }) }) test("agent options merge correctly", async () => { await using tmp = await tmpdir({ config: { agent: { build: { options: { custom_option: true, another_option: "value", }, }, }, }, }) await Instance.provide({ directory: tmp.path, fn: async () => { const build = await Agent.get("build") expect(build?.options.custom_option).toBe(true) expect(build?.options.another_option).toBe("value") }, }) }) test("multiple custom agents can be defined", async () => { await using tmp = await tmpdir({ config: { agent: { agent_a: { description: "Agent A", mode: "subagent", }, agent_b: { description: "Agent B", mode: "primary", }, }, }, }) await Instance.provide({ directory: tmp.path, fn: async () => { const agentA = await Agent.get("agent_a") const agentB = await Agent.get("agent_b") expect(agentA?.description).toBe("Agent A") expect(agentA?.mode).toBe("subagent") expect(agentB?.description).toBe("Agent B") expect(agentB?.mode).toBe("primary") }, }) }) test("Agent.get returns undefined for non-existent agent", async () => { await using tmp = await tmpdir() await Instance.provide({ directory: tmp.path, fn: async () => { const nonExistent = await Agent.get("does_not_exist") expect(nonExistent).toBeUndefined() }, }) }) test("default permission includes doom_loop and external_directory as ask", async () => { await using tmp = await tmpdir() await Instance.provide({ directory: tmp.path, fn: async () => { const build = await Agent.get("build") expect(evalPerm(build, "doom_loop")).toBe("ask") expect(evalPerm(build, "external_directory")).toBe("ask") }, }) }) test("webfetch is allowed by default", async () => { await using tmp = await tmpdir() await Instance.provide({ directory: tmp.path, fn: async () => { const build = await Agent.get("build") expect(evalPerm(build, "webfetch")).toBe("allow") }, }) }) test("legacy tools config converts to permissions", async () => { await using tmp = await tmpdir({ config: { agent: { build: { tools: { bash: false, read: false, }, }, }, }, }) await Instance.provide({ directory: tmp.path, fn: async () => { const build = await Agent.get("build") expect(evalPerm(build, "bash")).toBe("deny") expect(evalPerm(build, "read")).toBe("deny") }, }) }) test("legacy tools config maps write/edit/patch/multiedit to edit permission", async () => { await using tmp = await tmpdir({ config: { agent: { build: { tools: { write: false, }, }, }, }, }) await Instance.provide({ directory: tmp.path, fn: async () => { const build = await Agent.get("build") expect(evalPerm(build, "edit")).toBe("deny") }, }) })