fix(paths): preserve native selections across launch and publication

This commit is contained in:
morluto committed 2026-10-26 17:56:47 +08:00
1 parent da526fc014
commit f2b4468183
15 files changed
+536 -95

No files matched your search

+76 -4
View File
@@ -1,6 +1,6 @@
import { parseEvidence } from "../../../src/domain/evidence.js";
import { ok as resultOk, err } from "../../../src/domain/result.js";
import { rm, symlink, writeFile } from "node:fs/promises";
import { mkdir, realpath, rm, symlink, writeFile } from "node:fs/promises";
import { basename, join } from "node:path";
import { pathToFileURL } from "node:url";
@@ -659,9 +659,9 @@ const evidenceFor = (value: unknown) => {
return parseEvidence(value);
};
it("reports every unresolved Electron path without starting a provider", async () => {
const root = await createTestTempDirectory("rea-electron-invalid-paths-");
const capture = vi.fn();
const activePathAdmissionClient = async (
capture: ElectronActiveObservationPort["capture"],
): Promise<Client> => {
const session = createTestBinarySession(() => {
throw new Error("must not start");
});
@@ -680,6 +680,13 @@ it("reports every unresolved Electron path without starting a provider", async (
resources.push(client, server, session);
await server.connect(serverTransport);
await client.connect(clientTransport);
return client;
};
it("reports every unresolved Electron path without starting a provider", async () => {
const root = await createTestTempDirectory("rea-electron-invalid-paths-");
const capture = vi.fn();
const client = await activePathAdmissionClient(capture);
const fields = ["executable_path", "application_path", "application_root"];
const result = await client.callTool({
name: "capture_electron_scenario",
@@ -705,6 +712,71 @@ it("reports every unresolved Electron path without starting a provider", async (
await client.ping();
});
it.skipIf(process.platform === "win32")(
"refuses obstructed Electron selections and admits native symlink traversal through MCP",
async () => {
const root = await realpath(
await createTestTempDirectory("rea-electron-obstructed-paths-"),
);
await mkdir(join(root, "application/child"), { recursive: true });
await writeFile(join(root, "file"), "not a directory");
await writeFile(join(root, "electron"), "#!/bin/sh\n", { mode: 0o755 });
await writeFile(join(root, "main.js"), "module.exports = {};\n");
await writeFile(
join(root, "application/main.js"),
"module.exports = {};\n",
);
await symlink("application/child", join(root, "link"));
const capture = vi.fn<ElectronActiveObservationPort["capture"]>(
async (input) =>
resultOk(
createElectronActiveObservationFixtureResult(input.application_path),
),
);
const client = await activePathAdmissionClient(capture);
const selected = {
executable_path: join(root, "electron"),
application_path: join(root, "main.js"),
application_root: join(root, "application"),
};
for (const field of [
"executable_path",
"application_path",
"application_root",
] as const) {
const refused = await client.callTool({
name: "capture_electron_scenario",
arguments: {
...selected,
[field]: `${root}/file/../${basename(selected[field])}`,
},
});
expect(refused.isError).toBe(true);
expect(parseMcpToolError(refused)).toMatchObject({
error: {
code: "invalid_request",
details: { issues: [{ path: [field] }] },
},
});
expect(capture).not.toHaveBeenCalled();
}
const admitted = await client.callTool({
name: "capture_electron_scenario",
arguments: {
...selected,
application_path: `${root}/link/../main.js`,
application_root: `${root}/link/..`,
},
});
expect(admitted.isError, JSON.stringify(admitted)).not.toBe(true);
expect(capture).toHaveBeenCalledOnce();
expect(capture.mock.calls[0]?.[0]).toMatchObject({
application_path: join(root, "application/main.js"),
application_root: join(root, "application"),
});
},
);
const expectPaddedElectronTargetRejected = async (
client: Client,
browser: FakeCdpBrowser,
@@ -0,0 +1,184 @@
import { createHash } from "node:crypto";
import {
access,
mkdir,
readFile,
readdir,
symlink,
writeFile,
} from "node:fs/promises";
import { dirname, join } from "node:path";
import { expect, onTestFinished, test } from "vitest";
import { AdbProvider } from "../../../../src/adb/AdbProvider.js";
import { AdbDeviceAnalysisService } from "../../../../src/application/adb/AdbDeviceAnalysisService.js";
import { PrivateRuntimeRoot } from "../../../../src/process/PrivateRuntimeRoot.js";
import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js";
const it = test.skipIf(process.platform === "win32");
const sourceBytes = "selected native bytes";
const pulledBytes = "pulled fixture";
const bugreportBytes = "bugreport fixture";
const png = Buffer.from(
"89504e470d0a1a0a0000000d494844520000000400000003",
"hex",
);
const digest = (bytes: string | Buffer) =>
createHash("sha256").update(bytes).digest("hex");
// All ADB commands run a selected local script. No device, server or real adb
// executable participates; file snapshots, publication and Evidence are real.
const fixture = async () => {
const root = await createTestTempDirectory("rea-adb-selected-path-");
const physical = join(root, "physical");
await mkdir(join(physical, "nested"), { recursive: true });
await symlink(join(physical, "nested"), join(root, "alias"));
const binary = join(root, "adb");
const quote = (value: string) => `'${value.replaceAll("'", "'\\''")}'`;
await writeFile(
binary,
`#!/bin/sh
if [ "$1" = version ]; then printf 'Android Debug Bridge version 1.0.41\\nVersion 34.0.5-stub\\nInstalled as %s\\n' "$0"; exit 0; fi
if [ "$3" = shell ] && [ "$4" = pm ] && [ "$5" = path ]; then printf 'package:/data/app/fixture/base.apk\\n'; exit 0; fi
if [ "$3" = shell ]; then exit 1; fi
if [ "$3" = push ] || [ "$3" = install ]; then
printf '%s' "$4" > ${quote(join(root, "snapshot-path"))}
/bin/cat "$4" > ${quote(join(root, "consumed"))}
printf 'Success\\n'; exit 0
fi
if [ "$3" = pull ]; then printf '${pulledBytes}' > "$5"; exit 0; fi
if [ "$3" = exec-out ]; then printf '\\211PNG\\r\\n\\032\\n\\0\\0\\0\\rIHDR\\0\\0\\0\\004\\0\\0\\0\\003'; exit 0; fi
if [ "$3" = bugreport ]; then
printf '${bugreportBytes}' > "$4/bugreport.zip"
printf 'Bug report copied to %s/bugreport.zip\\n' "$4"; exit 0
fi
exit 1
`,
{ mode: 0o755 },
);
const runtimePaths: string[] = [];
const provider = new AdbProvider({
environment: { REA_ADB_PATH: binary },
createRuntimeRoot: async (options) => {
const runtime = await PrivateRuntimeRoot.create(options);
runtimePaths.push(runtime.path);
return runtime;
},
});
onTestFinished(() => provider.close());
return {
root,
physical,
runtimePaths,
service: new AdbDeviceAnalysisService(provider),
};
};
it.each(["push_adb_file", "install_adb_package"] as const)(
"preserves native symlink traversal in %s snapshots and Evidence",
async (operation) => {
const f = await fixture();
await writeFile(join(f.physical, "payload.apk"), sourceBytes);
await writeFile(join(f.root, "payload.apk"), "wrong lexical bytes");
const selected = `${f.root}/alias/../payload.apk`;
const result = await f.service.execute(
operation,
operation === "push_adb_file"
? {
serial: "fixture",
local_path: selected,
device_path: "/data/local/tmp/payload.apk",
overwrite: false,
}
: { serial: "fixture", apk_path: selected, replace: false },
);
if (!result.ok) throw result.error;
expect(await readFile(join(f.root, "consumed"), "utf8")).toBe(sourceBytes);
const field = operation === "push_adb_file" ? "local_path" : "apk_path";
expect(result.value.parameters).toMatchObject({ [field]: selected });
expect(result.value.normalized_result).toMatchObject({
[field]: selected,
bytes: Buffer.byteLength(sourceBytes),
sha256: digest(sourceBytes),
});
const snapshot = await readFile(join(f.root, "snapshot-path"), "utf8");
expect(snapshot).not.toBe(selected);
await expect(access(dirname(snapshot))).rejects.toMatchObject({
code: "ENOENT",
});
},
);
it.each([
"pull_adb_file",
"pull_adb_package",
"capture_adb_screen",
"collect_adb_bugreport",
] as const)(
"publishes %s through the selected output traversal and cleans owned staging",
async (operation) => {
const f = await fixture();
const selected = `${f.root}/alias/../out`;
const input = {
serial: "fixture",
output_directory: selected,
...(operation === "pull_adb_file"
? { device_path: "/data/local/tmp/pulled.bin" }
: {}),
...(operation === "pull_adb_package"
? { package: "com.example.fixture" }
: {}),
};
const result = await f.service.execute(operation, input);
if (!result.ok) throw result.error;
expect(result.value.parameters).toMatchObject({
output_directory: selected,
});
const relative =
operation === "pull_adb_package"
? "com.example.fixture/base.apk"
: operation === "pull_adb_file"
? "pulled.bin"
: operation === "capture_adb_screen"
? "screen.png"
: "bugreport.zip";
const expectedBytes =
operation === "capture_adb_screen"
? png
: Buffer.from(
operation === "collect_adb_bugreport"
? bugreportBytes
: pulledBytes,
);
expect(
(await readFile(join(f.physical, "out", relative))).equals(expectedBytes),
).toBe(true);
const file = {
local_path: `${selected}/${relative}`,
bytes: expectedBytes.byteLength,
sha256: digest(expectedBytes),
};
expect(result.value.normalized_result).toMatchObject(
operation === "pull_adb_package"
? {
output_directory: selected,
artifacts: [file],
coverage: "complete",
}
: file,
);
await expect(access(join(f.root, "out"))).rejects.toMatchObject({
code: "ENOENT",
});
expect(
(await readdir(join(f.physical, "out"))).filter((name) =>
name.startsWith(".rea-"),
),
).toEqual([]);
for (const path of f.runtimePaths) {
if (operation === "capture_adb_screen")
expect(dirname(path)).toBe(join(f.physical, "out"));
await expect(access(path)).rejects.toMatchObject({ code: "ENOENT" });
}
},
);
@@ -37,7 +37,11 @@ const request: Parameters<NativeCallTracer["trace"]>[0] = {
backtrace_frames: 0,
},
};
const tool = { path: process.execPath, sha256: "0".repeat(64) };
const tool = {
invocationPath: process.execPath,
resolvedPath: process.execPath,
sha256: "0".repeat(64),
};
const createTracer = (
launch?: ConstructorParameters<typeof LldbCallTracer>[1],
@@ -43,6 +43,7 @@ import { ProviderStartupDeadline } from "../../../../src/process/ProviderDeadlin
import {
NativeFixtureRunner,
nativeMachoTarget,
nativeResolvedToolFixture,
} from "../../../fixtures/nativeCommands.js";
const EVENT = {
@@ -72,7 +73,7 @@ const request: Parameters<NativeCallTracer["trace"]>[0] = {
}),
};
const fakeTool = { path: "/usr/bin/lldb", sha256: "b".repeat(64) };
const fakeTool = nativeResolvedToolFixture("/usr/bin/lldb", "b".repeat(64));
const successfulTracerOutput = {
status: "traced",
version: "lldb-fixture",
@@ -251,7 +252,11 @@ describe("LLDB completed trace retention through finalization", () => {
phase === "runtime cleanup failure" ? "cleanup-failure" : "cancelled",
},
target: { launch_identity: successfulTracerOutput.target_identity },
debugger: { ...fakeTool, version: successfulTracerOutput.version },
debugger: {
path: fakeTool.invocationPath,
sha256: fakeTool.sha256,
version: successfulTracerOutput.version,
},
process: {
outcome: "exited",
exit_status: 0,
@@ -284,7 +289,8 @@ const expectCompletedCleanupFacts = (
completed: typeof successfulTracerOutput,
) => {
expect(partial.debugger).toEqual({
...fakeTool,
path: fakeTool.invocationPath,
sha256: fakeTool.sha256,
version: completed.version,
});
expect(partial.target.launch_identity).toEqual(completed.target_identity);
@@ -728,7 +734,7 @@ describe("LLDB primary outcome during owned process cleanup", () => {
return observedStop;
});
const tracer = new LldbCallTracer({}, undefined, async () =>
ok({ path: fakeLldb, sha256: "c".repeat(64) }),
ok(nativeResolvedToolFixture(fakeLldb, "c".repeat(64))),
);
try {
@@ -821,7 +827,7 @@ describe("LLDB process exit while output is still open", () => {
return stopReceipt;
});
const tracer = new LldbCallTracer({}, undefined, async () =>
ok({ path: fakeLldb, sha256: "d".repeat(64) }),
ok(nativeResolvedToolFixture(fakeLldb, "d".repeat(64))),
);
try {
@@ -950,7 +956,7 @@ describe("LLDB target proof published before interruption", () => {
)
: undefined;
const tracer = new LldbCallTracer({}, undefined, async () =>
ok({ path: fakeLldb, sha256: "c".repeat(64) }),
ok(nativeResolvedToolFixture(fakeLldb, "c".repeat(64))),
);
let retainedRoot: string | undefined;
try {
@@ -1032,7 +1038,7 @@ describe("LLDB target proof read by cleanup retry", () => {
: originalStop.call(this, options);
});
const tracer = new LldbCallTracer({}, undefined, async () =>
ok({ path: fakeLldb, sha256: "c".repeat(64) }),
ok(nativeResolvedToolFixture(fakeLldb, "c".repeat(64))),
);
let retainedRoot: string | undefined;
try {
@@ -1160,7 +1166,7 @@ describe("LLDB process exit at output-close loop completion", () => {
return closed;
});
const tracer = new LldbCallTracer({}, undefined, async () =>
ok({ path: fixture.fakeLldb, sha256: "e".repeat(64) }),
ok(nativeResolvedToolFixture(fixture.fakeLldb, "e".repeat(64))),
);
try {
const result = await tracer.trace(request, controller.signal);
@@ -1254,7 +1260,7 @@ describe("LLDB process exit at output-close loop completion", () => {
return stopReceipt;
});
const tracer = new LldbCallTracer({}, undefined, async () =>
ok({ path: fakeLldb, sha256: "f".repeat(64) }),
ok(nativeResolvedToolFixture(fakeLldb, "f".repeat(64))),
);
try {
@@ -1,17 +1,23 @@
import { createHash } from "node:crypto";
import { execFile } from "node:child_process";
import {
access,
chmod,
mkdir,
readFile,
realpath,
symlink,
writeFile,
} from "node:fs/promises";
import { dirname, join } from "node:path";
import { promisify } from "node:util";
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 {
resolveXcrunTool,
XcrunCommandRunner,
} 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";
@@ -22,27 +28,88 @@ const readySchema = z.object({
config_path: z.string(),
});
const shellArgument = (value: string) => `'${value.replaceAll("'", "'\\''")}'`;
const exec = promisify(execFile);
const xcrunSelection = async (
tool: string,
environment: NodeJS.ProcessEnv,
): Promise<string> => {
const { stdout } = await exec("/usr/bin/xcrun", ["--find", tool], {
env: environment,
});
expect(stdout.endsWith("\n")).toBe(true);
return stdout.slice(0, -1);
};
const externalToolchain = async (root: string, tool: string) => {
const toolchains = join(root, "Toolchains");
const toolchain = join(toolchains, "Audit.xctoolchain");
const invocationPath = join(toolchain, "usr/bin", tool);
const resolvedPath = join(root, "tool-implementation");
await mkdir(dirname(invocationPath), { 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.selected</string>
<key>CompatibilityVersion</key><integer>2</integer>
</dict></plist>`,
);
await symlink(resolvedPath, invocationPath);
return {
invocationPath,
resolvedPath,
environment: {
...process.env,
DEVELOPER_DIR: undefined,
SDKROOT: undefined,
EXTERNAL_TOOLCHAINS_DIR: toolchains,
TOOLCHAINS: "org.rea.audit.selected",
xcrun_nocache: "1",
},
};
};
it.skipIf(process.platform !== "darwin")(
"invokes the xcrun-selected native tool spelling and its sibling resources",
async () => {
const root = await createTestTempDirectory("rea-native-selection-");
const tool = await externalToolchain(root, "nm");
const source = `#!/bin/sh\nprintf '%s\\n' "$0"\ncat "$(dirname "$0")/selected-resource"\n`;
await writeFile(tool.resolvedPath, source, { mode: 0o755 });
await writeFile(
join(dirname(tool.invocationPath), "selected-resource"),
"selected sibling resource\n",
);
const selectedPath = await xcrunSelection("nm", tool.environment);
const runner = new XcrunCommandRunner(tool.environment);
try {
const result = await runner.run("nm", [], {});
if (!result.ok) throw result.error;
expect(result.value.stdout).toBe(
`${selectedPath}\nselected sibling resource\n`,
);
expect(result.value.executable).toBe(selectedPath);
expect(result.value.executable).not.toBe(tool.resolvedPath);
expect(result.value.executableSha256).toBe(
createHash("sha256").update(source).digest("hex"),
);
} finally {
expect(await runner.retryCleanup()).toEqual([]);
}
},
);
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 tool = await externalToolchain(root, "lldb");
const child = join(dirname(tool.invocationPath), "safe-child.mjs");
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(
@@ -63,21 +130,15 @@ writeFileSync(${JSON.stringify(readyPath)}, JSON.stringify({
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 launcherText = `#!/bin/sh\nexec ${shellArgument(process.execPath)} "$(dirname "$0")/safe-child.mjs" "$@"\n`;
await writeFile(tool.resolvedPath, launcherText);
await chmod(tool.resolvedPath, 0o755);
const { environment } = tool;
const selected = await resolveXcrunTool("lldb", environment);
if (!selected.ok) throw selected.error;
expect(selected.value).toEqual({
path: await realpath(launcher),
invocationPath: await xcrunSelection("lldb", environment),
resolvedPath: await realpath(tool.invocationPath),
sha256: createHash("sha256").update(launcherText).digest("hex"),
});
// Production discovery, acquisition and native process ownership terminate
@@ -12,7 +12,11 @@ import { LldbCallTracer } from "../../../../src/native/LldbCallTracer.js";
import { DarwinProcessOwnershipInspectionError } from "../../../../src/process/DarwinProcessRunTokenReader.js";
import * as processBoundary from "../../../../src/process/ProviderProcess.js";
const tool = { path: "/usr/bin/true", sha256: "b".repeat(64) };
const tool = {
invocationPath: "/usr/bin/true",
resolvedPath: "/usr/bin/true",
sha256: "b".repeat(64),
};
const request = {
executable: "/fixture",
architecture: "arm64" as const,
@@ -85,6 +85,37 @@ it("keeps a realpath I/O failure primary when cancellation is observed before re
expect(result.error.cause).toBe(ioFailure);
});
it("retains exact cancellation observed after successful tool path resolution", async () => {
let finishRealpath!: () => void;
let realpathStarted!: () => void;
const started = new Promise<void>((resolve) => {
realpathStarted = resolve;
});
realpathState.resolve = () =>
new Promise<string>((resolve) => {
finishRealpath = () => resolve("/controlled/physical-tool");
realpathStarted();
});
const controller = new AbortController();
const cancellationReason = { source: "caller", phase: "tool selection" };
const pending = resolveXcrunTool("file", {}, controller.signal, launcher);
await started;
controller.abort(cancellationReason);
finishRealpath();
const result = await pending;
if (result.ok) throw new Error("Expected selection cancellation");
expect(result.error).toMatchObject({
reason: "cancelled",
cancellation: { reason: cancellationReason },
});
expect(result.error.cancellation?.reason).toBe(cancellationReason);
expect(result.error.cause).toMatchObject({
reason: "cancelled",
cause: cancellationReason,
});
});
it.skipIf(process.platform === "win32")(
"uses the AbortError cause as the exact tool-hash cancellation fact",
async () => {
@@ -76,7 +76,11 @@ it.skipIf(process.platform === "win32")(
PATH: "/selected-path",
};
const runner = new XcrunCommandRunner(environment, async () =>
ok({ path: process.execPath, sha256: "a".repeat(64) }),
ok({
invocationPath: process.execPath,
resolvedPath: process.execPath,
sha256: "a".repeat(64),
}),
);
environment.REA_NATIVE_SELECTED = "mutated-after-construction";
const captured = await runner.run(
@@ -141,7 +145,12 @@ it.skipIf(process.platform === "win32")(
};
const runner = new XcrunCommandRunner(
{},
async () => ok({ path: process.execPath, sha256: "a".repeat(64) }),
async () =>
ok({
invocationPath: process.execPath,
resolvedPath: process.execPath,
sha256: "a".repeat(64),
}),
launcher,
);
@@ -195,7 +204,11 @@ it.skipIf(process.platform === "win32")(
.update(await readFile(process.execPath))
.digest("hex");
const runner = new XcrunCommandRunner({}, async () =>
ok({ path: process.execPath, sha256 }),
ok({
invocationPath: process.execPath,
resolvedPath: process.execPath,
sha256,
}),
);
const exited = await runner.run("file", ["-e", "process.exit(1)"], {
acceptNonZero: true,
@@ -40,6 +40,7 @@ import {
nativeFixture as fixture,
nativeMachoTarget as machoTarget,
nativeMachoTargetForFile,
nativeResolvedToolFixture,
} from "../../../fixtures/nativeCommands.js";
/** The file runner identifies selected .plist inputs as XML. */
@@ -222,7 +223,7 @@ describe("native command output collection", () => {
const digest = createHash("sha256")
.update(await readFile(executable))
.digest("hex");
return ok({ path: executable, sha256: digest });
return ok(nativeResolvedToolFixture(executable, digest));
});
const originalDigest = await install("original-tool-output");
@@ -248,7 +249,9 @@ describe("native command output collection", () => {
it("retains the complete native command output", async () => {
const runner = new XcrunCommandRunner({}, () =>
Promise.resolve(ok({ path: process.execPath, sha256: "a".repeat(64) })),
Promise.resolve(
ok(nativeResolvedToolFixture(process.execPath, "a".repeat(64))),
),
);
const output = "x".repeat(4096);
const captured = await runner.run(
@@ -262,7 +265,9 @@ describe("native command output collection", () => {
it("retains both output streams and exit status when a command fails", async () => {
const runner = new XcrunCommandRunner({}, () =>
Promise.resolve(ok({ path: process.execPath, sha256: "a".repeat(64) })),
Promise.resolve(
ok(nativeResolvedToolFixture(process.execPath, "a".repeat(64))),
),
);
const result = await runner.run(
"file",
@@ -292,7 +297,9 @@ describe("native command output collection", () => {
it("retains accepted nonzero output as a complete command capture", async () => {
const runner = new XcrunCommandRunner({}, () =>
Promise.resolve(ok({ path: process.execPath, sha256: "a".repeat(64) })),
Promise.resolve(
ok(nativeResolvedToolFixture(process.execPath, "a".repeat(64))),
),
);
const result = await runner.run(
"file",
@@ -318,7 +325,9 @@ describe("native command output collection", () => {
describe("native command failure capture and limits", () => {
it("projects captured diagnostics from a failed native provider command", async () => {
const runner = new XcrunCommandRunner({}, () =>
Promise.resolve(ok({ path: process.execPath, sha256: "a".repeat(64) })),
Promise.resolve(
ok(nativeResolvedToolFixture(process.execPath, "a".repeat(64))),
),
);
const result = await new NativeMacOSProvider({}, runner, "darwin")
.createClient(machoTarget("/private/fixture"))
@@ -353,7 +362,9 @@ describe("native command failure capture and limits", () => {
const runner = new XcrunCommandRunner(
{},
() =>
Promise.resolve(ok({ path: process.execPath, sha256: "a".repeat(64) })),
Promise.resolve(
ok(nativeResolvedToolFixture(process.execPath, "a".repeat(64))),
),
() => Promise.reject(inspection),
);
const result = await new NativeMacOSProvider({}, runner, "darwin")
@@ -374,7 +385,9 @@ describe("native command failure capture and limits", () => {
it("retains output written before cancellation and reaps the stalled command", async () => {
const runner = new XcrunCommandRunner({}, () =>
Promise.resolve(ok({ path: process.execPath, sha256: "a".repeat(64) })),
Promise.resolve(
ok(nativeResolvedToolFixture(process.execPath, "a".repeat(64))),
),
);
const controller = new AbortController();
const pending = runner.run(
@@ -429,7 +442,7 @@ describe("native owned cancellation boundary", () => {
spawnOwnedProviderProcess({ ...options, expectedCommand: null });
const runner = new XcrunCommandRunner(
{},
async () => ok({ path: executable, sha256: executableSha256 }),
async () => ok(nativeResolvedToolFixture(executable, executableSha256)),
launcher,
);
const client = new NativeMacOSProvider({}, runner, "darwin").createClient(
@@ -544,7 +557,7 @@ describe("native cancellation cleanup provenance", () => {
};
const runner = new XcrunCommandRunner(
{},
async () => ok({ path: executable, sha256: executableSha256 }),
async () => ok(nativeResolvedToolFixture(executable, executableSha256)),
launcher,
);
const client = new NativeMacOSProvider({}, runner, "darwin").createClient(
@@ -681,7 +694,7 @@ describe("native returned cleanup ownership", () => {
};
const runner = new XcrunCommandRunner(
{},
async () => ok({ path: executable, sha256: executableSha256 }),
async () => ok(nativeResolvedToolFixture(executable, executableSha256)),
launcher,
);
const client = new NativeMacOSProvider({}, runner, "darwin").createClient(
@@ -824,7 +837,9 @@ describe("native late cancellation with cleanup", () => {
const runner = new XcrunCommandRunner(
{},
async () =>
ok({ path: "/controlled/native-tool", sha256: "a".repeat(64) }),
ok(
nativeResolvedToolFixture("/controlled/native-tool", "a".repeat(64)),
),
launcher,
);
const client = new NativeMacOSProvider({}, runner, "darwin").createClient(
@@ -877,7 +892,9 @@ describe("native late cancellation with cleanup", () => {
describe("native command failure capture and limits", () => {
it("bounds both failure streams at a UTF-8 boundary while keeping their totals", async () => {
const runner = new XcrunCommandRunner({}, () =>
Promise.resolve(ok({ path: process.execPath, sha256: "a".repeat(64) })),
Promise.resolve(
ok(nativeResolvedToolFixture(process.execPath, "a".repeat(64))),
),
);
// "é" is two bytes and straddles the excerpt boundary, so it is dropped whole.
const stdout = `${"x".repeat(NATIVE_FAILURE_DIAGNOSTIC_BYTES - 1)}é${"y".repeat(1024)}`;
@@ -907,7 +924,9 @@ describe("native command failure capture and limits", () => {
it("rejects output beyond the complete native-command budget", async () => {
const runner = new XcrunCommandRunner({}, () =>
Promise.resolve(ok({ path: process.execPath, sha256: "a".repeat(64) })),
Promise.resolve(
ok(nativeResolvedToolFixture(process.execPath, "a".repeat(64))),
),
);
const megabytesOverBudget =
Math.ceil(NATIVE_COMMAND_OUTPUT_BUDGET_BYTES / (1024 * 1024)) + 1;
+7
View File
@@ -10,8 +10,15 @@ import type {
NativeCommandCapture,
NativeCommandOptions,
NativeCommandRunner,
ResolvedTool,
} from "../../src/native/CommandRunner.js";
/** Resolver seam for fixtures with identical invocation and physical paths. */
export const nativeResolvedToolFixture = (
path: string,
sha256: string,
): ResolvedTool => ({ invocationPath: path, resolvedPath: path, sha256 });
/** Replay captured native command outputs through the production runner seam. */
export class NativeFixtureRunner implements NativeCommandRunner {
constructor(