Files
stanp-opencode/tests/multi-work.test.ts
T
2026-09-18 20:13:54 +02:00

829 lines
30 KiB
TypeScript

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;
rootWorkspaceID = "w1";
rootRepo = "/root/.git";
creations = 0;
verified = 0;
delayCreation = false;
creationAborted = false;
createdBase: string | undefined;
createdBranch: string | undefined;
createdLabel: string | undefined;
creationStarted = Promise.withResolvers<void>();
override async root(directory: string) {
return this.eligible && directory === "/root"
? { directory, workspaceID: this.rootWorkspaceID, repo: this.rootRepo }
: 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<void>((_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<string, Parameters<Context["storage"]["set"]>[1]>();
const agents = new Map<string, NonNullable<ReturnType<AgentEditor["get"]>>>();
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<void>();
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<void, Tool.Error>)[] =
[];
const hooks: ((event: SessionContext) => Effect.Effect<void>)[] = [];
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<AgentEditor["get"]>
>;
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<Context["storage"]["set"]>[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<void>) =>
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<void, Tool.Error>,
) =>
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<Context, "options">,
buildPermissions: () => Agent.Info["permissions"],
herdr: Herdr,
) => ReturnType<typeof installMultiWork>;
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<typeof request>, 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("coordinate can create and resume a root-local background child without Herdr creation or movement", async () => {
const f = fixture();
f.parent.permissions = [{ action: "herdr_worktree", resource: "*", effect: "deny" }];
await Effect.runPromise(
Effect.scoped(
Effect.gen(function* () {
yield* f.install;
const created = yield* f.execute({
agent: "coordinate",
description: "local edit",
prompt: "edit assigned files",
background: true,
});
expect(created.metadata).toMatchObject({
sessionID: "ses_child1",
status: "running",
directory: "/root",
workspaceID: "w1",
branch: "root",
});
expect(f.herdr.creations).toBe(0);
expect(f.order).toEqual(["create", "prompt:/root"]);
expect(bindingSchema.parse(f.storage.get(bindingKey("ses_child1")))).toMatchObject({
owner: "ses_parent",
directory: "/root",
workspaceID: "w1",
branch: "root",
ready: true,
});
yield* f.execute({
agent: "coordinate",
description: "resume local",
prompt: "continue",
sessionID: "ses_child1",
});
expect(f.herdr.creations).toBe(0);
expect(f.herdr.verified).toBe(0);
}),
),
);
});
test("root-local resume rejects location, binding, and live-root changes", async () => {
for (const tamper of ["session", "workspace", "root", "branch", "live-root"] as const) {
const f = fixture();
await Effect.runPromise(
Effect.scoped(
Effect.gen(function* () {
yield* f.install;
yield* f.execute({
agent: "coordinate",
description: "local edit",
prompt: "work",
});
if (tamper === "session")
f.sessions.get("ses_child1")!.location.directory = "/other";
else if (tamper === "live-root") f.herdr.rootWorkspaceID = "foreign";
else {
const binding = bindingSchema.parse(
f.storage.get(bindingKey("ses_child1")),
);
const changed = { ...binding };
if (tamper === "workspace") changed.workspaceID = "foreign";
if (tamper === "root") {
changed.root = { ...binding.root, workspaceID: "foreign" };
changed.workspaceID = "foreign";
}
if (tamper === "branch") changed.branch = "main";
f.storage.set(bindingKey("ses_child1"), changed);
}
expect(
(yield* Effect.exit(
f.execute({
agent: "coordinate",
description: "resume",
prompt: "continue",
sessionID: "ses_child1",
}),
))._tag,
).toBe("Failure");
expect(f.prompts).toBe(1);
}),
),
);
}
});
test("interrupted root-local creation retains its binding and cannot be blindly replayed", 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.execute({
agent: "coordinate",
description: "local edit",
prompt: "work",
});
}),
),
{ signal: controller.signal },
);
await f.entered.promise;
controller.abort();
await expect(running).rejects.toBeDefined();
expect(bindingSchema.parse(f.storage.get(bindingKey("ses_child1"))).ready).toBe(true);
expect(f.storage.has(`multi-work-call:${f.call.sessionID}:${f.call.id}`)).toBe(true);
const replay = await Effect.runPromise(
Effect.exit(
f.execute({
agent: "coordinate",
description: "local edit",
prompt: "work",
}),
),
);
expect(replay._tag).toBe("Failure");
expect(f.prompts).toBe(1);
});
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("worktree resumes retain the herdr_worktree permission guard", async () => {
const f = fixture();
await Effect.runPromise(
Effect.scoped(
Effect.gen(function* () {
yield* f.install;
yield* f.invoke();
f.parent.permissions = [
{ action: "herdr_worktree", resource: "*", effect: "deny" },
];
const resumed = yield* Effect.exit(
f.execute({
agent: "coordinate",
description: "resume",
prompt: "continue",
sessionID: "ses_child1",
}),
);
expect(resumed._tag).toBe("Failure");
expect(f.herdr.verified).toBe(0);
expect(f.prompts).toBe(1);
}),
),
);
});
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<void>();
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);
});