import { expect, test } from "bun:test"; import { Effect } from "effect"; import { Agent } from "@opencode/schema/agent"; import { Session } from "@opencode/schema/session"; import { SessionMessage } from "@opencode/schema/session-message"; import { Tool } from "@opencode/schema/tool"; import type { Context } from "@opencode/plugin/effect/plugin"; import type { ToolEditor, ToolHooks } from "@opencode/plugin/effect/tool"; import type { AgentEditor } from "@opencode/plugin/effect/agent"; import type { SessionContext } from "@opencode/plugin/effect/session"; import { z } from "zod"; import { Herdr, type HerdrRoot } from "../src/herdr.js"; import { bindingKey, bindingSchema, installMultiWork } from "../src/multi-work.js"; class FixtureHerdr extends Herdr { eligible = true; creations = 0; verified = 0; delayCreation = false; creationAborted = false; createdBase: string | undefined; createdBranch: string | undefined; createdLabel: string | undefined; creationStarted = Promise.withResolvers(); override async root(directory: string) { return this.eligible && directory === "/root" ? { directory, workspaceID: "w1", repo: "/root/.git" } : undefined; } override async create( _root: HerdrRoot, base: string, branch: string, signal: AbortSignal, label?: string, ) { this.creations++; this.createdBase = base; this.createdBranch = branch; this.createdLabel = label; if (!this.eligible) throw new Error("root closed"); if (this.delayCreation) { this.creationStarted.resolve(); await new Promise((_resolve, reject) => { signal.addEventListener( "abort", () => { this.creationAborted = true; reject(new Error("creation interrupted")); }, { once: true }, ); }); } return { directory: "/worktree", workspaceID: "w2", branch }; } override async verify() { this.verified++; } } interface FixtureSession { id: Session.ID; projectID: string; agent: string; parentID: string | undefined; location: { directory: string }; permissions: Agent.Info["permissions"]; } function fixture() { const herdr = new FixtureHerdr(); const storage = new Map[1]>(); const agents = new Map>>(); for (const name of ["coordinate", "fast-lo", "fast-hi", "good-lo", "good-hi"]) agents.set(name, { ...Agent.Info.default(Agent.ID.make(name)), permissions: [] }); const parent: FixtureSession = { id: Session.ID.make("ses_parent"), projectID: "project-fixture", agent: "multi-work", parentID: undefined, location: { directory: "/root" }, permissions: [], }; const sessions = new Map([[String(parent.id), parent]]); const order: string[] = []; let cancelled = false; let hold = false; let delayMove = false; let crossProject = false; let destinationAvailable = true; let warming = false; let agentReads = 0; let prompts = 0; let serial = 0; const entered = Promise.withResolvers(); const progress: Tool.Metadata[] = []; const native: Tool.Info = { name: "subagent", description: "native", input: z.object({}), options: { codemode: false }, execute: (raw, call) => Effect.gen(function* () { const input = z .object({ agent: z.string(), sessionID: z.string().optional(), background: z.boolean().optional(), }) .parse(raw); const id = Session.ID.make(input.sessionID ?? `ses_child${++serial}`); if (!input.sessionID) sessions.set(id, { ...parent, id, parentID: call.sessionID, agent: input.agent, location: { directory: "/root" }, }); order.push("create"); yield* call.progress({ sessionID: id, status: "running" }); order.push(`prompt:${sessions.get(id)!.location.directory}`); prompts++; entered.resolve(); if (hold) yield* Effect.never.pipe( Effect.onInterrupt(() => Effect.sync(() => { cancelled = true; }), ), ); return { content: "native result", metadata: { sessionID: id, status: input.background ? "running" : "completed" }, }; }), }; let tool = native; const toolHooks: ((event: ToolHooks["execute.before"]) => Effect.Effect)[] = []; const hooks: ((event: SessionContext) => Effect.Effect)[] = []; let transform!: (editor: AgentEditor) => void; const registration = Effect.succeed({ dispose: Effect.void }); const editAgents = (): AgentEditor => ({ get: (id) => agents.get(id), list: () => [...agents.values()], default() {}, remove(id) { agents.delete(id); }, update(id, update) { // SAFETY: Agent.Info.default returns a fresh draft that the fixture exclusively owns. const agent = (agents.get(id) ?? Agent.Info.default(Agent.ID.make(id))) as NonNullable< ReturnType >; update(agent); agents.set(id, agent); }, }); const editTools: ToolEditor = { list: () => [{ ...tool, id: tool.name }], get: () => ({ ...tool, id: tool.name }), add() {}, namespace() {}, remove() {}, update(_id, update) { const draft = { ...tool }; update(draft); tool = draft; }, }; const host = { options: {}, location: { directory: "/root" }, storage: { get: (key: string) => Effect.sync(() => storage.get(key)), set: (key: string, value: Parameters[1]) => Effect.sync(() => { storage.set(key, value); }), }, agent: { transform: (callback: typeof transform) => Effect.sync(() => { transform = callback; callback(editAgents()); }).pipe(Effect.andThen(registration)), reload: () => Effect.sync(() => { if (transform) transform(editAgents()); }), get: () => Effect.succeed({ data: agents.get("multi-work")! }), list: () => Effect.sync(() => { agentReads++; if (!destinationAvailable) throw new Error("Destination location unavailable"); return { data: warming && agentReads === 1 ? [] : [...agents.values()] }; }), }, session: { get: ({ sessionID }: { sessionID: string }) => Effect.sync(() => sessions.get(sessionID)!), move: ({ sessionID, directory }: { sessionID: string; directory: string }) => Effect.sync(() => { order.push("move-request"); if (!delayMove) { sessions.get(sessionID)!.location.directory = directory; order.push("moved"); if (crossProject) sessions.get(sessionID)!.projectID = "another-project"; } entered.resolve(); }), hook: (_name: string, callback: (event: SessionContext) => Effect.Effect) => Effect.sync(() => { hooks.push(callback); }).pipe(Effect.andThen(registration)), }, tool: { transform: (callback: (editor: ToolEditor) => void) => Effect.sync(() => callback(editTools)).pipe(Effect.andThen(registration)), hook: ( _name: string, callback: (event: ToolHooks["execute.before"]) => Effect.Effect, ) => Effect.sync(() => { toolHooks.push(callback); }).pipe(Effect.andThen(registration)), }, }; // SAFETY: The test setup only receives its exercised services; unexpected host dependencies fail loudly. const install = installMultiWork as ( context: Pick, buildPermissions: () => Agent.Info["permissions"], herdr: Herdr, ) => ReturnType; const call: Tool.Context = { sessionID: parent.id, agent: Agent.ID.make("multi-work"), messageID: SessionMessage.ID.make("msg_test"), id: Tool.CallID.make("call_test"), progress: (value) => Effect.sync(() => { progress.push(value); }), }; return { herdr, agents, sessions, parent, storage, order, entered, hooks, toolHooks, call, progress, install: install( host, () => agents.get("build")?.permissions ?? 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), execute: (input: z.infer, caller = call) => tool.execute(input, caller), hold() { hold = true; }, delayMove() { delayMove = true; }, missingDestination() { destinationAvailable = false; }, crossProject() { crossProject = true; }, warmDestination() { warming = true; }, get agentReads() { return agentReads; }, get cancelled() { return cancelled; }, get prompts() { return prompts; }, get tool() { return tool; }, }; } const request = z.object({ agent: z.string(), description: z.string(), prompt: z.string(), worktree: z .object({ base: z.string(), branch: z.string().optional(), label: z.string().optional(), }) .optional(), sessionID: z.string().optional(), background: z.boolean().optional(), }); test("Effect-native wrapper preserves name, metadata and result; moves before prompt and resumes its bound child", async () => { const f = fixture(); await Effect.runPromise( Effect.scoped( Effect.gen(function* () { yield* f.install; expect(f.agents.get("multi-work")?.mode).toBe("primary"); expect(f.tool.name).toBe("subagent"); expect(f.tool.options?.codemode).toBe(false); expect(f.agents.get("coordinate")?.mode).toBe("all"); const result = yield* f.invoke(); expect(result.content).toStartWith("native result"); expect(result.content).toContain("Checkout: /worktree"); expect(result.metadata?.sessionID).toBe("ses_child1"); expect(f.order).toEqual(["create", "move-request", "moved", "prompt:/worktree"]); expect(f.parent.location.directory).toBe("/root"); expect(bindingSchema.parse(f.storage.get(bindingKey("ses_child1"))).ready).toBe( true, ); const resume = yield* f.execute({ agent: "coordinate", description: "resume", prompt: "continue", sessionID: "ses_child1", background: true, }); expect(resume.metadata?.status).toBe("running"); expect(f.herdr.creations).toBe(1); expect(f.herdr.verified).toBe(1); }), ), ); }); test("local fast-lo uses native delegation without a worktree", async () => { const f = fixture(); await Effect.runPromise( Effect.scoped( Effect.gen(function* () { yield* f.install; yield* f.execute({ agent: "fast-lo", description: "lookup", prompt: "read" }); expect(f.herdr.creations).toBe(0); expect(f.order).toEqual(["create", "prompt:/root"]); }), ), ); }); test("custom branch and workspace label pass through delegation", async () => { const f = fixture(); await Effect.runPromise( Effect.scoped( Effect.gen(function* () { yield* f.install; const result = yield* f.execute({ agent: "coordinate", description: "Fix login timeout", prompt: "work", worktree: { base: "main", branch: "fix/login-timeout", label: "Fix login timeout", }, }); expect(f.herdr.createdBase).toBe("main"); expect(f.herdr.createdBranch).toBe("fix/login-timeout"); expect(f.herdr.createdLabel).toBe("Fix login timeout"); expect(result.metadata?.branch).toBe("fix/login-timeout"); }), ), ); }); test("omitted naming retains generated branch and no explicit label", async () => { const f = fixture(); await Effect.runPromise( Effect.scoped( Effect.gen(function* () { yield* f.install; yield* f.invoke(); expect(f.herdr.createdBranch).toMatch(/^multi-work\/[0-9a-f-]{36}$/); expect(f.herdr.createdLabel).toBeUndefined(); }), ), ); }); test("rejects empty naming fields and naming on resume", async () => { const f = fixture(); await Effect.runPromise( Effect.scoped( Effect.gen(function* () { yield* f.install; for (const worktree of [ { base: "main", branch: " " }, { base: "main", label: " " }, ]) expect( (yield* Effect.exit( f.execute({ agent: "coordinate", description: "invalid", prompt: "work", worktree, }), ))._tag, ).toBe("Failure"); yield* f.invoke(); expect( (yield* Effect.exit( f.execute({ agent: "coordinate", description: "resume", prompt: "work", sessionID: "ses_child1", worktree: { base: "main", branch: "fix/other" }, }), ))._tag, ).toBe("Failure"); expect(f.herdr.creations).toBe(1); }), ), ); }); test("root loss, session permission denial and missing destination fail before prompting", async () => { for (const failure of ["root", "permission", "ask", "destination", "project"] as const) { const f = fixture(); await Effect.runPromise( Effect.scoped( Effect.gen(function* () { yield* f.install; if (failure === "root") f.herdr.eligible = false; if (failure === "permission") f.parent.permissions = [ { action: "herdr_worktree", resource: "*", effect: "deny" }, ]; if (failure === "destination") f.missingDestination(); if (failure === "project") f.crossProject(); if (failure === "ask") f.parent.permissions = [ { action: "herdr_worktree", resource: "*", effect: "ask" }, ]; const result = yield* Effect.exit(f.invoke()); expect(result._tag).toBe("Failure"); expect(f.prompts).toBe(0); expect(f.herdr.creations).toBe( failure === "destination" || failure === "project" ? 1 : 0, ); }), ), ); } }); test("interruption crosses the wrapper into the native executor finalizer", async () => { const f = fixture(); f.hold(); const controller = new AbortController(); const running = Effect.runPromise( Effect.scoped( Effect.gen(function* () { yield* f.install; return yield* f.invoke(); }), ), { signal: controller.signal }, ); await f.entered.promise; // entered may resolve at move request; yield until the native foreground wait is reached. while (f.prompts === 0) await Bun.sleep(1); controller.abort(); await expect(running).rejects.toBeDefined(); expect(f.cancelled).toBe(true); }); test("move admission alone never admits a task; cancellation retains an unready binding", async () => { const f = fixture(); f.delayMove(); const controller = new AbortController(); const running = Effect.runPromise( Effect.scoped( Effect.gen(function* () { yield* f.install; return yield* f.invoke(); }), ), { signal: controller.signal }, ); await f.entered.promise; expect(f.prompts).toBe(0); controller.abort(); await expect(running).rejects.toBeDefined(); expect(f.prompts).toBe(0); expect(bindingSchema.parse(f.storage.get(bindingKey("ses_child1"))).ready).toBe(false); }); test("rejects foreign and changed-agent resumes, non-primary multi-work and unbound coordinate calls", async () => { const f = fixture(); await Effect.runPromise( Effect.scoped( Effect.gen(function* () { yield* f.install; yield* f.invoke(); f.sessions.get("ses_child1")!.agent = "fast-lo"; expect( (yield* Effect.exit( f.execute({ agent: "coordinate", description: "resume", prompt: "x", sessionID: "ses_child1", }), ))._tag, ).toBe("Failure"); f.parent.parentID = "ses_foreign"; expect((yield* Effect.exit(f.invoke()))._tag).toBe("Failure"); f.parent.parentID = undefined; expect( (yield* Effect.exit( f.invoke(undefined, { ...f.call, agent: Agent.ID.make("build") }), ))._tag, ).toBe("Failure"); expect( (yield* Effect.exit( f.execute({ agent: "multi-work", description: "x", prompt: "x" }), ))._tag, ).toBe("Failure"); expect(f.herdr.creations).toBe(1); }), ), ); }); test("ineligible roots do not register multi-work; multi-work cannot execute shell even with session overrides", async () => { const f = fixture(); f.herdr.eligible = false; await Effect.runPromise( Effect.scoped( Effect.gen(function* () { yield* f.install; expect(f.agents.has("multi-work")).toBe(false); const hook = f.toolHooks[0]!; expect( (yield* Effect.exit(hook({ ...f.call, tool: "shell", input: {} })))._tag, ).toBe("Failure"); }), ), ); }); test("interruption during Herdr creation aborts the command and retains an inspectable attempt without prompting", async () => { const f = fixture(); f.herdr.delayCreation = true; const controller = new AbortController(); const running = Effect.runPromise( Effect.scoped( Effect.gen(function* () { yield* f.install; return yield* f.invoke(); }), ), { signal: controller.signal }, ); await f.herdr.creationStarted.promise; controller.abort(); await expect(running).rejects.toBeDefined(); expect(f.herdr.creationAborted).toBe(true); expect(f.prompts).toBe(0); expect(f.storage.has(`multi-work-call:${f.call.sessionID}:${f.call.id}`)).toBe(true); }); test("waits for asynchronous destination plugin activation before moving or prompting", async () => { const f = fixture(); f.warmDestination(); await Effect.runPromise( Effect.scoped( Effect.gen(function* () { yield* f.install; yield* f.invoke(); expect(f.agentReads).toBe(2); expect(f.prompts).toBe(1); expect(f.order).toEqual(["create", "move-request", "moved", "prompt:/worktree"]); }), ), ); }); test("configured build restrictions remain effective for worktree preparation", async () => { const f = fixture(); f.agents.set("build", { ...Agent.Info.default(Agent.ID.make("build")), permissions: [{ action: "herdr_worktree", resource: "*", effect: "deny" }], }); await Effect.runPromise( Effect.scoped( Effect.gen(function* () { yield* f.install; expect((yield* Effect.exit(f.invoke()))._tag).toBe("Failure"); expect(f.herdr.creations).toBe(0); }), ), ); }); test("failed initialization records the attempt and never blindly recreates on replay", async () => { const f = fixture(); f.missingDestination(); await Effect.runPromise( Effect.scoped( Effect.gen(function* () { yield* f.install; expect((yield* Effect.exit(f.invoke()))._tag).toBe("Failure"); expect((yield* Effect.exit(f.invoke()))._tag).toBe("Failure"); expect(f.herdr.creations).toBe(1); expect(f.prompts).toBe(0); const record = z .object({ child: z.string(), branch: z.string() }) .parse(f.storage.get(`multi-work-call:${f.call.sessionID}:${f.call.id}`)); expect(record.child).toBe("ses_child1"); expect(record.branch).toStartWith("multi-work/"); }), ), ); }); test("same-child concurrent resumes are rejected while the native foreground executor is active", async () => { const f = fixture(); f.hold(); const controller = new AbortController(); const checked = Promise.withResolvers(); const running = Effect.runPromise( Effect.scoped( Effect.gen(function* () { yield* f.install; yield* f.invoke().pipe(Effect.forkScoped); yield* Effect.promise(() => f.entered.promise); const second = yield* Effect.exit( f.execute({ agent: "coordinate", description: "resume", prompt: "continue", sessionID: "ses_child1", }), ); expect(second._tag).toBe("Failure"); expect(f.herdr.creations).toBe(1); checked.resolve(); return yield* Effect.never; }), ), { signal: controller.signal }, ); await Promise.race([checked.promise, running]); controller.abort(); await expect(running).rejects.toBeDefined(); expect(f.cancelled).toBe(true); });