Official
claude
Claude Code agent provider: runs the Claude Code CLI headless for each account.
The app opens the listing; nothing installs until an agent in your Plugins workspace has read the files and you enable the plugin. In a terminal: cvg install convergence/claude@0.2.0
Permissions in 0.2.0
Files
agent.test.ts26.8 KB
// The agent against a scripted `claude` CLI (the SDK's FakePeer): the
// Rust plugin's live tests (`agent.rs`) as fakes, and the host's calls
// end to end: initialize, the catalog probe, create, prompt, approvals,
// rollback, fork, history, sign-in.
import * as Scope from "effect/Scope";
import * as Exit from "effect/Exit";
import assert from "node:assert/strict";
import type { Api } from "convergence";
import type { Record as ClaudeRecord } from "./wire.ts";
import { contentAs, required, writtenSchema, resources, useApi, test, run as runEffect, tempDir } from "./testing.ts";
import type { PeerScript } from "../sdk/testing.ts";
import type { SpawnOptions } from "../sdk/process.ts";
import type { Instance } from "./instances.ts";
import type { AgentEvent } from "./types.ts";
import { mkdirSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { ClaudeAgent } from "./agent.ts";
import { defaultAccount } from "./instances.ts";
import { encodeWorkspace } from "./history.ts";
import { diskFs, fakeApi, memoryStorage, settle } from "../sdk/testing.ts";
import { clearCache } from "../sdk/maintenance.ts";
const INIT = {
models: [
{
value: "haiku",
resolvedModel: "claude-haiku-4-5-20251001",
displayName: "Haiku",
supportsAdaptiveThinking: true,
},
],
commands: [{ name: "compact", description: "Compact the conversation" }],
};
/// Finishes a one-shot program the plugin ran (`--version`, `auth status`).
function answerRun(api: Pick<ReturnType<typeof fakeApi>, "peers">, stdout: string, code = 0) {
queueMicrotask(() => {
const peer = api.peers.at(-1);
required(peer).stdout.push(stdout);
required(peer).exit(code);
});
return () => {};
}
/// A scripted CLI: `claude --version`, `claude auth …`, and sessions that
/// answer the control requests and turn every user message into a short
/// streamed reply (`reply(message, peer)` can do more).
function harness({
auth = {
loggedIn: true,
email: "a@b.com",
subscriptionType: "max",
authMethod: "claude.ai",
apiProvider: "firstParty",
configDirectory: "/nonexistent-home/.claude",
orgId: "org-a",
orgName: "Org A",
},
which = "/usr/local/bin/claude",
env = {},
instance = defaultAccount(),
reply,
nativeAccount,
usage,
usageError,
}: {
auth?: {
loggedIn: boolean;
email?: string;
subscriptionType?: string;
authMethod?: string;
apiProvider?: string;
configDirectory?: string;
orgId?: string;
orgName?: string;
};
which?: string | null;
env?: Record<string, string>;
instance?: Instance;
reply?: PeerScript;
nativeAccount?: Record<string, unknown>;
usage?: Record<string, unknown>;
usageError?: string;
} = {}) {
clearCache();
const storage = memoryStorage();
const spawns: { program: string; args: readonly string[]; options: SpawnOptions }[] = [];
const api: Omit<ReturnType<typeof fakeApi>, "fs"> & { fs: Api["fs"] } = fakeApi({
fs: diskFs(),
env: { HOME: "/nonexistent-home", ...env },
onHost: (method, params) => {
const stored = storage(method, params);
if (stored) return stored;
if (method === "host/process.which")
return which ? { path: which, realPath: which } : { path: null, realPath: null };
if (method === "host/tools.call") return { content: [{ type: "text", text: "7319" }] };
return {};
},
onSpawn: (program, args, options) => {
spawns.push({ program, args, options });
if (program !== "claude") return answerRun(api, "", 1);
if (args[0] === "--version") return answerRun(api, "2.1.281 (Claude Code)\n");
if (args[0] === "auth" && args[1] === "status") return answerRun(api, JSON.stringify(auth));
if (args[0] === "auth") return answerRun(api, "", 0);
return (message, peer) => {
if (message.type === "control_request") {
const request = writtenSchema.parse(message).request;
assert.ok(request);
const ok = (response: unknown) =>
peer.send({
type: "control_response",
response: { subtype: "success", request_id: message.request_id, response },
});
if (request.subtype === "initialize")
ok({
...INIT,
account: nativeAccount ?? {
email: auth.email,
organization: auth.orgName,
apiProvider: auth.apiProvider,
},
});
else if (request.subtype === "get_usage") {
if (usageError)
peer.send({
type: "control_response",
response: { subtype: "error", request_id: message.request_id, error: usageError },
});
else ok(usage ?? { rate_limits: { five_hour: { utilization: 12, resets_at: "2026-09-26T10:00:00Z" } } });
} else ok({});
return;
}
if (message.type === "control_response") return;
if (message.type === "user") {
if (reply) return reply(message, peer);
peer.send({ type: "system", subtype: "init", session_id: "x" });
peer.send({
type: "stream_event",
parent_tool_use_id: null,
event: { type: "message_start", message: { id: "msg_1", model: "claude-haiku-4-5" } },
});
peer.send({
type: "stream_event",
parent_tool_use_id: null,
event: { type: "content_block_start", index: 0, content_block: { type: "text", text: "" } },
});
peer.send({
type: "stream_event",
parent_tool_use_id: null,
event: { type: "content_block_delta", index: 0, delta: { type: "text_delta", text: "PO" } },
});
peer.send({
type: "stream_event",
parent_tool_use_id: null,
event: { type: "content_block_delta", index: 0, delta: { type: "text_delta", text: "NG" } },
});
peer.send({
type: "assistant",
parent_tool_use_id: null,
message: {
id: "msg_1",
model: "claude-haiku-4-5",
content: [{ type: "text", text: "PONG" }],
usage: { input_tokens: 5, output_tokens: 1 },
},
});
peer.send({
type: "result",
subtype: "success",
is_error: false,
usage: { input_tokens: 5, output_tokens: 1 },
modelUsage: { "claude-haiku-4-5": { contextWindow: 200000 } },
});
}
};
},
});
const events: AgentEvent[] = [];
const agent = new ClaudeAgent({ ...resources(useApi(api)), instance, emit: (event) => events.push(event) });
return { agent, api, events, spawns, storage };
}
async function until(test: () => boolean, what: string, timeout = 2000) {
const deadline = Date.now() + timeout;
while (!test()) {
if (Date.now() > deadline) assert.fail(`timed out waiting for ${what}`);
await new Promise((resolve) => setTimeout(resolve, 2));
}
}
const sessionSpawns = (spawns: { program: string; args: readonly string[]; options: SpawnOptions }[]) =>
spawns.filter((spawn) => spawn.program === "claude" && spawn.args[0] === "--output-format");
test("initialize reports the installed CLI, the account and the capabilities", async () => {
const { agent } = harness();
const info = await runEffect(agent.initialize());
assert.equal(info.id, "claude");
assert.equal(info.name, "Claude Code");
assert.deepEqual(info.status, { state: "ready" });
assert.equal(info.version, "2.1.281 (Claude Code)");
assert.deepEqual(info.maintenance, { canUpdate: false, installedVersion: "2.1.281 (Claude Code)" });
assert.match(required(info.description), /Signed in as a@b.com on the Max plan\.$/);
assert.equal(info.family, "claude");
assert.equal(info.continuationKey, "", "the default directory");
assert.ok(required(info.icon).startsWith("<svg"));
assert.equal(
required(info.capabilities).steer,
true,
"native next picks up input after current tools without interruption",
);
assert.ok(
required(info.capabilities).subagents &&
required(info.capabilities).cancelTask &&
required(info.capabilities).rollback &&
required(info.capabilities).fork,
);
assert.deepEqual(info.promptKeywords, [
{ word: "ultracode", label: "Ultracode", description: "This turn runs as a dynamic workflow." },
]);
assert.deepEqual(
required(info.authMethods).map((method) => method.id),
["anthropic"],
);
});
test("initialize says what is wrong: no CLI, or nobody signed in", async () => {
let { agent } = harness({ which: null });
assert.deepEqual((await runEffect(agent.initialize())).status, {
state: "unavailable",
message: "the claude CLI is not on PATH",
});
const home = tempDir();
({ agent } = harness({ auth: { loggedIn: false }, instance: { id: "work", name: null, configDir: home, env: {} } }));
const info = await runEffect(agent.initialize());
assert.equal(info.id, "claude:work");
assert.deepEqual(info.status, {
state: "auth_required",
message: `Run \`claude auth login\` with CLAUDE_CONFIG_DIR=${home} to sign in.`,
});
assert.equal(info.continuationKey, home);
// Workflows switched off in the account's settings: no keyword.
writeFileSync(join(home, "settings.json"), JSON.stringify({ enableWorkflows: false }));
assert.equal((await runEffect(agent.initialize())).promptKeywords, undefined);
});
test("the options come from a probe that runs nothing of the user's", async () => {
const { agent, spawns, api } = harness();
const { options } = await runEffect(agent.listOptions({ workspace: "/w" }));
assert.deepEqual(
options.map((option) => option.id),
["model", "effort", "permission_mode", "thinking"],
);
const [probe] = sessionSpawns(spawns);
assert.ok(probe.args.includes("--strict-mcp-config"));
assert.ok(!probe.args.includes("--permission-mode"));
assert.equal(probe.options.cwd, "/w");
await until(() => api.peers.every((peer) => peer.status !== null), "the probe to stop");
assert.equal(required(required(agent.limits).windows)[0].id, "five_hour", "the probe read the limits");
// Read once.
await runEffect(agent.listOptions({ workspace: "/w" }));
assert.equal(sessionSpawns(spawns).length, 1);
const cached = required(agent.limits);
const refreshed = required((await runEffect(agent.usageLimits())).limits);
assert.notEqual(refreshed, cached, "refresh observes a newly initialized process, not cached permission");
assert.equal(sessionSpawns(spawns).length, 2);
assert.deepEqual(refreshed.windows, cached.windows);
assert.equal(required(refreshed.recovery).identity, required(cached.recovery).identity);
});
test("current-session quota identity matches a fresh usage probe and changes with native account/config evidence", async () => {
const auth = {
loggedIn: true,
email: "a@b.com",
subscriptionType: "max",
authMethod: "claude.ai",
apiProvider: "firstParty",
configDirectory: "/nonexistent-home/.claude",
orgId: "org-a",
orgName: "Org A",
};
const { agent, api, events, spawns } = harness({ auth, reply: () => {} });
const { sessionId } = await runEffect(agent.createSession({ workspace: "/w" }));
const current = agent.session(sessionId);
const original = required(current.identity);
const { runId } = await runEffect(
agent.prompt({ sessionId, input: { blocks: [{ type: "text", text: "tool work" }] } }),
);
const peer = required(api.peers.find((peer) => peer.status === null));
peer.send({
type: "rate_limit_event",
rate_limit_info: { status: "rejected", rateLimitType: "five_hour", resetsAt: 1791200000 },
});
await until(() => events.some((event) => event.event === "usage_blocked"), "native rejection");
const blocked = required(events.find((event) => event.event === "usage_blocked"));
assert.ok(blocked.event === "usage_blocked");
assert.equal(blocked.recovery.identity, original);
assert.equal(blocked.runId, runId);
assert.equal(required((await runEffect(agent.usageLimits())).limits?.recovery).identity, original);
assert.equal(current.run, runId, "usage refresh never prompts, interrupts or replaces the active session");
auth.email = "other@b.com";
auth.orgId = "org-b";
auth.orgName = "Org B";
const switched = required((await runEffect(agent.usageLimits())).limits?.recovery);
assert.notEqual(switched.identity, original);
assert.equal(switched.availability, "unknown", "fresh meters are not native permission");
assert.equal(current.identity, original, "the old process's evidence is not relabelled as the new account");
agent.instance.configDir = "/other-config";
auth.configDirectory = "/other-config";
const moved = required((await runEffect(agent.usageLimits())).limits?.recovery);
assert.notEqual(moved.identity, switched.identity);
assert.ok(
spawns
.filter((spawn) => spawn.args[0] === "auth" && spawn.args[1] === "status")
.every((spawn) => spawn.options.cwd === "/w"),
);
assert.ok(
api.peers
.filter((candidate) => candidate !== peer)
.every((candidate) => !candidate.received.some((message) => message.type === "user")),
"refresh processes never spend a real prompt to poll",
);
});
test("missing, conflicting or API-key identity cannot become an invented subscription account", async () => {
for (const nativeAccount of [
{},
{ email: "a@b.com", organization: "Other org", apiProvider: "firstParty" },
{ email: "a@b.com", organization: "Org A", apiProvider: "firstParty", apiKeySource: "ANTHROPIC_API_KEY" },
]) {
const { agent } = harness({ nativeAccount });
const { sessionId } = await runEffect(agent.createSession({ workspace: "/w" }));
assert.equal(agent.session(sessionId).identity, null);
assert.equal(required((await runEffect(agent.usageLimits())).limits?.recovery).identity, undefined);
}
const { agent } = harness({ usage: { rate_limits_available: false, rate_limits: null } });
assert.deepEqual(await runEffect(agent.usageLimits()), { limits: null }, "no workspace/native observation yet");
await runEffect(agent.listOptions({ workspace: "/w" }));
assert.equal(required((await runEffect(agent.usageLimits())).limits?.recovery).availability, "unsupported");
});
test("failed native usage refresh propagates the error instead of returning cached permission", async () => {
const { agent, api } = harness({ usageError: "usage read failed" });
await runEffect(agent.createSession({ workspace: "/w" }));
const cached = {
recovery: {
availability: "allowed",
observedAt: "2026-10-01T00:00:00Z",
source: "cache",
reason: "old permission",
},
} as const;
agent.limits = cached;
await assert.rejects(runEffect(agent.usageLimits()), /usage read failed/);
assert.equal(agent.limits, cached);
assert.ok(api.peers.every((peer) => !peer.received.some((message) => message.type === "user")));
assert.equal(
api.peers.filter((peer) => peer.status === null).length,
1,
"the failed refresh process is closed, not the current session",
);
});
test("a prompt streams and ends its run, with the plugin tools declared", async () => {
const { agent, api, spawns, events } = harness();
const tools = [{ name: "lucky_number", description: "d", inputSchema: { type: "object" } }];
const { sessionId } = await runEffect(
agent.createSession({ workspace: "/w", options: { model: "claude-haiku-4-5" }, tools, instructions: "Be kind." }),
);
assert.match(sessionId, /^[0-9a-f-]{36}$/);
const [spawn] = sessionSpawns(spawns);
assert.equal(spawn.args[spawn.args.indexOf("--session-id") + 1], sessionId);
assert.equal(spawn.args[spawn.args.indexOf("--model") + 1], "claude-haiku-4-5");
assert.deepEqual(
spawn.options,
{ cwd: "/w", env: { CLAUDE_CODE_EMIT_SESSION_STATE_EVENTS: "1" } },
"no CLAUDE_CONFIG_DIR for the default account",
);
const peer = required(agent.sessions.get(sessionId)).proc;
assert.ok(required(peer).alive);
const initialize = required(api.peers.find((peer) => peer.args.includes(sessionId)))
.received.map((message) => writtenSchema.parse(message))
.find((message) => message.request?.subtype === "initialize");
assert.deepEqual(required(required(initialize).request).sdkMcpServers, ["convergence"]);
assert.equal(required(required(initialize).request).appendSystemPrompt, "Be kind.");
assert.ok(events.some((event) => event.event === "commands"));
assert.ok(events.some((event) => event.event === "config_options"));
assert.deepEqual(
(await runEffect(agent.listCommands({ sessionId }))).commands.map((command) => command.name),
["compact"],
);
const { runId } = await runEffect(
agent.prompt({ sessionId, input: { blocks: [{ type: "text", text: "Reply PONG" }], itemId: "host-1" } }),
);
await until(() => events.some((event) => event.event === "run_finished"), "the run to end");
const text = events
.filter((event) => event.event === "text_delta")
.map((event) => event.text)
.join("");
assert.equal(text, "PONG");
const finished = events.find((event) => event.event === "run_finished");
assert.deepEqual(finished, { sessionId, runId, event: "run_finished", outcome: { status: "completed" } });
const usage = events.find((event) => event.event === "usage");
assert.equal(required(usage).contextWindow, 200000);
assert.ok(!events.some((event) => event.event === "run_started"), "a prompted run never sends run_started");
await runEffect(agent.closeSession({ sessionId }));
assert.equal(agent.sessions.size, 0);
});
test("approvals and questions route back to their session by id", async () => {
const { agent, api, events } = harness({
reply: (_message, peer) => {
peer.send({
type: "control_request",
request_id: "cli-7",
request: {
subtype: "can_use_tool",
tool_name: "Write",
tool_use_id: "toolu_w",
input: { file_path: "/w/note.txt", content: "hello" },
},
});
},
});
const { sessionId } = await runEffect(agent.createSession({ workspace: "/w", options: {} }));
await runEffect(agent.prompt({ sessionId, input: { blocks: [{ type: "text", text: "write" }] } }));
await until(() => events.some((event) => event.event === "approval"), "the approval");
const approval = events.find((event) => event.event === "approval");
assert.equal(required(approval).id, `${sessionId}::cli-7`);
assert.equal(required(required(approval).toolCall).kind, "edit");
assert.equal(contentAs(required(required(required(approval).toolCall).content)[0], "diff").newText, "hello");
await runEffect(agent.respondToApproval({ approvalId: required(approval).id, optionId: "allow_once" }));
const peer = api.peers.find((peer) => peer.args.includes(sessionId));
await until(() => required(peer).received.some((message) => message.type === "control_response"), "the answer");
assert.deepEqual(required(required(peer).received.find((message) => message.type === "control_response")).response, {
subtype: "success",
request_id: "cli-7",
response: { behavior: "allow" },
});
await assert.rejects(
runEffect(agent.respondToApproval({ approvalId: "nonsense", optionId: "allow_once" })),
/malformed/,
);
await runEffect(agent.shutdown());
});
test("a rewind restarts the process at the record before the message", async () => {
const home = tempDir();
const instance = { id: "", name: null, configDir: home, env: {} };
const { agent, spawns } = harness({ instance });
const { sessionId } = await runEffect(agent.createSession({ workspace: "/w", options: { effort: "high" } }));
const dir = join(home, "projects", encodeWorkspace("/w"));
mkdirSync(dir, { recursive: true });
const records: ClaudeRecord[] = [
{ type: "user", uuid: "u1", parentUuid: null, message: { role: "user", content: "one" } },
{ type: "assistant", uuid: "a1", parentUuid: "u1", message: { content: [{ type: "text", text: "ok" }] } },
];
writeFileSync(join(dir, `${sessionId}.jsonl`), records.map((record) => JSON.stringify(record)).join("\n"));
// The second prompt becomes the second user record.
await runEffect(agent.prompt({ sessionId, input: { blocks: [{ type: "text", text: "two" }], itemId: "host-2" } }));
records.push({ type: "user", uuid: "u2", parentUuid: "a1", message: { role: "user", content: "two" } });
writeFileSync(join(dir, `${sessionId}.jsonl`), records.map((record) => JSON.stringify(record)).join("\n"));
await until(() => !required(agent.sessions.get(sessionId)).run, "the run to end");
await runEffect(agent.rollback({ sessionId, itemId: "host-2" }));
const restarted = sessionSpawns(spawns).at(-1);
const at = required(restarted).args.indexOf("--resume");
assert.deepEqual(required(restarted).args.slice(at, at + 4), ["--resume", sessionId, "--resume-session-at", "a1"]);
assert.equal(
required(restarted).args[required(restarted).args.indexOf("--effort") + 1],
"high",
"the option values carry over",
);
assert.equal(required(required(restarted).options.env).CLAUDE_CONFIG_DIR, home, "a chosen directory is exported");
// Back to before the first message: a new conversation under a new id.
await runEffect(agent.rollback({ sessionId, itemId: "u1" })).then(
() => assert.fail("the first message has nothing before it by uuid"),
(error) => assert.match(error.message, /nothing recorded before/),
);
required(agent.sessions.get(sessionId)).sent["host-1"] = 0;
await runEffect(agent.rollback({ sessionId, itemId: "host-1" }));
const fresh = sessionSpawns(spawns).at(-1);
const cli = required(fresh).args[required(fresh).args.indexOf("--session-id") + 1];
assert.notEqual(cli, sessionId, "the recorded id cannot be reused");
assert.equal(await runEffect(agent.store.cliIdOf(sessionId)), cli);
await runEffect(agent.shutdown());
});
test("a fork copies the conversation into a new id and keeps the settings", async () => {
const { agent, spawns } = harness();
const { sessionId } = await runEffect(
agent.createSession({ workspace: "/w", options: { model: "claude-haiku-4-5" } }),
);
const { sessionId: copy } = await runEffect(agent.forkSession({ sessionId, workspace: "/w", options: {} }));
assert.notEqual(copy, sessionId);
const forked = sessionSpawns(spawns).at(-1);
const at = required(forked).args.indexOf("--resume");
assert.deepEqual(required(forked).args.slice(at, at + 5), [
"--resume",
sessionId,
"--fork-session",
"--session-id",
copy,
]);
assert.equal(required(forked).args[required(forked).args.indexOf("--model") + 1], "claude-haiku-4-5");
await runEffect(agent.shutdown());
});
test("history is read under the CLI's id for the session", async () => {
const home = tempDir();
const { agent, storage } = harness({ instance: { id: "", name: null, configDir: home, env: {} } });
const dir = join(home, "projects", encodeWorkspace("/w"));
mkdirSync(dir, { recursive: true });
writeFileSync(
join(dir, "cli-9.jsonl"),
JSON.stringify({
type: "user",
uuid: "u1",
timestamp: "2026-09-14T12:00:00Z",
message: { role: "user", content: "hello there" },
}),
);
storage.values.set("session:host-9", { cli: "cli-9", sent: {} });
const { items } = await runEffect(agent.readSession({ workspace: "/w", sessionId: "host-9" }));
assert.deepEqual(items, [
{ id: "u1", createdAt: "2026-09-14T12:00:00.000Z", role: "user", blocks: [{ type: "text", text: "hello there" }] },
]);
const { sessions } = await runEffect(agent.listSessions({ workspace: "/w" }));
assert.deepEqual(
sessions.map((session) => [session.id, session.title]),
[["cli-9", "hello there"]],
);
});
test("an account whose folder no grant covers says so instead of showing no history", async () => {
const { agent, api } = harness({ instance: { id: "personal", name: null, configDir: "/elsewhere/claude", env: {} } });
api.fs = {
...api.fs,
stat: (path) =>
Promise.reject(Object.assign(new Error(`PermissionNotGranted: ${path}`), { name: "PermissionNotGranted" })),
};
for (const call of [
() => runEffect(agent.listSessions({ workspace: "/w" })),
() => runEffect(agent.readSession({ workspace: "/w", sessionId: "s" })),
]) {
await assert.rejects(call(), /the personal account's sessions in \/elsewhere\/claude.*Settings > Claude Code/);
}
});
test("sign-in and sign-out run the CLI for the account's directory", async () => {
const home = tempDir();
const { agent, spawns } = harness({
instance: { id: "work", name: null, configDir: home, env: { ANTHROPIC_BASE_URL: "https://x" } },
});
await runEffect(agent.authenticate());
await runEffect(agent.logout());
const auth = spawns.filter((spawn) => spawn.args[0] === "auth");
assert.deepEqual(
auth.map((spawn) => spawn.args),
[
["auth", "login"],
["auth", "logout"],
],
);
assert.deepEqual(auth[0].options.env, { ANTHROPIC_BASE_URL: "https://x", CLAUDE_CONFIG_DIR: home });
});
test("the host's option changes reach the running process", async () => {
const { agent, api } = harness();
const { sessionId } = await runEffect(agent.createSession({ workspace: "/w", options: {} }));
const { options } = await runEffect(agent.setOption({ sessionId, optionId: "permission_mode", value: "full" }));
assert.equal(required(options.find((option) => option.id === "permission_mode")).value, "full");
const peer = api.peers.find((peer) => peer.args.includes(sessionId));
await settle();
assert.ok(
required(peer)
.received.map((message) => writtenSchema.parse(message))
.some(
(message) => message.request?.subtype === "set_permission_mode" && message.request.mode === "bypassPermissions",
),
);
await runEffect(agent.shutdown());
});
test("concurrent catalog callers share one scoped probe", async () => {
const { agent, api, spawns } = harness();
const [first, second] = await Promise.all([runEffect(agent.loadCatalog("/w")), runEffect(agent.loadCatalog("/w"))]);
assert.equal(first, second);
assert.equal(sessionSpawns(spawns).length, 1);
assert.ok(api.peers.every((peer) => peer.status !== null));
});
test("closing the provider scope drains final output and stops its session", async () => {
const { agent, api, events } = harness({ reply: () => {} });
const { sessionId } = await runEffect(agent.createSession({ workspace: "/w" }));
const { runId } = await runEffect(agent.prompt({ sessionId, input: { blocks: [{ type: "text", text: "Wait" }] } }));
const peer = required(api.peers.find((peer) => peer.args.includes(sessionId)));
peer.stdout.push("{broken\n");
peer.send({ type: "assistant", message: { id: "last", content: [{ type: "text", text: "final output" }] } });
await runEffect(Scope.close(resources().scope, Exit.void));
assert.ok(peer.stdinClosed);
assert.ok(peer.kills.includes("SIGTERM"));
assert.ok(peer.status !== null);
assert.equal(required(agent.sessions.get(sessionId)).proc, null);
assert.ok(events.some((event) => event.event === "text_delta" && event.text === "final output"));
assert.equal(events.filter((event) => event.event === "run_finished" && event.runId === runId).length, 1);
});Versions
| Version | Published | Plugin API | Size | Permissions | Status |
|---|---|---|---|---|---|
| 0.2.0latest | Oct 5, 2026 | >=2 <3 | 128.7 KB | 4 permissions | Listed |
No comments yet.