import { describe, expect } from "bun:test" import { Effect, Exit, Scope } from "effect" import { AgentV2 } from "@opencode-ai/core/agent" import { Location } from "@opencode-ai/core/location" import { AgentPlugin } from "@opencode-ai/core/plugin/agent" import { AbsolutePath } from "@opencode-ai/core/schema" import { location } from "./fixture/location" import { testEffect } from "./lib/effect" import { agentHost, host } from "./plugin/host" const it = testEffect(AgentV2.locationLayer) describe("AgentV2", () => { it.effect("starts without agents", () => Effect.gen(function* () { const agent = yield* AgentV2.Service expect(yield* agent.all()).toEqual([]) expect(yield* agent.get(AgentV2.ID.make("build"))).toBeUndefined() }), ) it.effect("materializes replayable agent transforms", () => Effect.gen(function* () { const agent = yield* AgentV2.Service const id = AgentV2.ID.make("reviewer") yield* agent.transform((editor) => editor.update(id, (info) => { info.description = "Reviews code" info.mode = "subagent" }), ) expect(yield* agent.get(id)).toMatchObject({ id, description: "Reviews code", mode: "subagent" }) expect((yield* agent.all()).map((info) => info.id)).toEqual([id]) }), ) it.effect("rebuilds state when a transform is replaced", () => Effect.gen(function* () { const agent = yield* AgentV2.Service const id = AgentV2.ID.make("reviewer") let description = "Old description" let hidden = true yield* agent.transform((editor) => editor.update(id, (info) => { info.description = description info.hidden = hidden }), ) description = "New description" hidden = false yield* agent.reload() expect(yield* agent.get(id)).toMatchObject({ description: "New description", hidden: false }) }), ) it.effect("removes a transform when its scope closes", () => Effect.gen(function* () { const agent = yield* AgentV2.Service const id = AgentV2.ID.make("scoped") const scope = yield* Scope.make() yield* agent.transform((editor) => editor.update(id, () => {})).pipe(Scope.provide(scope)) expect(yield* agent.get(id)).toBeDefined() yield* Scope.close(scope, Exit.void) expect(yield* agent.get(id)).toBeUndefined() }), ) it.effect("applies direct agent updates", () => Effect.gen(function* () { const agent = yield* AgentV2.Service const id = AgentV2.ID.make("build") yield* agent.transform((editor) => editor.update(id, (info) => { info.mode = "primary" info.hidden = true }), ) expect(yield* agent.get(id)).toMatchObject({ id, mode: "primary", hidden: true }) }), ) it.effect("creates agents with runtime defaults and supports direct removal", () => Effect.gen(function* () { const agent = yield* AgentV2.Service const id = AgentV2.ID.make("custom") yield* agent.transform((editor) => editor.update(id, () => {})) expect(yield* agent.get(id)).toEqual(AgentV2.Info.empty(id)) yield* agent.transform((editor) => editor.remove(id)) expect(yield* agent.get(id)).toBeUndefined() }), ) it.effect("does not ambiently opt built-in agents into bash", () => Effect.gen(function* () { const agent = yield* AgentV2.Service yield* AgentPlugin.Plugin.effect( host({ agent: agentHost(agent), }), ).pipe( Effect.provideService( Location.Service, Location.Service.of(location({ directory: AbsolutePath.make("/project") })), ), ) const agents = yield* agent.all() expect(agents.map((item) => String(item.id)).sort()).toEqual([ "build", "compaction", "explore", "general", "plan", "summary", "title", ]) for (const item of agents) { expect(item.permissions.some((rule) => rule.action === "bash" && rule.effect !== "deny")).toBe(false) } }), ) })