fix(javascript): preserve source-map preparation capability errors (#6149)

This commit is contained in:
Diya authored and GitHub committed 2026-10-22 04:24:31 +08:00
1 parent f5acf82a78
commit 3dbe314ada
3 files changed
+141 -9

No files matched your search

+10 -8
View File
@@ -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();
});