Official

opencode

OpenCode agent provider: runs opencode serve and talks to it over HTTP and server-sent events.

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/opencode@0.2.0

Permissions in 0.2.0

  • Read files fs.readMediumReads files in the listed places.Read, once, the servers the previous OpenCode provider keptPlaces: its own data folder
  • Provide agents agents.provideMediumAdds agents to the app.Provide the OpenCode agent, and serve plugin tools to it through the host's loopback MCP server
  • Run named programs processMediumStarts the listed programs.Run the OpenCode server (`opencode serve`, or the binary you choose in Settings), read its catalog from the command line when the server cannot answer, upgrade it (`opencode upgrade`), and ask or tell the npm installation that owns it about a newer versionPrograms: opencodenpm${settings.binaryPath}
  • Network access netMediumConnects to the listed hosts.Talk to the OpenCode server it starts on this computer, at the port it picks for each launch, and to the external server you choose in SettingsHosts: localhost:*${settings.serverUrl}
  • Environment variables envMediumReads the listed environment variables.Expand ~ in a configured binary, and read the OpenCode settings you set in the environment: the binary to run, an external server's address, and the server's user name and passwordVariables: HOMEOPENCODE_PATHOPENCODE_SERVER_URLOPENCODE_SERVER_USERNAMEOPENCODE_SERVER_PASSWORDCONVERGENCE_OPENCODE_SERVER_PASSWORD

Files

agent.test.ts51.4 KB
// Scripted scenarios for the opencode conversion: the agent against a
// scripted server behind `api.net.fetch`, with the same assertions as the
// JavaScript suite.
import { test } from "node:test";
import assert from "node:assert/strict";
import { fieldAnswer } from "./agent.ts";
import { defaultInstance } from "./instances.ts";
import { questionId } from "./map.ts";
import { permissionMode } from "../sdk/agent.ts";
import { fakeApi, settle } from "../sdk/testing.ts";
import type { FakeOptions, HostScript } from "../sdk/testing.ts";
import { BASE, FakeOpenCode, apiFor, collect, sse, testAgent } from "./testing.test.ts";
import type { Event } from "./types.ts";
import { obj } from "./map.ts";

type Keys<T> = T extends unknown ? keyof T : never;
type Field<T, K> = T extends unknown ? (K extends keyof T ? T[K] : never) : never;
type ObservedEvent = Event & { [K in Keys<Event>]?: Field<Event, K> };

/// An agent on the fake server, as a server the user runs (so nothing is
/// started), with its events kept in `emitted`.
function agentOn(
  server: FakeOpenCode,
  {
    onHost = () => ({}),
    instance = {},
    onFetch,
  }: {
    onHost?: HostScript;
    instance?: Partial<import("./instances.ts").Instance>;
    onFetch?: FakeOptions["onFetch"];
  } = {},
) {
  const api = onFetch ? fakeApi({ onHost, onFetch }) : apiFor(server, { onHost });
  const emitted: ObservedEvent[] = [];
  const agent = testAgent(api, {}, { ...defaultInstance(), serverUrl: BASE, ...instance });
  agent.emit = (event) => {
    emitted.push(event);
  };
  return { agent, api, emitted };
}

const tasks = (events: ObservedEvent[]) => events.filter((event) => event.event === "task");

function present<T>(value: T | null | undefined): T {
  assert.ok(value != null);
  return value;
}

test("a dropped event stream reconnects and withdraws what the server forgot", async () => {
  const asked = {
    type: "permission.asked",
    properties: {
      id: "per_1",
      sessionID: "ses_1",
      permission: "bash",
      patterns: ["echo hi"],
      metadata: { command: "echo hi" },
      always: [],
    },
  };
  // First connection: one approval, then the stream drops.
  const server = new FakeOpenCode({ events: [sse(asked), ""] });
  const { agent, emitted } = agentOn(server);
  // The run is there before the stream opens, or nothing would fail.
  agent.runs.set("ses_1", { id: "run_1", error: null });
  await agent.resumeSession({ sessionId: "ses_1", workspace: "/w", options: {} });
  const seen = await collect(emitted, (event) => event.event === "approval_resolved", 5000);
  assert.equal(seen[0]?.event, "approval", `the approval first: ${JSON.stringify(seen)}`);
  assert.equal(seen[0]?.id, "per_1");
  assert.equal(seen[0]?.runId, "run_1");
  assert.ok(
    seen.some((event) => event.event === "approval_resolved" && event.id === "per_1"),
    "the approval the server forgot was withdrawn",
  );
  assert.ok(!seen.some((event) => event.event === "run_finished"), "a reconnect must not end the run");
  assert.ok(server.connections >= 2, "the stream reconnected");
  assert.ok(agent.runs.has("ses_1"));
  await agent.shutdown();
});

test("a run whose idle event was lost while the stream was down ends after the reconnect", async () => {
  // The stream drops; meanwhile ses_1's turn ended, ses_2 still works.
  const server = new FakeOpenCode({ events: ["", ""], status: { ses_2: { type: "busy" } } });
  const { agent, emitted } = agentOn(server);
  agent.runs.set("ses_1", { id: "run_1", error: null, startedAt: 0 });
  agent.runs.set("ses_2", { id: "run_2", error: null, startedAt: 0 });
  await agent.resumeSession({ sessionId: "ses_2", workspace: "/w", options: {} });
  await agent.resumeSession({ sessionId: "ses_1", workspace: "/w", options: {} });
  const seen = await collect(emitted, (event) => event.event === "run_finished", 5000);
  const finished = seen.find((event) => event.event === "run_finished");
  assert.deepEqual(
    [finished?.sessionId, finished?.runId, finished?.outcome],
    ["ses_1", "run_1", { status: "completed" }],
  );
  assert.ok(agent.runs.has("ses_2"), "a busy session keeps its run");
  const call = server.calls.find((call) => call.path === "/session/status");
  assert.equal(call?.query.directory, "/w");
  await agent.shutdown();
});

test("subagent events are reported against the chat that started them", async () => {
  const events = [
    {
      type: "session.created",
      properties: {
        sessionID: "ses_child",
        info: { id: "ses_child", parentID: "ses_1", title: "Search the tests", agent: "explore" },
      },
    },
    {
      type: "message.part.delta",
      properties: { sessionID: "ses_child", messageID: "msg_c", partID: "prt_c", field: "text", delta: "found it" },
    },
    { type: "session.idle", properties: { sessionID: "ses_child" } },
  ];
  const server = new FakeOpenCode({ events: [events.map(sse).join("")] });
  const { agent, emitted } = agentOn(server);
  agent.runs.set("ses_1", { id: "run_1", error: null });
  await agent.resumeSession({ sessionId: "ses_1", workspace: "/w", options: {} });
  const seen = await collect(emitted, (event) => event.event === "task" && event.status === "completed");
  const started = tasks(seen).find((task) => task.status === "running");
  assert.equal(present(started).sessionId, "ses_1", "the task belongs to the chat");
  assert.equal(present(started).id, "ses_child");
  assert.equal(present(started).title, "Search the tests");
  assert.equal(present(started).name, "explore");
  const text = seen.find((event) => event.event === "text_delta");
  assert.equal(present(text).sessionId, "ses_1");
  assert.equal(present(text).taskId, "ses_child", "the text is the subagent's");
  assert.equal(present(text).runId, "run_1");
  // The subagent finishing is not the chat finishing.
  assert.ok(!seen.some((event) => event.event === "run_finished"));
  assert.ok(agent.runs.has("ses_1"));
  await agent.shutdown();
});

test("the context window follows the model that ran", async () => {
  const ran = {
    type: "message.updated",
    properties: {
      info: { id: "msg_a", role: "assistant", sessionID: "ses_1", providerID: "opencode", modelID: "big-pickle" },
    },
  };
  const step = {
    type: "message.part.updated",
    properties: {
      sessionID: "ses_1",
      part: {
        type: "step-finish",
        id: "prt_s",
        messageID: "msg_a",
        sessionID: "ses_1",
        tokens: { total: 4000, input: 3900, output: 100 },
      },
    },
  };
  const server = new FakeOpenCode({ events: [sse(ran) + sse(step)] });
  const { agent, emitted } = agentOn(server);
  agent.catalogs.set("/w", {
    providers: [
      {
        id: "opencode",
        name: "OpenCode",
        models: { "big-pickle": { id: "big-pickle", name: "Big Pickle", limit: { context: 200000 } } },
      },
    ],
  });
  await agent.resumeSession({ sessionId: "ses_1", workspace: "/w", options: {} });
  const seen = await collect(emitted, (event) => event.event === "usage");
  const usage = present(seen.at(-1));
  assert.equal(usage.event, "usage");
  assert.equal(usage.usedTokens, 4000);
  assert.equal(usage.contextWindow, 200000);
  await agent.shutdown();
});

test("a subagent is linked, followed and settled live", async () => {
  const events = [
    {
      type: "session.created",
      properties: {
        sessionID: "ses_child",
        info: { id: "ses_child", parentID: "ses_1", title: "Find the parser (@explore subagent)", agent: "explore" },
      },
    },
    {
      type: "message.part.updated",
      properties: {
        sessionID: "ses_1",
        part: {
          type: "tool",
          id: "prt_task",
          messageID: "msg_a",
          sessionID: "ses_1",
          callID: "call_t",
          tool: "task",
          state: {
            status: "running",
            title: "Find the parser",
            input: { description: "Find the parser", prompt: "Look for the parser.", subagent_type: "explore" },
            metadata: { sessionId: "ses_child", model: { providerID: "opencode", modelID: "big-pickle" } },
            time: { start: 1700000000000 },
          },
        },
      },
    },
    {
      type: "message.part.updated",
      properties: {
        sessionID: "ses_child",
        part: {
          type: "tool",
          id: "prt_grep",
          messageID: "msg_c",
          sessionID: "ses_child",
          callID: "c1",
          tool: "grep",
          state: { status: "running", input: { pattern: "parse" }, title: "parse" },
        },
      },
    },
    {
      type: "message.part.updated",
      properties: {
        sessionID: "ses_child",
        part: {
          type: "step-finish",
          id: "prt_step",
          messageID: "msg_c",
          sessionID: "ses_child",
          tokens: { total: 4000, input: 3900, output: 100 },
        },
      },
    },
    {
      type: "session.created",
      properties: {
        sessionID: "ses_grand",
        info: { id: "ses_grand", parentID: "ses_child", title: "Read it (@general subagent)", agent: "general" },
      },
    },
    {
      type: "message.part.delta",
      properties: {
        sessionID: "ses_child",
        messageID: "msg_c",
        partID: "prt_txt",
        field: "text",
        delta: "It is in src/parse.rs.",
      },
    },
    { type: "session.idle", properties: { sessionID: "ses_child" } },
  ];
  const server = new FakeOpenCode({ events: [events.map(sse).join("")] });
  const { agent, emitted } = agentOn(server);
  agent.runs.set("ses_1", { id: "run_1", error: null });
  await agent.resumeSession({ sessionId: "ses_1", workspace: "/w", options: {} });
  const seen = await collect(
    emitted,
    (event) => event.event === "task" && event.id === "ses_child" && event.status === "completed",
  );
  const all = tasks(seen);
  assert.ok(
    all.every((task) => task.title !== "task"),
    "a tool name is never a title",
  );
  const linked = all.find((task) => task.id === "ses_child" && task.toolCallId);
  assert.equal(present(linked).toolCallId, "prt_task");
  assert.equal(present(linked).title, "Find the parser");
  assert.equal(present(linked).prompt, "Look for the parser.");
  assert.equal(present(linked).model, "big-pickle");
  assert.equal(present(linked).name, "explore");
  assert.ok(
    all.some((task) => task.id === "ses_child" && task.activity === "parse"),
    "the running tool showed as activity",
  );
  assert.ok(!seen.some((event) => event.event === "usage"), "a subagent's usage never reaches the chat's meter");
  const grand = all.find((task) => task.id === "ses_grand");
  assert.equal(present(grand).parentTaskId, "ses_child");
  assert.equal(present(grand).title, "Read it");
  const done = present(all.at(-1));
  assert.equal(done.status, "completed");
  assert.equal(done.summary, "It is in src/parse.rs.", "the answer is its last text");
  assert.equal(done.usage?.usedTokens, 4000);
  assert.equal(done.toolUses, 1);
  assert.ok(done.endedAt);
  assert.equal(done.activity, undefined);
  const tool = seen.find((event) => event.event === "tool_call_started" && event.id === "prt_grep");
  assert.equal(present(tool).taskId, "ses_child");
  assert.equal(present(tool).sessionId, "ses_1");
  await agent.shutdown();
});

test("one subagent can be stopped while the chat's run goes on", async () => {
  const created = {
    type: "session.created",
    properties: { sessionID: "ses_child", info: { id: "ses_child", parentID: "ses_1", agent: "explore" } },
  };
  const server = new FakeOpenCode({ events: [sse(created)] });
  const { agent, emitted } = agentOn(server);
  await agent.resumeSession({ sessionId: "ses_1", workspace: "/w", options: {} });
  agent.runs.set("ses_1", { id: "run_1", error: null });
  await collect(emitted, (event) => event.event === "task");
  await assert.rejects(agent.cancelTask({ sessionId: "ses_1", taskId: "ses_other" }), /no opencode subagent/);
  await agent.cancelTask({ sessionId: "ses_1", taskId: "ses_child" });
  assert.ok(tasks(emitted).some((task) => task.status === "cancelled"));
  const aborted = server.calls.filter((call) => call.path.endsWith("/abort")).map((call) => call.path);
  assert.deepEqual(aborted, ["/session/ses_child/abort"]);
  assert.ok(agent.runs.has("ses_1"), "the chat's run goes on");
  // The abort error it reports afterwards does not turn it into a failure.
  await agent.apply({ type: "failed", session: "ses_child", message: "Aborted" }, "/w");
  assert.equal(agent.children.get("ses_child")?.status, "cancelled");
  assert.ok(!emitted.some((event) => event.event === "notice"), "a stop is not news");
  await agent.shutdown();
});

test("cancelling a chat stops its subagents, one the plugin never saw start too", async () => {
  const created = {
    type: "session.created",
    properties: { sessionID: "ses_child", info: { id: "ses_child", parentID: "ses_1", agent: "explore" } },
  };
  const server = new FakeOpenCode({
    events: [sse(created)],
    routes: (call) =>
      call.path === "/session/ses_1/children" ? { body: [{ id: "ses_unseen", parentID: "ses_1" }] } : undefined,
  });
  const { agent, emitted } = agentOn(server);
  await agent.resumeSession({ sessionId: "ses_1", workspace: "/w", options: {} });
  agent.runs.set("ses_1", { id: "run_1", error: null });
  await collect(emitted, (event) => event.event === "task");
  await agent.cancel({ sessionId: "ses_1" });
  const aborted = server.calls.filter((call) => call.path.endsWith("/abort")).map((call) => call.path);
  assert.ok(aborted.includes("/session/ses_1/abort"), "the chat was aborted");
  assert.ok(aborted.includes("/session/ses_child/abort"), "the subagent was stopped");
  assert.ok(aborted.includes("/session/ses_unseen/abort"), "the server was asked for the children too");
  const finished = emitted.filter((event) => event.event === "run_finished");
  assert.deepEqual(
    finished.map((event) => event.outcome?.status),
    ["cancelled"],
  );
  await agent.shutdown();
});

test("the permission ruleset is pushed on resume and on a mode change, never twice", async () => {
  const server = new FakeOpenCode();
  const { agent } = agentOn(server);
  await agent.resumeSession({ sessionId: "ses_1", workspace: "/w", options: { permission_mode: "auto_edits" } });
  const pushed = await server.waitFor((call) => call.method === "PATCH" && call.path === "/session/ses_1");
  const body = obj(pushed.body);
  const permission = Array.isArray(body["permission"]) ? body["permission"] : [];
  const edit = permission.find((rule) => obj(rule)["permission"] === "edit");
  assert.equal(obj(edit)["action"], "allow", "auto-accept edits reaches the session");
  assert.ok(permission.some((rule) => obj(rule)["permission"] === "read" && obj(rule)["pattern"] === "*.env"));
  server.calls.length = 0;
  // Rules a session already runs are left alone.
  const { ruleset } = await import("./permission.ts");
  const stored = ruleset("supervised");
  await agent.pushRules("ses_1", "/w", "supervised", stored);
  assert.ok(!server.calls.some((call) => call.method === "PATCH"));
  await agent.pushRules("ses_1", "/w", "full", stored);
  assert.ok(
    server.calls.some((call) => call.method === "PATCH"),
    "a mode change is pushed",
  );
  await agent.shutdown();
});

test("full access answers permission requests without asking the user, once", async () => {
  const asked = {
    type: "permission.asked",
    properties: {
      id: "per_1",
      sessionID: "ses_1",
      permission: "bash",
      patterns: ["echo hi"],
      metadata: { command: "echo hi" },
      always: ["echo *"],
    },
  };
  const server = new FakeOpenCode({ events: [sse(asked)] });
  const { agent, emitted } = agentOn(server);
  await agent.resumeSession({ sessionId: "ses_1", workspace: "/w", options: { permission_mode: permissionMode.FULL } });
  const reply = await server.waitFor((call) => call.path === "/permission/per_1/reply");
  assert.deepEqual(reply.body, { reply: "once" }, "an `always` grant would widen other chats on this folder");
  await settle();
  assert.ok(!emitted.some((event) => event.event === "approval"), "full access showed no card");
  await agent.shutdown();
});

test("question answers follow their id, not their position", () => {
  const fields = [
    { id: questionId(0, "Database"), header: "Database", question: "Which database?", kind: "select" },
    { id: questionId(1, "Migrate now?"), header: "Migrate now?", question: "Run the migration now?", kind: "boolean" },
  ];
  assert.deepEqual(
    fields.map((field) => field.id),
    ["question-0-database", "question-1-migrate-now"],
  );
  const answer = (values: Record<string, unknown>) => fields.map((field) => fieldAnswer(field, values));
  assert.deepEqual(answer({ "question-1-migrate-now": true, "question-0-database": "Postgres" }), [
    ["Postgres"],
    ["true"],
  ]);
  // An answer keyed the way OpenCode names the question still lands.
  assert.deepEqual(answer({ Database: ["SQLite"], "Run the migration now?": false }), [["SQLite"], ["false"]]);
  assert.deepEqual(answer({}), [[], []]);
});

test("a question round trip: asked, answered in the server's order, resolved", async () => {
  const asked = {
    type: "question.asked",
    properties: {
      id: "que_1",
      sessionID: "ses_1",
      questions: [
        { question: "Which database?", header: "Database", options: [{ label: "Postgres" }, { label: "SQLite" }] },
        { question: "Why?", header: "", options: [], custom: true },
      ],
    },
  };
  const server = new FakeOpenCode({ events: [sse(asked)] });
  const { agent, emitted } = agentOn(server);
  await agent.resumeSession({ sessionId: "ses_1", workspace: "/w", options: {} });
  const [question] = (await collect(emitted, (event) => event.event === "question")).filter(
    (event) => event.event === "question",
  );
  assert.deepEqual(
    present(question).fields?.map((field) => field.id),
    ["question-0-database", "question-1"],
  );
  await agent.respondToQuestion({
    questionId: "que_1",
    answer: { values: { "question-1": "speed", "question-0-database": "SQLite" }, cancelled: false },
  });
  const reply = await server.waitFor((call) => call.path === "/question/que_1/reply");
  assert.deepEqual(reply.body, { answers: [["SQLite"], ["speed"]] });
  await assert.rejects(agent.respondToQuestion({ questionId: "que_1", answer: {} }), /no pending opencode question/);
  await agent.shutdown();
});

test("a prompt carries the host's message id, the chosen model, mode, variant and the plugin instructions", async () => {
  const server = new FakeOpenCode({
    routes: (call) => (call.path === "/command" ? { body: [{ name: "review" }] } : undefined),
  });
  const { agent, emitted } = agentOn(server);
  const { sessionId } = await agent.createSession({
    workspace: "/w",
    options: { model: "opencode/big-pickle", agent: "plan", variant: "high", permission_mode: "supervised" },
    instructions: "## Rules from the x plugin\n\nBe brief.",
  });
  assert.equal(server.connections, 1, "the folder's event stream is open before the session is handed out");
  const created = server.calls.find((call) => call.method === "POST" && call.path === "/session");
  assert.deepEqual(present(created).query, { directory: "/w" });
  const createdBody = obj(present(created).body);
  assert.equal(createdBody["agent"], "plan");
  assert.deepEqual(createdBody["model"], { providerID: "opencode", id: "big-pickle" });
  const createdPermission = Array.isArray(createdBody["permission"]) ? createdBody["permission"] : [];
  assert.equal(obj(createdPermission[0])["action"], "ask");
  const { runId } = await agent.prompt({
    sessionId,
    input: { blocks: [{ type: "text", text: "hello" }], itemId: "0f1e-22" },
  });
  assert.match(runId, /^opencode-run-/);
  const sent = server.calls.find((call) => call.path === `/session/${sessionId}/prompt_async`);
  assert.deepEqual(present(sent).body, {
    parts: [{ type: "text", text: "hello" }],
    messageID: "msg_0f1e-22",
    agent: "plan",
    model: { providerID: "opencode", modelID: "big-pickle" },
    variant: "high",
    system: "## Rules from the x plugin\n\nBe brief.",
  });
  // A known command goes to the command endpoint instead.
  await agent.finish(sessionId, { status: "completed" });
  await agent.prompt({ sessionId, input: { blocks: [{ type: "text", text: "/review the parser" }] } });
  const command = await server.waitFor((call) => call.path === `/session/${sessionId}/command`);
  assert.deepEqual(command.body, {
    command: "review",
    arguments: "the parser",
    agent: "plan",
    model: "opencode/big-pickle",
    variant: "high",
  });
  // A model change drops the variant.
  const { options } = await agent
    .setOption({ sessionId, optionId: "model", value: "opencode/other" })
    .catch(() => ({ options: null }));
  assert.equal(agent.sessions.get(sessionId)?.options.variant, undefined);
  assert.equal(options, null, "the fake server has no catalog, which is an error, never an invented list");
  assert.ok(emitted.every((event) => event.event !== "run_finished" || event.outcome?.status === "completed"));
  await agent.shutdown();
});

test("a failed prompt leaves no run behind", async () => {
  const server = new FakeOpenCode({
    routes: (call) => (call.path.endsWith("/prompt_async") ? { status: 400, body: "bad model" } : undefined),
  });
  const { agent } = agentOn(server);
  const { sessionId } = await agent.createSession({ workspace: "/w", options: {} });
  await assert.rejects(agent.prompt({ sessionId, input: { blocks: [{ type: "text", text: "x" }] } }), /400: bad model/);
  assert.equal(agent.runs.size, 0);
  await assert.rejects(
    agent.prompt({ sessionId, input: { blocks: [{ type: "image", mimeType: "application/zip", data: "AA" }] } }),
    /cannot attach/,
  );
  assert.equal(agent.runs.size, 0);
  await agent.shutdown();
});

const delivery = { inputId: "input_1", attemptId: "attempt_1", intent: "send" as const };
const userMessage = (sessionID: string, id: string) => ({
  type: "message.updated",
  properties: { info: { id, sessionID, role: "user" } },
});
const assistantMessage = (sessionID: string, parentID: string) => ({
  type: "message.updated",
  properties: { info: { id: "msg_answer", sessionID, role: "assistant", parentID } },
});

test("durable input is consumed once by its native assistant parent, not its write or user echo", async () => {
  const server = new FakeOpenCode();
  const { agent, emitted } = agentOn(server);
  await agent.resumeSession({ sessionId: "ses_1", workspace: "/w" });
  const reply = await agent.prompt({
    sessionId: "ses_1",
    input: { itemId: "host_1", delivery, blocks: [{ type: "text", text: "hello" }] },
  });
  assert.deepEqual(obj(reply)["receipt"], { evidence: "local_write", nativeInputId: "msg_host_1" });
  assert.equal(emitted.filter((event) => event.event === "input_consumed").length, 0);
  server.push(
    userMessage("ses_1", "msg_host_1"),
    assistantMessage("ses_other", "msg_host_1"),
    assistantMessage("ses_1", "msg_unrelated"),
    { type: "session.updated", properties: { sessionID: "ses_1", info: { id: "ses_1", title: "Echo received" } } },
  );
  await collect(emitted, (event) => event.event === "session_info");
  assert.equal(emitted.filter((event) => event.event === "input_consumed").length, 0);
  server.push(assistantMessage("ses_1", "msg_host_1"), assistantMessage("ses_1", "msg_host_1"), {
    type: "session.idle",
    properties: { sessionID: "ses_1" },
  });
  await collect(emitted, (event) => event.event === "run_finished");
  assert.deepEqual(
    emitted.filter((event) => event.event === "input_consumed"),
    [
      {
        sessionId: "ses_1",
        runId: reply.runId,
        event: "input_consumed",
        inputId: "input_1",
        nativeInputId: "msg_host_1",
      },
    ],
  );
  assert.equal(emitted.at(-1)?.event, "run_finished", "pickup is visible before completion releases FIFO");
  await agent.shutdown();
});

test("idle success and cancellation without parent evidence never fabricate consumption", async () => {
  const server = new FakeOpenCode();
  const { agent, emitted } = agentOn(server);
  await agent.resumeSession({ sessionId: "ses_1", workspace: "/w" });
  await agent.prompt({ sessionId: "ses_1", input: { itemId: "first", delivery, blocks: [] } });
  server.push(userMessage("ses_1", "msg_first"), { type: "session.idle", properties: { sessionID: "ses_1" } });
  await collect(emitted, (event) => event.event === "run_finished");
  assert.equal(emitted.filter((event) => event.event === "input_consumed").length, 0);
  await agent.prompt({
    sessionId: "ses_1",
    input: { itemId: "second", delivery: { ...delivery, inputId: "input_2", attemptId: "attempt_2" }, blocks: [] },
  });
  await agent.cancel({ sessionId: "ses_1" });
  server.push(assistantMessage("ses_1", "msg_second"));
  await settle(10);
  assert.equal(emitted.filter((event) => event.event === "input_consumed").length, 0);
  assert.deepEqual(
    emitted.filter((event) => event.event === "run_finished").map((event) => event.outcome?.status),
    ["completed", "cancelled"],
  );
  assert.equal(agent.inputRuns.size, 1, "the completed but unproven input remains reconcilable");
  await agent.shutdown();
  assert.equal(agent.inputRuns.size, 0);
});

test("a synchronous completed command resolves its durable input and ends once without an idle event", async () => {
  const server = new FakeOpenCode({
    routes: (call) => {
      if (call.path === "/command") return { body: [{ name: "review" }] };
      if (call.path.endsWith("/command"))
        return {
          body: {
            info: {
              id: "msg_answer",
              sessionID: "ses_1",
              role: "assistant",
              parentID: "msg_host_1",
              time: { completed: 1 },
            },
            parts: [],
          },
        };
      return undefined;
    },
  });
  const { agent, emitted } = agentOn(server);
  await agent.resumeSession({ sessionId: "ses_1", workspace: "/w" });
  const { runId } = await agent.prompt({
    sessionId: "ses_1",
    input: { itemId: "host_1", delivery, blocks: [{ type: "text", text: "/review code" }] },
  });
  await collect(emitted, (event) => event.event === "run_finished");
  assert.equal(
    obj(present(server.calls.find((call) => call.path === "/session/ses_1/command")).body)["messageID"],
    "msg_host_1",
  );
  assert.deepEqual(
    emitted.filter((event) => event.event === "input_consumed" || event.event === "run_finished"),
    [
      { sessionId: "ses_1", runId, event: "input_consumed", inputId: "input_1", nativeInputId: "msg_host_1" },
      { sessionId: "ses_1", runId, event: "run_finished", outcome: { status: "completed" } },
    ],
  );
  assert.equal(agent.runs.size, 0);
  await agent.shutdown();
});

test("a completed command racing Stop cannot consume input or finish a successor", async () => {
  const commandEntered = Promise.withResolvers<void>();
  const commandRelease = Promise.withResolvers<void>();
  const abortEntered = Promise.withResolvers<void>();
  const abortRelease = Promise.withResolvers<void>();
  const server = new FakeOpenCode({
    routes: (call) => {
      if (call.path === "/command") return { body: [{ name: "review" }] };
      if (call.path === "/session/ses_1/command")
        return {
          body: {
            info: {
              id: "msg_answer",
              role: "assistant",
              sessionID: "ses_1",
              parentID: "msg_first",
              time: { completed: 1 },
            },
            parts: [],
          },
        };
      return undefined;
    },
  });
  const { agent, emitted } = agentOn(server, {
    onFetch: async (url, init) => {
      const response = server.handle(url, init);
      const path = new URL(url).pathname;
      if (path === "/session/ses_1/command") {
        commandEntered.resolve();
        await commandRelease.promise;
      }
      if (path === "/session/ses_1/abort") {
        abortEntered.resolve();
        await abortRelease.promise;
      }
      return response;
    },
  });
  await agent.resumeSession({ sessionId: "ses_1", workspace: "/w" });
  const first = await agent.prompt({
    sessionId: "ses_1",
    input: { itemId: "first", delivery, blocks: [{ type: "text", text: "/review" }] },
  });
  await commandEntered.promise;
  const stopping = agent.cancel({ sessionId: "ses_1" });
  await abortEntered.promise;
  commandRelease.resolve();
  await collect(emitted, (event) => event.event === "run_finished");
  assert.equal(emitted.filter((event) => event.event === "input_consumed").length, 0);
  assert.deepEqual(
    emitted.filter((event) => event.event === "run_finished").map((event) => [event.runId, event.outcome?.status]),
    [[first.runId, "cancelled"]],
  );
  const next = await agent.prompt({ sessionId: "ses_1", input: { itemId: "next", blocks: [] } });
  abortRelease.resolve();
  await stopping;
  assert.equal(agent.runs.get("ses_1")?.id, next.runId);
  assert.equal(emitted.filter((event) => event.event === "run_finished").length, 1);
  await agent.shutdown();
});

test("durable input without a user-row id names its delivery attempt", async () => {
  const server = new FakeOpenCode();
  const { agent, emitted } = agentOn(server);
  await agent.resumeSession({ sessionId: "ses_1", workspace: "/w" });
  const { runId, receipt } = await agent.prompt({ sessionId: "ses_1", input: { delivery, blocks: [] } });
  assert.deepEqual(receipt, { evidence: "local_write", nativeInputId: "msg_attempt_1" });
  assert.equal(
    obj(present(server.calls.find((call) => call.path.endsWith("/prompt_async"))).body)["messageID"],
    "msg_attempt_1",
  );
  server.push(assistantMessage("ses_1", "msg_attempt_1"));
  await collect(emitted, (event) => event.event === "input_consumed");
  assert.equal(emitted.at(-1)?.runId, runId);
  assert.equal(emitted.at(-1)?.inputId, "input_1");
  await agent.shutdown();
});

test("late native parent evidence reconciles a completed input without finishing its successor", async () => {
  const server = new FakeOpenCode();
  const { agent, emitted } = agentOn(server);
  await agent.resumeSession({ sessionId: "ses_1", workspace: "/w" });
  const first = await agent.prompt({ sessionId: "ses_1", input: { itemId: "first", delivery, blocks: [] } });
  await agent.finish("ses_1", { status: "completed" });
  const next = await agent.prompt({
    sessionId: "ses_1",
    input: { itemId: "next", delivery: { ...delivery, inputId: "input_2", attemptId: "attempt_2" }, blocks: [] },
  });
  server.push(assistantMessage("ses_1", "msg_first"));
  await collect(emitted, (event) => event.event === "input_consumed");
  assert.equal(emitted.at(-1)?.runId, first.runId);
  assert.equal(emitted.at(-1)?.inputId, "input_1");
  assert.equal(agent.runs.get("ses_1")?.id, next.runId);
  assert.equal(emitted.filter((event) => event.event === "run_finished").length, 1);
  await agent.shutdown();
});

for (const failed of [false, true])
  test(`native pickup before an async ${failed ? "failed" : "successful"} reply stays correlated`, async () => {
    const entered = Promise.withResolvers<void>();
    const release = Promise.withResolvers<void>();
    const server = new FakeOpenCode();
    let delayed = true;
    const { agent, emitted } = agentOn(server, {
      onFetch: async (url, init) => {
        const response = server.handle(url, init);
        if (url.includes("/prompt_async") && delayed) {
          delayed = false;
          entered.resolve();
          await release.promise;
          if (failed) throw new Error("connection lost after sending");
        }
        return response;
      },
    });
    await agent.resumeSession({ sessionId: "ses_1", workspace: "/w" });
    const sending = agent.prompt({ sessionId: "ses_1", input: { itemId: "first", delivery, blocks: [] } });
    await entered.promise;
    const firstRun = present(agent.runs.get("ses_1")).id;
    server.push(assistantMessage("ses_1", "msg_first"), { type: "session.idle", properties: { sessionID: "ses_1" } });
    await collect(emitted, (event) => event.event === "run_finished");
    const next = await agent.prompt({ sessionId: "ses_1", input: { itemId: "next", blocks: [] } });
    release.resolve();
    if (failed) await assert.rejects(sending, /connection lost after sending/);
    else assert.equal((await sending).runId, firstRun);
    assert.equal(agent.runs.get("ses_1")?.id, next.runId, "late cleanup cannot delete a newer run");
    assert.equal(
      emitted.filter((event) => event.event === "input_rejected").length,
      0,
      "ambiguous transport is not rejection",
    );
    assert.deepEqual(
      emitted.filter((event) => event.event === "input_consumed").map((event) => [event.runId, event.inputId]),
      [[firstRun, "input_1"]],
    );
    await agent.shutdown();
  });

test("an ambiguous failed send can still be reconciled by a later native parent", async () => {
  const server = new FakeOpenCode();
  const { agent, emitted } = agentOn(server, {
    onFetch: (url, init) => {
      const response = server.handle(url, init);
      if (url.includes("/prompt_async")) throw new Error("lost response");
      return response;
    },
  });
  await agent.resumeSession({ sessionId: "ses_1", workspace: "/w" });
  await assert.rejects(
    agent.prompt({ sessionId: "ses_1", input: { itemId: "first", delivery, blocks: [] } }),
    /lost response/,
  );
  assert.equal(agent.runs.size, 0);
  assert.equal(
    emitted.filter((event) => event.event === "input_rejected" || event.event === "input_consumed").length,
    0,
  );
  const original = present(agent.inputRuns.get("ses_1/msg_first")).id;
  server.push(assistantMessage("ses_1", "msg_first"));
  await collect(emitted, (event) => event.event === "input_consumed");
  assert.equal(emitted.at(-1)?.runId, original);
  assert.equal(emitted.at(-1)?.inputId, "input_1");
  assert.equal(agent.runs.size, 0, "reconciliation never revives a run");
  await agent.shutdown();
});

for (const kind of ["success", "failure", "cancelled"] as const)
  test(`a late command ${kind} cannot finish or consume a successor`, async () => {
    const entered = Promise.withResolvers<void>();
    const release = Promise.withResolvers<void>();
    const server = new FakeOpenCode({
      routes: (call) => {
        if (call.path === "/command") return { body: [{ name: "review" }] };
        if (call.path === "/session/ses_1/command")
          return kind === "failure"
            ? { status: 500, body: "lost command response" }
            : {
                body: {
                  info: {
                    id: "msg_answer",
                    role: "assistant",
                    sessionID: "ses_1",
                    parentID: "msg_first",
                    time: { completed: 1 },
                  },
                  parts: [],
                },
              };
        return undefined;
      },
    });
    const { agent, emitted } = agentOn(server, {
      onFetch: async (url, init) => {
        const response = server.handle(url, init);
        if (new URL(url).pathname === "/session/ses_1/command") {
          entered.resolve();
          await release.promise;
        }
        return response;
      },
    });
    await agent.resumeSession({ sessionId: "ses_1", workspace: "/w" });
    const first = await agent.prompt({
      sessionId: "ses_1",
      input: { itemId: "first", delivery, blocks: [{ type: "text", text: "/review" }] },
    });
    await entered.promise;
    if (kind === "cancelled") await agent.cancel({ sessionId: "ses_1" });
    else await agent.finish("ses_1", { status: "completed" });
    const next = await agent.prompt({ sessionId: "ses_1", input: { itemId: "next", blocks: [] } });
    release.resolve();
    if (kind === "success") await collect(emitted, (event) => event.event === "input_consumed");
    else await settle(10);
    assert.equal(agent.runs.get("ses_1")?.id, next.runId);
    assert.deepEqual(
      emitted.filter((event) => event.event === "run_finished").map((event) => event.runId),
      [first.runId],
    );
    assert.deepEqual(
      emitted.filter((event) => event.event === "input_consumed").map((event) => [event.runId, event.inputId]),
      kind === "success" ? [[first.runId, "input_1"]] : [],
    );
    await agent.shutdown();
  });

for (const [name, extra] of Object.entries({
  unrelated: { parentID: "msg_other" },
  unfinished: { time: { created: 1 } },
  user: { role: "user" },
  foreign: { sessionID: "ses_other" },
  aborted: { error: { name: "MessageAbortedError" } },
}))
  test(`a ${name} command response is not consumption evidence`, async () => {
    const server = new FakeOpenCode({
      routes: (call) => {
        if (call.path === "/command") return { body: [{ name: "review" }] };
        if (call.path.endsWith("/command"))
          return {
            body: {
              info: {
                id: "msg_answer",
                role: "assistant",
                sessionID: "ses_1",
                parentID: "msg_first",
                time: { completed: 1 },
                ...extra,
              },
              parts: [],
            },
          };
        return undefined;
      },
    });
    const { agent, emitted } = agentOn(server);
    await agent.resumeSession({ sessionId: "ses_1", workspace: "/w" });
    await agent.prompt({
      sessionId: "ses_1",
      input: { itemId: "first", delivery, blocks: [{ type: "text", text: "/review" }] },
    });
    await server.waitFor((call) => call.path === "/session/ses_1/command");
    await settle(10);
    assert.equal(emitted.filter((event) => event.event === "input_consumed").length, 0);
    assert.ok(!emitted.some((event) => event.event === "run_finished" && event.outcome?.status === "completed"));
    await agent.shutdown();
  });

test("preparation reserves the run, rejects unsupported steering/queues and cleans up unsent input", async () => {
  const entered = Promise.withResolvers<void>();
  const release = Promise.withResolvers<void>();
  const server = new FakeOpenCode({ routes: (call) => (call.path === "/command" ? { body: [] } : undefined) });
  const { agent, emitted } = agentOn(server, {
    onFetch: async (url, init) => {
      const response = server.handle(url, init);
      if (new URL(url).pathname === "/command") {
        entered.resolve();
        await release.promise;
      }
      return response;
    },
  });
  await agent.resumeSession({ sessionId: "ses_1", workspace: "/w" });
  const preparing = agent.prompt({
    sessionId: "ses_1",
    input: {
      delivery,
      blocks: [
        { type: "text", text: "/unknown" },
        { type: "image", mimeType: "application/zip", data: "AA" },
      ],
    },
  });
  await entered.promise;
  const original = present(agent.runs.get("ses_1")).id;
  await assert.rejects(
    agent.prompt({
      sessionId: "ses_1",
      input: { delivery: { inputId: "steer", attemptId: "steer_attempt", intent: "steer" }, blocks: [] },
    }),
    /no proven native steering/,
  );
  await assert.rejects(
    agent.prompt({
      sessionId: "ses_1",
      input: { delivery: { inputId: "queue", attemptId: "queue_attempt", intent: "queue" }, blocks: [] },
    }),
    /held by the host/,
  );
  assert.equal(agent.runs.get("ses_1")?.id, original);
  release.resolve();
  await assert.rejects(preparing, /cannot attach/);
  assert.equal(agent.runs.size, 0);
  assert.equal(agent.inputRuns.size, 0);
  assert.equal(
    server.calls.filter(
      (call) =>
        call.path.endsWith("/prompt_async") || (call.path.startsWith("/session/") && call.path.endsWith("/command")),
    ).length,
    0,
  );
  assert.deepEqual(
    emitted.filter((event) => event.event === "input_rejected").map((event) => [event.inputId, event.attemptId]),
    [
      ["steer", "steer_attempt"],
      ["queue", "queue_attempt"],
      ["input_1", "attempt_1"],
    ],
  );
  assert.ok(!emitted.some((event) => event.event === "input_consumed" || event.event === "run_finished"));
  await agent.shutdown();
});

test("a preparation defect also clears the reserved run without native dispatch", async () => {
  const server = new FakeOpenCode();
  const { agent, emitted } = agentOn(server);
  await agent.resumeSession({ sessionId: "ses_1", workspace: "/w" });
  const block = { type: "text", text: "hello" };
  Object.defineProperty(block, "text", {
    get() {
      throw new Error("preparation defect");
    },
  });
  await assert.rejects(
    agent.prompt({ sessionId: "ses_1", input: { delivery, blocks: [block] } }),
    /preparation defect/,
  );
  assert.equal(agent.runs.size, 0);
  assert.equal(agent.inputRuns.size, 0);
  assert.equal(emitted.at(-1)?.event, "input_rejected");
  assert.equal(emitted.at(-1)?.attemptId, "attempt_1");
  assert.equal(server.calls.filter((call) => call.path.endsWith("/prompt_async")).length, 0);
  await agent.shutdown();
});

test("an idle session ends the run as completed, or failed when it reported an error", async () => {
  const server = new FakeOpenCode();
  const { agent, emitted } = agentOn(server);
  await agent.resumeSession({ sessionId: "ses_1", workspace: "/w", options: {} });
  const { runId } = await agent.prompt({ sessionId: "ses_1", input: { blocks: [{ type: "text", text: "x" }] } });
  server.push({
    type: "session.error",
    properties: { sessionID: "ses_1", error: { name: "APIError", data: { message: "rate limited" } } },
  });
  server.push({ type: "session.idle", properties: { sessionID: "ses_1" } });
  const seen = await collect(emitted, (event) => event.event === "run_finished");
  assert.deepEqual(
    seen.filter((event) => event.event === "notice").map((event) => [event.level, event.message]),
    [["error", "rate limited"]],
  );
  assert.deepEqual(seen.at(-1), {
    sessionId: "ses_1",
    runId,
    event: "run_finished",
    outcome: { status: "failed", message: "rate limited" },
  });
  // A second idle is a no-op: one run_finished per run.
  server.push({ type: "session.idle", properties: { sessionID: "ses_1" } });
  await settle(10);
  assert.equal(emitted.filter((event) => event.event === "run_finished").length, 1);
  await agent.shutdown();
});

test("plugin tools reach OpenCode through the host's loopback MCP server, once per folder and tool list", async () => {
  const hostCalls: { method: string; params: unknown }[] = [];
  const server = new FakeOpenCode();
  const { agent } = agentOn(server, {
    onHost: (method, params) => {
      hostCalls.push({ method, params });
      return method === "host/mcp.serve"
        ? { url: (params as { url?: string }).url ?? "http://127.0.0.1:5555/mcp/token" }
        : {};
    },
  });
  const tools = [
    {
      name: "lucky_number",
      description: "Returns today's lucky number.",
      inputSchema: { type: "object", properties: {} },
    },
  ];
  const { sessionId } = await agent.createSession({ workspace: "/w", options: {}, tools });
  assert.deepEqual(hostCalls, [{ method: "host/mcp.serve", params: { agentId: "opencode", workspace: "/w", tools } }]);
  const added = server.calls.filter((call) => call.method === "POST" && call.path === "/mcp");
  assert.equal(added.length, 1);
  assert.deepEqual(added[0]?.query, { directory: "/w" });
  assert.deepEqual(added[0]?.body, {
    name: "convergence",
    config: { type: "remote", url: "http://127.0.0.1:5555/mcp/token", enabled: true, oauth: false },
  });

  // A second chat in the folder with the same tools changes nothing; a
  // prompt checks again, cheaply.
  await agent.createSession({ workspace: "/w", options: {}, tools });
  await agent.prompt({ sessionId, input: { blocks: [{ type: "text", text: "x" }] } });
  assert.equal(hostCalls.length, 1);
  assert.equal(server.calls.filter((call) => call.path === "/mcp").length, 1);

  // New tools: the same address serves them, and OpenCode reconnects.
  const more = [...tools, { name: "echo", inputSchema: { type: "object" } }];
  await agent.resumeSession({ sessionId, workspace: "/w", options: {}, tools: more });
  assert.deepEqual(hostCalls[1]?.params, {
    agentId: "opencode",
    workspace: "/w",
    tools: more,
    url: "http://127.0.0.1:5555/mcp/token",
  });
  assert.equal(server.calls.filter((call) => call.path === "/mcp").length, 2);
  await agent.shutdown();
});

test("tools that cannot reach OpenCode are said so in the chat, and the session still starts", async () => {
  const server = new FakeOpenCode({
    routes: (call) =>
      call.path === "/mcp" ? { body: { convergence: { status: "failed", error: "connection refused" } } } : undefined,
  });
  const { agent, emitted } = agentOn(server, {
    onHost: (method) => (method === "host/mcp.serve" ? { url: "http://127.0.0.1:1/mcp/t" } : {}),
  });
  const { sessionId } = await agent.createSession({ workspace: "/w", options: {}, tools: [{ name: "t" }] });
  const notice = emitted.find((event) => event.event === "notice");
  assert.equal(present(notice).sessionId, sessionId);
  assert.equal(present(notice).level, "warning");
  assert.match(
    String(present(notice).message),
    /Plugin tools are not available to OpenCode: .*failed: connection refused/,
  );

  // A server on another computer cannot reach this one's loopback.
  const remote = new FakeOpenCode();
  const far = agentOn(remote, {
    instance: { id: "team", serverUrl: "https://opencode.example:4096" },
    onFetch: (url, init) => remote.handle(url.replace("https://opencode.example:4096", BASE), init),
  });
  await far.agent.createSession({ workspace: "/w", options: {}, tools: [{ name: "t" }] });
  assert.match(
    String(far.emitted.find((event) => event.event === "notice")?.message),
    /only to an OpenCode server on this computer/,
  );
  assert.ok(!far.api.hostCalls.some((call) => call.method === "host/mcp.serve"));
  await agent.shutdown();
  await far.agent.shutdown();
});

test("initialize reports the version, the sign-in methods, and a rejected password as a sign-in problem", async () => {
  const server = new FakeOpenCode({
    routes: (call) => {
      if (call.path === "/global/health") return { body: { healthy: true, version: "1.18.29" } };
      if (call.path === "/provider/auth")
        return {
          body: {
            openai: [
              { type: "oauth", label: "ChatGPT" },
              { type: "api", label: "API key" },
            ],
            anthropic: [{ type: "api", label: "API key" }],
          },
        };
      return undefined;
    },
  });
  const { agent } = agentOn(server, { instance: { id: "team", password: "p" } });
  const info = await agent.initialize();
  assert.equal(info.id, "opencode:team");
  assert.deepEqual(info.status, { state: "ready" });
  assert.equal(info.version, "1.18.29");
  assert.equal(info.maintenance, undefined, "a server somebody else runs is not ours to upgrade");
  assert.equal(info.continuationKey, BASE);
  assert.equal(info.capabilities?.steer, false);
  assert.equal(info.capabilities?.rollback, true);
  assert.deepEqual(info.authMethods, [
    { id: "anthropic", name: "anthropic", description: "API key" },
    { id: "openai", name: "openai", description: "ChatGPT, API key" },
  ]);
  assert.ok(info.icon?.startsWith("<svg"));
  assert.equal(
    server.calls[0]?.headers.authorization,
    `Basic ${Buffer.from("opencode:p").toString("base64")}`,
    "the instance's own password",
  );

  const locked = new FakeOpenCode({ routes: () => ({ status: 401, body: "Unauthorized" }) });
  const refused = await agentOn(locked).agent.initialize();
  assert.equal(refused.status?.state, "auth_required");
  assert.match(String(refused.status?.message), /CONVERGENCE_OPENCODE_SERVER_PASSWORD/);

  const old = new FakeOpenCode({
    routes: (call) => (call.path === "/global/health" ? { body: { version: "1.9.0" } } : undefined),
  });
  const tooOld = await agentOn(old).agent.initialize();
  assert.equal(tooOld.status?.state, "unavailable");
  assert.match(String(tooOld.status?.message), /1\.9\.0 is too old/);
});

test("rollback forgets the named message and everything after it, newest first, and touches no files", async () => {
  const messages = [{ info: { id: "msg_1" } }, { info: { id: "msg_host-2" } }, { info: { id: "msg_3" } }];
  const server = new FakeOpenCode({
    routes: (call) =>
      call.method === "GET" && call.path === "/session/ses_1/message" ? { body: messages } : undefined,
  });
  const { agent } = agentOn(server);
  await agent.resumeSession({ sessionId: "ses_1", workspace: "/w", options: {} });
  await agent.rollback({ sessionId: "ses_1", itemId: "host-2" });
  const deleted = server.calls.filter((call) => call.method === "DELETE").map((call) => call.path);
  assert.deepEqual(deleted, ["/session/ses_1/message/msg_3", "/session/ses_1/message/msg_host-2"]);
  assert.ok(!server.calls.some((call) => call.path.includes("revert")), "files belong to the host's checkpoints");
  await assert.rejects(agent.rollback({ sessionId: "ses_1", itemId: "nope" }), /does not know message nope/);
  await agent.shutdown();
});

test("sessions list only the chats, newest first, without placeholder titles; history nests subagents", async () => {
  const server = new FakeOpenCode({
    routes: (call) => {
      if (call.method === "GET" && call.path === "/session") {
        return {
          body: [
            { id: "ses_old", title: "Old", time: { created: 1, updated: 1000 } },
            { id: "ses_new", title: "New session - 2026-09-16T09:01:02.003Z", time: { created: 2, updated: 2000 } },
            { id: "ses_kid", parentID: "ses_old", title: "Kid", time: { created: 3, updated: 3000 } },
          ],
        };
      }
      if (call.path === "/session/ses_old/message") {
        return {
          body: [
            {
              info: { id: "msg_u", role: "user", time: { created: 1 } },
              parts: [{ type: "text", id: "p1", messageID: "msg_u", text: "hi" }],
            },
            {
              info: { id: "msg_a", role: "assistant", time: { created: 2 } },
              parts: [{ type: "text", id: "p2", messageID: "msg_a", text: "hello" }],
            },
          ],
        };
      }
      return undefined;
    },
  });
  const { agent } = agentOn(server);
  const { sessions } = await agent.listSessions({ workspace: "/w" });
  assert.deepEqual(
    sessions.map((session) => [session.id, session.title]),
    [
      ["ses_new", undefined],
      ["ses_old", "Old"],
    ],
  );
  assert.deepEqual(server.calls[0]?.query, { directory: "/w", roots: "true" });
  const { items } = await agent.readSession({ workspace: "/w", sessionId: "ses_old" });
  assert.deepEqual(
    items.map((item) => {
      if (item.role === "user") {
        const block = item.blocks[0];
        return [item.role, block && block.type === "text" ? block.text : undefined];
      }
      if (item.role === "assistant") return [item.role, item.text];
      return [item.role, undefined];
    }),
    [
      ["user", "hi"],
      ["assistant", "hello"],
    ],
  );
});

test("sign-in opens the provider's page in the browser; an API key method says how instead", async () => {
  const server = new FakeOpenCode({
    routes: (call) => {
      if (call.path === "/provider/auth")
        return {
          body: { openai: [{ type: "oauth", label: "ChatGPT" }], anthropic: [{ type: "api", label: "API key" }] },
        };
      if (call.path === "/provider/openai/oauth/authorize")
        return { body: { url: "https://auth.openai.com/authorize?x=1", method: "auto", instructions: "" } };
      return undefined;
    },
  });
  const { agent, api } = agentOn(server);
  await agent.authenticate({ method: "openai" });
  assert.deepEqual(api.opened, ["https://auth.openai.com/authorize?x=1"]);
  const authorize = server.calls.find((call) => call.path === "/provider/openai/oauth/authorize");
  assert.deepEqual(authorize?.body, { method: 0 });
  await assert.rejects(agent.authenticate({ method: "anthropic" }), /has no browser login; run `opencode auth login`/);
  await assert.rejects(agent.authenticate({ method: "nobody" }), /no sign-in methods for nobody/);
  api.openUrl = async () => {
    throw new Error("not allowed without a click");
  };
  await assert.rejects(
    agent.authenticate({ method: "openai" }),
    /open https:\/\/auth\.openai\.com\/authorize\?x=1 in your browser/,
  );
  api.openUrl = async () => ({ opened: false, reason: "a link opens only right after you click" });
  await assert.rejects(
    agent.authenticate({ method: "openai" }),
    /sign in to openai \(a link opens only right after you click\)/,
  );
});

Versions

VersionPublishedPlugin APISizePermissionsStatus
0.2.0latestOct 5, 2026>=2 <394.8 KB5 permissionsListed

Reviews and comments

0 threads · 0 reviews

No comments yet.