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

  • Provide agents agents.provideMediumAdds agents to the app.Provide the Claude Code agent and pass its tool calls to plugin tools
  • Run named programs processMediumStarts the listed programs.Run the Claude Code CLI (sessions, sign-in, `claude update`), and ask the npm installation that owns it about a newer versionPrograms: claudenpm
  • Read files fs.readMediumReads files in the listed places.Read Claude Code's sessions, settings and skills (in ~/.claude, other accounts' ~/.claude-* folders, the shared skills and the folder you choose in Settings), the project's .claude folder, the administrator's skill policy, and once, what the previous provider keptPlaces: ~/.claude/**~/.claude*/**~/.agents/skills/**the open workspaceits own data folder/Library/Application Support/ClaudeCode/managed-settings.json/etc/claude-code/managed-settings.json${settings.configDir}
  • Environment variables envMediumReads the listed environment variables.Find Claude Code's configuration directory, and the launch settings you set in CLAUDE_* variables (extra arguments, MCP servers, setting sources, appended system prompt, extra folders)Variables: HOMECLAUDE_CONFIG_DIRCLAUDE_EXTRA_ARGSCLAUDE_MCP_CONFIGCLAUDE_STRICT_MCP_CONFIGCLAUDE_SETTING_SOURCESCLAUDE_APPEND_SYSTEM_PROMPTCLAUDE_ADD_DIRS

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

VersionPublishedPlugin APISizePermissionsStatus
0.2.0latestOct 5, 2026>=2 <3128.7 KB4 permissionsListed

Reviews and comments

0 threads · 0 reviews

No comments yet.