From b99a26ecc962a11151cd143351ad04f79199d03d Mon Sep 17 00:00:00 2001 From: Will Washburn Date: Tue, 16 Jun 2026 02:51:45 -0400 Subject: [PATCH] fix(cli): mock @agent-relay/sdk boundary in MCP startup test The query_nodes MCP tool constructs `new AgentRelay(...)` from @agent-relay/sdk and calls `.nodes.list()`. AgentRelay wraps @relaycast/sdk internally, so the test mocking only @relaycast/sdk left this path dependent on packages/cli and packages/sdk resolving the SAME physical @relaycast/sdk copy. On main that copy hoists singly and the mock applies. But the Publish Package job regenerates the lockfile (`rm -rf package-lock.json && npm install`) at the freshly-bumped version, and the tree's pre-existing @relaycast/sdk skew (a stray transitive 1.2.0, plus @relayflows/core pinning ^1.1.0) tips the resolver into nesting a duplicate @relaycast/sdk under packages/sdk. AgentRelay's internal client is then the real (unmocked) one and `nodes.list()` escapes to a live HTTP request -> "RelayError: Route not found", failing the release. Mock the direct boundary (@agent-relay/sdk's AgentRelay) instead, so the query_nodes path is independent of node_modules hoisting. Verified by forcing the nested duplicate locally: pre-fix reproduces the exact failure, post-fix passes (16/16). Production code is untouched; in a real install @relaycast/sdk@^4.0.0 dedupes to one copy. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/cli/agent-relay-mcp.startup.test.ts | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/packages/cli/src/cli/agent-relay-mcp.startup.test.ts b/packages/cli/src/cli/agent-relay-mcp.startup.test.ts index 0fc7ad639..982f8ced2 100644 --- a/packages/cli/src/cli/agent-relay-mcp.startup.test.ts +++ b/packages/cli/src/cli/agent-relay-mcp.startup.test.ts @@ -199,6 +199,27 @@ async function loadAgentRelayMcpModule(options: LoadOptions = {}) { }) as any; RelayCast.createWorkspace = vi.fn((name: string) => behavior.createWorkspaceImpl(name)); + // The query_nodes tool constructs `new AgentRelay(...)` from @agent-relay/sdk + // and calls `.nodes.list()`. AgentRelay wraps @relaycast/sdk internally, so + // mocking only @relaycast/sdk leaves this path dependent on the two packages + // resolving the SAME physical @relaycast/sdk copy. A fresh publish-time + // `npm install` can nest a duplicate @relaycast/sdk under packages/sdk, at + // which point AgentRelay's internal client is the real (unmocked) one and the + // call escapes to a live HTTP request. Mock the direct boundary so the test is + // independent of node_modules hoisting. + const agentRelayNodesList = vi.fn(async (_query?: { capability?: string; name?: string }) => [ + { + name: 'node-a', + status: 'online', + capabilities: [{ name: 'spawn:codex' }], + }, + ]); + const AgentRelayMock = vi.fn(function (this: unknown) { + return { + nodes: { list: agentRelayNodesList }, + }; + }) as any; + vi.doMock('@modelcontextprotocol/sdk/server/mcp.js', () => ({ McpServer: FakeMcpServer, ResourceTemplate: class ResourceTemplate { @@ -218,6 +239,10 @@ async function loadAgentRelayMcpModule(options: LoadOptions = {}) { RelayCast, SDK_VERSION: 'test-sdk-version', })); + vi.doMock('@agent-relay/sdk', async () => { + const actual = await vi.importActual>('@agent-relay/sdk'); + return { ...actual, AgentRelay: AgentRelayMock }; + }); vi.doMock('./telemetry/index.js', () => ({ initTelemetry: telemetryInit, shutdown: telemetryShutdown,