diff --git a/tests/boundary/filesystem/ghidraPspExtension.test.ts b/tests/boundary/filesystem/ghidraPspExtension.test.ts index 81fdbb0226..572269b119 100644 --- a/tests/boundary/filesystem/ghidraPspExtension.test.ts +++ b/tests/boundary/filesystem/ghidraPspExtension.test.ts @@ -422,30 +422,18 @@ describe.skipIf(process.platform === "win32")( "PSP inspected ABI declaration boundary", () => { it("refuses a PSP image whose section headers lie beyond the captured file", async () => { - const { directory, provider } = await installation(); + const { directory } = await installation(); const path = join(directory, "psp-truncated-sections.elf"); await writeFile(path, pspElfBytes({ sectionHeadersBeyondEof: true })); const parsed = await parseBinaryTarget(path); - if (!parsed.ok) throw parsed.error; - if ( - parsed.value.kind !== "executable" || - parsed.value.architecture !== "mips" - ) - throw new Error("PSP ELF was not recognized as MIPS"); - expect(parsed.value.mips.abiFlags).toEqual(pspAbiFlags); - expect(parsed.value.mips.limitations).toContainEqual( - expect.stringContaining("ABI declarations in unavailable headers"), - ); - expect(provider.inspectTargetSupport(parsed.value).status).toBe( - "unsupported", - ); - const result = await provider.resolveAnalysisProfile(parsed.value); - expect(result.ok).toBe(false); - if (result.ok) throw new Error("Incomplete ABI inspection was admitted"); - expect(result.error._tag).toBe("AnalysisUnsupportedTargetError"); - expect(result.error.message).toContain( - "MIPS ABI declaration inspection is incomplete", - ); + expect(parsed).toMatchObject({ + ok: false, + error: { + _tag: "BinaryTargetError", + constraint: "file_format_rejected", + reason: expect.stringContaining("truncated ELF section header table"), + }, + }); }); it("retains independently inspected PSP ABI bytes through the real target reader", async () => { @@ -559,10 +547,16 @@ describe.skipIf(process.platform === "win32")( expect(result.ok).toBe(false); if (result.ok) throw new Error("Conflicting language override was accepted"); - expect(result.error._tag).toBe("AnalysisUnsupportedTargetError"); - expect(result.error.message).toContain( - "does not accept REA_GHIDRA_LANGUAGE_ID overrides", - ); + expect(result.error).toMatchObject({ + _tag: "ConfigurationError", + settings: [ + { + setting: "REA_GHIDRA_LANGUAGE_ID", + constraint: + "The bounded PSP Allegrex profile does not accept processor or compiler specification overrides.", + }, + ], + }); }); it("keeps PSP and generic language-override launch paths mutually exclusive", () => { diff --git a/tests/boundary/providers/hopper/hopperAnalysisProfile.test.ts b/tests/boundary/providers/hopper/hopperAnalysisProfile.test.ts index af087e7b1a..8234f25930 100644 --- a/tests/boundary/providers/hopper/hopperAnalysisProfile.test.ts +++ b/tests/boundary/providers/hopper/hopperAnalysisProfile.test.ts @@ -67,7 +67,7 @@ describe("Hopper analysis profiles", () => { expect("loaderArgs" in input).toBe(false); }); - it("reports an unresolved profile when the launcher version is unavailable", async () => { + it("refuses a profile when the selected launcher cannot be fingerprinted", async () => { const resolved = await resolveHopperAnalysisProfile( target("elf", "x86_64"), { @@ -77,10 +77,15 @@ describe("Hopper analysis profiles", () => { resourceScope: inspectionResources.owner, }, ); - expect(resolved).toEqual({ - ok: true, - value: { - profile: null, + expect(resolved).toMatchObject({ + ok: false, + error: { + _tag: "ProviderAdapterError", + operation: "resolve_analysis_profile", + diagnostics: { + launcher_path: "/missing/hopper", + system_code: "ENOENT", + }, }, }); }); diff --git a/tests/boundary/providers/hopper/hopperBridgeFacade.test.ts b/tests/boundary/providers/hopper/hopperBridgeFacade.test.ts index f47f579fac..4165b78366 100644 --- a/tests/boundary/providers/hopper/hopperBridgeFacade.test.ts +++ b/tests/boundary/providers/hopper/hopperBridgeFacade.test.ts @@ -5,6 +5,7 @@ import { promisify } from "node:util"; import { describe, expect, it } from "vitest"; import { z } from "zod"; import { OFFICIAL_TOOL_CONTRACTS } from "../../../../src/contracts/officialToolContracts.js"; +import { nativeSymbolMetadataSchema } from "../../../../src/domain/native/nativeSymbol.js"; import { functionDossierSchema } from "../../../../src/domain/hopperValues.js"; const execute = promisify(execFile); @@ -30,6 +31,7 @@ const probeResultSchema = z.strictObject({ procedure: z.strictObject({ address: z.literal("0x401000"), name: z.literal("fixture-procedure"), + symbol: nativeSymbolMetadataSchema.nullable(), classification: z.null(), body: z.strictObject({ available: z.literal(false), @@ -44,6 +46,7 @@ const probeResultSchema = z.strictObject({ length: z.number(), signature: z.literal("int fixture-procedure()"), locals: z.array(z.unknown()), + symbol: nativeSymbolMetadataSchema.nullable(), classification: z.null(), body: z.strictObject({ available: z.literal(false), @@ -55,6 +58,7 @@ const probeResultSchema = z.strictObject({ procedure: z.strictObject({ address: z.literal("0x401000"), name: z.literal("fixture-procedure"), + symbol: nativeSymbolMetadataSchema.nullable(), classification: z.null(), body: z.strictObject({ available: z.literal(false), diff --git a/tests/boundary/providers/native/artifactProvider.directoryErrors.test.ts b/tests/boundary/providers/native/artifactProvider.directoryErrors.test.ts index d14fb3033c..58629628be 100644 --- a/tests/boundary/providers/native/artifactProvider.directoryErrors.test.ts +++ b/tests/boundary/providers/native/artifactProvider.directoryErrors.test.ts @@ -5,13 +5,13 @@ import { describe, expect, it } from "vitest"; import { parseBinaryTarget } from "../../../../src/application/BinaryTargetResolver.js"; import { ArtifactProvider } from "../../../../src/artifacts/ArtifactProvider.js"; -import { projectAnalysisError } from "../../../../src/domain/analysisErrorProjection.js"; +import { artifactInventoryResultSchema } from "../../../../src/domain/artifactGraph.js"; import { thinMach } from "../../../../src/domain/binaryTarget.fixture.js"; import { createTestTempDirectory } from "../../../fixtures/temporaryDirectory.js"; describe("artifact directory diagnostics", () => { it.skipIf(process.platform === "win32" || process.getuid?.() === 0)( - "preserves the inaccessible member path and errno through provider projection", + "retains an inaccessible member and its errno alongside readable inventory facts", async () => { const root = await createTestTempDirectory("rea-directory-eacces-"); const appPath = join(root, "Denied.app"); @@ -32,20 +32,31 @@ describe("artifact directory diagnostics", () => { const result = await new ArtifactProvider(process.env) .createClient(target.value) .execute("inventory_artifact", {}); - expect(result.ok).toBe(false); - if (result.ok) return; - expect(projectAnalysisError(result.error)).toMatchObject({ - code: "artifact_operation_failed", - details: { - operation: "inventory_artifact", - reason: "io", - }, + if (!result.ok) throw result.error; + const inventory = artifactInventoryResultSchema.parse( + result.value.result, + ); + expect(inventory.coverage.status).toBe("unknown"); + const blocked = inventory.occurrences.find( + ({ logical_path }) => logical_path === "Contents/blocked.txt", + ); + expect(blocked).toMatchObject({ + artifact_id: null, + hash_status: "unavailable", }); - const projected = projectAnalysisError(result.error); - const detail = projected.details?.detail; - expect(typeof detail).toBe("string"); + const detail = blocked?.limitations.join("\n"); expect(detail).toContain("Denied.app/Contents/blocked.txt"); expect(detail).toContain("EACCES"); + expect(inventory.limitations).toContainEqual( + expect.stringContaining("EACCES"), + ); + expect(inventory.occurrences).toContainEqual( + expect.objectContaining({ + logical_path: "Contents/MacOS/Fixture", + hash_status: "verified", + artifact_id: expect.any(String), + }), + ); } finally { await chmod(blockedPath, 0o600); } diff --git a/tests/boundary/providers/native/lldbCallTracerFailures.test.ts b/tests/boundary/providers/native/lldbCallTracerFailures.test.ts index 1a1de0c009..2ab65131ec 100644 --- a/tests/boundary/providers/native/lldbCallTracerFailures.test.ts +++ b/tests/boundary/providers/native/lldbCallTracerFailures.test.ts @@ -610,6 +610,7 @@ const completedOutputCloseFixture = async () => { join(tmpdir(), "rea-lldb-complete-output-"), ); const fakeLldb = join(fixtureRoot, "fake-lldb"); + const completedPidPath = join(fixtureRoot, "completed-target.pid"); const completed = { ...successfulTracerOutput, target_output: { @@ -628,6 +629,7 @@ const config = JSON.parse(fs.readFileSync(command.slice("rea_trace ".length), "u const child = spawnSync(process.execPath, ["-e", ""]); const result = ${JSON.stringify(completed)}; result.pid = child.pid; +fs.writeFileSync(${JSON.stringify(completedPidPath)}, String(child.pid)); fs.writeFileSync(config.pid_path, String(child.pid)); fs.writeFileSync(config.stdout_capture_path, "ok"); fs.writeFileSync(config.stderr_capture_path, ""); @@ -637,7 +639,18 @@ process.exit(0); `, { mode: 0o700 }, ); - return { fixtureRoot, fakeLldb, completed }; + return { + fixtureRoot, + fakeLldb, + readCompleted: async () => ({ + ...completed, + pid: z.coerce + .number() + .int() + .positive() + .parse(await readFile(completedPidPath, "utf8")), + }), + }; }; describe("LLDB process exit at output-close loop completion", () => { @@ -645,8 +658,7 @@ describe("LLDB process exit at output-close loop completion", () => { it.skipIf(process.platform === "win32")( `retains completed trace facts after output closure (cancelled=${String(abort)})`, async () => { - const { fixtureRoot, fakeLldb, completed } = - await completedOutputCloseFixture(); + const fixture = await completedOutputCloseFixture(); const controller = new AbortController(); const reason = { requested: "stop after completed output closure" }; let interrupted = false; @@ -672,10 +684,11 @@ describe("LLDB process exit at output-close loop completion", () => { return closed; }); const tracer = new LldbCallTracer({}, undefined, async () => - ok({ path: fakeLldb, sha256: "e".repeat(64) }), + ok({ path: fixture.fakeLldb, sha256: "e".repeat(64) }), ); try { const result = await tracer.trace(request, controller.signal); + const completed = await fixture.readCompleted(); expect(interrupted).toBe(abort); if (result.ok) { expect(abort).toBe(false); @@ -698,6 +711,7 @@ describe("LLDB process exit at output-close loop completion", () => { coverage: { status: "partial", reason: "cancelled" }, debugger: { version: completed.version }, process: { + pid: completed.pid, outcome: "exited", exit_status: 0, terminated: true, @@ -712,7 +726,7 @@ describe("LLDB process exit at output-close loop completion", () => { } finally { waitSpy.mockRestore(); expect((await tracer.close()).ok).toBe(true); - await rm(fixtureRoot, { recursive: true, force: true }); + await rm(fixture.fixtureRoot, { recursive: true, force: true }); } }, ); diff --git a/tests/boundary/providers/native/nativeSignatureTargetBinding.test.ts b/tests/boundary/providers/native/nativeSignatureTargetBinding.test.ts index 28aef75063..8c05be76a5 100644 --- a/tests/boundary/providers/native/nativeSignatureTargetBinding.test.ts +++ b/tests/boundary/providers/native/nativeSignatureTargetBinding.test.ts @@ -16,6 +16,7 @@ import { dirname, join } from "node:path"; import { describe, expect, it, onTestFinished } from "vitest"; import { signatureInspectionFixture } from "../../../fixtures/nativeCommands.js"; import { resolvedAnalysisError } from "../../../../src/domain/analysisErrorBase.js"; +import { parseBinaryTarget } from "../../../../src/application/BinaryTargetResolver.js"; import { NativeMacOSProvider } from "../../../../src/native/NativeMacOSProvider.js"; import { ArtifactResourceScope } from "../../../../src/artifacts/ArtifactResourceScope.js"; import { @@ -77,19 +78,20 @@ const registerSignatureObservationCases = () => { .createClient(target) .execute("inspect_signature", {}); if (!result.ok) throw result.error; - expect(captures).toHaveLength(3); - expect( - captures.every( - (args) => args[args.indexOf("-a") + 1] === target.architecture, - ), - ).toBe(true); + expect(captures.map((args) => args[args.indexOf("-a") + 1])).toEqual([ + target.architecture, + target.architecture, + target.architecture, + ...target.availableArchitectures, + ]); expect(result.value.result).toMatchObject({ signed: true, identifier: "com.example.fixture", designated_requirement: "identifier com.example.fixture", - limitations: [ + limitations: expect.arrayContaining([ "Inspected a digest-verified Mach-O byte snapshot; filesystem-bound signing context and trust validation are not examined.", - ], + expect.stringContaining("Partial signature coverage:"), + ]), }); }); @@ -271,9 +273,11 @@ it.each([ }, }; let failed = false; + let displayCaptures = 0; const runner: NativeCommandRunner = { async run(tool, args) { - const slice = args.includes("-a"); + const display = args.includes("--verbose=4"); + const slice = display && displayCaptures++ > 0; const selected = stage === "slice" ? slice @@ -281,9 +285,7 @@ it.each([ ? args.includes("-r-") : stage === "entitlements" ? args.includes("--entitlements") - : !args.includes("-r-") && - !args.includes("--entitlements") && - !slice; + : display && !slice; if (selected) { failed = true; await rm(path); @@ -443,7 +445,11 @@ it.skipIf(process.platform !== "darwin")( ); const path = join(directory, "source\ncode object is not signed\noperand"); await copyFile("/usr/bin/true", path); - const target = await nativeMachoTargetForFile(path); + const admitted = await parseBinaryTarget(path); + if (!admitted.ok) throw admitted.error; + const target = admitted.value; + if (target.kind !== "executable") + throw new Error("Expected the copied system executable"); const originalBytes = await readFile(path); const real = new XcrunCommandRunner({}); let snapshotPath: string | undefined; @@ -487,7 +493,11 @@ it.skipIf(process.platform !== "darwin")( const path = join(directory, "source\ncode object is not signed\noperand"); await copyFile("/usr/bin/true", path); await runFile("/usr/bin/codesign", ["--remove-signature", path]); - const target = await nativeMachoTargetForFile(path); + const admitted = await parseBinaryTarget(path); + if (!admitted.ok) throw admitted.error; + const target = admitted.value; + if (target.kind !== "executable") + throw new Error("Expected the copied system executable"); const result = await new NativeMacOSProvider( {}, new XcrunCommandRunner({}), @@ -498,7 +508,22 @@ it.skipIf(process.platform !== "darwin")( if (!result.ok) throw result.error; const parsed = inspectSignatureSchema.parse(result.value.result); expect(parsed).toMatchObject({ signed: false }); - expect(parsed.provenance).toHaveLength(3); + const selectedArchitecture = + target.architecture === "x86" ? "i386" : target.architecture; + expect( + parsed.provenance.map( + ({ command }) => command[command.indexOf("-a") + 1], + ), + ).toEqual([ + selectedArchitecture, + selectedArchitecture, + selectedArchitecture, + ...(target.availableArchitectures.length > 1 + ? target.availableArchitectures.map((architecture) => + architecture === "x86" ? "i386" : architecture, + ) + : []), + ]); expect(parsed.provenance.every(({ exit }) => exit.code === 1)).toBe(true); }, ); @@ -620,7 +645,11 @@ it.skipIf(process.platform !== "darwin")( program, ]); } - const target = await nativeMachoTargetForFile(path); + const admitted = await parseBinaryTarget(path); + if (!admitted.ok) throw admitted.error; + const target = admitted.value; + if (target.kind !== "executable") + throw new Error("Expected the copied system executable"); const real = new XcrunCommandRunner({}); let capturedPath: string | undefined; const runner: NativeCommandRunner = { @@ -702,7 +731,7 @@ for (const cleanupFails of [false, true]) expect(result.error.cleanupIncomplete).toBe(cleanupFails); const facts = inspectSignatureSchema.parse(result.error.partialObservation); expect(facts.signed).toBe(true); - expect(facts.provenance).toHaveLength(3); + expect(facts.provenance).toHaveLength(5); expect( projectAnalysisError(result.error).details?.partial_observation, ).toEqual(facts);