Przeglądaj źródła

test(core): cover mcp instruction producer (#37303)

Kit Langton 3 tygodni temu
rodzic
commit
eec6cd5234
1 zmienionych plików z 150 dodań i 0 usunięć
  1. 150 0
      packages/core/test/mcp-instructions.test.ts

+ 150 - 0
packages/core/test/mcp-instructions.test.ts

@@ -0,0 +1,150 @@
+import { describe, expect } from "bun:test"
+import { Effect, Layer } from "effect"
+import { AgentV2 } from "@opencode-ai/core/agent"
+import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
+import { MCP } from "@opencode-ai/core/mcp/index"
+import { McpInstructions } from "@opencode-ai/core/mcp/instructions"
+import { PermissionV2 } from "@opencode-ai/core/permission"
+import { McpTool } from "@opencode-ai/core/tool/mcp"
+import { it } from "./lib/effect"
+import { readInitial, readUpdate } from "./lib/instructions"
+
+const build = AgentV2.ID.make("build")
+
+const selection = (permissions: PermissionV2.Ruleset = []) => {
+  const info = AgentV2.Info.make({ ...AgentV2.Info.empty(build), permissions })
+  return { id: info.id, info }
+}
+
+const instructions = (server: string, text: string) =>
+  new MCP.ServerInstructions({ server: MCP.ServerName.make(server), instructions: text })
+
+const tool = (server: string, name = "search") => new MCP.Tool({ server: MCP.ServerName.make(server), name })
+
+const layer = (catalog: () => MCP.ServerInstructions[], tools: () => MCP.Tool[]) =>
+  AppNodeBuilder.build(McpInstructions.node, [
+    [
+      MCP.node,
+      Layer.mock(MCP.Service, {
+        instructions: () => Effect.succeed(catalog()),
+        tools: () => Effect.succeed(tools()),
+      }),
+    ],
+  ])
+
+describe("McpInstructions", () => {
+  it.effect("renders instructions for servers with at least one permitted tool", () =>
+    Effect.gen(function* () {
+      const service = yield* McpInstructions.Service
+      const generation = yield* service
+        .load(
+          selection([
+            { action: McpTool.name("alpha", "restricted"), resource: "*", effect: "deny" },
+            { action: McpTool.name("hidden", "search"), resource: "*", effect: "deny" },
+          ]),
+        )
+        .pipe(Effect.flatMap(readInitial))
+
+      expect(generation.text).toBe(
+        [
+          "<mcp_instructions>",
+          '  <server name="alpha">',
+          '    Use tools from this server through `execute` under `tools["alpha"]`.',
+          "    Alpha line one",
+          "    Alpha line two",
+          "  </server>",
+          '  <server name="beta">',
+          '    Use tools from this server through `execute` under `tools["beta"]`.',
+          "    Beta instructions",
+          "  </server>",
+          "</mcp_instructions>",
+        ].join("\n"),
+      )
+    }).pipe(
+      Effect.provide(
+        layer(
+          () => [
+            instructions("beta", "Beta instructions"),
+            instructions("unused", "No tools"),
+            instructions("hidden", "Denied tool"),
+            instructions("alpha", "Alpha line one\nAlpha line two"),
+          ],
+          () => [tool("alpha"), tool("alpha", "restricted"), tool("beta"), tool("hidden")],
+        ),
+      ),
+    ),
+  )
+
+  it.effect("omits instructions when the agent cannot use execute", () =>
+    Effect.gen(function* () {
+      const service = yield* McpInstructions.Service
+      const generation = yield* service
+        .load(selection([{ action: "execute", resource: "*", effect: "deny" }]))
+        .pipe(Effect.flatMap(readInitial))
+
+      expect(generation.text).toBe("")
+    }).pipe(
+      Effect.provide(
+        layer(
+          () => [instructions("alpha", "Alpha instructions")],
+          () => [tool("alpha")],
+        ),
+      ),
+    ),
+  )
+
+  it.effect("renders additions, changes, and removal", () => {
+    let catalog = [instructions("alpha", "Alpha instructions")]
+    const tools = [tool("alpha"), tool("beta")]
+    return Effect.gen(function* () {
+      const service = yield* McpInstructions.Service
+      const initialized = yield* service.load(selection()).pipe(Effect.flatMap(readInitial))
+
+      catalog = [instructions("alpha", "Alpha instructions"), instructions("beta", "Beta instructions")]
+      const added = yield* readUpdate(yield* service.load(selection()), initialized)
+      expect(added.text).toBe(
+        [
+          "New MCP server instructions are available in addition to those previously listed:",
+          '  <server name="beta">',
+          '    Use tools from this server through `execute` under `tools["beta"]`.',
+          "    Beta instructions",
+          "  </server>",
+        ].join("\n"),
+      )
+
+      catalog = [instructions("alpha", "Updated alpha"), instructions("beta", "Beta instructions")]
+      const changed = yield* readUpdate(yield* service.load(selection()), added)
+      expect(changed.text).toBe(
+        [
+          "The available MCP server instructions have changed. This list supersedes the previous one.",
+          "<mcp_instructions>",
+          '  <server name="alpha">',
+          '    Use tools from this server through `execute` under `tools["alpha"]`.',
+          "    Updated alpha",
+          "  </server>",
+          '  <server name="beta">',
+          '    Use tools from this server through `execute` under `tools["beta"]`.',
+          "    Beta instructions",
+          "  </server>",
+          "</mcp_instructions>",
+        ].join("\n"),
+      )
+
+      catalog = [instructions("beta", "Beta instructions")]
+      const removed = yield* readUpdate(yield* service.load(selection()), changed)
+      expect(removed.text).toBe("Instructions for the following MCP servers are no longer available: alpha.")
+
+      catalog = []
+      expect((yield* readUpdate(yield* service.load(selection()), removed)).text).toBe(
+        "MCP server instructions are no longer available.",
+      )
+    }).pipe(
+      Effect.provide(
+        layer(
+          () => catalog,
+          () => tools,
+        ),
+      ),
+    )
+  })
+})