|
|
@@ -1,6 +1,7 @@
|
|
|
import { afterEach, test, expect } from "bun:test"
|
|
|
+import { Effect } from "effect"
|
|
|
import path from "path"
|
|
|
-import { tmpdir } from "../fixture/fixture"
|
|
|
+import { provideInstance, tmpdir } from "../fixture/fixture"
|
|
|
import { Instance } from "../../src/project/instance"
|
|
|
import { Agent } from "../../src/agent/agent"
|
|
|
import { Permission } from "../../src/permission"
|
|
|
@@ -11,6 +12,10 @@ function evalPerm(agent: Agent.Info | undefined, permission: string): Permission
|
|
|
return Permission.evaluate(permission, "*", agent.permission).action
|
|
|
}
|
|
|
|
|
|
+function load<A>(dir: string, fn: (svc: Agent.Interface) => Effect.Effect<A>) {
|
|
|
+ return Effect.runPromise(provideInstance(dir)(Agent.Service.use(fn)).pipe(Effect.provide(Agent.defaultLayer)))
|
|
|
+}
|
|
|
+
|
|
|
afterEach(async () => {
|
|
|
await Instance.disposeAll()
|
|
|
})
|
|
|
@@ -20,7 +25,7 @@ test("returns default native agents when no config", async () => {
|
|
|
await Instance.provide({
|
|
|
directory: tmp.path,
|
|
|
fn: async () => {
|
|
|
- const agents = await Agent.list()
|
|
|
+ const agents = await load(tmp.path, (svc) => svc.list())
|
|
|
const names = agents.map((a) => a.name)
|
|
|
expect(names).toContain("build")
|
|
|
expect(names).toContain("plan")
|
|
|
@@ -38,7 +43,7 @@ test("build agent has correct default properties", async () => {
|
|
|
await Instance.provide({
|
|
|
directory: tmp.path,
|
|
|
fn: async () => {
|
|
|
- const build = await Agent.get("build")
|
|
|
+ const build = await load(tmp.path, (svc) => svc.get("build"))
|
|
|
expect(build).toBeDefined()
|
|
|
expect(build?.mode).toBe("primary")
|
|
|
expect(build?.native).toBe(true)
|
|
|
@@ -53,7 +58,7 @@ test("plan agent denies edits except .opencode/plans/*", async () => {
|
|
|
await Instance.provide({
|
|
|
directory: tmp.path,
|
|
|
fn: async () => {
|
|
|
- const plan = await Agent.get("plan")
|
|
|
+ const plan = await load(tmp.path, (svc) => svc.get("plan"))
|
|
|
expect(plan).toBeDefined()
|
|
|
// Wildcard is denied
|
|
|
expect(evalPerm(plan, "edit")).toBe("deny")
|
|
|
@@ -68,7 +73,7 @@ test("explore agent denies edit and write", async () => {
|
|
|
await Instance.provide({
|
|
|
directory: tmp.path,
|
|
|
fn: async () => {
|
|
|
- const explore = await Agent.get("explore")
|
|
|
+ const explore = await load(tmp.path, (svc) => svc.get("explore"))
|
|
|
expect(explore).toBeDefined()
|
|
|
expect(explore?.mode).toBe("subagent")
|
|
|
expect(evalPerm(explore, "edit")).toBe("deny")
|
|
|
@@ -84,7 +89,7 @@ test("explore agent asks for external directories and allows Truncate.GLOB", asy
|
|
|
await Instance.provide({
|
|
|
directory: tmp.path,
|
|
|
fn: async () => {
|
|
|
- const explore = await Agent.get("explore")
|
|
|
+ const explore = await load(tmp.path, (svc) => svc.get("explore"))
|
|
|
expect(explore).toBeDefined()
|
|
|
expect(Permission.evaluate("external_directory", "/some/other/path", explore!.permission).action).toBe("ask")
|
|
|
expect(Permission.evaluate("external_directory", Truncate.GLOB, explore!.permission).action).toBe("allow")
|
|
|
@@ -97,7 +102,7 @@ test("general agent denies todo tools", async () => {
|
|
|
await Instance.provide({
|
|
|
directory: tmp.path,
|
|
|
fn: async () => {
|
|
|
- const general = await Agent.get("general")
|
|
|
+ const general = await load(tmp.path, (svc) => svc.get("general"))
|
|
|
expect(general).toBeDefined()
|
|
|
expect(general?.mode).toBe("subagent")
|
|
|
expect(general?.hidden).toBeUndefined()
|
|
|
@@ -111,7 +116,7 @@ test("compaction agent denies all permissions", async () => {
|
|
|
await Instance.provide({
|
|
|
directory: tmp.path,
|
|
|
fn: async () => {
|
|
|
- const compaction = await Agent.get("compaction")
|
|
|
+ const compaction = await load(tmp.path, (svc) => svc.get("compaction"))
|
|
|
expect(compaction).toBeDefined()
|
|
|
expect(compaction?.hidden).toBe(true)
|
|
|
expect(evalPerm(compaction, "bash")).toBe("deny")
|
|
|
@@ -137,7 +142,7 @@ test("custom agent from config creates new agent", async () => {
|
|
|
await Instance.provide({
|
|
|
directory: tmp.path,
|
|
|
fn: async () => {
|
|
|
- const custom = await Agent.get("my_custom_agent")
|
|
|
+ const custom = await load(tmp.path, (svc) => svc.get("my_custom_agent"))
|
|
|
expect(custom).toBeDefined()
|
|
|
expect(String(custom?.model?.providerID)).toBe("openai")
|
|
|
expect(String(custom?.model?.modelID)).toBe("gpt-4")
|
|
|
@@ -166,7 +171,7 @@ test("custom agent config overrides native agent properties", async () => {
|
|
|
await Instance.provide({
|
|
|
directory: tmp.path,
|
|
|
fn: async () => {
|
|
|
- const build = await Agent.get("build")
|
|
|
+ const build = await load(tmp.path, (svc) => svc.get("build"))
|
|
|
expect(build).toBeDefined()
|
|
|
expect(String(build?.model?.providerID)).toBe("anthropic")
|
|
|
expect(String(build?.model?.modelID)).toBe("claude-3")
|
|
|
@@ -189,9 +194,9 @@ test("agent disable removes agent from list", async () => {
|
|
|
await Instance.provide({
|
|
|
directory: tmp.path,
|
|
|
fn: async () => {
|
|
|
- const explore = await Agent.get("explore")
|
|
|
+ const explore = await load(tmp.path, (svc) => svc.get("explore"))
|
|
|
expect(explore).toBeUndefined()
|
|
|
- const agents = await Agent.list()
|
|
|
+ const agents = await load(tmp.path, (svc) => svc.list())
|
|
|
const names = agents.map((a) => a.name)
|
|
|
expect(names).not.toContain("explore")
|
|
|
},
|
|
|
@@ -215,7 +220,7 @@ test("agent permission config merges with defaults", async () => {
|
|
|
await Instance.provide({
|
|
|
directory: tmp.path,
|
|
|
fn: async () => {
|
|
|
- const build = await Agent.get("build")
|
|
|
+ const build = await load(tmp.path, (svc) => svc.get("build"))
|
|
|
expect(build).toBeDefined()
|
|
|
// Specific pattern is denied
|
|
|
expect(Permission.evaluate("bash", "rm -rf *", build!.permission).action).toBe("deny")
|
|
|
@@ -236,7 +241,7 @@ test("global permission config applies to all agents", async () => {
|
|
|
await Instance.provide({
|
|
|
directory: tmp.path,
|
|
|
fn: async () => {
|
|
|
- const build = await Agent.get("build")
|
|
|
+ const build = await load(tmp.path, (svc) => svc.get("build"))
|
|
|
expect(build).toBeDefined()
|
|
|
expect(evalPerm(build, "bash")).toBe("deny")
|
|
|
},
|
|
|
@@ -255,8 +260,8 @@ test("agent steps/maxSteps config sets steps property", async () => {
|
|
|
await Instance.provide({
|
|
|
directory: tmp.path,
|
|
|
fn: async () => {
|
|
|
- const build = await Agent.get("build")
|
|
|
- const plan = await Agent.get("plan")
|
|
|
+ const build = await load(tmp.path, (svc) => svc.get("build"))
|
|
|
+ const plan = await load(tmp.path, (svc) => svc.get("plan"))
|
|
|
expect(build?.steps).toBe(50)
|
|
|
expect(plan?.steps).toBe(100)
|
|
|
},
|
|
|
@@ -274,7 +279,7 @@ test("agent mode can be overridden", async () => {
|
|
|
await Instance.provide({
|
|
|
directory: tmp.path,
|
|
|
fn: async () => {
|
|
|
- const explore = await Agent.get("explore")
|
|
|
+ const explore = await load(tmp.path, (svc) => svc.get("explore"))
|
|
|
expect(explore?.mode).toBe("primary")
|
|
|
},
|
|
|
})
|
|
|
@@ -291,7 +296,7 @@ test("agent name can be overridden", async () => {
|
|
|
await Instance.provide({
|
|
|
directory: tmp.path,
|
|
|
fn: async () => {
|
|
|
- const build = await Agent.get("build")
|
|
|
+ const build = await load(tmp.path, (svc) => svc.get("build"))
|
|
|
expect(build?.name).toBe("Builder")
|
|
|
},
|
|
|
})
|
|
|
@@ -308,7 +313,7 @@ test("agent prompt can be set from config", async () => {
|
|
|
await Instance.provide({
|
|
|
directory: tmp.path,
|
|
|
fn: async () => {
|
|
|
- const build = await Agent.get("build")
|
|
|
+ const build = await load(tmp.path, (svc) => svc.get("build"))
|
|
|
expect(build?.prompt).toBe("Custom system prompt")
|
|
|
},
|
|
|
})
|
|
|
@@ -328,7 +333,7 @@ test("unknown agent properties are placed into options", async () => {
|
|
|
await Instance.provide({
|
|
|
directory: tmp.path,
|
|
|
fn: async () => {
|
|
|
- const build = await Agent.get("build")
|
|
|
+ const build = await load(tmp.path, (svc) => svc.get("build"))
|
|
|
expect(build?.options.random_property).toBe("hello")
|
|
|
expect(build?.options.another_random).toBe(123)
|
|
|
},
|
|
|
@@ -351,7 +356,7 @@ test("agent options merge correctly", async () => {
|
|
|
await Instance.provide({
|
|
|
directory: tmp.path,
|
|
|
fn: async () => {
|
|
|
- const build = await Agent.get("build")
|
|
|
+ const build = await load(tmp.path, (svc) => svc.get("build"))
|
|
|
expect(build?.options.custom_option).toBe(true)
|
|
|
expect(build?.options.another_option).toBe("value")
|
|
|
},
|
|
|
@@ -376,8 +381,8 @@ test("multiple custom agents can be defined", async () => {
|
|
|
await Instance.provide({
|
|
|
directory: tmp.path,
|
|
|
fn: async () => {
|
|
|
- const agentA = await Agent.get("agent_a")
|
|
|
- const agentB = await Agent.get("agent_b")
|
|
|
+ const agentA = await load(tmp.path, (svc) => svc.get("agent_a"))
|
|
|
+ const agentB = await load(tmp.path, (svc) => svc.get("agent_b"))
|
|
|
expect(agentA?.description).toBe("Agent A")
|
|
|
expect(agentA?.mode).toBe("subagent")
|
|
|
expect(agentB?.description).toBe("Agent B")
|
|
|
@@ -405,7 +410,7 @@ test("Agent.list keeps the default agent first and sorts the rest by name", asyn
|
|
|
await Instance.provide({
|
|
|
directory: tmp.path,
|
|
|
fn: async () => {
|
|
|
- const names = (await Agent.list()).map((a) => a.name)
|
|
|
+ const names = (await load(tmp.path, (svc) => svc.list())).map((a) => a.name)
|
|
|
expect(names[0]).toBe("plan")
|
|
|
expect(names.slice(1)).toEqual(names.slice(1).toSorted((a, b) => a.localeCompare(b)))
|
|
|
},
|
|
|
@@ -417,7 +422,7 @@ test("Agent.get returns undefined for non-existent agent", async () => {
|
|
|
await Instance.provide({
|
|
|
directory: tmp.path,
|
|
|
fn: async () => {
|
|
|
- const nonExistent = await Agent.get("does_not_exist")
|
|
|
+ const nonExistent = await load(tmp.path, (svc) => svc.get("does_not_exist"))
|
|
|
expect(nonExistent).toBeUndefined()
|
|
|
},
|
|
|
})
|
|
|
@@ -428,7 +433,7 @@ test("default permission includes doom_loop and external_directory as ask", asyn
|
|
|
await Instance.provide({
|
|
|
directory: tmp.path,
|
|
|
fn: async () => {
|
|
|
- const build = await Agent.get("build")
|
|
|
+ const build = await load(tmp.path, (svc) => svc.get("build"))
|
|
|
expect(evalPerm(build, "doom_loop")).toBe("ask")
|
|
|
expect(evalPerm(build, "external_directory")).toBe("ask")
|
|
|
},
|
|
|
@@ -440,7 +445,7 @@ test("webfetch is allowed by default", async () => {
|
|
|
await Instance.provide({
|
|
|
directory: tmp.path,
|
|
|
fn: async () => {
|
|
|
- const build = await Agent.get("build")
|
|
|
+ const build = await load(tmp.path, (svc) => svc.get("build"))
|
|
|
expect(evalPerm(build, "webfetch")).toBe("allow")
|
|
|
},
|
|
|
})
|
|
|
@@ -462,7 +467,7 @@ test("legacy tools config converts to permissions", async () => {
|
|
|
await Instance.provide({
|
|
|
directory: tmp.path,
|
|
|
fn: async () => {
|
|
|
- const build = await Agent.get("build")
|
|
|
+ const build = await load(tmp.path, (svc) => svc.get("build"))
|
|
|
expect(evalPerm(build, "bash")).toBe("deny")
|
|
|
expect(evalPerm(build, "read")).toBe("deny")
|
|
|
},
|
|
|
@@ -484,7 +489,7 @@ test("legacy tools config maps write/edit/patch/multiedit to edit permission", a
|
|
|
await Instance.provide({
|
|
|
directory: tmp.path,
|
|
|
fn: async () => {
|
|
|
- const build = await Agent.get("build")
|
|
|
+ const build = await load(tmp.path, (svc) => svc.get("build"))
|
|
|
expect(evalPerm(build, "edit")).toBe("deny")
|
|
|
},
|
|
|
})
|
|
|
@@ -502,7 +507,7 @@ test("Truncate.GLOB is allowed even when user denies external_directory globally
|
|
|
await Instance.provide({
|
|
|
directory: tmp.path,
|
|
|
fn: async () => {
|
|
|
- const build = await Agent.get("build")
|
|
|
+ const build = await load(tmp.path, (svc) => svc.get("build"))
|
|
|
expect(Permission.evaluate("external_directory", Truncate.GLOB, build!.permission).action).toBe("allow")
|
|
|
expect(Permission.evaluate("external_directory", Truncate.DIR, build!.permission).action).toBe("deny")
|
|
|
expect(Permission.evaluate("external_directory", "/some/other/path", build!.permission).action).toBe("deny")
|
|
|
@@ -526,7 +531,7 @@ test("Truncate.GLOB is allowed even when user denies external_directory per-agen
|
|
|
await Instance.provide({
|
|
|
directory: tmp.path,
|
|
|
fn: async () => {
|
|
|
- const build = await Agent.get("build")
|
|
|
+ const build = await load(tmp.path, (svc) => svc.get("build"))
|
|
|
expect(Permission.evaluate("external_directory", Truncate.GLOB, build!.permission).action).toBe("allow")
|
|
|
expect(Permission.evaluate("external_directory", Truncate.DIR, build!.permission).action).toBe("deny")
|
|
|
expect(Permission.evaluate("external_directory", "/some/other/path", build!.permission).action).toBe("deny")
|
|
|
@@ -549,7 +554,7 @@ test("explicit Truncate.GLOB deny is respected", async () => {
|
|
|
await Instance.provide({
|
|
|
directory: tmp.path,
|
|
|
fn: async () => {
|
|
|
- const build = await Agent.get("build")
|
|
|
+ const build = await load(tmp.path, (svc) => svc.get("build"))
|
|
|
expect(Permission.evaluate("external_directory", Truncate.GLOB, build!.permission).action).toBe("deny")
|
|
|
expect(Permission.evaluate("external_directory", Truncate.DIR, build!.permission).action).toBe("deny")
|
|
|
},
|
|
|
@@ -581,7 +586,7 @@ description: Permission skill.
|
|
|
await Instance.provide({
|
|
|
directory: tmp.path,
|
|
|
fn: async () => {
|
|
|
- const build = await Agent.get("build")
|
|
|
+ const build = await load(tmp.path, (svc) => svc.get("build"))
|
|
|
const skillDir = path.join(tmp.path, ".opencode", "skill", "perm-skill")
|
|
|
const target = path.join(skillDir, "reference", "notes.md")
|
|
|
expect(Permission.evaluate("external_directory", target, build!.permission).action).toBe("allow")
|
|
|
@@ -597,7 +602,7 @@ test("defaultAgent returns build when no default_agent config", async () => {
|
|
|
await Instance.provide({
|
|
|
directory: tmp.path,
|
|
|
fn: async () => {
|
|
|
- const agent = await Agent.defaultAgent()
|
|
|
+ const agent = await load(tmp.path, (svc) => svc.defaultAgent())
|
|
|
expect(agent).toBe("build")
|
|
|
},
|
|
|
})
|
|
|
@@ -612,7 +617,7 @@ test("defaultAgent respects default_agent config set to plan", async () => {
|
|
|
await Instance.provide({
|
|
|
directory: tmp.path,
|
|
|
fn: async () => {
|
|
|
- const agent = await Agent.defaultAgent()
|
|
|
+ const agent = await load(tmp.path, (svc) => svc.defaultAgent())
|
|
|
expect(agent).toBe("plan")
|
|
|
},
|
|
|
})
|
|
|
@@ -632,7 +637,7 @@ test("defaultAgent respects default_agent config set to custom agent with mode a
|
|
|
await Instance.provide({
|
|
|
directory: tmp.path,
|
|
|
fn: async () => {
|
|
|
- const agent = await Agent.defaultAgent()
|
|
|
+ const agent = await load(tmp.path, (svc) => svc.defaultAgent())
|
|
|
expect(agent).toBe("my_custom")
|
|
|
},
|
|
|
})
|
|
|
@@ -647,7 +652,7 @@ test("defaultAgent throws when default_agent points to subagent", async () => {
|
|
|
await Instance.provide({
|
|
|
directory: tmp.path,
|
|
|
fn: async () => {
|
|
|
- await expect(Agent.defaultAgent()).rejects.toThrow('default agent "explore" is a subagent')
|
|
|
+ await expect(load(tmp.path, (svc) => svc.defaultAgent())).rejects.toThrow('default agent "explore" is a subagent')
|
|
|
},
|
|
|
})
|
|
|
})
|
|
|
@@ -661,7 +666,7 @@ test("defaultAgent throws when default_agent points to hidden agent", async () =
|
|
|
await Instance.provide({
|
|
|
directory: tmp.path,
|
|
|
fn: async () => {
|
|
|
- await expect(Agent.defaultAgent()).rejects.toThrow('default agent "compaction" is hidden')
|
|
|
+ await expect(load(tmp.path, (svc) => svc.defaultAgent())).rejects.toThrow('default agent "compaction" is hidden')
|
|
|
},
|
|
|
})
|
|
|
})
|
|
|
@@ -675,7 +680,9 @@ test("defaultAgent throws when default_agent points to non-existent agent", asyn
|
|
|
await Instance.provide({
|
|
|
directory: tmp.path,
|
|
|
fn: async () => {
|
|
|
- await expect(Agent.defaultAgent()).rejects.toThrow('default agent "does_not_exist" not found')
|
|
|
+ await expect(load(tmp.path, (svc) => svc.defaultAgent())).rejects.toThrow(
|
|
|
+ 'default agent "does_not_exist" not found',
|
|
|
+ )
|
|
|
},
|
|
|
})
|
|
|
})
|
|
|
@@ -691,7 +698,7 @@ test("defaultAgent returns plan when build is disabled and default_agent not set
|
|
|
await Instance.provide({
|
|
|
directory: tmp.path,
|
|
|
fn: async () => {
|
|
|
- const agent = await Agent.defaultAgent()
|
|
|
+ const agent = await load(tmp.path, (svc) => svc.defaultAgent())
|
|
|
// build is disabled, so it should return plan (next primary agent)
|
|
|
expect(agent).toBe("plan")
|
|
|
},
|
|
|
@@ -711,7 +718,7 @@ test("defaultAgent throws when all primary agents are disabled", async () => {
|
|
|
directory: tmp.path,
|
|
|
fn: async () => {
|
|
|
// build and plan are disabled, no primary-capable agents remain
|
|
|
- await expect(Agent.defaultAgent()).rejects.toThrow("no primary visible agent found")
|
|
|
+ await expect(load(tmp.path, (svc) => svc.defaultAgent())).rejects.toThrow("no primary visible agent found")
|
|
|
},
|
|
|
})
|
|
|
})
|