fix(javascript): preserve source-map preparation capability errors (#6149)
This commit is contained in:
3 files changed
+141
-9
No files matched your search
@@ -106,14 +106,16 @@ export const runSourceMapCommand = async (
|
||||
? new AnalysisCancelledError(OPERATION, {
|
||||
cause: options.signal.reason,
|
||||
})
|
||||
: new ProviderAdapterError(PROVIDER, OPERATION, {
|
||||
cause,
|
||||
diagnostics: {
|
||||
map_path: path,
|
||||
error_message: message(cause),
|
||||
phase: "process_ownership_preparation",
|
||||
},
|
||||
}),
|
||||
: cause instanceof AnalysisError
|
||||
? cause
|
||||
: new ProviderAdapterError(PROVIDER, OPERATION, {
|
||||
cause,
|
||||
diagnostics: {
|
||||
map_path: path,
|
||||
error_message: message(cause),
|
||||
phase: "process_ownership_preparation",
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
const deadline = new ProviderStartupDeadline(
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import { expect, it, vi } from "vitest";
|
||||
import { AnalysisCancelledError } from "../../../src/domain/analysisErrorCore.js";
|
||||
import { ProviderAdapterError } from "../../../src/domain/providerAdapterError.js";
|
||||
import { ProviderCleanupError } from "../../../src/domain/providerCleanupError.js";
|
||||
import { runSourceMapCommand } from "../../../src/javascript/sourceMaps/SourceMapCommand.js";
|
||||
|
||||
it.each([
|
||||
null,
|
||||
new ProviderCleanupError("caller", ["caller-owned-resource"], {}),
|
||||
])(
|
||||
"preserves opaque cancellation during ownership preparation: %s",
|
||||
async (reason) => {
|
||||
const controller = new AbortController();
|
||||
const createRuntime = vi.fn(async () => {
|
||||
throw new Error("Cancelled preparation must not acquire a runtime");
|
||||
});
|
||||
const launcher = vi.fn(async () => {
|
||||
throw new Error("Cancelled preparation must not launch a codec");
|
||||
});
|
||||
const result = await runSourceMapCommand(
|
||||
"{}",
|
||||
"/selected.map",
|
||||
{ signal: controller.signal },
|
||||
{
|
||||
prepareOwnership: async (signal) => {
|
||||
controller.abort(reason);
|
||||
signal?.throwIfAborted();
|
||||
},
|
||||
createRuntime,
|
||||
launcher,
|
||||
},
|
||||
);
|
||||
if (result.ok) throw new Error("Preparation must return cancellation");
|
||||
expect(result.error).toBeInstanceOf(AnalysisCancelledError);
|
||||
expect(result.error.cause).toBe(reason);
|
||||
expect(result.error.cleanupIncomplete).toBe(false);
|
||||
expect(result.error.cleanupResources).toEqual([]);
|
||||
if (reason instanceof ProviderCleanupError) {
|
||||
expect(reason.cleanupIncomplete).toBe(true);
|
||||
expect(reason.cleanupResources).toEqual(["caller-owned-resource"]);
|
||||
}
|
||||
expect(createRuntime).not.toHaveBeenCalled();
|
||||
expect(launcher).not.toHaveBeenCalled();
|
||||
},
|
||||
);
|
||||
|
||||
it("retains diagnostics for an unknown ownership preparation failure", async () => {
|
||||
const cause = new Error("unexpected ownership failure");
|
||||
const createRuntime = vi.fn(async () => {
|
||||
throw new Error("Failed preparation must not acquire a runtime");
|
||||
});
|
||||
const launcher = vi.fn(async () => {
|
||||
throw new Error("Failed preparation must not launch a codec");
|
||||
});
|
||||
const result = await runSourceMapCommand("{}", "/selected.map", undefined, {
|
||||
prepareOwnership: async () => {
|
||||
throw cause;
|
||||
},
|
||||
createRuntime,
|
||||
launcher,
|
||||
});
|
||||
if (result.ok) throw new Error("Preparation must return its failure");
|
||||
expect(result.error).toBeInstanceOf(ProviderAdapterError);
|
||||
expect(result.error.cause).toBe(cause);
|
||||
expect(result.error).toMatchObject({
|
||||
diagnostics: {
|
||||
map_path: "/selected.map",
|
||||
error_message: cause.message,
|
||||
phase: "process_ownership_preparation",
|
||||
},
|
||||
});
|
||||
expect(createRuntime).not.toHaveBeenCalled();
|
||||
expect(launcher).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -2,7 +2,8 @@ import { traceSourceMap } from "../../../src/javascript/sourceMaps/TraceSourceMa
|
||||
import { webSourceLocationArgs as args } from "../../fixtures/webSourceLocation.js";
|
||||
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
|
||||
import { Ajv2020 } from "ajv/dist/2020.js";
|
||||
import { expect, it, onTestFinished } from "vitest";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import { expect, it, onTestFinished, vi } from "vitest";
|
||||
import { WebSourceLocationService } from "../../../src/application/WebSourceLocationService.js";
|
||||
import { toolContract } from "../../../src/contracts/toolContracts.js";
|
||||
import { parseEvidence } from "../../../src/domain/evidence.js";
|
||||
@@ -10,6 +11,10 @@ import { ok } from "../../../src/domain/result.js";
|
||||
import { createServer } from "../../../src/server/createServer.js";
|
||||
import { createTestBinarySession } from "../../fixtures/binarySession.js";
|
||||
import { webSourceLocationFixture } from "../../fixtures/webSourceLocation.js";
|
||||
import { SourceMapDecoder } from "../../../src/javascript/sourceMaps/SourceMapDecoder.js";
|
||||
import { createSystemProcessOwnershipHost } from "../../../src/process/ProcessOwnershipObservation.js";
|
||||
import { createApplicationMcpHarness } from "../../fixtures/applicationMcpHarness.js";
|
||||
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
|
||||
|
||||
it("publishes valid SDK schemas and records the named module trace without a binary target", async () => {
|
||||
const service = new WebSourceLocationService(
|
||||
@@ -83,3 +88,54 @@ it("publishes valid SDK schemas and records the named module trace without a bin
|
||||
});
|
||||
expect(invalid.isError).toBe(true);
|
||||
});
|
||||
|
||||
it("preserves missing Linux ps capability and recovery through source-map tracing", async () => {
|
||||
const directory = await createTestTempDirectory("source-map-missing-ps-");
|
||||
const prepare = createSystemProcessOwnershipHost("linux", {
|
||||
PATH: directory,
|
||||
}).prepare;
|
||||
if (prepare === undefined)
|
||||
throw new Error("Ownership preparation is required");
|
||||
const createRuntime = vi.fn(async () => {
|
||||
throw new Error("Missing prerequisite must not acquire a runtime");
|
||||
});
|
||||
const launcher = vi.fn(async () => {
|
||||
throw new Error("Missing prerequisite must not launch a codec");
|
||||
});
|
||||
const decoder = new SourceMapDecoder({
|
||||
prepareOwnership: prepare,
|
||||
createRuntime,
|
||||
launcher,
|
||||
});
|
||||
const harness = await createApplicationMcpHarness({
|
||||
serverOptions: {
|
||||
webSourceLocation: new WebSourceLocationService(
|
||||
{ load: async () => ok(webSourceLocationFixture()) },
|
||||
decoder,
|
||||
),
|
||||
},
|
||||
});
|
||||
onTestFinished(() => harness.close());
|
||||
for (let attempt = 0; attempt < 2; attempt += 1) {
|
||||
const response = await harness.client.callTool({
|
||||
name: "trace_web_source_location",
|
||||
arguments: args,
|
||||
});
|
||||
const { error } = parseMcpToolError(response);
|
||||
expect(error).toMatchObject({
|
||||
code: "capability_unavailable",
|
||||
category: "unsupported_provider",
|
||||
retryable: false,
|
||||
message: expect.stringContaining("procps-compatible ps"),
|
||||
remediation: { action: expect.stringContaining("procps-compatible ps") },
|
||||
details: {
|
||||
provider_id: "process-ownership",
|
||||
operation: "prepare_owned_process",
|
||||
reason: expect.stringContaining("ENOENT"),
|
||||
},
|
||||
});
|
||||
}
|
||||
await harness.client.ping();
|
||||
expect(createRuntime).not.toHaveBeenCalled();
|
||||
expect(launcher).not.toHaveBeenCalled();
|
||||
});
|
||||
Reference in new issue
Block a user