fix(process): preserve selected launch paths and completed command output

This commit is contained in:
morluto committed 2026-10-26 16:46:53 +08:00
1 parent 86e7cd9b48
commit eb51eb6915
12 files changed
+882 -88

No files matched your search

+32 -5
View File
@@ -17,7 +17,7 @@ import {
import { parsePacketOutput } from "./parsePacketOutput.js";
import { randomUUID } from "node:crypto";
import { constants } from "node:fs";
import { access, mkdir, realpath, writeFile } from "node:fs/promises";
import { access, mkdir, writeFile } from "node:fs/promises";
import { isAbsolute, join } from "node:path";
import {
createAnalysisExecution,
@@ -32,6 +32,10 @@ import {
type ArtifactAnalysisRuntime,
} from "../process/ArtifactAnalysisResources.js";
import { readStableArtifact } from "../artifacts/readStableArtifact.js";
import {
assertArtifactCommandPath,
selectArtifactCommandPath,
} from "../process/ArtifactCommandPath.js";
import {
AnalysisError,
type AnalysisCapturedOutput,
@@ -155,7 +159,11 @@ export class TsharkCaptureProvider implements PacketCapturePort {
throw new Error(
`TShark 4.x was not found on PATH; add it to PATH or set absolute ${TSHARK_COMMAND_ENV}.`,
);
const command = await realpath(configured);
const selectedCommand = await selectArtifactCommandPath(
configured,
options?.signal,
);
const command = selectedCommand.resolvedPath;
await access(command, constants.X_OK);
const toolSnapshot = await this.#resources.read(
command,
@@ -167,7 +175,11 @@ export class TsharkCaptureProvider implements PacketCapturePort {
this.environment.REA_TSHARK_PRLIMIT_COMMAND ?? "/usr/bin/prlimit";
if (!isAbsolute(configuredLimiter))
throw new Error("REA_TSHARK_PRLIMIT_COMMAND must be absolute.");
const limiter = await realpath(configuredLimiter);
const selectedLimiter = await selectArtifactCommandPath(
configuredLimiter,
options?.signal,
);
const limiter = selectedLimiter.resolvedPath;
await access(limiter, constants.X_OK);
const limiterIdentity = await this.#resources.read(
limiter,
@@ -175,7 +187,10 @@ export class TsharkCaptureProvider implements PacketCapturePort {
options?.signal,
);
effectiveLimits = await this.#resources.readLimits(limits);
const limitArguments = prlimitArgumentVector(effectiveLimits, command);
const limitArguments = prlimitArgumentVector(
effectiveLimits,
selectedCommand.invocationPath,
);
phase = "workspace";
root = await this.createRuntime();
this.#resources.retain(root, undefined);
@@ -184,9 +199,15 @@ export class TsharkCaptureProvider implements PacketCapturePort {
await mkdir(configurationPath, { mode: 0o700 });
const execute = async (args: readonly string[]) => {
capturedOutput = undefined;
await assertArtifactCommandPath(selectedCommand, operation, {
signal: options?.signal,
});
await assertArtifactCommandPath(selectedLimiter, operation, {
signal: options?.signal,
});
const output = await runOwnedCommand(
{
command: limiter,
command: selectedLimiter.invocationPath,
arguments: [...limitArguments, ...args],
...(root === undefined ? {} : { cwd: root.path }),
expectedCommand: null,
@@ -209,6 +230,12 @@ export class TsharkCaptureProvider implements PacketCapturePort {
},
);
capturedOutput = artifactCommandOutput(output);
await assertArtifactCommandPath(selectedCommand, operation, {
capturedOutput,
});
await assertArtifactCommandPath(selectedLimiter, operation, {
capturedOutput,
});
const current = await this.#resources.readCommandAfterRun(
command,
{ path: limiter, sha256: limiterIdentity.sha256 },
+3 -1
View File
@@ -1404,7 +1404,9 @@ const runLldb = async (
command: executable,
arguments: arguments_,
runId: randomUUID(),
expectedCommand: executable,
// xcrun-selected toolchains may supply interpreter or exec launchers.
// Cleanup still requires native generation and run-token ownership.
expectedCommand: null,
signal: deadline.signal,
hostEnvironment: options.environment,
});
+30 -14
View File
@@ -54,7 +54,11 @@ import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js";
import { artifactFileCancellationCause } from "../artifacts/ArtifactFileOperation.js";
import { ArtifactAnalysisResources } from "../process/ArtifactAnalysisResources.js";
import { readLinuxResourceLimits } from "../process/LinuxResourceLimits.js";
import { realpath } from "node:fs/promises";
import {
assertArtifactCommandPath,
selectArtifactCommandPath,
type ArtifactCommandPath,
} from "../process/ArtifactCommandPath.js";
export const PCAP_PROVIDER_IDENTITY = {
id: "pcap",
@@ -208,7 +212,7 @@ export class PcapProvider {
private readonly launcher: PcapCommandLauncher | undefined;
private readonly wiresharkIdentities = new Map<
string,
{ readonly path: string; readonly identity: WiresharkExecutableIdentity }
ArtifactCommandPath & { readonly identity: WiresharkExecutableIdentity }
>();
private readonly snapshots = new Set<PcapSnapshot>();
private readonly snapshotFailures = new Map<
@@ -282,21 +286,22 @@ export class PcapProvider {
const selected = this.wiresharkIdentities.get(command);
try {
if (selected !== undefined) {
await assertArtifactCommandPath(selected, operation, { signal });
const before = await this.readToolIdentity(
operation,
selected.path,
selected.resolvedPath,
signal,
);
assertWiresharkExecutableIdentity(
operation,
selected.path,
selected.resolvedPath,
selected.identity,
wiresharkExecutableIdentity(before),
);
}
output = await runPcapCommand({
operation,
command: selected?.path ?? command,
command: selected?.invocationPath ?? command,
arguments: args,
limits,
signal,
@@ -327,10 +332,20 @@ export class PcapProvider {
});
commandReturned = true;
if (selected !== undefined) {
const after = await this.readToolIdentity(operation, selected.path);
await assertArtifactCommandPath(selected, operation, {
capturedOutput: {
stdout: output.stdout,
stderr: output.stderr,
truncated: output.cleanup?.capturedOutput.truncated ?? false,
},
});
const after = await this.readToolIdentity(
operation,
selected.resolvedPath,
);
assertWiresharkExecutableIdentity(
operation,
selected.path,
selected.resolvedPath,
selected.identity,
wiresharkExecutableIdentity(after),
{
@@ -403,9 +418,9 @@ export class PcapProvider {
] as const) {
const path = locateWiresharkTool(selected, this.environment);
if (path === undefined) continue;
let resolvedPath: string;
let commandPath: ArtifactCommandPath;
try {
resolvedPath = await realpath(path);
commandPath = await selectArtifactCommandPath(path, signal);
} catch (cause) {
if (
cause instanceof Error &&
@@ -422,11 +437,11 @@ export class PcapProvider {
}
const snapshot = await this.readToolIdentity(
operation,
resolvedPath,
commandPath.resolvedPath,
signal,
);
this.wiresharkIdentities.set(selected.command, {
path: resolvedPath,
...commandPath,
identity: wiresharkExecutableIdentity(snapshot),
});
}
@@ -461,7 +476,8 @@ export class PcapProvider {
tshark_source: selection.tshark.source,
tshark_version: identity.tsharkVersion,
tshark_resolved_path:
this.wiresharkIdentities.get(selection.tshark.command)?.path ?? null,
this.wiresharkIdentities.get(selection.tshark.command)
?.resolvedPath ?? null,
tshark_sha256:
this.wiresharkIdentities.get(selection.tshark.command)?.identity
.sha256 ?? null,
@@ -474,8 +490,8 @@ export class PcapProvider {
capinfos_source: selection.capinfos.source,
capinfos_version: identity.capinfosVersion,
capinfos_resolved_path:
this.wiresharkIdentities.get(selection.capinfos.command)?.path ??
null,
this.wiresharkIdentities.get(selection.capinfos.command)
?.resolvedPath ?? null,
capinfos_sha256:
this.wiresharkIdentities.get(selection.capinfos.command)?.identity
.sha256 ?? null,
+60
View File
@@ -0,0 +1,60 @@
import { realpath } from "node:fs/promises";
import { assertArtifactReadActive } from "../artifacts/ArtifactFileOperation.js";
import type { AnalysisCapturedOutput } from "../domain/analysisErrorBase.js";
import { AnalysisArtifactChangedError } from "../domain/analysisErrorCore.js";
/** The selected launch spelling and the physical file whose bytes are fingerprinted. */
export interface ArtifactCommandPath {
readonly invocationPath: string;
readonly resolvedPath: string;
}
/** Resolve identity without changing the caller's absolute executable selection. */
export const selectArtifactCommandPath = async (
invocationPath: string,
signal?: AbortSignal,
): Promise<ArtifactCommandPath> => {
assertArtifactReadActive(signal);
const resolvedPath = await realpath(invocationPath);
assertArtifactReadActive(signal);
return Object.freeze({ invocationPath, resolvedPath });
};
/** An invocation must still select the admitted physical executable. */
export const assertArtifactCommandPath = async (
selected: ArtifactCommandPath,
operation: string,
options: {
readonly signal?: AbortSignal | undefined;
readonly capturedOutput?: AnalysisCapturedOutput;
} = {},
): Promise<void> => {
assertArtifactReadActive(options.signal);
const changed = (cause?: unknown) =>
new AnalysisArtifactChangedError(
operation,
selected.invocationPath,
"Selected command no longer resolves to its admitted executable.",
{
cause,
...(options.capturedOutput === undefined
? {}
: { capturedOutput: options.capturedOutput }),
},
);
let current: string;
try {
current = await realpath(selected.invocationPath);
} catch (cause: unknown) {
if (
cause instanceof Error &&
"code" in cause &&
(cause.code === "ENOENT" || cause.code === "ENOTDIR")
)
throw changed(cause);
throw cause;
}
if (current !== selected.resolvedPath) throw changed();
assertArtifactReadActive(options.signal);
};
+32 -5
View File
@@ -19,7 +19,7 @@ import {
} from "../process/LinuxResourceLimits.js";
import { randomUUID } from "node:crypto";
import { constants } from "node:fs";
import { access, realpath, writeFile } from "node:fs/promises";
import { access, writeFile } from "node:fs/promises";
import { basename, isAbsolute, join } from "node:path";
import {
createAnalysisExecution,
@@ -34,6 +34,10 @@ import {
type ArtifactAnalysisRuntime,
} from "../process/ArtifactAnalysisResources.js";
import { readStableArtifact } from "../artifacts/readStableArtifact.js";
import {
assertArtifactCommandPath,
selectArtifactCommandPath,
} from "../process/ArtifactCommandPath.js";
import {
AnalysisError,
type AnalysisCapturedOutput,
@@ -147,7 +151,11 @@ export class SigrokRecordingProvider implements SignalRecordingPort {
throw new Error(
"Supply absolute REA_SIGROK_COMMAND for sigrok-cli 0.7.2 with libsigrokdecode.",
);
const command = await realpath(configured);
const selectedCommand = await selectArtifactCommandPath(
configured,
options?.signal,
);
const command = selectedCommand.resolvedPath;
await access(command, constants.X_OK);
const tool = await this.#resources.read(
command,
@@ -158,7 +166,11 @@ export class SigrokRecordingProvider implements SignalRecordingPort {
this.environment.REA_SIGROK_PRLIMIT_COMMAND ?? "/usr/bin/prlimit";
if (!isAbsolute(configuredLimiter))
throw new Error("REA_SIGROK_PRLIMIT_COMMAND must be absolute.");
const limiter = await realpath(configuredLimiter);
const selectedLimiter = await selectArtifactCommandPath(
configuredLimiter,
options?.signal,
);
const limiter = selectedLimiter.resolvedPath;
await access(limiter, constants.X_OK);
const limiterIdentity = await this.#resources.read(
limiter,
@@ -166,7 +178,10 @@ export class SigrokRecordingProvider implements SignalRecordingPort {
options?.signal,
);
effectiveLimits = await this.#resources.readLimits(LIMITS);
const limitArguments = prlimitArgumentVector(effectiveLimits, command);
const limitArguments = prlimitArgumentVector(
effectiveLimits,
selectedCommand.invocationPath,
);
phase = "workspace";
root = await this.createRuntime();
this.#resources.retain(root, undefined);
@@ -180,9 +195,15 @@ export class SigrokRecordingProvider implements SignalRecordingPort {
selection?: SigrokSelection,
) => {
capturedOutput = undefined;
await assertArtifactCommandPath(selectedCommand, operation, {
signal: options?.signal,
});
await assertArtifactCommandPath(selectedLimiter, operation, {
signal: options?.signal,
});
const output = await runOwnedCommand(
{
command: limiter,
command: selectedLimiter.invocationPath,
arguments: [...limitArguments, ...args],
...(root === undefined ? {} : { cwd: root.path }),
expectedCommand: null,
@@ -202,6 +223,12 @@ export class SigrokRecordingProvider implements SignalRecordingPort {
},
);
capturedOutput = artifactCommandOutput(output);
await assertArtifactCommandPath(selectedCommand, operation, {
capturedOutput,
});
await assertArtifactCommandPath(selectedLimiter, operation, {
capturedOutput,
});
const current = await this.#resources.readCommandAfterRun(
command,
{ path: limiter, sha256: limiterIdentity.sha256 },
-7
View File
@@ -284,7 +284,6 @@ it.each([
"-e",
`process.stdout.write(${JSON.stringify(stdout)}); process.stderr.write(${JSON.stringify(stderr)});`,
],
expectedCommand: null,
});
},
);
@@ -497,7 +496,6 @@ it.each([
"-e",
`process.stdout.write(${JSON.stringify(selected ? stdout : "1.0.42\n")}); process.stderr.write(${JSON.stringify(selected ? stderr : "")}); process.exit(${String(selected ? exitCode : 0)});`,
],
expectedCommand: null,
});
},
);
@@ -564,7 +562,6 @@ it("retries a retained input descriptor before admitting the next WABT operation
"-e",
`process.stdout.write(${JSON.stringify(stdout)}); process.exit(0);`,
],
expectedCommand: null,
});
return launched;
},
@@ -768,7 +765,6 @@ const launchWabtOutcome = async (
hostEnvironment: nodeHostEnvironment(input.hostEnvironment),
command: process.execPath,
arguments: ["-e", script],
expectedCommand: null,
});
if (!version && mode === "cancelled") {
setImmediate(() => controller.abort());
@@ -793,7 +789,6 @@ it("enforces a selected command deadline and completes a corrected retry after c
"-e",
'setTimeout(() => process.stdout.write("1.0.42\\n"), 250)',
],
expectedCommand: null,
});
}
return launchWabtOutcome(input, "success", {
@@ -1237,7 +1232,6 @@ it("reports WABT output beyond the budget as a resource constraint with a bounde
hostEnvironment: nodeHostEnvironment(input.hostEnvironment),
command: process.execPath,
arguments: ["-e", script],
expectedCommand: null,
});
},
);
@@ -1302,7 +1296,6 @@ it("does not attribute wasm-objdump output to a wasm2wat launch failure", async
hostEnvironment: nodeHostEnvironment(input.hostEnvironment),
command: process.execPath,
arguments: ["-e", script],
expectedCommand: null,
});
},
);
+36 -44
View File
@@ -1,7 +1,8 @@
import { randomUUID, createHash } from "node:crypto";
import { access, realpath, writeFile } from "node:fs/promises";
import { access, writeFile } from "node:fs/promises";
import { constants } from "node:fs";
import { isAbsolute, join } from "node:path";
import { appendCallerPath } from "../filesystem/callerPath.js";
import type {
WasmArtifactExecutionOptions,
WasmArtifactExecution,
@@ -45,6 +46,12 @@ import {
import { err, ok, type Result } from "../domain/result.js";
import { runOwnedCommand } from "../process/OwnedCommand.js";
import { PrivateRuntimeRoot } from "../process/PrivateRuntimeRoot.js";
import { artifactCommandOutput } from "../process/ArtifactAnalysisResources.js";
import {
assertArtifactCommandPath,
selectArtifactCommandPath,
type ArtifactCommandPath,
} from "../process/ArtifactCommandPath.js";
import { WabtRuntime } from "./WabtRuntime.js";
import {
WABT_PROVIDER_IDENTITY,
@@ -212,18 +219,21 @@ export class WabtArtifactProvider implements WasmArtifactPort {
sha256: string;
version_banner: string;
arguments: string[];
selection: ArtifactCommandPath;
}[] = [];
for (const tool of [
"wasm-validate",
"wasm-objdump",
"wasm2wat",
] as const) {
const path = await realpath(
join(
const selection = await selectArtifactCommandPath(
appendCallerPath(
directory,
`${tool}${process.platform === "win32" ? ".exe" : ""}`,
),
options?.signal,
);
const path = selection.resolvedPath;
await access(path, constants.X_OK);
const binary = await readStableArtifact(
path,
@@ -235,6 +245,7 @@ export class WabtArtifactProvider implements WasmArtifactPort {
commands.push({
tool,
path,
selection,
sha256: binary.sha256,
version_banner: "",
arguments:
@@ -265,6 +276,7 @@ export class WabtArtifactProvider implements WasmArtifactPort {
OPERATION,
input.path,
"Private WASM snapshot bytes changed.",
{ capturedOutput },
);
};
const execute = async (
@@ -273,13 +285,16 @@ export class WabtArtifactProvider implements WasmArtifactPort {
acceptNonZeroExit = false,
) => {
capturedOutput = undefined;
await assertArtifactCommandPath(command.selection, OPERATION, options);
await verifySnapshot();
const snapshot = await runOwnedCommand(
{
command: command.path,
command: command.selection.invocationPath,
arguments: args,
cwd: root.path,
expectedCommand: command.path,
// Trusted caller tools may be interpreter shims; run-token, parent
// and process-group identity own cleanup after their exec handoff.
expectedCommand: null,
runId: `rea-wabt-${randomUUID()}`,
hostEnvironment: this.environment,
},
@@ -295,6 +310,11 @@ export class WabtArtifactProvider implements WasmArtifactPort {
...(this.launcher === undefined ? {} : { launcher: this.launcher }),
},
);
capturedOutput = artifactCommandOutput(snapshot);
await assertArtifactCommandPath(command.selection, OPERATION, {
...(options?.signal === undefined ? {} : { signal: options.signal }),
capturedOutput,
});
const current = await readStableArtifact(
command.path,
WABT_LIMITS.toolBytes,
@@ -306,6 +326,7 @@ export class WabtArtifactProvider implements WasmArtifactPort {
throw new AnalysisOutputError(
OPERATION,
"Configured WABT executable changed during inspection.",
{ capturedOutput },
);
await verifySnapshot();
return snapshot;
@@ -347,10 +368,13 @@ export class WabtArtifactProvider implements WasmArtifactPort {
source: "https://github.com/WebAssembly/wabt",
release: WABT_VERSION,
source_commit: "ff0ef7e0009402740c805a9744c09b05be063e48",
commands,
commands: commands.map(
({ selection: _selection, ...command }) => command,
),
timeout_ms: commandTimeoutMs,
output_budget_bytes: WABT_LIMITS.outputBytes,
};
capturedOutput = undefined;
phase = "artifact";
const selected = await readSelectedInput(input.path, (snapshot) => {
const observed = {
@@ -457,11 +481,7 @@ export class WabtArtifactProvider implements WasmArtifactPort {
],
},
),
capturedOutput: {
stdout: validation.stdout.text,
stderr: validation.stderr.text,
truncated: false,
},
capturedOutput: artifactCommandOutput(validation),
},
[
{
@@ -488,11 +508,7 @@ export class WabtArtifactProvider implements WasmArtifactPort {
OPERATION,
"Unexpected WABT diagnostic output.",
{
capturedOutput: {
stdout: output.stdout.text,
stderr: output.stderr.text,
truncated: false,
},
capturedOutput: artifactCommandOutput(output),
},
);
if (warnings.length > 0)
@@ -500,23 +516,14 @@ export class WabtArtifactProvider implements WasmArtifactPort {
};
retainWarnings("wasm-validate", validation);
const dump = await execute(objdump, objdump.arguments);
capturedOutput = {
stdout: dump.stdout.text,
stderr: dump.stderr.text,
truncated: false,
};
retainWarnings("wasm-objdump", dump);
const wat = await execute(decode, decode.arguments);
capturedOutput = {
stdout: wat.stdout.text,
stderr: wat.stderr.text,
truncated: false,
};
retainWarnings("wasm2wat", wat);
if (!wat.stdout.text.startsWith("(module"))
throw new AnalysisOutputError(
OPERATION,
"Unexpected WABT decoding output.",
{ capturedOutput },
);
const {
imports,
@@ -545,11 +552,7 @@ export class WabtArtifactProvider implements WasmArtifactPort {
optionalLimitations.push(
"WABT reported optional custom-section interpretation warnings; exact warning output is retained in custom_section_warnings.",
);
capturedOutput = {
stdout: dump.stdout.text,
stderr: dump.stderr.text,
truncated: false,
};
capturedOutput = artifactCommandOutput(dump);
const { sections, headers, details } = parseWabtOutput(
dump.stdout.text,
selected.bytes.length,
@@ -606,6 +609,7 @@ export class WabtArtifactProvider implements WasmArtifactPort {
{
commandTimeoutMs,
inputField: artifactInput.field,
observedOutput: capturedOutput,
...(versionProbe === undefined ? {} : { versionProbe }),
},
);
@@ -614,19 +618,7 @@ export class WabtArtifactProvider implements WasmArtifactPort {
if (partial !== undefined) failure.retainPartialObservation(partial);
}
runtime?.retainCommandFailure(cause, failure, this.#resources);
result = err(
failure instanceof AnalysisOutputError &&
failure.capturedOutput === undefined &&
capturedOutput !== undefined
? new AnalysisOutputError(OPERATION, failure.reason, {
cause: failure,
capturedOutput,
cleanup: failure.cleanup,
diagnostics: failure.diagnostics,
partialObservation: failure.partialObservation,
}).bindCleanupDependencies([failure])
: failure,
);
result = err(failure);
}
if (runtime !== undefined) {
const cleanup = await this.#resources.release({
+19 -11
View File
@@ -6,7 +6,10 @@ import {
} from "../artifacts/ArtifactFileOperation.js";
import { boundedUtf8Text } from "../process/boundedUtf8Text.js";
import type { ProviderProcessSnapshot } from "../process/ProviderProcess.js";
import { AnalysisError } from "../domain/analysisErrorBase.js";
import {
AnalysisError,
type AnalysisCapturedOutput,
} from "../domain/analysisErrorBase.js";
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import {
AnalysisCancelledError,
@@ -48,6 +51,7 @@ type WabtFailureContext = {
readonly commandTimeoutMs: number;
readonly versionProbe?: WabtVersionProbe;
readonly inputField?: readonly (string | number)[];
readonly observedOutput?: AnalysisCapturedOutput | undefined;
};
/** Configuration is captured by the server; fixing files and changing its environment are distinct repairs. */
@@ -99,8 +103,12 @@ const classifyWabtFailure = (
if (cause instanceof AnalysisError) return cause;
const artifactCleanup =
cause instanceof ArtifactReaderFailure ? cause.cleanup : undefined;
const cleanupOptions =
artifactCleanup === undefined ? {} : { cleanup: artifactCleanup };
const artifactOptions = {
...(artifactCleanup === undefined ? {} : { cleanup: artifactCleanup }),
...(execution?.observedOutput === undefined
? {}
: { capturedOutput: execution.observedOutput }),
};
const reason = cause instanceof Error ? cause.message : String(cause);
if (
(cause instanceof ArtifactReaderFailure && cause.reason === "cancelled") ||
@@ -111,7 +119,7 @@ const classifyWabtFailure = (
cause instanceof ArtifactReaderFailure
? artifactFileCancellationCause(cause)
: cause,
...cleanupOptions,
...artifactOptions,
});
if (cause instanceof OwnedCommandFailure) {
const capturedOutput =
@@ -224,7 +232,7 @@ const classifyWabtFailure = (
cause.message,
{
cause,
...cleanupOptions,
...artifactOptions,
userMessage: `Configured WABT tools exceed the ${String(WABT_LIMITS.toolBytes)}-byte tool allowance. This release has no runtime override.`,
remediationAction: `The WABT tool allowance is ${String(WABT_LIMITS.toolBytes)} bytes. This release has no runtime override. command_timeout_ms and REA_WABT_BIN_DIRECTORY do not raise it. Select tools within the allowance. Do not repeat the unchanged call.`,
},
@@ -236,7 +244,7 @@ const classifyWabtFailure = (
reason,
{
cause,
...cleanupOptions,
...artifactOptions,
userMessage: reason,
remediationAction: wabtConfigurationRecoveryAction(cause),
},
@@ -254,7 +262,7 @@ const classifyWabtFailure = (
{ maximum_input_bytes: WABT_LIMITS.inputBytes },
{
cause,
...cleanupOptions,
...artifactOptions,
retryable: false,
remediationAction: `Select a suitable stable artifact within the reported ${String(WABT_LIMITS.inputBytes)}-byte input byte allowance. This release has no runtime override. command_timeout_ms does not raise it. Do not truncate required bytes; retrying the unchanged oversized input will fail again.`,
},
@@ -268,7 +276,7 @@ const classifyWabtFailure = (
"inspect_wasm_artifact",
path,
reason,
{ cause, ...cleanupOptions },
{ cause, ...artifactOptions },
);
if (
phase === "artifact" &&
@@ -280,12 +288,12 @@ const classifyWabtFailure = (
"inspect_wasm_artifact",
path,
cause.code,
{ cause, ...cleanupOptions },
{ cause, ...artifactOptions },
);
if (phase === "artifact")
return new AnalysisInputError(
"inspect_wasm_artifact",
{ cause, ...cleanupOptions },
{ cause, ...artifactOptions },
[
{
path: [...(execution?.inputField ?? ["path"])],
@@ -296,6 +304,6 @@ const classifyWabtFailure = (
);
return new AnalysisOutputError("inspect_wasm_artifact", reason, {
cause,
...cleanupOptions,
...artifactOptions,
});
};
@@ -0,0 +1,323 @@
import { createHash } from "node:crypto";
import {
access,
chmod,
mkdir,
readFile,
readdir,
symlink,
writeFile,
} from "node:fs/promises";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { expect } from "vitest";
import { z } from "zod";
import { analysisErrorProjectionSchema } from "../../../src/contracts/errorSchemas.js";
import { parseEvidence } from "../../../src/domain/evidence.js";
import { wasmArtifactSchema } from "../../../src/domain/wasm/wasmArtifact.js";
import { observeProcessStartIdentity } from "../../../src/process/ProcessOwnershipObservation.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { cliTest } from "../../support/cli/cliFixture.js";
const tools = ["wasm-validate", "wasm-objdump", "wasm2wat"] as const;
const commandTraceSchema = z.object({
tool: z.enum(tools),
pid: z.int().positive(),
cwd: z.string(),
run_id: z.string(),
version: z.boolean(),
snapshot_sha256: z.string().nullable(),
});
const fixtureUrl = (name: string) =>
fileURLToPath(new URL(`../../fixtures/wasm/${name}`, import.meta.url));
/** Real shell/Node launchers with authored WABT output, not installed WABT engines. */
const createFixture = async () => {
const root = await createTestTempDirectory("rea-wabt-launcher-");
const artifact = join(root, "selected module.wasm");
const hold = join(root, "hold-decoder");
const trace = join(root, "commands.jsonl");
const scratch = join(root, "scratch");
const runner = join(root, "wabt-runner.cjs");
await mkdir(scratch);
const bytes = Buffer.from(
"0061736d01000000010401600000020b0103656e7603666f6f0000030201000503010001071002066d656d6f727902000372756e00010a0601040001010b000c04746573747061796c6f6164",
"hex",
);
await writeFile(artifact, bytes);
await writeFile(
runner,
`
const fs = require("node:fs");
const tool = process.argv[2];
const version = process.argv[3] === "--version";
const snapshot_sha256 = version ? null : require("node:crypto").createHash("sha256").update(fs.readFileSync("module.wasm")).digest("hex");
fs.appendFileSync(process.env.REA_WABT_TEST_TRACE, JSON.stringify({ tool, pid: process.pid, version, cwd: process.cwd(), run_id: process.env.REA_PROCESS_RUN_ID, snapshot_sha256 }) + "\\n");
if (version) {
process.stdout.write("1.0.42\\n");
if (tool === "wasm-validate" && process.env.REA_WABT_TEST_REVOKE_COMMAND)
fs.chmodSync(process.env.REA_WABT_TEST_REVOKE_COMMAND, 0);
}
else if (tool === "wasm-objdump") { process.stdout.write(fs.readFileSync(process.env.REA_WABT_TEST_DUMP)); }
else if (tool === "wasm2wat") {
const decode = () => process.stdout.write(fs.readFileSync(process.env.REA_WABT_TEST_WAT));
if (fs.existsSync(process.env.REA_WABT_TEST_HOLD)) {
setTimeout(decode, 4000);
} else { decode(); }
}
`,
);
const install = async (directory: string, decoy = false) => {
await mkdir(directory, { recursive: true });
for (const tool of tools)
await writeFile(
join(directory, tool),
decoy
? "#!/bin/sh\nprintf 'unselected-decoy\\n'\n"
: `#!/bin/sh\nexec "$REA_WABT_TEST_NODE" "$REA_WABT_TEST_RUNNER" "${tool}" "$@"\n`,
{ mode: 0o700 },
);
};
const environment = {
...process.env,
REA_WABT_TEST_NODE: process.execPath,
REA_WABT_TEST_RUNNER: runner,
REA_WABT_TEST_DUMP: fixtureUrl("objdump.txt"),
REA_WABT_TEST_WAT: fixtureUrl("decoded.wat"),
REA_WABT_TEST_HOLD: hold,
REA_WABT_TEST_TRACE: trace,
TMPDIR: scratch,
TMP: scratch,
TEMP: scratch,
};
return {
root,
artifact,
bytes,
hold,
trace,
scratch,
install,
environment,
};
};
cliTest.skipIf(process.platform === "win32")(
"cleans up real selected WABT interpreter shims after a command deadline",
async ({ cli }) => {
const fixture = await createFixture();
const selected = join(fixture.root, "selected tools");
await fixture.install(selected);
const invoke = (timeout: number) =>
cli.run({
arguments: [
"inspect-wasm-artifact",
fixture.artifact,
"--command-timeout-ms",
String(timeout),
"--json",
],
environment: {
...fixture.environment,
REA_WABT_BIN_DIRECTORY: selected,
},
});
const control = await invoke(10_000);
expect(control.exitCode, `${control.stderr}\n${control.stdout}`).toBe(0);
const evidence = parseEvidence(control.json);
const report = wasmArtifactSchema.parse(evidence.normalized_result);
expect(report.artifact.sha256).toBe(
createHash("sha256").update(fixture.bytes).digest("hex"),
);
expect(report.validation).toBe("valid");
const trace = (await readFile(fixture.trace, "utf8"))
.trim()
.split("\n")
.map((line) => commandTraceSchema.parse(JSON.parse(line)));
expect(trace.length).toBeGreaterThan(0);
expect(new Set(trace.map(({ cwd }) => cwd)).size).toBe(1);
for (const command of trace) {
expect(command.cwd.startsWith(join(fixture.scratch, "rea-wabt-"))).toBe(
true,
);
expect(command.run_id).toMatch(/^rea-wabt-/u);
expect(command.snapshot_sha256).toBe(
command.version ? null : report.artifact.sha256,
);
}
expect(await readdir(fixture.scratch)).toEqual([]);
await writeFile(fixture.hold, "hold this invocation");
try {
const invocation = await invoke(2_000);
expect(invocation.exitCode, invocation.stderr).toBe(1);
const failure = analysisErrorProjectionSchema.parse(invocation.json);
expect(failure, invocation.stdout).toMatchObject({
code: "provider_timeout",
details: {
partial_observation: {
normalized_result: {
validation: "valid",
artifact: { sha256: report.artifact.sha256 },
},
},
},
});
expect(await readdir(fixture.scratch)).toEqual([]);
expect((await readFile(fixture.artifact)).equals(fixture.bytes)).toBe(
true,
);
} finally {
// The authored shim self-exits even on the unfixed cleanup-refusal path.
// Observe its actual process lifetime; token-owned cleanup can use SIGKILL.
const commands = (await readFile(fixture.trace, "utf8"))
.trim()
.split("\n")
.map((line) => commandTraceSchema.parse(JSON.parse(line)));
const decoder = commands.findLast(
(command) => command.tool === "wasm2wat" && !command.version,
);
if (decoder === undefined)
throw new Error("No decoder process was observed");
await expect
.poll(
() =>
observeProcessStartIdentity(decoder.pid, undefined, decoder.run_id),
{ timeout: 6000 },
)
.toBeUndefined();
}
},
);
cliTest.skipIf(process.platform === "win32" || process.getuid?.() === 0)(
"retains completed WABT version output when its post-command fingerprint read is denied",
async ({ cli }) => {
const fixture = await createFixture();
const selected = join(fixture.root, "tools");
await fixture.install(selected);
const revoked = join(selected, "wasm-validate");
try {
const invocation = await cli.run({
arguments: ["inspect-wasm-artifact", fixture.artifact, "--json"],
environment: {
...fixture.environment,
REA_WABT_BIN_DIRECTORY: selected,
REA_WABT_TEST_REVOKE_COMMAND: revoked,
},
});
expect(invocation.exitCode, invocation.stderr).toBe(1);
const failure = analysisErrorProjectionSchema.parse(invocation.json);
expect(failure, invocation.stdout).toMatchObject({
code: "capability_unavailable",
details: {
captured_output: {
stdout: "1.0.42\n",
stderr: "",
truncated: false,
exit_code: 0,
signal: null,
},
},
});
expect((await readFile(fixture.artifact)).equals(fixture.bytes)).toBe(
true,
);
expect(await readdir(fixture.scratch)).toEqual([]);
} finally {
await chmod(revoked, 0o700);
}
},
);
cliTest.skipIf(process.platform === "win32")(
"launches selected WABT symlinks with their own sibling resources and fingerprints canonical tools",
async ({ cli }) => {
const fixture = await createFixture();
const targets = join(fixture.root, "canonical tools");
const selected = join(fixture.root, "selected links");
await fixture.install(targets);
await mkdir(selected);
await writeFile(
join(selected, "wabt-environment"),
"# selected installation resource\n",
);
for (const tool of tools) {
const target = join(targets, tool);
const script = await readFile(target, "utf8");
await writeFile(
target,
script.replace(
"#!/bin/sh\n",
'#!/bin/sh\n. "${0%/*}/wabt-environment"\n',
),
);
await symlink(target, join(selected, tool));
}
const invocation = await cli.run({
arguments: ["inspect-wasm-artifact", fixture.artifact, "--json"],
environment: { ...fixture.environment, REA_WABT_BIN_DIRECTORY: selected },
});
expect(
invocation.exitCode,
`${invocation.stderr}\n${invocation.stdout}`,
).toBe(0);
const report = wasmArtifactSchema.parse(
parseEvidence(invocation.json).normalized_result,
);
expect(report.tool_profile.commands.map(({ path }) => path)).toEqual(
tools.map((tool) => join(targets, tool)),
);
for (const command of report.tool_profile.commands)
expect(command.sha256).toBe(
createHash("sha256")
.update(await readFile(command.path))
.digest("hex"),
);
expect((await readFile(fixture.artifact)).equals(fixture.bytes)).toBe(true);
expect(await readdir(fixture.scratch)).toEqual([]);
},
);
cliTest.skipIf(process.platform === "win32")(
"selects a WABT directory through literal symlink and parent traversal",
async ({ cli }) => {
const fixture = await createFixture();
const intended = join(fixture.root, "intended");
await mkdir(join(intended, "nested"), { recursive: true });
await symlink(join(intended, "nested"), join(fixture.root, "alias"));
const selected = join(intended, "tools");
await fixture.install(selected);
await fixture.install(join(fixture.root, "tools"), true);
const invocation = await cli.run({
arguments: ["inspect-wasm-artifact", fixture.artifact, "--json"],
environment: {
...fixture.environment,
REA_WABT_BIN_DIRECTORY: `${fixture.root}/alias/../tools`,
},
});
expect(
invocation.exitCode,
`${invocation.stderr}\n${invocation.stdout}`,
).toBe(0);
const report = wasmArtifactSchema.parse(
parseEvidence(invocation.json).normalized_result,
);
expect(report.tool_profile.commands.map(({ path }) => path)).toEqual(
tools.map((tool) => join(selected, tool)),
);
for (const command of report.tool_profile.commands)
expect(command.sha256).toBe(
createHash("sha256")
.update(await readFile(command.path))
.digest("hex"),
);
expect(report.artifact.path).toBe(fixture.artifact);
expect((await readFile(fixture.artifact)).equals(fixture.bytes)).toBe(true);
expect(await readdir(fixture.scratch)).toEqual([]);
await expect(
access(join(fixture.root, "tools", "wasm-validate")),
).resolves.toBeUndefined();
},
);
@@ -7,8 +7,19 @@
* refusal paths, and cancellation before spawn.
*/
import { createHash } from "node:crypto";
import { open, readFile, realpath, symlink, writeFile } from "node:fs/promises";
import {
chmod,
mkdir,
open,
readFile,
realpath,
symlink,
unlink,
writeFile,
} from "node:fs/promises";
import { join } from "node:path";
import { execFile } from "node:child_process";
import { promisify } from "node:util";
import { expect, it, onTestFinished, vi } from "vitest";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
@@ -47,6 +58,72 @@ const providerOver = (): PcapProvider => {
return provider;
};
it.skipIf(isWindows)(
"preserves selected Wireshark wrappers and re-admits changed symlink targets after success",
async () => {
const directory = await createTestTempDirectory(
"rea-pcap-selected-wrapper-",
);
const bin = join(directory, "bin"),
libexec = join(directory, "libexec");
await mkdir(bin);
await mkdir(libexec);
const source = join(libexec, "wrapper"),
replacement = join(libexec, "replacement");
const script =
'#!/bin/sh\nselected="${0%/*}/${0##*/}"\nIFS= read -r banner < "$selected.version" || exit 19\nprintf "%s\\n" "$banner"\nif [ -f "$selected.retarget" ]; then\n IFS= read -r target < "$selected.retarget"\n /bin/ln -sf "$target" "$0"\nfi\n';
for (const path of [source, replacement]) {
await writeFile(path, script);
await chmod(path, 0o755);
}
const tshark = join(bin, "tshark"),
capinfos = join(bin, "capinfos");
for (const [path, name] of [
[tshark, "TShark"],
[capinfos, "Capinfos"],
] as const) {
await symlink(source, path);
await writeFile(`${path}.version`, `${name} (Wireshark) 4.6.6-stub\n`);
}
expect(
(await promisify(execFile)(tshark, ["--version"])).stdout.trim(),
).toBe("TShark (Wireshark) 4.6.6-stub");
const provider = new PcapProvider({
environment: {
REA_TSHARK_COMMAND: tshark,
REA_CAPINFOS_COMMAND: capinfos,
},
});
onTestFinished(() => provider.close());
const request = { operation: "inspect_pcap_client", input: {} } as const;
const first = await evidenceResult(provider, request);
expect(first.client).toMatchObject({
tshark_command: tshark,
tshark_resolved_path: await realpath(source),
capinfos_command: capinfos,
});
await writeFile(`${tshark}.retarget`, `${replacement}\n`);
const changed = await provider.execute(request);
if (changed.ok) throw new Error("Changed selected wrapper was accepted");
expect(projectAnalysisError(changed.error)).toMatchObject({
code: "artifact_changed",
details: {
path: tshark,
captured_output: { stdout: "TShark (Wireshark) 4.6.6-stub\n" },
},
});
await unlink(`${tshark}.retarget`);
const fresh = await evidenceResult(provider, request);
expect(fresh.client).toMatchObject({
tshark_command: tshark,
tshark_resolved_path: await realpath(replacement),
});
expect(first.client).toMatchObject({
tshark_resolved_path: await realpath(source),
});
},
);
const evidenceResult = async (
provider: PcapProvider,
request: PcapRequest,
@@ -0,0 +1,119 @@
import {
chmod,
mkdir,
readdir,
realpath,
symlink,
unlink,
writeFile,
} from "node:fs/promises";
import { join } from "node:path";
import { expect, it, onTestFinished } from "vitest";
import { PacketCaptureService } from "../../../src/application/captures/PacketCaptureService.js";
import { SignalRecordingService } from "../../../src/application/signals/SignalRecordingService.js";
import { TsharkCaptureProvider } from "../../../src/captures/TsharkCaptureProvider.js";
import { SigrokRecordingProvider } from "../../../src/signals/SigrokRecordingProvider.js";
import { readStableArtifact } from "../../../src/artifacts/readStableArtifact.js";
import { PrivateRuntimeRoot } from "../../../src/process/PrivateRuntimeRoot.js";
import type { LinuxResourceLimits } from "../../../src/process/LinuxResourceLimits.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
// Real children and filesystem, with simulated Linux admission/limits. These
// scripts do not establish native TShark, sigrok or prlimit behavior.
it.skipIf(process.platform === "win32").each(["tshark", "sigrok"] as const)(
"executes selected %s and limiter wrappers, retaining identity failure output after success",
async (kind) => {
const directory = await createTestTempDirectory("rea-selected-producer-");
const bin = join(directory, "bin"),
libexec = join(directory, "libexec");
await mkdir(bin);
await mkdir(libexec);
const command = join(bin, kind),
limiter = join(bin, "prlimit");
const commandSource = join(libexec, "producer"),
limiterSource = join(libexec, "limiter");
const replacement = join(libexec, "replacement");
const script =
'#!/bin/sh\nselected="${0%/*}/${0##*/}"\nIFS= read -r role < "$selected.role" || exit 19\nif [ "$role" = limiter ]; then\n if [ -f "$selected.retarget" ]; then\n IFS= read -r target < "$selected.retarget"\n /bin/ln -sf "$target" "$0"\n fi\n while [ "$1" != -- ]; do shift; done\n shift\n exec "$@"\nfi\nif [ "$1" = --version ]; then\n if [ "$role" = tshark ]; then printf "TShark (Wireshark) 4.6.9\\n"; else printf "sigrok-cli 0.7.2\\nlibsigrokdecode 0.5.3\\n"; fi\nelse\n if [ "$role" = tshark ]; then printf "P\\tInternet Protocol\\tip\\n"; else printf "Supported protocol decoders:\\n uart UART\\n"; fi\nfi\n';
for (const path of [commandSource, limiterSource, replacement]) {
await writeFile(path, script);
await chmod(path, 0o755);
}
await symlink(commandSource, command);
await symlink(limiterSource, limiter);
await writeFile(`${command}.role`, `${kind}\n`);
await writeFile(`${limiter}.role`, "limiter\n");
const environment = {
...process.env,
...(kind === "tshark"
? { REA_TSHARK_COMMAND: command, REA_TSHARK_PRLIMIT_COMMAND: limiter }
: { REA_SIGROK_COMMAND: command, REA_SIGROK_PRLIMIT_COMMAND: limiter }),
};
const runtime = () =>
PrivateRuntimeRoot.create({ parent: directory, prefix: "workspace-" });
const limits = async (configured: LinuxResourceLimits) => configured;
const provider =
kind === "tshark"
? new TsharkCaptureProvider(
environment,
undefined,
runtime,
"linux",
readStableArtifact,
limits,
)
: new SigrokRecordingProvider(
environment,
undefined,
runtime,
"linux",
readStableArtifact,
limits,
);
onTestFinished(() => provider.close());
const inspect =
provider instanceof TsharkCaptureProvider
? () => new PacketCaptureService(provider).inspectFields({})
: () => new SignalRecordingService(provider).inspectDecoders({});
const first = await inspect();
if (!first.ok) throw first.error;
expect(first.value.normalized_result).toMatchObject({
tool: { configured_path: command, path: await realpath(commandSource) },
resource_limiter: {
configured_path: limiter,
path: await realpath(limiterSource),
arguments: expect.arrayContaining([command]),
},
});
// Identical bytes at another physical path must not pass the current
// invocation's identity verification, even though the producer succeeds.
await writeFile(`${limiter}.retarget`, `${replacement}\n`);
const changed = await inspect();
if (changed.ok) throw new Error("Retargeted limiter was accepted");
expect(changed.error).toMatchObject({
_tag: "AnalysisArtifactChangedError",
path: limiter,
capturedOutput: {
stdout:
kind === "tshark"
? "TShark (Wireshark) 4.6.9\n"
: "sigrok-cli 0.7.2\nlibsigrokdecode 0.5.3\n",
},
});
await unlink(`${limiter}.retarget`);
const fresh = await inspect();
if (!fresh.ok) throw fresh.error;
expect(fresh.value.normalized_result).toMatchObject({
resource_limiter: { path: await realpath(replacement) },
});
expect(first.value.normalized_result).toMatchObject({
resource_limiter: { path: await realpath(limiterSource) },
});
expect(
(await readdir(directory)).filter((name) =>
name.startsWith("workspace-"),
),
).toEqual([]);
},
);
@@ -0,0 +1,150 @@
import { createHash } from "node:crypto";
import {
access,
chmod,
mkdir,
readFile,
realpath,
writeFile,
} from "node:fs/promises";
import { dirname, join } from "node:path";
import { expect, it } from "vitest";
import { z } from "zod";
import { nativeCallObservationInputSchema } from "../../../../src/domain/native/nativeCallObservation.js";
import { resolveXcrunTool } from "../../../../src/native/CommandRunner.js";
import { LldbCallTracer } from "../../../../src/native/LldbCallTracer.js";
import { observeProcessStartIdentity } from "../../../../src/process/ProcessOwnershipObservation.js";
import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js";
const readySchema = z.object({
pid: z.int().positive(),
run_id: z.string().min(1),
config_path: z.string(),
});
const shellArgument = (value: string) => `'${value.replaceAll("'", "'\\''")}'`;
it.skipIf(process.platform !== "darwin")(
"cleans an xcrun-selected LLDB wrapper after cancellation without requiring its pre-exec argv",
async () => {
const root = await createTestTempDirectory("rea-lldb-wrapper-");
const child = join(root, "safe-child.mjs");
const toolchains = join(root, "Toolchains");
const toolchain = join(toolchains, "Audit.xctoolchain");
const launcher = join(toolchain, "usr/bin/lldb");
const readyPath = join(root, "ready.json");
await mkdir(dirname(launcher), { recursive: true });
// LLVM documents this external toolchain layout and selection mechanism:
// llvm/tools/xcode-toolchain/CMakeLists.txt. No installed toolchain is changed.
await writeFile(
join(toolchain, "Info.plist"),
`<?xml version="1.0"?>
<plist version="1.0"><dict>
<key>CFBundleIdentifier</key><string>org.rea.audit.lldb</string>
<key>CompatibilityVersion</key><integer>2</integer>
</dict></plist>`,
);
// This producer launches no debugger or target. Its delayed self-exit also
// bounds the baseline regression's deliberately refused cleanup.
await writeFile(
child,
`import { readFileSync, writeFileSync } from 'node:fs';
const command = process.argv.find(value => value.startsWith('rea_trace '));
const configPath = command.slice('rea_trace '.length);
const config = JSON.parse(readFileSync(configPath, 'utf8'));
writeFileSync(config.result_path, JSON.stringify({
status: 'launch-error', error: 'fixture deliberately launches no target',
target_process_created: false, target_process_exit_observed: true
}));
for (const path of [config.observation_path, config.stdout_capture_path, config.stderr_capture_path])
writeFileSync(path, '');
writeFileSync(${JSON.stringify(readyPath)}, JSON.stringify({
pid: process.pid, run_id: process.env.REA_PROCESS_RUN_ID, config_path: configPath
}));
setTimeout(() => process.exit(0), 10000);
`,
);
const launcherText = `#!/bin/sh\nexec ${shellArgument(process.execPath)} ${shellArgument(child)} "$@"\n`;
await writeFile(launcher, launcherText);
await chmod(launcher, 0o755);
const environment = {
...process.env,
DEVELOPER_DIR: undefined,
SDKROOT: undefined,
EXTERNAL_TOOLCHAINS_DIR: toolchains,
TOOLCHAINS: "org.rea.audit.lldb",
xcrun_nocache: "1",
};
const selected = await resolveXcrunTool("lldb", environment);
if (!selected.ok) throw selected.error;
expect(selected.value).toEqual({
path: await realpath(launcher),
sha256: createHash("sha256").update(launcherText).digest("hex"),
});
// Production discovery, acquisition and native process ownership terminate
// the real child reached through this caller-selected external toolchain.
const tracer = new LldbCallTracer(environment);
const controller = new AbortController();
const reason = new Error("caller cancels the acquired wrapper fixture");
let started: z.infer<typeof readySchema> | undefined;
try {
const pending = tracer.trace(
{
executable: "/fixture/target-is-never-launched",
architecture: "arm64",
expectedSha256: "b".repeat(64),
input: nativeCallObservationInputSchema.parse({
breakpoints: [{ kind: "function", name: "main" }],
duration_ms: 100,
}),
},
controller.signal,
);
await expect
.poll(
async () => {
started = readySchema.parse(
JSON.parse(await readFile(readyPath, "utf8")),
);
return started.run_id;
},
{ timeout: 5_000 },
)
.toEqual(expect.any(String));
if (started === undefined) throw new Error("Wrapper did not start");
const identity = await observeProcessStartIdentity(
started.pid,
undefined,
started.run_id,
);
expect(identity?.state).toBe("readable");
controller.abort(reason);
const result = await pending;
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Expected caller cancellation");
expect(result.error).toMatchObject({
_tag: "AnalysisCancelledError",
cause: reason,
cleanupIncomplete: false,
});
expect(await observeProcessStartIdentity(started.pid)).toBeUndefined();
await expect(access(dirname(started.config_path))).rejects.toMatchObject({
code: "ENOENT",
});
} finally {
controller.abort(reason);
const closed = await tracer.close();
if (!closed.ok && started !== undefined) {
// The unpatched implementation retains its exact owner until the safe
// fixture self-exits. Do not bypass ownership with an unverified PID kill.
const pid = started.pid;
await expect
.poll(async () => await observeProcessStartIdentity(pid), {
timeout: 12_000,
})
.toBeUndefined();
expect((await tracer.close()).ok).toBe(true);
} else expect(closed.ok).toBe(true);
}
},
15_000,
);