fix(process): preserve availability when macOS ownership preparation fails (#7856)
* fix(process): preserve availability when macOS ownership preparation fails * test(process): verify affected macOS provider availability
This commit is contained in:
1 parent
6c205d8a79
commit
29945d435d
3 files changed
+222
-12
No files matched your search
@@ -0,0 +1,84 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { AnalysisCancelledError } from "../domain/analysisErrorCore.js";
|
||||
import { ProviderAdapterError } from "../domain/providerAdapterError.js";
|
||||
import { DarwinProcessOwnershipInspectionError } from "../process/DarwinProcessRunTokenReader.js";
|
||||
import { OwnedCommandFailure } from "../process/OwnedCommand.js";
|
||||
import { ownedProcessPrerequisiteAvailability } from "./ownedProcessAvailability.js";
|
||||
|
||||
describe("Darwin ownership prerequisite discovery", () => {
|
||||
it("retains compiler diagnostics when a provider probe cannot prepare ownership", () => {
|
||||
const preparation = new DarwinProcessOwnershipInspectionError(
|
||||
"macOS process ownership helper compilation failed: missing DEVELOPER_DIR",
|
||||
{
|
||||
compilerOutcome: {
|
||||
outcome: "exited",
|
||||
code: 1,
|
||||
signal: null,
|
||||
stderr: "missing DEVELOPER_DIR",
|
||||
stderrTruncated: false,
|
||||
outputTruncated: false,
|
||||
killed: false,
|
||||
},
|
||||
},
|
||||
);
|
||||
const wrapped = new ProviderAdapterError(
|
||||
"apktool",
|
||||
"inspect_apktool_client",
|
||||
{
|
||||
cause: new OwnedCommandFailure(
|
||||
"process",
|
||||
"preparation failed",
|
||||
null,
|
||||
null,
|
||||
{
|
||||
preparationFailure: preparation,
|
||||
},
|
||||
),
|
||||
diagnostics: { stage: "ownership_preparation" },
|
||||
},
|
||||
);
|
||||
expect(ownedProcessPrerequisiteAvailability(wrapped)).toMatchObject({
|
||||
status: "unavailable",
|
||||
code: "runtime_unavailable",
|
||||
reason: expect.stringContaining("missing DEVELOPER_DIR") as unknown,
|
||||
diagnostics: {
|
||||
stage: "ownership_preparation",
|
||||
compiler_outcome: { outcome: "exited", code: 1 },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("does not downgrade cancellation, failed cleanup, or unrelated provider failures", () => {
|
||||
const preparation = new DarwinProcessOwnershipInspectionError(
|
||||
"compiler failed",
|
||||
);
|
||||
expect(
|
||||
ownedProcessPrerequisiteAvailability(new AnalysisCancelledError("probe")),
|
||||
).toBeUndefined();
|
||||
expect(
|
||||
ownedProcessPrerequisiteAvailability(
|
||||
new ProviderAdapterError("apktool", "probe", {
|
||||
cause: preparation,
|
||||
diagnostics: { stage: "cleanup" },
|
||||
}),
|
||||
),
|
||||
).toBeUndefined();
|
||||
expect(
|
||||
ownedProcessPrerequisiteAvailability(
|
||||
new ProviderAdapterError("apktool", "probe", {
|
||||
cause: preparation,
|
||||
diagnostics: { stage: "ownership_preparation" },
|
||||
cleanup: { reason: "still owned", resources: ["helper"] },
|
||||
}),
|
||||
),
|
||||
).toBeUndefined();
|
||||
expect(
|
||||
ownedProcessPrerequisiteAvailability(
|
||||
new DarwinProcessOwnershipInspectionError("cancelled", {
|
||||
cancellation: { reason: "stop" },
|
||||
}),
|
||||
),
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -1,10 +1,17 @@
|
||||
import { AnalysisCapabilityUnavailableError } from "../domain/analysisErrorCore.js";
|
||||
import { ProviderAdapterError } from "../domain/providerAdapterError.js";
|
||||
import { jsonValueSchema } from "../domain/jsonValue.js";
|
||||
import { DarwinProcessOwnershipInspectionError } from "../process/DarwinProcessRunTokenReader.js";
|
||||
import { OwnedCommandFailure } from "../process/OwnedCommand.js";
|
||||
import { LinuxOwnedProcessPrerequisiteError } from "../process/ProcessOwnershipObservation.js";
|
||||
import type { ProviderAvailability } from "./AnalysisProvider.js";
|
||||
|
||||
const REMEDIATION =
|
||||
"Install your distribution's procps or procps-ng package, or put a procps-compatible ps on REA's PATH, then retry.";
|
||||
|
||||
const DARWIN_REMEDIATION =
|
||||
"Check the selected Xcode or Command Line Tools and Swift compiler configuration, then restart REA and retry.";
|
||||
|
||||
// Provider adapters re-attribute capability errors and keep the original as cause.
|
||||
const prerequisiteRefusal = (
|
||||
cause: unknown,
|
||||
@@ -23,21 +30,66 @@ const prerequisiteRefusal = (
|
||||
return undefined;
|
||||
};
|
||||
|
||||
// Recognize only failures attributed to owned-process preparation. Cancellation
|
||||
// and failed cleanup remain errors so their owner and completion are not lost.
|
||||
const darwinPrerequisiteRefusal = (
|
||||
cause: unknown,
|
||||
): DarwinProcessOwnershipInspectionError | undefined => {
|
||||
let current = cause;
|
||||
while (current instanceof ProviderAdapterError) {
|
||||
if (
|
||||
current.cleanupIncomplete ||
|
||||
current.diagnostics?.stage !== "ownership_preparation"
|
||||
)
|
||||
return undefined;
|
||||
current = current.cause;
|
||||
}
|
||||
if (current instanceof OwnedCommandFailure) {
|
||||
if (
|
||||
current.reason !== "process" ||
|
||||
current.cleanupOwner !== undefined ||
|
||||
current.cleanupFailure !== null ||
|
||||
current.cleanupResult !== undefined ||
|
||||
current.cancellation !== undefined
|
||||
)
|
||||
return undefined;
|
||||
current = current.preparationFailure;
|
||||
}
|
||||
return current instanceof DarwinProcessOwnershipInspectionError &&
|
||||
current.cleanupFailure === undefined &&
|
||||
current.cleanupOwner === undefined &&
|
||||
current.cancellation === undefined
|
||||
? current
|
||||
: undefined;
|
||||
};
|
||||
|
||||
/** A host prerequisite gap is the probed provider's unavailability, not a probe failure. */
|
||||
export const ownedProcessPrerequisiteAvailability = (
|
||||
cause: unknown,
|
||||
): ProviderAvailability | undefined => {
|
||||
const refusal = prerequisiteRefusal(cause);
|
||||
return refusal === undefined
|
||||
? undefined
|
||||
: {
|
||||
status: "unavailable",
|
||||
code: "runtime_unavailable",
|
||||
reason: `${refusal.reason}. ${REMEDIATION}`,
|
||||
diagnostics: {
|
||||
stage: "ownership_preparation",
|
||||
prerequisite_provider_id: refusal.providerId,
|
||||
prerequisite_operation: refusal.operation,
|
||||
},
|
||||
};
|
||||
if (refusal !== undefined)
|
||||
return {
|
||||
status: "unavailable",
|
||||
code: "runtime_unavailable",
|
||||
reason: `${refusal.reason}. ${REMEDIATION}`,
|
||||
diagnostics: {
|
||||
stage: "ownership_preparation",
|
||||
prerequisite_provider_id: refusal.providerId,
|
||||
prerequisite_operation: refusal.operation,
|
||||
},
|
||||
};
|
||||
const darwin = darwinPrerequisiteRefusal(cause);
|
||||
if (darwin === undefined) return undefined;
|
||||
return {
|
||||
status: "unavailable",
|
||||
code: "runtime_unavailable",
|
||||
reason: `${darwin.message}. ${DARWIN_REMEDIATION}`,
|
||||
diagnostics: {
|
||||
stage: "ownership_preparation",
|
||||
...(darwin.compilerOutcome === undefined
|
||||
? {}
|
||||
: { compiler_outcome: jsonValueSchema.parse(darwin.compilerOutcome) }),
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,74 @@
|
||||
import { mkdir, writeFile } from "node:fs/promises";
|
||||
import { join, resolve } from "node:path";
|
||||
import { Client } from "@modelcontextprotocol/client";
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
||||
import { expect } from "vitest";
|
||||
import { z } from "incur";
|
||||
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
|
||||
import { cliTest } from "../support/cli/cliFixture.js";
|
||||
|
||||
cliTest.skipIf(process.platform !== "darwin")(
|
||||
"target-free MCP discovery survives an unavailable macOS compiler",
|
||||
async () => {
|
||||
const root = await createTestTempDirectory("rea-darwin-unavailable-");
|
||||
const tmp = join(root, "tmp");
|
||||
await mkdir(tmp);
|
||||
const apktool = join(root, "apktool");
|
||||
await writeFile(apktool, "#!/bin/sh\necho 2.9.3\n", { mode: 0o755 });
|
||||
const transport = new StdioClientTransport({
|
||||
command: process.execPath,
|
||||
args: [resolve("scripts/rea.mjs"), "mcp"],
|
||||
cwd: process.cwd(),
|
||||
env: {
|
||||
PATH: process.env.PATH ?? "",
|
||||
HOME: root,
|
||||
TMPDIR: tmp,
|
||||
DEVELOPER_DIR: join(root, "missing-developer"),
|
||||
REA_LOG_LEVEL: "silent",
|
||||
REA_APKTOOL_COMMAND: apktool,
|
||||
},
|
||||
stderr: "pipe",
|
||||
});
|
||||
transport.stderr?.on("data", () => undefined);
|
||||
const client = new Client({ name: "darwin-availability", version: "1" });
|
||||
try {
|
||||
await client.connect(transport);
|
||||
const result = await client.callTool({
|
||||
name: "binary_session",
|
||||
arguments: {},
|
||||
});
|
||||
expect(result.isError, JSON.stringify(result)).not.toBe(true);
|
||||
expect(result.structuredContent).toBeDefined();
|
||||
const discovery = z
|
||||
.object({
|
||||
result: z.object({
|
||||
tool_availability: z.object({
|
||||
groups: z.array(
|
||||
z.object({
|
||||
names: z.array(z.string()),
|
||||
available: z.boolean(),
|
||||
reason: z.string(),
|
||||
remediation: z.string().nullable(),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
.parse(result.structuredContent);
|
||||
const apktoolGroup = discovery.result.tool_availability.groups.find(
|
||||
({ names }) => names.includes("inspect_apktool_client"),
|
||||
);
|
||||
expect(apktoolGroup).toMatchObject({
|
||||
available: false,
|
||||
reason: "provider_unavailable",
|
||||
remediation: expect.stringContaining(
|
||||
"macOS process ownership helper compilation failed",
|
||||
),
|
||||
});
|
||||
expect(apktoolGroup?.remediation).toContain("missing-developer");
|
||||
} finally {
|
||||
await client.close();
|
||||
await transport.close();
|
||||
}
|
||||
},
|
||||
);
|
||||
Reference in new issue
Block a user