From e79ad6ca8ebbf2e96b5095e7c2b8bfaf27ca7559 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Thu, 29 Oct 2026 01:30:24 +0800 Subject: [PATCH] fix(jeb): preserve selected endpoint in failures (#7835) * fix(jeb): preserve selected endpoint in failures * fix(jeb): retain endpoint on typed failures --- src/jeb/JebProvider.test.ts | 93 +++++++++++++++++++ src/jeb/JebProvider.ts | 28 ++++-- .../jebConfigurationIsolation.test.ts | 76 +++++++++++++++ 3 files changed, 188 insertions(+), 9 deletions(-) diff --git a/src/jeb/JebProvider.test.ts b/src/jeb/JebProvider.test.ts index fbe614c76b..54bd186af1 100644 --- a/src/jeb/JebProvider.test.ts +++ b/src/jeb/JebProvider.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest"; import { createJebAnalysisProvider } from "../composition/jeb.js"; import { JebProvider } from "./JebProvider.js"; +import { decodeJebToolResult } from "./JebMcpConnection.js"; import type { JebMcpConnection, JebMcpConnectionTimeouts, @@ -52,6 +53,40 @@ const providerWith = (connection: JebMcpConnection) => }); describe("JebProvider live availability", () => { + it("retains the caller-selected endpoint spelling in results and failures", async () => { + const endpoint = "HTTP://127.0.0.1:08425/mcp"; + const provider = new JebProvider({ + environment: { REA_JEB_MCP_URL: endpoint }, + connectionFactory: () => + scriptedConnection({ + get_client_information: { success: true, version: "5.48.0" }, + list_units: new Error("connection closed"), + }), + }); + try { + expect((await provider.inspectAvailability()).diagnostics).toMatchObject({ + endpoint, + }); + const observation = await provider.execute({ + operation: "inspect_jeb_client", + input: {}, + }); + if (!observation.ok) throw observation.error; + expect( + jebResultSchemas.inspect_jeb_client.parse(observation.value.result) + .engine.endpoint, + ).toBe(endpoint); + const failure = await provider.execute({ + operation: "list_jeb_units", + input: { count: 10, index: 0 }, + }); + if (failure.ok) throw new Error("Expected transport failure"); + expect(failure.error.diagnostics).toMatchObject({ endpoint }); + } finally { + await provider.close(); + } + }); + it("passes its validated environment timeout to the production connection", async () => { let configured: JebMcpConnectionTimeouts | undefined; const provider = createJebAnalysisProvider( @@ -125,6 +160,59 @@ describe("JebProvider live availability", () => { }); }); +describe("JebProvider endpoint diagnostics", () => { + it("retains the selected endpoint on malformed MCP results without replacing the protocol failure", async () => { + let failure: unknown; + try { + decodeJebToolResult({ content: [] }); + } catch (cause) { + failure = cause; + } + if (!(failure instanceof AnalysisProtocolError)) + throw new Error("Expected a protocol failure"); + failure.retainDiagnostics({ phase: "decode" }); + const connection = scriptedConnection({ get_client_information: failure }); + const provider = providerWith(connection); + try { + const result = await provider.execute({ + operation: "inspect_jeb_client", + input: {}, + }); + if (result.ok) throw new Error("Expected a malformed response failure"); + expect(result.error).toBe(failure); + expect(result.error.diagnostics).toEqual({ + phase: "decode", + endpoint: "http://127.0.0.1:8425/mcp", + }); + } finally { + await provider.close(); + } + }); + + it("retains the selected endpoint when cancelled before execution", async () => { + const connection = scriptedConnection({}); + const provider = providerWith(connection); + const controller = new AbortController(); + const reason = new Error("caller stopped"); + controller.abort(reason); + try { + const result = await provider.execute( + { operation: "inspect_jeb_client", input: {} }, + { signal: controller.signal }, + ); + if (result.ok) throw new Error("Expected cancellation"); + expect(result.error).toBeInstanceOf(AnalysisCancelledError); + expect(result.error.cause).toBe(reason); + expect(result.error.diagnostics).toEqual({ + endpoint: "http://127.0.0.1:8425/mcp", + }); + expect(connection.calls).toEqual([]); + } finally { + await provider.close(); + } + }); +}); + describe("JebProvider observations and failures", () => { it("normalizes client information and caches engine identity", async () => { const connection = scriptedConnection({ @@ -170,6 +258,9 @@ describe("JebProvider observations and failures", () => { if (result.ok) return; expect(result.error).toBeInstanceOf(AnalysisCapabilityUnavailableError); expect(result.error.message).toContain("ECONNREFUSED"); + expect(projectAnalysisError(result.error)).toMatchObject({ + details: { diagnostics: { endpoint: "http://127.0.0.1:8425/mcp" } }, + }); }); it("maps transport failures during an operation to provider unavailability", async () => { @@ -508,6 +599,7 @@ it("preserves provider-authored timeout guidance when projecting the requested o const reason = new Error("selected deadline elapsed"); const timeout = new AnalysisTimeoutError("list_units", 123, { cause: reason, + diagnostics: { phase: "request" }, userMessage: "The selected MCP request budget elapsed.", remediationAction: "Increase the selected MCP request budget.", }); @@ -523,6 +615,7 @@ it("preserves provider-authored timeout guidance when projecting the requested o timeoutMs: 123, userMessage: timeout.userMessage, remediationAction: timeout.remediationAction, + diagnostics: { phase: "request", endpoint: "http://127.0.0.1:8425/mcp" }, }); expect(result.error.cause).toBe(reason); } finally { diff --git a/src/jeb/JebProvider.ts b/src/jeb/JebProvider.ts index 8f2380528e..ab085d7a92 100644 --- a/src/jeb/JebProvider.ts +++ b/src/jeb/JebProvider.ts @@ -159,6 +159,8 @@ const parseJebEnvelope = ( export class JebProvider implements JebAnalysisPort { readonly #connection: JebMcpConnection; readonly #endpoint: URL; + /** Caller-selected spelling, retained only after endpoint admission succeeds. */ + readonly #selectedEndpoint: string; #clientInformation: | Pick, "version" | "gui_client"> | undefined; @@ -175,6 +177,8 @@ export class JebProvider implements JebAnalysisPort { const endpoint = parseJebMcpEndpoint(environment.REA_JEB_MCP_URL); if (!endpoint.ok) throw endpoint.error; this.#endpoint = endpoint.value; + this.#selectedEndpoint = + environment.REA_JEB_MCP_URL || DEFAULT_JEB_MCP_ENDPOINT; const createConnection = options.connectionFactory ?? createStreamableHttpJebMcpConnection; const timeouts = { @@ -208,7 +212,7 @@ export class JebProvider implements JebAnalysisPort { Object.keys(JEB_ENGINE_TOOLS) as JebOperation[] ).filter((operation) => tools.has(JEB_ENGINE_TOOLS[operation])), diagnostics: { - endpoint: this.#endpoint.toString(), + endpoint: this.#selectedEndpoint, engine_version: information.version ?? null, }, }; @@ -221,8 +225,8 @@ export class JebProvider implements JebAnalysisPort { code: "runtime_unavailable", reason: projected.message, diagnostics: { - endpoint: this.#endpoint.toString(), ...cause.diagnostics, + endpoint: this.#selectedEndpoint, }, }; } @@ -230,7 +234,7 @@ export class JebProvider implements JebAnalysisPort { status: "unavailable", code: "not_configured", reason: `${cause instanceof Error ? cause.message : "The JEB MCP endpoint did not respond"}. ${JEB_CONNECTION_REMEDIATION}`, - diagnostics: { endpoint: this.#endpoint.toString() }, + diagnostics: { endpoint: this.#selectedEndpoint }, }; } } @@ -247,6 +251,7 @@ export class JebProvider implements JebAnalysisPort { return err( new AnalysisCancelledError(request.operation, { cause: options.signal.reason, + diagnostics: { endpoint: this.#selectedEndpoint }, }), ); try { @@ -300,7 +305,7 @@ export class JebProvider implements JebAnalysisPort { return { name: "jeb", version: information.version ?? null, - endpoint: this.#endpoint.toString(), + endpoint: this.#selectedEndpoint, gui_client: information.gui_client ?? null, }; } @@ -375,7 +380,7 @@ export class JebProvider implements JebAnalysisPort { engine: { name: "jeb", version: information.version ?? null, - endpoint: this.#endpoint.toString(), + endpoint: this.#selectedEndpoint, gui_client: information.gui_client ?? null, }, startup_ts: information.startup_ts ?? null, @@ -524,7 +529,7 @@ export class JebProvider implements JebAnalysisPort { const parsed = parseJebEnvelope( payload, operation, - this.#endpoint.toString(), + this.#selectedEndpoint, input, ); if (!parsed.ok) throw parsed.error; @@ -546,7 +551,10 @@ export class JebProvider implements JebAnalysisPort { ) { const options = { cause: cause.cause, - diagnostics: cause.diagnostics, + diagnostics: { + ...cause.diagnostics, + endpoint: this.#selectedEndpoint, + }, capturedOutput: cause.capturedOutput, cleanup: cause.cleanup, partialObservation: cause.partialObservation, @@ -567,11 +575,12 @@ export class JebProvider implements JebAnalysisPort { .bindCleanupDependencies([cause]) .bindCleanupResourceOwner(cause); } - if (cause instanceof AnalysisError) return cause; + if (cause instanceof AnalysisError) + return cause.retainDiagnostics({ endpoint: this.#selectedEndpoint }); if (cause instanceof z.ZodError) return new AnalysisProtocolError( "JEB omitted or malformed required observation fields", - { cause }, + { cause, diagnostics: { endpoint: this.#selectedEndpoint } }, ); const physical = cause instanceof JebMcpPhysicalFailure ? cause.cause : cause; @@ -581,6 +590,7 @@ export class JebProvider implements JebAnalysisPort { physical instanceof Error ? physical.message : String(physical), { cause, + diagnostics: { endpoint: this.#selectedEndpoint }, userMessage: JEB_CONNECTION_REMEDIATION, }, ); diff --git a/tests/acceptance/providers/jebConfigurationIsolation.test.ts b/tests/acceptance/providers/jebConfigurationIsolation.test.ts index f4452b060b..66862a08c8 100644 --- a/tests/acceptance/providers/jebConfigurationIsolation.test.ts +++ b/tests/acceptance/providers/jebConfigurationIsolation.test.ts @@ -1,6 +1,8 @@ import { Client } from "@modelcontextprotocol/client"; import { StdioClientTransport } from "@modelcontextprotocol/client/stdio"; import { resolve } from "node:path"; +import { createServer } from "node:http"; +import { once } from "node:events"; import { expect } from "vitest"; import { parseMcpToolError } from "../../fixtures/mcpToolError.js"; @@ -26,6 +28,80 @@ const isolatedEnvironment = (workspace: TestWorkspace) => ({ REA_JEB_MCP_TIMEOUT_MS: "1.5", }); +test("retains the selected endpoint when a direct JEB connection fails", async ({ + cli, + workspace, +}) => { + const server = createServer((_request, response) => { + response.writeHead(404, { "content-type": "text/plain" }); + response.end("not found"); + }); + server.listen(0, "127.0.0.1"); + await once(server, "listening"); + try { + const address = server.address(); + if (address === null || typeof address === "string") + throw new Error("Missing loopback listener address"); + const endpoint = `HTTP://127.0.0.1:0${address.port}/mcp`; + const result = await cli.run({ + arguments: ["inspect-jeb-client", "--json"], + environment: { + ...isolatedEnvironment(workspace), + REA_JEB_MCP_TIMEOUT_MS: "3000", + REA_JEB_MCP_URL: endpoint, + }, + }); + expect(result.exitCode, result.stderr).toBe(1); + expect(result.json).toMatchObject({ + code: "capability_unavailable", + details: { + diagnostics: { endpoint }, + reason: expect.stringContaining("not found"), + }, + }); + const transport = new StdioClientTransport({ + command: process.execPath, + args: [resolve("scripts/rea.mjs"), "mcp"], + cwd: workspace.root, + env: { + ...isolatedEnvironment(workspace), + REA_JEB_MCP_TIMEOUT_MS: "3000", + REA_JEB_MCP_URL: endpoint, + }, + stderr: "pipe", + }); + const client = new Client({ + name: "jeb-endpoint-acceptance", + version: "1", + }); + try { + await client.connect(transport); + const definition = (await client.listTools()).tools.find( + ({ name }) => name === "inspect_jeb_client", + ); + if (definition === undefined) throw new Error("Missing JEB tool"); + const refusal = parseMcpToolError( + await client.callTool( + { name: "inspect_jeb_client", arguments: {} }, + { toolDefinition: definition }, + ), + ); + expect(refusal.error).toMatchObject({ + code: "capability_unavailable", + details: { diagnostics: { endpoint } }, + }); + } finally { + await client.close(); + await transport.close(); + } + } finally { + server.closeAllConnections(); + await new Promise((resolveClose, rejectClose) => + server.close((error) => (error ? rejectClose(error) : resolveClose())), + ); + } +}); + test("keeps compiled CLI discovery usable while JEB rejects its malformed timeout", async ({ cli, workspace,