| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267 |
- import { describe, expect, test } from "bun:test"
- import type { McpServer, SessionConfigOption } from "@agentclientprotocol/sdk"
- import { makeACPFixture, makeSession, secondModel } from "./service-fixture"
- describe("acp service directory behavior", () => {
- test("creates sessions from a catalog shared by concurrent callers in the same cwd", async () => {
- let created = 0
- await using fixture = makeACPFixture({
- fetch(request) {
- if (request.method !== "POST" || request.path !== "/api/session") return undefined
- created++
- return Response.json({
- data: makeSession(`ses_${created}`, { cwd: created <= 2 ? "/workspace" : "/other" }),
- })
- },
- })
- const first = await Promise.all([
- fixture.service.newSession({ cwd: "/workspace", mcpServers: [] }),
- fixture.service.newSession({ cwd: "/workspace", mcpServers: [] }),
- ])
- const other = await fixture.service.newSession({ cwd: "/other", mcpServers: [] })
- expect(first.map((session) => session.sessionId).toSorted()).toEqual(["ses_1", "ses_2"])
- expect(other.sessionId).toBe("ses_3")
- expect(currentValue(first[0], "model")).toBe("test/test-model")
- expect(currentValue(first[0], "mode")).toBe("build")
- expect(
- ["/api/model", "/api/model/default", "/api/agent", "/api/command", "/api/skill"].map((path) =>
- fixture.requests
- .filter((request) => request.path === path)
- .map((request) => request.query["location[directory]"]),
- ),
- ).toEqual([
- ["/workspace", "/other"],
- ["/workspace", "/other"],
- ["/workspace", "/other"],
- ["/workspace", "/other"],
- ["/workspace", "/other"],
- ])
- expect(
- fixture.requests
- .filter((request) => request.method === "POST" && request.path === "/api/session")
- .map((request) => request.body),
- ).toEqual([
- {
- agent: "build",
- model: { providerID: "test", id: "test-model", variant: "default" },
- location: { directory: "/workspace" },
- },
- {
- agent: "build",
- model: { providerID: "test", id: "test-model", variant: "default" },
- location: { directory: "/workspace" },
- },
- {
- agent: "build",
- model: { providerID: "test", id: "test-model", variant: "default" },
- location: { directory: "/other" },
- },
- ])
- expect(
- fixture.updates.map((item) =>
- item.update.sessionUpdate === "available_commands_update"
- ? item.update.availableCommands.map((command) => command.name)
- : [],
- ),
- ).toEqual([
- ["review", "verify"],
- ["review", "verify"],
- ["review", "verify"],
- ])
- })
- test("does not cache a failed catalog load", async () => {
- let modelCalls = 0
- await using fixture = makeACPFixture({
- fetch(request) {
- if (request.path === "/api/model") {
- modelCalls++
- if (modelCalls === 1) {
- return Response.json(
- { name: "ModelsNotReadyError", data: { message: "catalog is warming" } },
- { status: 503 },
- )
- }
- }
- if (request.method === "POST" && request.path === "/api/session") {
- return Response.json({ data: makeSession("ses_retry") })
- }
- return undefined
- },
- })
- const failure = await fixture.service
- .newSession({ cwd: "/workspace", mcpServers: [] })
- .catch((error: unknown) => error)
- expect(failure).toMatchObject({ name: "ModelsNotReadyError" })
- const retried = await fixture.service.newSession({ cwd: "/workspace", mcpServers: [] })
- expect(retried.sessionId).toBe("ses_retry")
- expect(modelCalls).toBe(2)
- })
- test("switches model, effort, and mode against the warm catalog", async () => {
- await using fixture = makeACPFixture({
- fetch(request) {
- if (request.method === "POST" && request.path === "/api/session") {
- return Response.json({ data: makeSession("ses_config") })
- }
- if (
- request.method === "POST" &&
- (request.path === "/api/session/ses_config/model" || request.path === "/api/session/ses_config/agent")
- ) {
- return new Response(null, { status: 204 })
- }
- return undefined
- },
- })
- const session = await fixture.service.newSession({ cwd: "/workspace", mcpServers: [] })
- const selectedModel = await fixture.service.setSessionConfigOption({
- sessionId: session.sessionId,
- configId: "model",
- value: "test/second-model",
- })
- const selectedEffort = await fixture.service.setSessionConfigOption({
- sessionId: session.sessionId,
- configId: "effort",
- value: "medium",
- })
- const selectedMode = await fixture.service.setSessionConfigOption({
- sessionId: session.sessionId,
- configId: "mode",
- value: "plan",
- })
- await fixture.service.setSessionMode({ sessionId: session.sessionId, modeId: "build" })
- expect(currentValue(selectedModel, "model")).toBe("test/second-model")
- expect(currentValue(selectedModel, "effort")).toBe("low")
- expect(currentValue(selectedEffort, "effort")).toBe("medium")
- expect(currentValue(selectedMode, "mode")).toBe("plan")
- expect(
- fixture.requests
- .filter((request) => request.path === "/api/session/ses_config/model")
- .map((request) => request.body),
- ).toEqual([
- { model: { providerID: "test", id: secondModel.id } },
- { model: { providerID: "test", id: secondModel.id, variant: "medium" } },
- ])
- expect(
- fixture.requests
- .filter((request) => request.path === "/api/session/ses_config/agent")
- .map((request) => request.body),
- ).toEqual([{ agent: "plan" }, { agent: "build" }])
- expect(fixture.requests.filter((request) => request.path === "/api/model")).toHaveLength(1)
- const invalidEffort = await fixture.service
- .setSessionConfigOption({
- sessionId: session.sessionId,
- configId: "effort",
- value: "maximum",
- })
- .catch((error: unknown) => error)
- const invalidMode = await fixture.service
- .setSessionConfigOption({
- sessionId: session.sessionId,
- configId: "mode",
- value: "missing",
- })
- .catch((error: unknown) => error)
- const invalidConfig = await fixture.service
- .setSessionConfigOption({
- sessionId: session.sessionId,
- configId: "missing",
- value: "value",
- })
- .catch((error: unknown) => error)
- expect(invalidEffort).toMatchObject({ _tag: "ACPInvalidEffortError" })
- expect(invalidMode).toMatchObject({ _tag: "ACPInvalidModeError" })
- expect(invalidConfig).toMatchObject({ _tag: "ACPInvalidConfigOptionError" })
- })
- test("converts MCP configs and deduplicates registrations per session and config", async () => {
- const local: McpServer = {
- name: "tools",
- command: "bun",
- args: ["server.ts"],
- env: [{ name: "TOKEN", value: "x" }],
- }
- const changed: McpServer = { ...local, args: ["changed.ts"] }
- const remote: McpServer = {
- type: "http",
- name: "docs",
- url: "https://example.com/mcp",
- headers: [{ name: "Authorization", value: "Bearer x" }],
- }
- let created = 0
- await using fixture = makeACPFixture({
- fetch(request) {
- if (request.method === "POST" && request.path === "/api/session") {
- created++
- return Response.json({ data: makeSession(`ses_${created}`) })
- }
- if (request.method === "GET" && request.path === "/api/session/ses_1") {
- return Response.json({ data: makeSession("ses_1") })
- }
- if (request.method === "PUT" && request.path.startsWith("/api/mcp/")) {
- return new Response(null, { status: 204 })
- }
- return undefined
- },
- })
- await fixture.service.newSession({ cwd: "/workspace", mcpServers: [local, local, remote] })
- await fixture.service.resumeSession({ cwd: "/workspace", sessionId: "ses_1", mcpServers: [local, remote] })
- await fixture.service.resumeSession({ cwd: "/workspace", sessionId: "ses_1", mcpServers: [changed] })
- await fixture.service.newSession({ cwd: "/workspace", mcpServers: [local] })
- const adds = fixture.requests.filter((request) => request.method === "PUT" && request.path.startsWith("/api/mcp/"))
- expect(adds).toHaveLength(4)
- expect(adds.filter((request) => request.path === "/api/mcp/tools").map((request) => request.body)).toEqual([
- {
- config: {
- type: "local",
- command: ["bun", "server.ts"],
- environment: { TOKEN: "x" },
- },
- },
- {
- config: {
- type: "local",
- command: ["bun", "changed.ts"],
- environment: { TOKEN: "x" },
- },
- },
- {
- config: {
- type: "local",
- command: ["bun", "server.ts"],
- environment: { TOKEN: "x" },
- },
- },
- ])
- expect(adds.find((request) => request.path === "/api/mcp/docs")?.body).toEqual({
- config: {
- type: "remote",
- url: "https://example.com/mcp",
- headers: { Authorization: "Bearer x" },
- oauth: false,
- },
- })
- expect(adds.map((request) => request.query)).toEqual([
- { "location[directory]": "/workspace" },
- { "location[directory]": "/workspace" },
- { "location[directory]": "/workspace" },
- { "location[directory]": "/workspace" },
- ])
- })
- })
- function currentValue(
- result: { readonly configOptions?: readonly SessionConfigOption[] | null } | undefined,
- id: string,
- ) {
- return result?.configOptions?.find((option) => option.id === id)?.currentValue
- }
|