service.test.ts 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110
  1. import { describe, expect, test } from "bun:test"
  2. import type { AgentSideConnection } from "@agentclientprotocol/sdk"
  3. import { OpenCode } from "@opencode-ai/client/promise"
  4. import { ACPService } from "../../src/acp/service"
  5. describe("acp service", () => {
  6. test("creates a v2 session, registers mcp, and publishes commands", async () => {
  7. const requests: Array<{ method: string; path: string; body?: unknown }> = []
  8. const updates: Parameters<AgentSideConnection["sessionUpdate"]>[0][] = []
  9. const server = Bun.serve({
  10. port: 0,
  11. async fetch(request) {
  12. const url = new URL(request.url)
  13. requests.push({
  14. method: request.method,
  15. path: url.pathname,
  16. body: request.method === "GET" ? undefined : await request.json().catch(() => undefined),
  17. })
  18. const location = { directory: "/workspace", project: { id: "global", directory: "/workspace" } }
  19. if (url.pathname === "/api/model") return Response.json({ location, data: [model] })
  20. if (url.pathname === "/api/model/default") return Response.json({ location, data: model })
  21. if (url.pathname === "/api/agent") return Response.json({ location, data: [agent] })
  22. if (url.pathname === "/api/command")
  23. return Response.json({ location, data: [{ name: "review", template: "" }] })
  24. if (url.pathname === "/api/skill") return Response.json({ location, data: [skill] })
  25. if (url.pathname === "/api/session" && request.method === "POST") return Response.json({ data: session })
  26. if (url.pathname === "/api/mcp/docs" && request.method === "PUT") return new Response(null, { status: 204 })
  27. return new Response(null, { status: 404 })
  28. },
  29. })
  30. const service = ACPService.make({
  31. client: OpenCode.make({ baseUrl: server.url.toString() }),
  32. connection: {
  33. sessionUpdate: async (update) => {
  34. updates.push(update)
  35. },
  36. requestPermission: async () => ({ outcome: { outcome: "cancelled" } }),
  37. },
  38. })
  39. try {
  40. const result = await service.newSession({
  41. cwd: "/workspace",
  42. mcpServers: [{ name: "docs", command: "bun", args: ["docs.ts"], env: [{ name: "TOKEN", value: "x" }] }],
  43. })
  44. expect(result.sessionId).toBe("ses_acp")
  45. expect(result.configOptions?.map((option) => option.id)).toEqual(["model", "effort", "mode"])
  46. expect(requests).toContainEqual({
  47. method: "PUT",
  48. path: "/api/mcp/docs",
  49. body: {
  50. config: { type: "local", command: ["bun", "docs.ts"], environment: { TOKEN: "x" } },
  51. },
  52. })
  53. expect(updates.at(-1)).toMatchObject({
  54. sessionId: "ses_acp",
  55. update: {
  56. sessionUpdate: "available_commands_update",
  57. availableCommands: [{ name: "review" }, { name: "verify", description: "Verify work" }],
  58. },
  59. })
  60. } finally {
  61. await server.stop(true)
  62. }
  63. })
  64. })
  65. const model = {
  66. id: "test-model",
  67. modelID: "test-model",
  68. providerID: "test",
  69. name: "Test Model",
  70. capabilities: { tools: true, input: ["text"], output: ["text"] },
  71. variants: [{ id: "default" }, { id: "high" }],
  72. time: { released: 0 },
  73. cost: [],
  74. status: "active" as const,
  75. enabled: true,
  76. limit: { context: 100_000, output: 10_000 },
  77. }
  78. const agent = {
  79. id: "build",
  80. name: "Build",
  81. request: { settings: {}, headers: {}, body: {} },
  82. mode: "primary" as const,
  83. hidden: false,
  84. permissions: [],
  85. }
  86. const skill = {
  87. id: "verify",
  88. name: "verify",
  89. description: "Verify work",
  90. slash: true,
  91. location: "/skills/verify.md",
  92. content: "verify",
  93. }
  94. const session = {
  95. id: "ses_acp",
  96. projectID: "global",
  97. agent: "build",
  98. model: { providerID: "test", id: "test-model", variant: "default" },
  99. cost: 0,
  100. tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
  101. time: { created: 0, updated: 0 },
  102. title: "New session",
  103. location: { directory: "/workspace" },
  104. }