mirror of
https://github.com/morluto/rea.git
synced 2026-11-11 01:13:39 +00:00
* test: strengthen integration oracles and verification * test: bound lifecycle gates and strengthen workflow checks * test: verify complete paged and extracted results * test: preserve bytecode and cancellation evidence * test: strengthen retained evidence and fixture cleanup * test: require cross-projection bridge identities * test: retain fixture owners through cleanup failures * test: verify graph examples and project selectors * test: tighten provider and lifecycle regressions * test: consolidate hex coverage and bound cutter cleanup * test: bound subprocess output waits and strengthen graph checks * test: check mcp recovery and preserve cleanup owners * test: verify structured mcp argument failures * test: preserve artifact cleanup and gdb limit evidence * test: preserve evidence identities and bound provider gates * test: retain browser and comparison cleanup owners * test: bound startup and strengthen fixture oracles * test: bound lifecycle gates and verify recovered evidence * test: verify public outcomes and lifecycle ownership * test: assert precise evidence and release lifecycle gates * test: verify selected facts and bound cleanup waits * test: check retained evidence and decoded records * test: verify final reports and retain cleanup ownership * test: bound reviewed startup gates and retain cleanup * test: verify PE errors and preserve MCP cleanup * test: preserve MCP fixture cleanup ownership * test: release provider gates before late startup
341 lines
12 KiB
TypeScript
341 lines
12 KiB
TypeScript
import { mkdtemp, readlink, rm, stat, writeFile } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { afterEach, describe, expect, it } from "vitest";
|
|
import {
|
|
NativeFixtureRunner,
|
|
nativeMachoTargetForFile,
|
|
} from "../../tests/fixtures/nativeCommands.js";
|
|
import { NativeMacOSProvider } from "./NativeMacOSProvider.js";
|
|
import type { NativeCallTracer } from "./LldbCallTracer.js";
|
|
import { NativeCommandFailure } from "./CommandRunner.js";
|
|
import { inspectMachoSchema } from "../domain/native/nativeInspection.js";
|
|
import { err, ok } from "../domain/result.js";
|
|
import { VTOOL_CONTAINER_REFUSAL_LIMITATION } from "./NativeMachoInspection.js";
|
|
|
|
const targetDirectories: string[] = [];
|
|
afterEach(async () => {
|
|
await Promise.all(
|
|
targetDirectories
|
|
.splice(0)
|
|
.map((directory) => rm(directory, { recursive: true, force: true })),
|
|
);
|
|
});
|
|
/** A FAT container whose arm64 row matches the canned lipo fixture. */
|
|
const parenthesizedFatTarget = () => {
|
|
const bytes = Buffer.alloc(40_960);
|
|
bytes.writeUInt32BE(0xcafebabe, 0);
|
|
bytes.writeUInt32BE(2, 4);
|
|
const slices = [
|
|
{ cpu: 0x01000007, subtype: 3, offset: 16_384, size: 4_096, align: 14 },
|
|
{ cpu: 0x0100000c, subtype: 0, offset: 32_768, size: 8_192, align: 14 },
|
|
];
|
|
for (const [index, slice] of slices.entries()) {
|
|
const at = 8 + index * 20;
|
|
bytes.writeUInt32BE(slice.cpu, at);
|
|
bytes.writeUInt32BE(slice.subtype, at + 4);
|
|
bytes.writeUInt32BE(slice.offset, at + 8);
|
|
bytes.writeUInt32BE(slice.size, at + 12);
|
|
bytes.writeUInt32BE(slice.align, at + 16);
|
|
}
|
|
const header = bytes.subarray(32_768, 32_800);
|
|
header.writeUInt32LE(0xfeedfacf, 0);
|
|
header.writeUInt32LE(0x0100000c, 4);
|
|
header.writeUInt32LE(0, 8);
|
|
header.writeUInt32LE(2, 12);
|
|
return bytes;
|
|
};
|
|
const parenthesizedTarget = async () => {
|
|
const directory = await mkdtemp(join(tmpdir(), "rea-macho-target-"));
|
|
targetDirectories.push(directory);
|
|
const path = join(directory, "app(foo)");
|
|
await writeFile(path, parenthesizedFatTarget());
|
|
return { directory, target: await nativeMachoTargetForFile(path) };
|
|
};
|
|
|
|
const tracer: NativeCallTracer = {
|
|
trace: async () => {
|
|
throw new Error("unexpected trace");
|
|
},
|
|
close: async () => ({ ok: true, value: null }),
|
|
};
|
|
|
|
describe("private otool operand lifecycle", () => {
|
|
it("uses a private alias for native commands and removes it after all commands settle", async () => {
|
|
const path = await mkdtemp(join(tmpdir(), "rea-macho-operand-test-"));
|
|
const selected = await parenthesizedTarget();
|
|
const target = selected.target.path;
|
|
const calls: { tool: string; operand: string | undefined }[] = [];
|
|
const fixture = new NativeFixtureRunner();
|
|
const runner = {
|
|
async run(tool: string, args: readonly string[]) {
|
|
calls.push({ tool, operand: args.at(-1) });
|
|
if (args.at(-1) === join(path, "artifact.macho"))
|
|
expect(await readlink(args.at(-1)!)).toBe(target);
|
|
return fixture.run(tool, args);
|
|
},
|
|
};
|
|
const client = new NativeMacOSProvider(
|
|
{},
|
|
runner,
|
|
"darwin",
|
|
() => tracer,
|
|
async () => ({
|
|
path,
|
|
close: async () => {
|
|
await rm(path, { recursive: true, force: true });
|
|
},
|
|
}),
|
|
).createClient(selected.target);
|
|
try {
|
|
const result = await client.execute("inspect_macho", {});
|
|
if (!result.ok) throw result.error;
|
|
const alias = join(path, "artifact.macho");
|
|
const report = inspectMachoSchema.parse(result.value.result);
|
|
expect(
|
|
report.provenance
|
|
.find((entry) => entry.tool === "lipo")
|
|
?.command.at(-1),
|
|
).toBe(alias);
|
|
expect(calls.some(({ operand }) => operand === alias)).toBe(true);
|
|
expect(calls.some(({ operand }) => operand === target)).toBe(false);
|
|
await expect(stat(path)).rejects.toMatchObject({ code: "ENOENT" });
|
|
} finally {
|
|
await client.close();
|
|
await rm(path, { recursive: true, force: true });
|
|
await rm(selected.directory, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it("keeps completed Mach-O facts when vtool refuses the private alias", async () => {
|
|
const path = await mkdtemp(join(tmpdir(), "rea-macho-operand-test-"));
|
|
const selected = await parenthesizedTarget();
|
|
const target = selected.target.path;
|
|
const alias = join(path, "artifact.macho");
|
|
const fixture = new NativeFixtureRunner();
|
|
const runner = {
|
|
async run(tool: string, args: readonly string[]) {
|
|
const captured = await fixture.run(tool, args);
|
|
if (!captured.ok || tool !== "vtool") return captured;
|
|
const operand = args.at(-1);
|
|
const stderr = `/usr/bin/vtool error: ${operand} file is not mach-o\n`;
|
|
return ok({
|
|
...captured.value,
|
|
stdout: "",
|
|
stdoutBytes: 0,
|
|
stderr,
|
|
stderrBytes: Buffer.byteLength(stderr),
|
|
exitCode: 1,
|
|
signal: null,
|
|
});
|
|
},
|
|
};
|
|
const client = new NativeMacOSProvider(
|
|
{},
|
|
runner,
|
|
"darwin",
|
|
() => tracer,
|
|
async () => ({
|
|
path,
|
|
close: async () => {
|
|
await rm(path, { recursive: true, force: true });
|
|
},
|
|
}),
|
|
).createClient(selected.target);
|
|
try {
|
|
const result = await client.execute("inspect_macho", {});
|
|
if (!result.ok) throw result.error;
|
|
const report = inspectMachoSchema.parse(result.value.result);
|
|
expect(report).toMatchObject({
|
|
format: "mach-o-universal",
|
|
selected_architecture: {
|
|
name: "arm64",
|
|
container_file_offset: 32_768,
|
|
},
|
|
});
|
|
const vtool = report.provenance.find((entry) => entry.tool === "vtool");
|
|
expect(vtool).toMatchObject({
|
|
exit: { code: 1, signal: null },
|
|
});
|
|
expect(vtool?.command.at(-1)).toBe(alias);
|
|
expect(report.limitations).toContain(VTOOL_CONTAINER_REFUSAL_LIMITATION);
|
|
expect(
|
|
report.provenance.find((entry) => entry.tool === "lipo"),
|
|
).toMatchObject({ exit: { code: 0, signal: null } });
|
|
expect(target.endsWith("app(foo)")).toBe(true);
|
|
} finally {
|
|
await client.close();
|
|
await rm(path, { recursive: true, force: true });
|
|
await rm(selected.directory, { recursive: true, force: true });
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("private operand command cleanup", () => {
|
|
it("retains the alias until command cleanup is confirmed", async () => {
|
|
const path = await mkdtemp(join(tmpdir(), "rea-macho-operand-test-"));
|
|
let unresolved = true;
|
|
class Runner extends NativeFixtureRunner {
|
|
async retryCleanup() {
|
|
if (unresolved) throw new Error("command remains owned");
|
|
return [];
|
|
}
|
|
}
|
|
const client = new NativeMacOSProvider(
|
|
{},
|
|
new Runner(),
|
|
"darwin",
|
|
() => tracer,
|
|
async () => ({
|
|
path,
|
|
close: async () => {
|
|
await rm(path, { recursive: true, force: true });
|
|
},
|
|
}),
|
|
).createClient((await parenthesizedTarget()).target);
|
|
try {
|
|
const result = await client.execute("inspect_macho", {});
|
|
expect(result.ok).toBe(false);
|
|
if (result.ok) throw new Error("expected cleanup failure");
|
|
expect(result.error.cleanupIncomplete).toBe(true);
|
|
expect(result.error.partialObservation).toMatchObject({
|
|
format: "mach-o-universal",
|
|
});
|
|
expect((await stat(path)).isDirectory()).toBe(true);
|
|
expect((await client.close()).ok).toBe(false);
|
|
expect((await stat(path)).isDirectory()).toBe(true);
|
|
unresolved = false;
|
|
expect((await client.close()).ok).toBe(true);
|
|
expect(result.error.cleanupIncomplete).toBe(false);
|
|
await expect(stat(path)).rejects.toMatchObject({ code: "ENOENT" });
|
|
} finally {
|
|
unresolved = false;
|
|
await client.close();
|
|
await rm(path, { recursive: true, force: true });
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("private otool operand cleanup", () => {
|
|
it.each([false, true])(
|
|
"preserves exact failed output with cleanup failure %s",
|
|
async (cleanupFails) => {
|
|
const path = await mkdtemp(join(tmpdir(), "rea-macho-operand-test-"));
|
|
const message = `lipo failed for ${join(path, "artifact.macho")}\n`;
|
|
const fixture = new NativeFixtureRunner();
|
|
const runner = {
|
|
async run(tool: string, args: readonly string[]) {
|
|
if (tool === "lipo")
|
|
return err(
|
|
new NativeCommandFailure("lipo", "nonzero-exit", 1, undefined, {
|
|
stdout: "",
|
|
stderr: message,
|
|
stdoutBytes: 0,
|
|
stderrBytes: Buffer.byteLength(message),
|
|
exitCode: 1,
|
|
signal: null,
|
|
truncated: false,
|
|
}),
|
|
);
|
|
return fixture.run(tool, args);
|
|
},
|
|
};
|
|
let attempts = 0;
|
|
const client = new NativeMacOSProvider(
|
|
{},
|
|
runner,
|
|
"darwin",
|
|
() => tracer,
|
|
async () => ({
|
|
path,
|
|
close: async () => {
|
|
if (cleanupFails && ++attempts === 1)
|
|
throw new Error("fixture cleanup refusal");
|
|
await rm(path, { recursive: true, force: true });
|
|
},
|
|
}),
|
|
).createClient((await parenthesizedTarget()).target);
|
|
try {
|
|
const result = await client.execute("inspect_macho", {});
|
|
expect(result.ok).toBe(false);
|
|
if (result.ok) throw new Error("expected native failure");
|
|
expect(result.error.capturedOutput?.stderr).toBe(message);
|
|
expect(result.error.cleanupIncomplete).toBe(cleanupFails);
|
|
if (cleanupFails) {
|
|
expect(result.error.cleanupResources).toContain(path);
|
|
expect((await client.close()).ok).toBe(true);
|
|
expect(result.error.cleanupIncomplete).toBe(false);
|
|
}
|
|
} finally {
|
|
await client.close();
|
|
await rm(path, { recursive: true, force: true });
|
|
}
|
|
},
|
|
);
|
|
|
|
it("retains a failed root cleanup and retries it on close", async () => {
|
|
const path = await mkdtemp(join(tmpdir(), "rea-macho-operand-test-"));
|
|
let attempts = 0;
|
|
const client = new NativeMacOSProvider(
|
|
{},
|
|
new NativeFixtureRunner(),
|
|
"darwin",
|
|
() => tracer,
|
|
async () => ({
|
|
path,
|
|
close: async () => {
|
|
if (++attempts < 2) throw new Error("fixture cleanup refusal");
|
|
await rm(path, { recursive: true, force: true });
|
|
},
|
|
}),
|
|
).createClient((await parenthesizedTarget()).target);
|
|
try {
|
|
const result = await client.execute("inspect_macho", {});
|
|
expect(result.ok).toBe(false);
|
|
if (result.ok) throw new Error("expected cleanup failure");
|
|
expect(result.error.cleanupIncomplete).toBe(true);
|
|
expect(result.error.cleanupResources).toContain(path);
|
|
expect((await stat(path)).isDirectory()).toBe(true);
|
|
expect((await client.close()).ok).toBe(true);
|
|
expect(result.error.cleanupIncomplete).toBe(false);
|
|
} finally {
|
|
await client.close();
|
|
await rm(path, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it("reports cancellation during final root cleanup", async () => {
|
|
const path = await mkdtemp(join(tmpdir(), "rea-macho-operand-test-"));
|
|
const controller = new AbortController();
|
|
const client = new NativeMacOSProvider(
|
|
{},
|
|
new NativeFixtureRunner(),
|
|
"darwin",
|
|
() => tracer,
|
|
async () => ({
|
|
path,
|
|
close: async () => {
|
|
controller.abort("cancelled while removing alias");
|
|
await rm(path, { recursive: true, force: true });
|
|
},
|
|
}),
|
|
).createClient((await parenthesizedTarget()).target);
|
|
try {
|
|
const result = await client.execute(
|
|
"inspect_macho",
|
|
{},
|
|
{ signal: controller.signal },
|
|
);
|
|
expect(result.ok).toBe(false);
|
|
if (result.ok) throw new Error("expected cancellation");
|
|
expect(result.error._tag).toBe("AnalysisCancelledError");
|
|
expect(result.error.partialObservation).toMatchObject({
|
|
format: "mach-o-universal",
|
|
});
|
|
} finally {
|
|
await client.close();
|
|
await rm(path, { recursive: true, force: true });
|
|
}
|
|
});
|
|
});
|