mirror of
https://github.com/morluto/rea
synced 2026-10-25 22:08:11 -04:00
fix(native): preserve LLDB discovery failure classifications
This commit is contained in:
@@ -743,6 +743,12 @@ caller-selected entries, records them and continues:
|
||||
- Objective-C methods by selector, optionally limited to one class and to
|
||||
instance or class methods.
|
||||
|
||||
LLDB discovery uses a separate 60000 ms `xcrun` deadline with no runtime
|
||||
override. Discovery timeouts report `provider_timeout` and retain timeout
|
||||
recovery, captured diagnostics and cleanup state. Output-budget and I/O failures
|
||||
retain their own error categories; only actual discovery unavailability reports
|
||||
missing LLDB. `duration_ms` controls the later target observation window.
|
||||
|
||||
```sh
|
||||
rea observe-native-calls /Applications/Example.app '{"breakpoints":[{"kind":"objc-method","class_name":"NSURLSession","selector":"dataTaskWithRequest:completionHandler:"},{"kind":"function","name":"open","module":"libsystem_kernel.dylib"}],"duration_ms":5000,"backtrace_frames":4}' --json
|
||||
```
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { translateNativeCommandFailure } from "./NativeCommandAnalysisError.js";
|
||||
import { DEFAULT_NATIVE_COMMAND_TIMEOUT_MS } from "../domain/native/nativeDeadlines.js";
|
||||
import { snapshotEnvironment } from "../process/snapshotEnvironment.js";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { writeFile } from "node:fs/promises";
|
||||
@@ -515,39 +517,19 @@ export class LldbCallTracer implements NativeCallTracer {
|
||||
const preparation = failure.preparationFailure;
|
||||
if (preparation !== undefined)
|
||||
return withResolverMarkers(lldbPreparationFailure(preparation), markers);
|
||||
const cleanup =
|
||||
markers.length === 0
|
||||
? undefined
|
||||
: {
|
||||
reason: markers
|
||||
.map((marker) =>
|
||||
typeof marker.diagnostics?.reason === "string"
|
||||
? marker.diagnostics.reason
|
||||
: marker.message,
|
||||
)
|
||||
.join("; "),
|
||||
resources: [
|
||||
...new Set(markers.flatMap((marker) => marker.cleanupResources)),
|
||||
],
|
||||
};
|
||||
const cleanupContext =
|
||||
cleanup === undefined ? {} : { cause: failure, cleanup };
|
||||
const error =
|
||||
failure.reason === "cancelled"
|
||||
? new AnalysisCancelledError(OPERATION, {
|
||||
...(cleanup === undefined ? {} : { cleanup }),
|
||||
...(failure.cancellation === undefined
|
||||
? cleanup === undefined
|
||||
? {}
|
||||
: { cause: failure }
|
||||
: { cause: failure.cancellation.reason }),
|
||||
})
|
||||
: new AnalysisCapabilityUnavailableError(
|
||||
PROVIDER,
|
||||
OPERATION,
|
||||
LLDB_REQUIREMENT,
|
||||
{ ...cleanupContext, userMessage: LLDB_REQUIREMENT },
|
||||
);
|
||||
const error = translateNativeCommandFailure(
|
||||
OPERATION,
|
||||
failure,
|
||||
DEFAULT_NATIVE_COMMAND_TIMEOUT_MS,
|
||||
{
|
||||
unavailableMessage: LLDB_REQUIREMENT,
|
||||
timeoutRecovery: {
|
||||
userMessage: `LLDB discovery reached REA's ${String(DEFAULT_NATIVE_COMMAND_TIMEOUT_MS)}ms xcrun deadline. This is not the client deadline.`,
|
||||
remediationAction:
|
||||
"Inspect the retained discovery output and cleanup state, then resolve the xcrun/toolchain condition before retrying. This LLDB discovery deadline has no runtime override; duration_ms controls the later target observation window.",
|
||||
},
|
||||
},
|
||||
);
|
||||
return markers.length === 0
|
||||
? error
|
||||
: error.bindCleanupDependencies(markers);
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
import type { AnalysisError } from "../domain/analysisErrorBase.js";
|
||||
import {
|
||||
AnalysisCancelledError,
|
||||
AnalysisCapabilityUnavailableError,
|
||||
AnalysisResourceConstraintError,
|
||||
AnalysisTimeoutError,
|
||||
} from "../domain/analysisErrorCore.js";
|
||||
import { ProviderAdapterError } from "../domain/providerAdapterError.js";
|
||||
import { NATIVE_MACOS_PROVIDER_IDENTITY as IDENTITY } from "./NativeMacOSProviderMetadata.js";
|
||||
import {
|
||||
DEFAULT_NATIVE_COMMAND_TIMEOUT_MS,
|
||||
fixedNativeAllowance,
|
||||
nativeDeadlineRecovery,
|
||||
} from "../domain/native/nativeDeadlines.js";
|
||||
import {
|
||||
NATIVE_COMMAND_OUTPUT_BUDGET_BYTES,
|
||||
NATIVE_FAILURE_DIAGNOSTIC_BYTES,
|
||||
type NativeCommandFailure,
|
||||
} from "./CommandRunner.js";
|
||||
|
||||
/** Operation-owned text for a command deadline or unavailable requirement. */
|
||||
export interface NativeCommandFailurePresentation {
|
||||
readonly unavailableMessage?: string;
|
||||
readonly timeoutRecovery?: {
|
||||
readonly userMessage: string;
|
||||
readonly remediationAction: string;
|
||||
};
|
||||
}
|
||||
|
||||
/** Translate the command owner's facts once, preserving capture and cleanup authority. */
|
||||
export const translateNativeCommandFailure = (
|
||||
operation: string,
|
||||
failure: NativeCommandFailure,
|
||||
timeoutMs = DEFAULT_NATIVE_COMMAND_TIMEOUT_MS,
|
||||
presentation: NativeCommandFailurePresentation = {},
|
||||
): AnalysisError => {
|
||||
const cleanupOwners = [
|
||||
...failure.cleanupFailures,
|
||||
...(failure.cleanupOwner === undefined
|
||||
? []
|
||||
: [
|
||||
{
|
||||
owner: failure.cleanupOwner,
|
||||
marker: failure.cleanupMarker,
|
||||
reason:
|
||||
failure.cleanupFailure ??
|
||||
"native command cleanup remains incomplete",
|
||||
resources: failure.cleanupResources,
|
||||
},
|
||||
]),
|
||||
];
|
||||
const markers = cleanupOwners.flatMap(({ marker }) =>
|
||||
marker === undefined ? [] : [marker],
|
||||
);
|
||||
const bindCleanup = (error: AnalysisError): AnalysisError =>
|
||||
cleanupOwners.length === 0 ||
|
||||
cleanupOwners.some(({ marker }) => marker === undefined)
|
||||
? error
|
||||
: error.bindCleanupDependencies([...new Set(markers)]);
|
||||
const preparation = failure.preparationFailure;
|
||||
if (preparation !== undefined) {
|
||||
const marker = preparation.cleanupFailure;
|
||||
const compiler = preparation.compilerOutcome;
|
||||
const options = {
|
||||
cause: failure,
|
||||
...(preparation.capturedOutput === undefined
|
||||
? {}
|
||||
: { capturedOutput: preparation.capturedOutput }),
|
||||
...(marker === undefined
|
||||
? {}
|
||||
: {
|
||||
cleanup: {
|
||||
reason: marker.message,
|
||||
resources: marker.cleanupResources,
|
||||
},
|
||||
}),
|
||||
diagnostics: {
|
||||
phase: "process-preparation",
|
||||
tool: failure.tool,
|
||||
reason: failure.reason,
|
||||
detail: preparation.message,
|
||||
...(compiler === undefined
|
||||
? {}
|
||||
: {
|
||||
compiler_outcome: {
|
||||
outcome: compiler.outcome,
|
||||
code: compiler.code,
|
||||
signal: compiler.signal,
|
||||
...(compiler.timeoutMs === undefined
|
||||
? {}
|
||||
: { timeout_ms: compiler.timeoutMs }),
|
||||
},
|
||||
}),
|
||||
},
|
||||
};
|
||||
const error =
|
||||
preparation.cancellation === undefined
|
||||
? new ProviderAdapterError(IDENTITY.id, operation, options)
|
||||
: failure.reason === "timeout"
|
||||
? new AnalysisTimeoutError(operation, timeoutMs, {
|
||||
...options,
|
||||
cause: preparation.cancellation.reason,
|
||||
...(presentation.timeoutRecovery ??
|
||||
nativeDeadlineRecovery({
|
||||
timeoutMs,
|
||||
setting: "native_command_timeout_ms",
|
||||
cliFlag: "--native-command-timeout-ms",
|
||||
defaultMs: DEFAULT_NATIVE_COMMAND_TIMEOUT_MS,
|
||||
phase: "macOS native command deadline",
|
||||
unchanged: `The ${String(NATIVE_COMMAND_OUTPUT_BUDGET_BYTES)}-byte command output allowance and the ${String(NATIVE_FAILURE_DIAGNOSTIC_BYTES)}-byte failure-diagnostic excerpt have no runtime override.`,
|
||||
})),
|
||||
})
|
||||
: new AnalysisCancelledError(operation, {
|
||||
...options,
|
||||
cause: preparation.cancellation.reason,
|
||||
});
|
||||
return bindCleanup(
|
||||
marker === undefined ? error : error.bindCleanupDependencies([marker]),
|
||||
);
|
||||
}
|
||||
const capturedOutput =
|
||||
failure.capture === null
|
||||
? undefined
|
||||
: {
|
||||
stdout: failure.capture.stdout,
|
||||
stderr: failure.capture.stderr,
|
||||
truncated: failure.capture.truncated,
|
||||
stdout_bytes: failure.capture.stdoutBytes,
|
||||
stderr_bytes: failure.capture.stderrBytes,
|
||||
exit_code: failure.capture.exitCode,
|
||||
signal: failure.capture.signal,
|
||||
};
|
||||
const cleanup =
|
||||
failure.cleanupFailure === null
|
||||
? undefined
|
||||
: {
|
||||
reason: failure.cleanupFailure,
|
||||
resources: [...failure.cleanupResources],
|
||||
};
|
||||
const errorOptions = {
|
||||
cause: failure,
|
||||
...(capturedOutput === undefined ? {} : { capturedOutput }),
|
||||
...(cleanup === undefined ? {} : { cleanup }),
|
||||
};
|
||||
if (failure.reason === "unavailable") {
|
||||
const reason =
|
||||
presentation.unavailableMessage ??
|
||||
`${failure.tool} is unavailable through xcrun.`;
|
||||
return bindCleanup(
|
||||
new AnalysisCapabilityUnavailableError(IDENTITY.id, operation, reason, {
|
||||
...errorOptions,
|
||||
userMessage: reason,
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (failure.reason === "cancelled")
|
||||
return bindCleanup(
|
||||
new AnalysisCancelledError(operation, {
|
||||
...errorOptions,
|
||||
...(failure.cancellation === undefined
|
||||
? {}
|
||||
: { cause: failure.cancellation.reason }),
|
||||
}),
|
||||
);
|
||||
if (failure.reason === "timeout")
|
||||
return bindCleanup(
|
||||
new AnalysisTimeoutError(operation, timeoutMs, {
|
||||
...errorOptions,
|
||||
...(presentation.timeoutRecovery ??
|
||||
nativeDeadlineRecovery({
|
||||
timeoutMs,
|
||||
setting: "native_command_timeout_ms",
|
||||
cliFlag: "--native-command-timeout-ms",
|
||||
defaultMs: DEFAULT_NATIVE_COMMAND_TIMEOUT_MS,
|
||||
phase: "macOS native command deadline",
|
||||
unchanged: `The ${String(NATIVE_COMMAND_OUTPUT_BUDGET_BYTES)}-byte command output allowance and the ${String(NATIVE_FAILURE_DIAGNOSTIC_BYTES)}-byte failure-diagnostic excerpt have no runtime override.`,
|
||||
})),
|
||||
}),
|
||||
);
|
||||
if (failure.reason === "output-limit")
|
||||
return bindCleanup(
|
||||
new AnalysisResourceConstraintError(
|
||||
operation,
|
||||
"memory",
|
||||
`Native command output exceeded ${String(NATIVE_COMMAND_OUTPUT_BUDGET_BYTES)} bytes. Complete output is required for analysis. This release has no runtime override. native_command_timeout_ms does not raise this allowance.`,
|
||||
{ max_output_bytes: NATIVE_COMMAND_OUTPUT_BUDGET_BYTES },
|
||||
{
|
||||
...errorOptions,
|
||||
...fixedNativeAllowance(
|
||||
"The native command output capture allowance",
|
||||
"capture",
|
||||
`${String(NATIVE_COMMAND_OUTPUT_BUDGET_BYTES)} bytes`,
|
||||
),
|
||||
},
|
||||
),
|
||||
);
|
||||
return bindCleanup(
|
||||
new ProviderAdapterError(IDENTITY.id, operation, {
|
||||
cause: failure,
|
||||
...errorOptions,
|
||||
diagnostics: {
|
||||
tool: failure.tool,
|
||||
reason: failure.reason,
|
||||
exit_code: failure.capture?.exitCode ?? failure.exitCode,
|
||||
signal: failure.capture?.signal ?? null,
|
||||
stdout_bytes: failure.capture?.stdoutBytes ?? 0,
|
||||
stderr_bytes: failure.capture?.stderrBytes ?? 0,
|
||||
...(failure.detail === null ? {} : { detail: failure.detail }),
|
||||
},
|
||||
}),
|
||||
);
|
||||
};
|
||||
@@ -1,3 +1,4 @@
|
||||
import { translateNativeCommandFailure } from "./NativeCommandAnalysisError.js";
|
||||
import {
|
||||
plistSourceFormat,
|
||||
isBinaryPlistDescription,
|
||||
@@ -57,16 +58,12 @@ import {
|
||||
AnalysisCapabilityUnavailableError,
|
||||
AnalysisInputError,
|
||||
AnalysisOutputError,
|
||||
AnalysisResourceConstraintError,
|
||||
AnalysisUnsupportedTargetError,
|
||||
AnalysisTimeoutError,
|
||||
} from "../domain/analysisErrorCore.js";
|
||||
import {
|
||||
DEFAULT_NATIVE_COMMAND_TIMEOUT_MS,
|
||||
DEFAULT_NATIVE_UI_DEADLINE_MS,
|
||||
MAX_NATIVE_TIMER_MS,
|
||||
fixedNativeAllowance,
|
||||
nativeDeadlineRecovery,
|
||||
readNativeDeadline,
|
||||
} from "../domain/native/nativeDeadlines.js";
|
||||
import { ProviderAdapterError } from "../domain/providerAdapterError.js";
|
||||
@@ -99,8 +96,6 @@ import {
|
||||
} from "../domain/native/nativeInspection.js";
|
||||
import { err, ok, type Result } from "../domain/result.js";
|
||||
import {
|
||||
NATIVE_COMMAND_OUTPUT_BUDGET_BYTES,
|
||||
NATIVE_FAILURE_DIAGNOSTIC_BYTES,
|
||||
NativeCommandFailure,
|
||||
XcrunCommandRunner,
|
||||
failureCapture,
|
||||
@@ -1221,7 +1216,7 @@ class NativeMacOSClient implements AnalysisClient {
|
||||
!options.acceptNonZeroIf(captured.value)
|
||||
)
|
||||
return err(
|
||||
translateCommandFailure(
|
||||
translateNativeCommandFailure(
|
||||
operation,
|
||||
new NativeCommandFailure(
|
||||
tool,
|
||||
@@ -1247,7 +1242,11 @@ class NativeMacOSClient implements AnalysisClient {
|
||||
return captured;
|
||||
}
|
||||
return err(
|
||||
translateCommandFailure(operation, captured.error, options.timeoutMs),
|
||||
translateNativeCommandFailure(
|
||||
operation,
|
||||
captured.error,
|
||||
options.timeoutMs,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1320,187 +1319,10 @@ const isNativeOperation = (
|
||||
): operation is NativeToolName =>
|
||||
NATIVE_TOOL_CONTRACTS.some(({ name }) => name === operation);
|
||||
|
||||
const translateCommandFailure = (
|
||||
operation: NativeToolName,
|
||||
failure: NativeCommandFailure,
|
||||
timeoutMs = DEFAULT_NATIVE_COMMAND_TIMEOUT_MS,
|
||||
): AnalysisError => {
|
||||
const cleanupOwners = [
|
||||
...failure.cleanupFailures,
|
||||
...(failure.cleanupOwner === undefined
|
||||
? []
|
||||
: [
|
||||
{
|
||||
owner: failure.cleanupOwner,
|
||||
marker: failure.cleanupMarker,
|
||||
reason:
|
||||
failure.cleanupFailure ??
|
||||
"native command cleanup remains incomplete",
|
||||
resources: failure.cleanupResources,
|
||||
},
|
||||
]),
|
||||
];
|
||||
const markers = cleanupOwners.flatMap(({ marker }) =>
|
||||
marker === undefined ? [] : [marker],
|
||||
);
|
||||
const bindCleanup = (error: AnalysisError): AnalysisError =>
|
||||
cleanupOwners.length === 0 ||
|
||||
cleanupOwners.some(({ marker }) => marker === undefined)
|
||||
? error
|
||||
: error.bindCleanupDependencies([...new Set(markers)]);
|
||||
const preparation = failure.preparationFailure;
|
||||
if (preparation !== undefined) {
|
||||
const marker = preparation.cleanupFailure;
|
||||
const compiler = preparation.compilerOutcome;
|
||||
const options = {
|
||||
cause: failure,
|
||||
...(preparation.capturedOutput === undefined
|
||||
? {}
|
||||
: { capturedOutput: preparation.capturedOutput }),
|
||||
...(marker === undefined
|
||||
? {}
|
||||
: {
|
||||
cleanup: {
|
||||
reason: marker.message,
|
||||
resources: marker.cleanupResources,
|
||||
},
|
||||
}),
|
||||
diagnostics: {
|
||||
phase: "process-preparation",
|
||||
tool: failure.tool,
|
||||
reason: failure.reason,
|
||||
detail: preparation.message,
|
||||
...(compiler === undefined
|
||||
? {}
|
||||
: {
|
||||
compiler_outcome: {
|
||||
outcome: compiler.outcome,
|
||||
code: compiler.code,
|
||||
signal: compiler.signal,
|
||||
...(compiler.timeoutMs === undefined
|
||||
? {}
|
||||
: { timeout_ms: compiler.timeoutMs }),
|
||||
},
|
||||
}),
|
||||
},
|
||||
};
|
||||
const error =
|
||||
preparation.cancellation === undefined
|
||||
? new ProviderAdapterError(IDENTITY.id, operation, options)
|
||||
: failure.reason === "timeout"
|
||||
? new AnalysisTimeoutError(operation, timeoutMs, {
|
||||
...options,
|
||||
cause: preparation.cancellation.reason,
|
||||
...nativeDeadlineRecovery({
|
||||
timeoutMs,
|
||||
setting: "native_command_timeout_ms",
|
||||
cliFlag: "--native-command-timeout-ms",
|
||||
defaultMs: DEFAULT_NATIVE_COMMAND_TIMEOUT_MS,
|
||||
phase: "macOS native command deadline",
|
||||
unchanged: `The ${String(NATIVE_COMMAND_OUTPUT_BUDGET_BYTES)}-byte command output allowance and the ${String(NATIVE_FAILURE_DIAGNOSTIC_BYTES)}-byte failure-diagnostic excerpt have no runtime override.`,
|
||||
}),
|
||||
})
|
||||
: new AnalysisCancelledError(operation, {
|
||||
...options,
|
||||
cause: preparation.cancellation.reason,
|
||||
});
|
||||
return bindCleanup(
|
||||
marker === undefined ? error : error.bindCleanupDependencies([marker]),
|
||||
);
|
||||
}
|
||||
const capturedOutput =
|
||||
failure.capture === null
|
||||
? undefined
|
||||
: {
|
||||
stdout: failure.capture.stdout,
|
||||
stderr: failure.capture.stderr,
|
||||
truncated: failure.capture.truncated,
|
||||
stdout_bytes: failure.capture.stdoutBytes,
|
||||
stderr_bytes: failure.capture.stderrBytes,
|
||||
exit_code: failure.capture.exitCode,
|
||||
signal: failure.capture.signal,
|
||||
};
|
||||
const cleanup =
|
||||
failure.cleanupFailure === null
|
||||
? undefined
|
||||
: {
|
||||
reason: failure.cleanupFailure,
|
||||
resources: [...failure.cleanupResources],
|
||||
};
|
||||
const errorOptions = {
|
||||
...(capturedOutput === undefined ? {} : { capturedOutput }),
|
||||
...(cleanup === undefined ? {} : { cleanup }),
|
||||
};
|
||||
if (failure.reason === "unavailable") {
|
||||
const reason = `${failure.tool} is unavailable through xcrun.`;
|
||||
return bindCleanup(
|
||||
new AnalysisCapabilityUnavailableError(IDENTITY.id, operation, reason, {
|
||||
...errorOptions,
|
||||
userMessage: reason,
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (failure.reason === "cancelled")
|
||||
return bindCleanup(
|
||||
new AnalysisCancelledError(operation, {
|
||||
...errorOptions,
|
||||
...(failure.cancellation === undefined
|
||||
? {}
|
||||
: { cause: failure.cancellation.reason }),
|
||||
}),
|
||||
);
|
||||
if (failure.reason === "timeout")
|
||||
return bindCleanup(
|
||||
new AnalysisTimeoutError(operation, timeoutMs, {
|
||||
...errorOptions,
|
||||
...nativeDeadlineRecovery({
|
||||
timeoutMs,
|
||||
setting: "native_command_timeout_ms",
|
||||
cliFlag: "--native-command-timeout-ms",
|
||||
defaultMs: DEFAULT_NATIVE_COMMAND_TIMEOUT_MS,
|
||||
phase: "macOS native command deadline",
|
||||
unchanged: `The ${String(NATIVE_COMMAND_OUTPUT_BUDGET_BYTES)}-byte command output allowance and the ${String(NATIVE_FAILURE_DIAGNOSTIC_BYTES)}-byte failure-diagnostic excerpt have no runtime override.`,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
if (failure.reason === "output-limit")
|
||||
return bindCleanup(
|
||||
new AnalysisResourceConstraintError(
|
||||
operation,
|
||||
"memory",
|
||||
`Native command output exceeded ${String(NATIVE_COMMAND_OUTPUT_BUDGET_BYTES)} bytes. Complete output is required for analysis. This release has no runtime override. native_command_timeout_ms does not raise this allowance.`,
|
||||
{ max_output_bytes: NATIVE_COMMAND_OUTPUT_BUDGET_BYTES },
|
||||
{
|
||||
...errorOptions,
|
||||
...fixedNativeAllowance(
|
||||
"The native command output capture allowance",
|
||||
"capture",
|
||||
`${String(NATIVE_COMMAND_OUTPUT_BUDGET_BYTES)} bytes`,
|
||||
),
|
||||
},
|
||||
),
|
||||
);
|
||||
return bindCleanup(
|
||||
new ProviderAdapterError(IDENTITY.id, operation, {
|
||||
cause: failure,
|
||||
...errorOptions,
|
||||
diagnostics: {
|
||||
tool: failure.tool,
|
||||
reason: failure.reason,
|
||||
exit_code: failure.capture?.exitCode ?? failure.exitCode,
|
||||
signal: failure.capture?.signal ?? null,
|
||||
stdout_bytes: failure.capture?.stdoutBytes ?? 0,
|
||||
stderr_bytes: failure.capture?.stderrBytes ?? 0,
|
||||
...(failure.detail === null ? {} : { detail: failure.detail }),
|
||||
},
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const translateCodeSignExitFailure = (
|
||||
capture: NativeCommandCapture,
|
||||
): AnalysisError =>
|
||||
translateCommandFailure(
|
||||
translateNativeCommandFailure(
|
||||
"inspect_signature",
|
||||
new NativeCommandFailure(
|
||||
"codesign",
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { expect, it, vi } from "vitest";
|
||||
|
||||
import { DEFAULT_NATIVE_COMMAND_TIMEOUT_MS } from "../../../../src/domain/native/nativeDeadlines.js";
|
||||
import { thinMach } from "../../../../src/domain/binaryTarget.fixture.js";
|
||||
import { projectAnalysisError } from "../../../../src/domain/analysisErrorProjection.js";
|
||||
import { err } from "../../../../src/domain/result.js";
|
||||
import { LldbCallTracer } from "../../../../src/native/LldbCallTracer.js";
|
||||
import { NativeCommandFailure } from "../../../../src/native/CommandRunner.js";
|
||||
import { NativeMacOSProvider } from "../../../../src/native/NativeMacOSProvider.js";
|
||||
import {
|
||||
NativeFixtureRunner,
|
||||
nativeMachoTargetForFile,
|
||||
} from "../../../fixtures/nativeCommands.js";
|
||||
import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js";
|
||||
|
||||
for (const [reason, code] of [
|
||||
["timeout", "provider_timeout"],
|
||||
["output-limit", "resource_constraint"],
|
||||
["io", "execution_failure"],
|
||||
["unavailable", "capability_unavailable"],
|
||||
["cancelled", "cancelled"],
|
||||
] as const)
|
||||
it(`preserves LLDB discovery ${reason} through the native provider client`, async () => {
|
||||
const directory = await createTestTempDirectory("rea-lldb-discovery-");
|
||||
const target = join(directory, "inert-header.macho");
|
||||
await writeFile(target, thinMach(0xfeedfacf, 0x0100000c));
|
||||
const controller = new AbortController();
|
||||
const abortReason = { requested: "cancel discovery" };
|
||||
const failure = new NativeCommandFailure(
|
||||
"lldb",
|
||||
reason,
|
||||
null,
|
||||
{
|
||||
cause: new Error("Fixture xcrun failure"),
|
||||
...(reason === "cancelled"
|
||||
? { cancellation: { reason: abortReason } }
|
||||
: {}),
|
||||
},
|
||||
{
|
||||
stdout: "fixture discovery output",
|
||||
stderr: "fixture discovery diagnostic",
|
||||
stdoutBytes: 24,
|
||||
stderrBytes: 28,
|
||||
exitCode: null,
|
||||
signal: null,
|
||||
truncated: false,
|
||||
},
|
||||
);
|
||||
const launch = vi.fn(async () => {
|
||||
throw new Error("Discovery failure cannot launch LLDB");
|
||||
});
|
||||
const tracer = new LldbCallTracer({}, launch, async (_tool, signal) => {
|
||||
expect(signal).toBe(controller.signal);
|
||||
if (reason === "cancelled") controller.abort(abortReason);
|
||||
return err(failure);
|
||||
});
|
||||
const client = new NativeMacOSProvider(
|
||||
{},
|
||||
new NativeFixtureRunner(),
|
||||
"darwin",
|
||||
() => tracer,
|
||||
).createClient(await nativeMachoTargetForFile(target));
|
||||
try {
|
||||
const result = await client.execute(
|
||||
"observe_native_calls",
|
||||
{
|
||||
breakpoints: [{ kind: "function", name: "main" }],
|
||||
},
|
||||
{ signal: controller.signal },
|
||||
);
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected discovery failure");
|
||||
expect(launch).not.toHaveBeenCalled();
|
||||
expect(result.error.cause).toBe(
|
||||
reason === "cancelled" ? abortReason : failure,
|
||||
);
|
||||
const projected = projectAnalysisError(result.error);
|
||||
expect(projected).toMatchObject({
|
||||
code,
|
||||
details: {
|
||||
captured_output: {
|
||||
stdout: failure.capture?.stdout,
|
||||
stderr: failure.capture?.stderr,
|
||||
},
|
||||
},
|
||||
});
|
||||
if (reason === "timeout") {
|
||||
expect(projected).toMatchObject({
|
||||
retryable: true,
|
||||
recovery: {
|
||||
kind: "inspect_timeout_state",
|
||||
operation: "observe_native_calls",
|
||||
timeout_ms: DEFAULT_NATIVE_COMMAND_TIMEOUT_MS,
|
||||
},
|
||||
remediation: {
|
||||
action: expect.stringContaining(
|
||||
"discovery deadline has no runtime override",
|
||||
),
|
||||
},
|
||||
});
|
||||
expect(projected.remediation.action).not.toContain(
|
||||
"Increase native_command_timeout_ms",
|
||||
);
|
||||
} else if (reason === "unavailable")
|
||||
expect(projected.message).toBe(
|
||||
"LLDB from Xcode or the Command Line Tools is not available",
|
||||
);
|
||||
else
|
||||
expect(projected.message).not.toBe(
|
||||
"LLDB from Xcode or the Command Line Tools is not available",
|
||||
);
|
||||
} finally {
|
||||
expect((await client.close()).ok).toBe(true);
|
||||
}
|
||||
});
|
||||
@@ -147,7 +147,7 @@ it("retries the tracer-owned resolver supervisor before another trace", async ()
|
||||
const result = await tracer.trace(request);
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) return;
|
||||
expect(result.error._tag).toBe("AnalysisCapabilityUnavailableError");
|
||||
expect(result.error._tag).toBe("ProviderAdapterError");
|
||||
expect(result.error.cleanupIncomplete).toBe(true);
|
||||
expect(result.error.cleanupResources).toEqual(["owned-resolver-process"]);
|
||||
expect(projectAnalysisError(result.error).code).toBe("cleanup_incomplete");
|
||||
|
||||
Reference in New Issue
Block a user