fix(jeb): attribute transport timeouts to requested operations

Project canonical timeout and cancellation facts at the provider boundary while preserving causes, partial observations, live cleanup dependencies and provider-authored deadline guidance.

Validation: four real SDK/HTTP catalog and tool timeout controls fail before the fix. After rebasing onto the Ghidra timeout guidance change, 189 tests across ten files pass, including public CLI/MCP workflows and affected Ghidra tests. Typecheck/lint, docs, format and discovery pass. No native JEB engine or target verification. Refs #4597.
This commit is contained in:
morluto committed 2026-10-17 06:31:16 +08:00
1 parent a6e8f5269f
commit d74e72406a
3 files changed
+107 -3

No files matched your search

+27
View File
@@ -11,6 +11,7 @@ import {
AnalysisInputError,
AnalysisProtocolError,
AnalysisCancelledError,
AnalysisTimeoutError,
} from "../domain/analysisErrorCore.js";
import type { JsonValue } from "../domain/jsonValue.js";
import { jebResultSchemas } from "../domain/jeb/jebAnalysis.js";
@@ -490,3 +491,29 @@ it("keeps concurrent client inspections bound to their own parsed metadata", asy
await provider.close();
}
});
it("preserves provider-authored timeout guidance when projecting the requested operation", async () => {
const reason = new Error("selected deadline elapsed");
const timeout = new AnalysisTimeoutError("list_units", 123, {
cause: reason,
userMessage: "The selected MCP request budget elapsed.",
remediationAction: "Increase the selected MCP request budget.",
});
const provider = providerWith(scriptedConnection({ list_units: timeout }));
try {
const result = await provider.execute({
operation: "list_jeb_units",
input: { count: 2, index: 0 },
});
if (result.ok) throw new Error("Timed-out request passed");
expect(result.error).toMatchObject({
operation: "list_jeb_units",
timeoutMs: 123,
userMessage: timeout.userMessage,
remediationAction: timeout.remediationAction,
});
expect(result.error.cause).toBe(reason);
} finally {
await provider.close();
}
});
+21 -3
View File
@@ -20,6 +20,7 @@ import {
AnalysisCancelledError,
AnalysisCapabilityUnavailableError,
AnalysisProtocolError,
AnalysisTimeoutError,
AnalysisUnsupportedTargetError,
} from "../domain/analysisErrorCore.js";
import { AnalysisError } from "../domain/analysisErrorBase.js";
@@ -425,16 +426,33 @@ export class JebProvider implements JebAnalysisPort {
)
.bindCleanupDependencies([cause.cleanupFailure])
.bindCleanupResourceOwner(cause.cleanupFailure);
if (cause instanceof AnalysisCancelledError)
return new AnalysisCancelledError(operation, {
if (
cause instanceof AnalysisCancelledError ||
cause instanceof AnalysisTimeoutError
) {
const options = {
cause: cause.cause,
diagnostics: cause.diagnostics,
capturedOutput: cause.capturedOutput,
cleanup: cause.cleanup,
partialObservation: cause.partialObservation,
})
};
const failure =
cause instanceof AnalysisTimeoutError
? new AnalysisTimeoutError(operation, cause.timeoutMs, {
...options,
...(cause.userMessage === undefined
? {}
: { userMessage: cause.userMessage }),
...(cause.remediationAction === undefined
? {}
: { remediationAction: cause.remediationAction }),
})
: new AnalysisCancelledError(operation, options);
return failure
.bindCleanupDependencies([cause])
.bindCleanupResourceOwner(cause);
}
if (cause instanceof AnalysisError) return cause;
if (cause instanceof z.ZodError)
return new AnalysisProtocolError(
@@ -0,0 +1,59 @@
import { expect, it } from "vitest";
import { JebProvider } from "../../../src/jeb/JebProvider.js";
import { createStreamableHttpJebMcpConnection } from "../../../src/jeb/JebMcpConnection.js";
import { AnalysisTimeoutError } from "../../../src/domain/analysisErrorCore.js";
import {
startJebMcpFixture,
jebPrimaryResponses,
} from "../../fixtures/jebMcp.js";
it.each([
{ operation: "inspect_jeb_client", stage: "catalog" },
{ operation: "inspect_jeb_client", stage: "tool" },
{ operation: "list_jeb_units", stage: "catalog" },
{ operation: "list_jeb_units", stage: "tool" },
] as const)(
"attributes a real SDK $stage timeout to $operation",
async ({ operation, stage }) => {
let release!: () => void;
const gate = new Promise<void>((resolve) => {
release = resolve;
});
const fixture = await startJebMcpFixture(
async (tool) => {
await gate;
return jebPrimaryResponses[tool]!;
},
stage === "catalog"
? async () => {
await gate;
return { tools: [] };
}
: undefined,
);
const provider = new JebProvider({
environment: { REA_JEB_MCP_URL: fixture.endpoint },
connectionFactory: (endpoint) =>
createStreamableHttpJebMcpConnection(endpoint, {
discoveryTimeoutMs: 100,
requestTimeoutMs: 100,
}),
});
try {
const result = await provider.execute(
operation === "inspect_jeb_client"
? { operation, input: {} }
: { operation, input: { count: 2, index: 0 } },
);
if (result.ok) throw new Error("Timed-out MCP request passed");
expect(result.error).toBeInstanceOf(AnalysisTimeoutError);
expect(result.error).toMatchObject({ operation, timeoutMs: 100 });
expect(result.error.cause).toBeDefined();
expect(result.error.cleanupIncomplete).toBe(false);
} finally {
release();
await provider.close();
await fixture.close();
}
},
);