From dcee59f273725b65e9142dee3889cc755cf04fbe Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Tue, 10 Nov 2026 17:03:59 -0500 Subject: [PATCH] test: strengthen independent oracles and consolidate boundary coverage Require delivered facts, exact cancellation and malformed-input boundaries, and independent metadata expectations. Transfer unique assertions into stronger keepers before removing duplicate cases and overwritten fixture writes; preserve all consumed core fixture bytes. Validation: 1,190 focused tests passed with 39 platform-dependent skips; 134 composition and conformance tests passed. Typecheck, lint, formatting, discovery and Knip passed. Independent preservation reviews and targeted regression controls passed. --- scripts/lib/real-hopper-fixture.d.mts | 1 + scripts/lib/recorded-core-fixtures.py | 3 +- scripts/verify-recorded-crash.mjs | 29 ++--- .../filesystem/dshClientConfiguration.test.ts | 43 +------ .../javascriptBundlerAdapters.test.ts | 33 +++-- .../javascriptExportShapeComparison.test.ts | 19 +++ .../filesystem/windowsNpxRegistration.test.ts | 61 +++++---- ...riptWorkerFinalizationCancellation.test.ts | 6 +- .../mcp/analysisSummaryDetailMcp.test.ts | 23 +++- .../mcp/applicationExportShapeMcp.test.ts | 52 ++------ .../mcp/batchDecompileViewsMcp.test.ts | 7 +- .../boundary/mcp/buildCaptureRetryMcp.test.ts | 9 +- .../mcp/cutterCancellationMcp.test.ts | 119 +++--------------- .../enhancedTools.callPathsAndBatches.test.ts | 32 ----- .../mcp/enhancedTools.limitations.test.ts | 39 ------ .../boundary/mcp/environmentBundleMcp.test.ts | 2 +- .../mcp/functionComparisonMcp.test.ts | 23 ---- tests/boundary/model/modelArtifact.test.ts | 7 +- tests/boundary/pcap/pcapProvider.test.ts | 13 -- .../process/adbVerifierCleanup.test.ts | 11 +- tests/boundary/process/fridaVerifier.test.ts | 5 +- .../boundary/process/jadxIntegration.test.ts | 36 +++--- .../process/ownedDebuggerSession.test.ts | 42 +------ .../adb/adbInputSnapshotOwnership.test.ts | 2 + .../hopper/hopperClient.cleanupRetry.test.ts | 27 +--- .../providers/kuna/kunaProvider.test.ts | 25 +--- ...anagedMemberComparisonFromMetadata.test.ts | 61 +++++++-- tests/composition/reverseEngineering.test.ts | 70 ++++++----- .../reverseEngineeringEdgeCases.test.ts | 21 ---- ...ngineeringLocalInspectionEdgeCases.test.ts | 31 ----- .../conformance/ida/providerContracts.test.ts | 52 ++------ .../package/packedTarListing.test.ts | 7 -- tests/fixtures/fakeBuildComputerRea.ts | 3 + tests/fixtures/il2cpp/metadata.ts | 112 ++++++++++++++++- 34 files changed, 408 insertions(+), 618 deletions(-) delete mode 100644 tests/composition/reverseEngineeringLocalInspectionEdgeCases.test.ts diff --git a/scripts/lib/real-hopper-fixture.d.mts b/scripts/lib/real-hopper-fixture.d.mts index a9b99837bd..03ea921e7f 100644 --- a/scripts/lib/real-hopper-fixture.d.mts +++ b/scripts/lib/real-hopper-fixture.d.mts @@ -24,6 +24,7 @@ export interface RealHopperFixtureTargets { readonly manifestPath: string; readonly primary: RealHopperFixtureTarget; readonly secondary: RealHopperFixtureTarget; + readonly unicode?: RealHopperFixtureTarget; readonly large: RealHopperFixtureTarget; readonly oracle: RealHopperFixtureOracle; readonly largeOracle: RealHopperLargeFixtureOracle; diff --git a/scripts/lib/recorded-core-fixtures.py b/scripts/lib/recorded-core-fixtures.py index f0bfa52b0b..b12f728ee7 100644 --- a/scripts/lib/recorded-core-fixtures.py +++ b/scripts/lib/recorded-core-fixtures.py @@ -34,9 +34,8 @@ def note_header(content, note, **changes): # Authored metadata carriers exercise actual parser projection through public # inspection, independently of the host that produced the captured amd64 core. +# The named ARCHEXT/UNWIND carriers are written once below using numeric type IDs. for name, machine, kind in [ - ("aarch64-archext", "EM_AARCH64", "PT_AARCH64_ARCHEXT"), - ("aarch64-unwind", "EM_AARCH64", "PT_AARCH64_UNWIND"), ("aarch64-mte-opaque", "EM_AARCH64", 0x70000002), ("amd64-processor-opaque", "EM_X86_64", 0x70000001), ]: diff --git a/scripts/verify-recorded-crash.mjs b/scripts/verify-recorded-crash.mjs index 74fcd0bee6..6a5be6c135 100644 --- a/scripts/verify-recorded-crash.mjs +++ b/scripts/verify-recorded-crash.mjs @@ -196,17 +196,6 @@ try { assert.equal(value.threads.length, 1); cases++; } - for (const [name, kind, label] of [ - ["aarch64-archext", 0x70000000, "PT_AARCH64_ARCHEXT"], - ["aarch64-unwind", 0x70000001, "PT_AARCH64_UNWIND"], - ]) { - const value = await basic.inspect(mode, fixtures[name]); - assert.equal(value.architecture, "aarch64-little-endian"); - assert.equal(value.segments[0]?.type_id, kind); - assert.equal(value.segments[0]?.type, label); - assert.equal(value.threads.length, 0); - cases++; - } const noSignals = await basic.inspect(mode, fixtures["unknown-signals"]); assert.equal(noSignals.signals.length, 0); assert.equal(noSignals.threads.length, 2); @@ -216,9 +205,19 @@ try { assert.equal(noNotes.threads.length, 0); assert.equal(noNotes.note_interpretation_completeness, "unknown"); cases++; - for (const [name, architecture, type] of [ - ["aarch64-archext", "aarch64-little-endian", "PT_AARCH64_ARCHEXT"], - ["aarch64-unwind", "aarch64-little-endian", "PT_AARCH64_UNWIND"], + for (const [name, architecture, type, typeId] of [ + [ + "aarch64-archext", + "aarch64-little-endian", + "PT_AARCH64_ARCHEXT", + 0x70000000, + ], + [ + "aarch64-unwind", + "aarch64-little-endian", + "PT_AARCH64_UNWIND", + 0x70000001, + ], ["aarch64-mte-opaque", "aarch64-little-endian", 0x70000002], ["amd64-processor-opaque", "x86_64-little-endian", 0x70000001], ]) { @@ -226,6 +225,8 @@ try { assert.equal(auxiliary.architecture, architecture); assert.equal(auxiliary.segments.length, 1); assert.equal(auxiliary.segments[0].type, type); + if (typeId !== undefined) + assert.equal(auxiliary.segments[0].type_id, typeId); assert.equal(auxiliary.notes.length, 0); assert.equal(auxiliary.threads.length, 0); assert.equal(auxiliary.note_interpretation_completeness, "unknown"); diff --git a/tests/boundary/filesystem/dshClientConfiguration.test.ts b/tests/boundary/filesystem/dshClientConfiguration.test.ts index deca95a211..d2103f9826 100644 --- a/tests/boundary/filesystem/dshClientConfiguration.test.ts +++ b/tests/boundary/filesystem/dshClientConfiguration.test.ts @@ -1159,44 +1159,6 @@ describe("DeepSeek Harness alias collapse", () => { }); describe("DeepSeek Harness alias targets", () => { - it("collapses an alias of the insert list", async () => { - const { client } = await harnessClient(); - await writeFile( - client.configPath, - `- holder: &rows - - id: other-tool - name: other - config: - command: keep-me - - id: rea-mcp - name: "@deepseek-ai/dsh-mcp-client" - config: - serverName: rea - transport: stdio - command: stale-launcher - args: [mcp] -- insert: *rows -`, - ); - - expect( - (await configureClientConfiguration(client, {}, command)).status, - ).toBe("configured"); - const text = await readFile(client.configPath, "utf8"); - expect(text).toContain("other-tool"); - expect(text).not.toContain("*rows"); - expect(text).toContain("stale-launcher"); - const patches = parseYaml(text) as { insert?: { id?: string }[] }[]; - expect( - patches.flatMap(({ insert }) => - (insert ?? []).filter((entry) => entry.id === "rea-mcp"), - ), - ).toHaveLength(1); - expect( - (await configureClientConfiguration(client, {}, command)).status, - ).toBe("unchanged"); - }); - it("keeps a Cordis tag when the config map is an alias", async () => { const { client } = await harnessClient(); await writeFile( @@ -1256,7 +1218,10 @@ describe("DeepSeek Harness alias identity", () => { expect( (await configureClientConfiguration(client, {}, command)).status, ).toBe("configured"); - const patches = parseYaml(await readFile(client.configPath, "utf8")) as { + const text = await readFile(client.configPath, "utf8"); + expect(text).not.toContain("*rows"); + expect(text).toContain("stale-launcher"); + const patches = parseYaml(text) as { insert?: { id?: string; config?: { command?: string } }[]; }[]; const inserted = patches.flatMap((patch) => patch.insert ?? []); diff --git a/tests/boundary/filesystem/javascript-applications/javascriptBundlerAdapters.test.ts b/tests/boundary/filesystem/javascript-applications/javascriptBundlerAdapters.test.ts index f20eaac1fb..4fb5a2f563 100644 --- a/tests/boundary/filesystem/javascript-applications/javascriptBundlerAdapters.test.ts +++ b/tests/boundary/filesystem/javascript-applications/javascriptBundlerAdapters.test.ts @@ -125,7 +125,9 @@ describe("Webpack and Rspack runtime adapters", () => { ), ).toBe(false); }); +}); +describe("esbuild and Vite runtime adapters", () => { it("recovers esbuild wrapper modules and Vite preload dependencies", async () => { const root = await esmRuntimeFixture(); const { graph } = await reconstructJavaScriptFixture(root); @@ -142,14 +144,31 @@ describe("Webpack and Rspack runtime adapters", () => { runtime: "esbuild-esm", module_key: "src/core.js", }); + const dependencies = graph.edges.filter( + ({ relation, properties }) => + relation === "imports" && + properties.kind === "dynamic-import" && + ["./feature.js", "./main.css"].includes(String(properties.specifier)), + ); expect( - graph.edges.filter( - ({ relation, properties }) => - relation === "imports" && - properties.kind === "dynamic-import" && - ["./feature.js", "./main.css"].includes(String(properties.specifier)), - ), - ).toHaveLength(2); + dependencies.map(({ properties }) => properties.specifier).sort(), + ).toEqual(["./feature.js", "./main.css"]); + expect( + dependencies.find( + ({ properties }) => properties.specifier === "./feature.js", + )?.properties, + ).toMatchObject({ + resolution_status: "resolved", + resolved_path: "feature.js", + }); + expect( + dependencies.find( + ({ properties }) => properties.specifier === "./main.css", + )?.properties, + ).toMatchObject({ + resolution_status: "unavailable", + candidate_path: "main.css", + }); }); }); diff --git a/tests/boundary/filesystem/javascriptExportShapeComparison.test.ts b/tests/boundary/filesystem/javascriptExportShapeComparison.test.ts index 199ddcc5ed..5469a3d39e 100644 --- a/tests/boundary/filesystem/javascriptExportShapeComparison.test.ts +++ b/tests/boundary/filesystem/javascriptExportShapeComparison.test.ts @@ -1195,6 +1195,25 @@ describe("JavaScript export return-shape projection", () => { expect(result.coverage.status).toBe("complete-within-inputs"); expect(result.coverage.left_omitted_fields).toBe(0); expect(result.coverage.right_omitted_fields).toBe(0); + const paths = [ + "/a_type", + ...Array.from({ length: 70 }, (_, i) => `/field${String(i)}`), + ].sort(); + expect(result.property_inventories.map(({ side }) => side).sort()).toEqual([ + "left", + "right", + ]); + for (const inventory of result.property_inventories) { + expect(inventory.paired).toBe(true); + expect([...inventory.properties].sort()).toEqual(paths); + } + expect(result.changes).toEqual([]); + expect(result.summary).toEqual({ + added: 0, + removed: 0, + changed: 0, + unknown: 0, + }); }); }); diff --git a/tests/boundary/filesystem/windowsNpxRegistration.test.ts b/tests/boundary/filesystem/windowsNpxRegistration.test.ts index f6c52d0f75..1d190ade9a 100644 --- a/tests/boundary/filesystem/windowsNpxRegistration.test.ts +++ b/tests/boundary/filesystem/windowsNpxRegistration.test.ts @@ -104,38 +104,35 @@ it.each([ }, ); -it.each(["npx", "bunx"] as const)( - "keeps the Windows %s launcher aligned through Codex TOML setup and removal", - async (runner) => { - const home = await createTestTempDirectory("rea-windows-codex-"); - await mkdir(join(home, ".codex")); - const configPath = join(home, ".codex", "config.toml"); - const original = - '\uFEFF# Keep this explanation.\nnotify = ["a", "b"]\nliteral = \'C:\\demo\\path\'\ndisabled_mcp_servers = ["rea"]\n\n[mcp_servers.other]\ncommand = "another-server"\n'; - await writeFile(configPath, original); - const client = { name: "codex", configPath, format: "toml" } as const; - const command = currentClientRegistrationCommand({ platform: "win32" }); - expect( - await configureClientConfiguration(client, {}, command), - ).toMatchObject({ status: "configured" }); - expect( - await readClientRegistrationStatuses(home, undefined, { - platform: "win32", - environment: {}, - }), - ).toContainEqual( - expect.objectContaining({ client: "codex", state: "aligned" }), - ); - expect(await configureClientConfiguration(client, {}, command)).toEqual({ - status: "unchanged", - }); - expect(await readFile(`${configPath}.rea.backup`, "utf8")).toBe(original); - expect(await systemUninstallHost(home).removeClient(client)).toMatchObject({ - status: "removed", - }); - expect(await readFile(configPath, "utf8")).toBe(original); - }, -); +it("keeps the Windows direct launcher aligned through Codex TOML setup and removal", async () => { + const home = await createTestTempDirectory("rea-windows-codex-"); + await mkdir(join(home, ".codex")); + const configPath = join(home, ".codex", "config.toml"); + const original = + '\uFEFF# Keep this explanation.\nnotify = ["a", "b"]\nliteral = \'C:\\demo\\path\'\ndisabled_mcp_servers = ["rea"]\n\n[mcp_servers.other]\ncommand = "another-server"\n'; + await writeFile(configPath, original); + const client = { name: "codex", configPath, format: "toml" } as const; + const command = currentClientRegistrationCommand({ platform: "win32" }); + expect(await configureClientConfiguration(client, {}, command)).toMatchObject( + { status: "configured" }, + ); + expect( + await readClientRegistrationStatuses(home, undefined, { + platform: "win32", + environment: {}, + }), + ).toContainEqual( + expect.objectContaining({ client: "codex", state: "aligned" }), + ); + expect(await configureClientConfiguration(client, {}, command)).toEqual({ + status: "unchanged", + }); + expect(await readFile(`${configPath}.rea.backup`, "utf8")).toBe(original); + expect(await systemUninstallHost(home).removeClient(client)).toMatchObject({ + status: "removed", + }); + expect(await readFile(configPath, "utf8")).toBe(original); +}); it("retains rea-named custom shell registrations without writing a backup", async () => { const home = await createTestTempDirectory("rea-custom-shell-registration-"); diff --git a/tests/boundary/javascript/javascriptWorkerFinalizationCancellation.test.ts b/tests/boundary/javascript/javascriptWorkerFinalizationCancellation.test.ts index 7f3f67de7f..c41b05559f 100644 --- a/tests/boundary/javascript/javascriptWorkerFinalizationCancellation.test.ts +++ b/tests/boundary/javascript/javascriptWorkerFinalizationCancellation.test.ts @@ -70,8 +70,8 @@ it.each([false, true])( cancelled: !cancelled.ok && (cancelled.error.error.cleanupPrimary ?? cancelled.error.error)._tag === 'AnalysisCancelledError', exactReason: !cancelled.ok && (cancelled.error.error.cleanupPrimary ?? cancelled.error.error).cause === reason, cleanupIncomplete: !cancelled.ok && cancelled.error.error.cleanupIncomplete, - parseStatus: facts.javascript?.parse_status, hasModule: facts.module !== null, - hasProjection: facts.projection !== null, reusable: worker.canReuse() }; + parseStatus: facts.javascript?.parse_status, hasModule: facts.module != null, + hasProjection: facts.projection != null, exportedNames: facts.module?.moduleLinks.filter(link => link.kind === 'export').map(link => link.exportedName), projectedModule: facts.projection?.nodes.some(node => node.kind === 'module' && node.identity.module_path === 'cancelled.mjs'), reusable: worker.canReuse() }; } finally { fs.promises.rm = nativeRm; syncBuiltinESMExports(); @@ -101,6 +101,8 @@ it.each([false, true])( parseStatus: "complete", hasModule: true, hasProjection: true, + exportedNames: ["observed"], + projectedModule: true, reusable: false, rootRemoved: true, childExited: true, diff --git a/tests/boundary/mcp/analysisSummaryDetailMcp.test.ts b/tests/boundary/mcp/analysisSummaryDetailMcp.test.ts index bb52c778ff..f49bee07cb 100644 --- a/tests/boundary/mcp/analysisSummaryDetailMcp.test.ts +++ b/tests/boundary/mcp/analysisSummaryDetailMcp.test.ts @@ -117,6 +117,10 @@ it("returns the summary of complete Evidence retained for later views", async () normalized_result: { kind: "page", parent_evidence_id: completeEvidence.evidence_id, + items: expect.arrayContaining([ + expect.objectContaining({ kind: "javascript-module", path: "main.js" }), + expect.objectContaining({ kind: "javascript-module", path: "util.js" }), + ]), }, }); }); @@ -171,7 +175,24 @@ it("returns the summary of retained binary layout Evidence", async () => { }); expect(sections.isError).not.toBe(true); expect(sections.structuredContent).toMatchObject({ - normalized_result: { kind: "page", parent_evidence_id: parentId }, + normalized_result: { + kind: "page", + parent_evidence_id: parentId, + items: [ + expect.objectContaining({ + index: 0, + name: expect.objectContaining({ display: ".text" }), + }), + expect.objectContaining({ + index: 1, + name: expect.objectContaining({ display: ".text" }), + }), + expect.objectContaining({ + index: 2, + name: expect.objectContaining({ display: ".data" }), + }), + ], + }, }); }); diff --git a/tests/boundary/mcp/applicationExportShapeMcp.test.ts b/tests/boundary/mcp/applicationExportShapeMcp.test.ts index 5649919e18..84ee05321b 100644 --- a/tests/boundary/mcp/applicationExportShapeMcp.test.ts +++ b/tests/boundary/mcp/applicationExportShapeMcp.test.ts @@ -318,6 +318,7 @@ describe("repeated partial export comparisons", () => { const evidence = parseEvidence(response.structuredContent); expect(evidence.normalized_result).toMatchObject({ changes: [], + summary: { added: 0, removed: 0, changed: 0, unknown: 0 }, coverage: { status: "partial" }, }); return evidence.evidence_id; @@ -366,6 +367,13 @@ describe("repeated partial export comparisons", () => { true, ); const retained = session.exportEvidenceBundle(); + for (const id of evidenceIds) + expect(retained.unknowns).toContainEqual( + expect.objectContaining({ + domain: "javascript-export-shape", + supporting_evidence_ids: [id], + }), + ); await Promise.all(pairs.map((pair) => compare(pair))); expect(session.exportEvidenceBundle()).toEqual(retained); }, @@ -373,50 +381,6 @@ describe("repeated partial export comparisons", () => { }); describe("source-produced export comparison inventories", () => { - it("records partial comparison coverage even when matching unknown projections produce no changes", async () => { - const source = `export default function make() { - const result = { kind: "record", count: 1 }; - delete result.count; - return result; - }`; - const [left, right] = await Promise.all([ - analyzeSourceEvidence(source), - analyzeSourceEvidence(source), - ]); - const { client, session, close } = await createApplicationMcpHarness(); - onTestFinished(close); - const response = await client.callTool({ - name: "compare_javascript_export_shapes", - arguments: { - left, - right, - left_module_path: "parser.mjs", - left_export_name: "default", - right_module_path: "parser.mjs", - right_export_name: "default", - }, - }); - expect(response.isError).not.toBe(true); - expect(response.structuredContent).toMatchObject({ - normalized_result: { - changes: [], - summary: { added: 0, removed: 0, changed: 0, unknown: 0 }, - coverage: { status: "partial" }, - }, - }); - const { evidence_id: evidenceId } = z - .object({ evidence_id: z.string() }) - .parse(response.structuredContent); - expect(session.exportEvidenceBundle().unknowns).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - domain: "javascript-export-shape", - supporting_evidence_ids: [evidenceId], - }), - ]), - ); - }); - it("retains large source-produced export inventories through comparison schemas", async () => { const fields = Array.from( { length: 64 }, diff --git a/tests/boundary/mcp/batchDecompileViewsMcp.test.ts b/tests/boundary/mcp/batchDecompileViewsMcp.test.ts index ffe79f260d..4ee5ca96de 100644 --- a/tests/boundary/mcp/batchDecompileViewsMcp.test.ts +++ b/tests/boundary/mcp/batchDecompileViewsMcp.test.ts @@ -196,6 +196,7 @@ const readBody = async ( ) => { let text = ""; let pages = 0; + const nextOffsets: (number | null)[] = []; let offset: number | null = 0; while (offset !== null) { const page = await view(harness, parentId, { @@ -208,9 +209,10 @@ const readBody = async ( offset = z .looseObject({ next_offset: z.number().nullable() }) .parse(page.coverage).next_offset; + nextOffsets.push(offset); pages++; } - return { text, pages }; + return { text, pages, nextOffsets }; }; /** A typed refusal whose issue names the rejected selection. */ @@ -408,7 +410,8 @@ it("pages one body exactly without splitting a Unicode character", async () => { expect(JSON.stringify(evidence.normalized_result)).toContain("int main"); // A limit ending inside a surrogate pair stops before it, so pages rebuild the text. - const { text, pages } = await readBody(harness, parent, 3, 30); + const { text, pages, nextOffsets } = await readBody(harness, parent, 3, 31); + expect(nextOffsets[0]).toBe(30); expect(text).toBe(EMOJI_BODY); expect(pages).toBeGreaterThan(1); expect( diff --git a/tests/boundary/mcp/buildCaptureRetryMcp.test.ts b/tests/boundary/mcp/buildCaptureRetryMcp.test.ts index a64d03249b..e4e28f924d 100644 --- a/tests/boundary/mcp/buildCaptureRetryMcp.test.ts +++ b/tests/boundary/mcp/buildCaptureRetryMcp.test.ts @@ -135,7 +135,7 @@ it("reports a failed package list download with apt's own message", async () => }); it("downloads the package lists again when an update exits 0 with no list", async () => { - const { client } = await fakes({ + const { client, rea } = await fakes({ env: { FAKE_APT_EMPTY: "1", FAKE_APT_IGNORE_ERROR_MODE: "1" }, }); const id = await startBuildComputer(client, BUILD_REQUEST); @@ -148,12 +148,13 @@ it("downloads the package lists again when an update exits 0 with no list", asyn expect(value.package_lists_error, `start ${String(attempt)}`).toContain( "without a package list", ); + expect(await rea.notes("apt.jsonl")).toHaveLength(attempt); await callOk(client, "stop_build_capture", { computer_id: id }); } }); it("retries an empty apt update that Error-Mode turns into a failure", async () => { - const { client } = await fakes({ env: { FAKE_APT_EMPTY: "1" } }); + const { client, rea } = await fakes({ env: { FAKE_APT_EMPTY: "1" } }); const id = await startBuildComputer(client, BUILD_REQUEST); for (const attempt of [1, 2]) { const value = toolContract("start_build_capture").outputSchema.parse( @@ -164,12 +165,13 @@ it("retries an empty apt update that Error-Mode turns into a failure", async () expect(value.package_lists_error, `start ${String(attempt)}`).toContain( "exited 100", ); + expect(await rea.notes("apt.jsonl")).toHaveLength(attempt); await callOk(client, "stop_build_capture", { computer_id: id }); } }); it("downloads the package lists again on the next start after an update that fetched some and failed", async () => { - const { client } = await fakes({ + const { client, rea } = await fakes({ env: { FAKE_APT_EXIT: "100", FAKE_APT_PARTIAL: "1" }, }); const id = await startBuildComputer(client, BUILD_REQUEST); @@ -182,6 +184,7 @@ it("downloads the package lists again on the next start after an update that fet expect(value.package_lists_error, `start ${String(attempt)}`).toContain( "exited 100", ); + expect(await rea.notes("apt.jsonl")).toHaveLength(attempt); await callOk(client, "stop_build_capture", { computer_id: id }); } }); diff --git a/tests/boundary/mcp/cutterCancellationMcp.test.ts b/tests/boundary/mcp/cutterCancellationMcp.test.ts index f8b520e511..f0f276331e 100644 --- a/tests/boundary/mcp/cutterCancellationMcp.test.ts +++ b/tests/boundary/mcp/cutterCancellationMcp.test.ts @@ -4,7 +4,6 @@ import { expect, it } from "vitest"; import { chmod, rm, writeFile } from "node:fs/promises"; import { createServer, type Socket } from "node:net"; import { join } from "node:path"; -import { randomUUID } from "node:crypto"; import { authenticatedServer, @@ -19,101 +18,6 @@ import { createToolResultDelivery } from "../../../src/server/toolResult.js"; import { silentLogger } from "../../../src/logger.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; -it.skipIf(process.platform === "win32")( - "closes cancelled discovery probes, stops queued probes and reuses the MCP connection", - async () => { - const directory = await createTestTempDirectory("rea-cutter-discovery-"); - await chmod(directory, 0o700); - const sockets = new Set(); - let accepted = 0; - const peer = createServer((socket) => { - accepted += 1; - sockets.add(socket); - socket.on("error", () => undefined); - socket.once("close", () => sockets.delete(socket)); - // Drain authentication bytes so a paused readable cannot hide peer EOF. - socket.resume(); - }); - const descriptors: string[] = []; - const service = new CutterBridgeService( - new CutterBridgeClient({ REA_CUTTER_BRIDGE_DIR: directory }), - ); - const server = new McpServer({ name: "cutter-discovery", version: "1" }); - registerCutterTools( - server, - service, - silentLogger, - createToolResultDelivery(undefined), - ); - const client = new Client({ name: "cutter-discovery", version: "1" }); - const [clientTransport, serverTransport] = - InMemoryTransport.createLinkedPair(); - const controller = new AbortController(); - try { - await new Promise((resolve) => - peer.listen(0, "127.0.0.1", resolve), - ); - const address = peer.address(); - if (address === null || typeof address === "string") - throw new Error("Expected private loopback listener"); - for (let index = 0; index < 9; index += 1) { - const sessionId = randomUUID(); - const path = join(directory, `cutter-${process.pid}-${sessionId}.json`); - descriptors.push(path); - await writeFile( - path, - JSON.stringify({ - session_id: sessionId, - pid: process.pid, - host: "127.0.0.1", - port: address.port, - token: "discovery-test-transport-token", - document_generation: 0, - current_file: null, - cutter_version: null, - identity_status: "partial", - }), - { mode: 0o600 }, - ); - } - await server.connect(serverTransport); - await client.connect(clientTransport); - const request = client.callTool( - { name: "list_cutter_sessions", arguments: {} }, - { signal: controller.signal }, - ); - await expect.poll(() => accepted, { timeout: 5_000 }).toBe(8); - controller.abort(); - await expect(request).rejects.toThrow(/abort/iu); - // Inspect the native peers; rejecting the SDK promise alone proves no cleanup. - await expect.poll(() => sockets.size, { timeout: 1_000 }).toBe(0); - expect(accepted).toBe(8); - for (const path of descriptors) await rm(path); - const reply = await client.callTool({ - name: "list_cutter_sessions", - arguments: {}, - }); - expect(reply.isError).not.toBe(true); - expect( - toolContract("list_cutter_sessions").outputSchema.parse( - reply.structuredContent, - ), - ).toMatchObject({ - sessions: [], - discovery_status: "no_live_bridge_found", - bridge_directory_security: "private_verified", - }); - expect(accepted).toBe(8); - } finally { - for (const socket of sockets) socket.destroy(); - await client.close(); - await server.close(); - await new Promise((resolve) => peer.close(() => resolve())); - await rm(directory, { recursive: true, force: true }); - } - }, -); - it("propagates MCP cancellation into an active Cutter command", async () => { let markDispatched: (() => void) | undefined; let observedAbort = false; @@ -275,15 +179,9 @@ it("shares one cleanup proof across concurrent Cutter MCP commands without Binar }); it("cancels active discovery sockets, stops queued probes and recovers on the same MCP connection", async () => { - const { createServer } = await import("node:net"); - const { writeFile, rm } = await import("node:fs/promises"); - const { join } = await import("node:path"); - const { CutterBridgeClient } = - await import("../../../src/cutter/CutterBridgeClient.js"); - const { createTestTempDirectory } = - await import("../../fixtures/temporaryDirectory.js"); const root = await createTestTempDirectory("rea-cutter-discovery-cancel-"); - const sockets = new Set(); + if (process.platform !== "win32") await chmod(root, 0o700); + const sockets = new Set(); let connections = 0; let ready!: () => void; const connected = new Promise((resolve) => { @@ -356,7 +254,7 @@ it("cancels active discovery sockets, stops queued probes and recovers on the sa ); await connected; controller.abort(new Error("selected discovery cancellation")); - await expect(request).rejects.toThrow(); + await expect(request).rejects.toThrow("selected discovery cancellation"); await expect.poll(() => settled).toBe(true); await expect.poll(() => sockets.size).toBe(0); expect(connections).toBe(8); @@ -366,6 +264,17 @@ it("cancels active discovery sockets, stops queued probes and recovers on the sa arguments: {}, }); expect(recovered.isError).not.toBe(true); + expect( + toolContract("list_cutter_sessions").outputSchema.parse( + recovered.structuredContent, + ), + ).toMatchObject({ + sessions: [], + discovery_status: "no_live_bridge_found", + ...(process.platform === "win32" + ? {} + : { bridge_directory_security: "private_verified" }), + }); expect(connections).toBe(8); } finally { controller.abort(); diff --git a/tests/boundary/mcp/enhancedTools.callPathsAndBatches.test.ts b/tests/boundary/mcp/enhancedTools.callPathsAndBatches.test.ts index f9880800dd..55eafbd7cc 100644 --- a/tests/boundary/mcp/enhancedTools.callPathsAndBatches.test.ts +++ b/tests/boundary/mcp/enhancedTools.callPathsAndBatches.test.ts @@ -455,38 +455,6 @@ describe("enhanced MCP batch cancellation", () => { expect(signals).toEqual(expectedCalls.map(() => false)); }, ); - - it("stops later batch stages across the input when one entry lookup is cancelled", async () => { - const calls: string[] = []; - const client = await connect({ - execute: async (name, arguments_) => { - const selector = String(arguments_.procedure ?? arguments_.address); - calls.push(`${name}:${selector}`); - if (name === "procedure_address") return ok(selector); - if (name === "address_name" && selector === "0x1") - return err(new AnalysisCancelledError(name)); - if (name === "address_name") return ok("fixture_name"); - if (name === "procedure_pseudo_code") return ok("return 0;"); - throw new Error(`Unexpected operation: ${name}`); - }, - }); - - const response = await client.callTool({ - name: "batch_decompile", - arguments: { addresses: ["0x1", "0x2"] }, - }); - - expect(response.isError).toBe(true); - expect(response.structuredContent).toBeUndefined(); - expect(parseMcpToolError(response)).toMatchObject({ - error: { code: "cancelled", details: { operation: "address_name" } }, - }); - expect(calls).toEqual([ - "procedure_address:0x1", - "procedure_address:0x2", - "address_name:0x1", - ]); - }); }); describe("enhanced MCP batches", () => { diff --git a/tests/boundary/mcp/enhancedTools.limitations.test.ts b/tests/boundary/mcp/enhancedTools.limitations.test.ts index 8067714807..78b0625f69 100644 --- a/tests/boundary/mcp/enhancedTools.limitations.test.ts +++ b/tests/boundary/mcp/enhancedTools.limitations.test.ts @@ -4,7 +4,6 @@ import { err, ok } from "../../../src/domain/result.js"; import { AnalysisCapabilityUnavailableError } from "../../../src/domain/analysisErrorCore.js"; import { parseEvidence } from "../../../src/domain/evidence.js"; import { connect, closeEnhancedToolResources } from "./enhancedToolsHarness.js"; -import { parseMcpToolError } from "../../fixtures/mcpToolError.js"; afterEach(closeEnhancedToolResources); @@ -98,41 +97,3 @@ it("counts canonical inventory rows in the composed overview without altering la }); expect([JSON.stringify(procedures), JSON.stringify(strings)]).toEqual(before); }); - -it.each(["list_procedures", "list_strings"])( - "refuses legacy %s dictionaries instead of declaring a complete overview", - async (invalidOperation) => { - const client = await connect({ - execute: async (operation) => { - if (operation === "read_binary_inventory_summary") - return err( - new AnalysisCapabilityUnavailableError( - "fixture", - operation, - "No native overview", - ), - ); - return ok( - createAnalysisExecution( - operation === "current_document" - ? "fixture" - : operation === invalidOperation - ? { "0x1000": "legacy" } - : [], - { id: "fixture", name: "Fixture", version: "1" }, - ), - ); - }, - }); - const response = await client.callTool({ - name: "binary_overview", - arguments: {}, - }); - expect(parseMcpToolError(response).error).toMatchObject({ - code: "unreadable_output", - details: { - operation: invalidOperation, - }, - }); - }, -); diff --git a/tests/boundary/mcp/environmentBundleMcp.test.ts b/tests/boundary/mcp/environmentBundleMcp.test.ts index f8835604e4..47244b7243 100644 --- a/tests/boundary/mcp/environmentBundleMcp.test.ts +++ b/tests/boundary/mcp/environmentBundleMcp.test.ts @@ -312,7 +312,7 @@ it("refuses an existing or parentless output directory and leaves it as it was", }); }); -it("refuses unknown, unnamed loopback and non-capture Evidence, and repeated rules", async () => { +it("refuses unknown and unnamed loopback Evidence, and repeated rules", async () => { const { client, session } = await connect(); const root = await createTestTempDirectory("rea-bundle-mcp-refuse-"); const output_directory = join(root, "env"); diff --git a/tests/boundary/mcp/functionComparisonMcp.test.ts b/tests/boundary/mcp/functionComparisonMcp.test.ts index cebdf560cb..eaf6b8f5b0 100644 --- a/tests/boundary/mcp/functionComparisonMcp.test.ts +++ b/tests/boundary/mcp/functionComparisonMcp.test.ts @@ -308,29 +308,6 @@ describe("function comparison MCP integration", () => { }, }); }); - - it("accepts inline Evidence without prior session registration", async () => { - const session = createTestBinarySession(() => ({ - health: () => Promise.resolve(), - execute: () => Promise.resolve(observed(null)), - close: () => Promise.resolve(resultOk(null)), - })); - const { client } = await createApplicationMcpHarness({ - session, - clientIdentity: { name: "function-link-test", version: "1" }, - }); - const result = await client.callTool({ - name: "compare_functions", - arguments: { - left: FUNCTION_COMPARISON_EXAMPLE.left, - right: FUNCTION_COMPARISON_EXAMPLE.right, - }, - }); - expect(result.isError).not.toBe(true); - expect(result.structuredContent).toMatchObject({ - normalized_result: { status: expect.any(String) }, - }); - }); }); describe("function comparison input-kind diagnostics", () => { diff --git a/tests/boundary/model/modelArtifact.test.ts b/tests/boundary/model/modelArtifact.test.ts index 188e8300eb..ed27ca40c3 100644 --- a/tests/boundary/model/modelArtifact.test.ts +++ b/tests/boundary/model/modelArtifact.test.ts @@ -1297,11 +1297,13 @@ it("bounds expansion before allocation and distinguishes unsupported pickle and const pickle = await provider.inspect({ path, hash: "header" }); if (pickle.ok) throw new Error("Pickle admitted"); expect(pickle.error._tag).toBe("AnalysisUnsupportedTargetError"); - await writeFile(path, ggufFixture({ version: 999 }).bytes); + await writeFile(path, ggufFixture().bytes); + expect((await provider.inspect({ path, hash: "header" })).ok).toBe(true); const alias = join(root, "link"); await symlink(path, alias); const linked = await provider.inspect({ path: alias, hash: "header" }); - expect(linked.ok).toBe(false); + if (linked.ok) throw new Error("Selected symlink admitted"); + expect(linked.error.message).toMatch(/symlink|symbolic.link/iu); const service = createModelArtifactService(); onTestFinished(() => service.close()); const invalid = await service.inspect({ @@ -1464,6 +1466,7 @@ it("yields while validating a large metadata array, preserves cancellation and r _tag: "AnalysisCancelledError", cause: reason, }); + expect(result.error.cause).toBe(reason); await writeFile(path, ggufFixture().bytes); const recovered = await provider.inspect({ path, hash: "header" }); if (!recovered.ok) throw recovered.error; diff --git a/tests/boundary/pcap/pcapProvider.test.ts b/tests/boundary/pcap/pcapProvider.test.ts index a5ffb8c229..c491841c34 100644 --- a/tests/boundary/pcap/pcapProvider.test.ts +++ b/tests/boundary/pcap/pcapProvider.test.ts @@ -409,19 +409,6 @@ it("reports a non-capture file as unsupported", async () => { }); }); -it("rejects a capture that is not a readable file", async () => { - const { provider } = await providerWith(); - const result = await provider.execute({ - operation: "inspect_pcap_capture", - input: { path: "/nonexistent/capture.pcap" }, - }); - expect(result.ok).toBe(false); - if (result.ok) return; - expect(projectAnalysisError(result.error)).toMatchObject({ - code: "invalid_request", - }); -}); - it("reports relative configuration and missing capinfos as capability gaps", async () => { const root = await createTestTempDirectory("rea-pcap-config-"); const { tshark } = await writeStubTools(root); diff --git a/tests/boundary/process/adbVerifierCleanup.test.ts b/tests/boundary/process/adbVerifierCleanup.test.ts index 1b56fb29c1..2b1a16e182 100644 --- a/tests/boundary/process/adbVerifierCleanup.test.ts +++ b/tests/boundary/process/adbVerifierCleanup.test.ts @@ -157,22 +157,13 @@ export class AdbDeviceAnalysisService { const posix = it.skipIf(process.platform === "win32"); posix( - "runs the production verifier successfully and closes its provider", + "removes exactly its own pushed probe before reporting success", async () => { const result = await runVerifier(); expect(result.status, `${result.stdout}\n${result.stderr}`).toBe(0); expect(result.stdout).toContain("all adb device checks passed"); expect(result.closed).toBe("closed"); - }, -); - -posix( - "removes exactly its own pushed probe before reporting success", - async () => { - const result = await runVerifier(); - - expect(result.status, `${result.stdout}\n${result.stderr}`).toBe(0); expect(result.adbCalls).toHaveLength(1); expect(result.adbCalls[0]).toMatch( /^-s test shell rm -- \/data\/local\/tmp\/rea-verify-\d+\.txt$/u, diff --git a/tests/boundary/process/fridaVerifier.test.ts b/tests/boundary/process/fridaVerifier.test.ts index e55fb24641..28f048e779 100644 --- a/tests/boundary/process/fridaVerifier.test.ts +++ b/tests/boundary/process/fridaVerifier.test.ts @@ -28,8 +28,8 @@ const service = \` export class FridaInstrumentationService { async listProcesses() { return { ok: true, value: { processes: [{ pid: 41, name: 'selected' }] } }; } async instrument() { - const pid = process.env.REA_PROBE_KIND === 'missing' ? undefined : process.env.REA_PROBE_KIND === 'correct' ? 41 : 42; - return { ok: true, value: { cleanupError: null, evidence: { normalized_result: { pid, messages: [{ type: 'send', payload: { rea_frida_verification: 'ok', pid } }] } } } }; + const pid = process.env.REA_PROBE_KIND === 'missing' ? undefined : ['correct', 'wrong-marker'].includes(process.env.REA_PROBE_KIND) ? 41 : 42; + return { ok: true, value: { cleanupError: null, evidence: { normalized_result: { pid, messages: [{ type: 'send', payload: { rea_frida_verification: 'ok', pid: process.env.REA_PROBE_KIND === 'wrong-marker' ? 42 : pid } }] } } } }; } }\`; const manager = \`export class FridaInstrumentationManager { async closeAll() {} }\`; @@ -56,6 +56,7 @@ registerHooks({ resolve(specifier, context, next) { for (const [kind, remote, message] of [ ["missing", false, "Frida returned no valid target PID"], ["wrong", true, "Frida observed a different target PID"], + ["wrong-marker", true, "Selected target did not return the Frida marker"], ] as const) { await expect(invoke(kind, remote)).rejects.toMatchObject({ stderr: expect.stringContaining(message), diff --git a/tests/boundary/process/jadxIntegration.test.ts b/tests/boundary/process/jadxIntegration.test.ts index 3c35768497..1acfd3d810 100644 --- a/tests/boundary/process/jadxIntegration.test.ts +++ b/tests/boundary/process/jadxIntegration.test.ts @@ -557,7 +557,7 @@ it("keeps idle retirement after refusing a mapping for a loaded engine", async ( }); it("completes large class inventories with exact case-sensitive search semantics", async () => { - const { service, apk, provider } = await setup("large-inventory"); + const { service, apk, provider, launches } = await setup("large-inventory"); const full = await service.execute("search_android_classes", { path: apk, query: "", @@ -578,7 +578,22 @@ it("completes large class inventories with exact case-sensitive search semantics mixedCase.value.normalized_result, ).matches, ).toEqual([]); + const filtered = await service.execute("search_android_classes", { + path: apk, + query: "Class", + }); + if (!filtered.ok) throw filtered.error; + const matches = androidResultSchemas.search_android_classes.parse( + filtered.value.normalized_result, + ); + expect(matches.total_classes).toBe(9001); + expect(matches.matches).toHaveLength(9000); + expect(matches.matches[0]).toBe("fixture.Class0"); + expect(matches.matches[4096]).toBe("fixture.Class4096"); + expect(matches.matches.at(-1)).toBe("fixture.Class8999"); + expect(launches).toHaveLength(1); await provider.close(); + await verifyCleanup(launches); }); it("close cancels active and queued work and prevents later acquisition", async () => { @@ -640,25 +655,6 @@ it("retains APK identity, original input, raw producer data and normalized obser await verifyCleanup(launches); }); -it("pages matching classes independently of the total APK class count", async () => { - const { service, apk, launches, provider } = await setup("large-inventory"); - const outcome = await service.execute("search_android_classes", { - path: apk, - query: "Class", - }); - if (!outcome.ok) throw outcome.error; - const result = androidResultSchemas.search_android_classes.parse( - outcome.value.normalized_result, - ); - expect(result.total_classes).toBe(9001); - expect(result.matches).toHaveLength(9000); - expect(result.matches[0]).toBe("fixture.Class0"); - expect(result.matches[4096]).toBe("fixture.Class4096"); - expect(result.matches.at(-1)).toBe("fixture.Class8999"); - await provider.close(); - await verifyCleanup(launches); -}); - it("preserves duplicate class-name rows and occurrence counts", async () => { const { service, apk, launches, provider } = await setup( "duplicate-class-name", diff --git a/tests/boundary/process/ownedDebuggerSession.test.ts b/tests/boundary/process/ownedDebuggerSession.test.ts index 7e9d64552f..70d901b081 100644 --- a/tests/boundary/process/ownedDebuggerSession.test.ts +++ b/tests/boundary/process/ownedDebuggerSession.test.ts @@ -408,25 +408,25 @@ it.each([ "POSIX Error", "linux", new Error("pipe state unknown"), - /inherited output stream settlement could not be verified: pipe state unknown/, + "Owned GDB inherited output stream settlement could not be verified: pipe state unknown", ], [ "POSIX non-Error", "linux", "pipe state unknown", - /inherited output stream settlement could not be verified: pipe state unknown/, + "Owned GDB inherited output stream settlement could not be verified: pipe state unknown", ], [ "Windows Error", "win32", new Error("job state unknown"), - /Windows Job Object settlement could not be verified: job state unknown/, + "Owned GDB Windows Job Object settlement could not be verified: job state unknown", ], [ "Windows non-Error", "win32", "job state unknown", - /Windows Job Object settlement could not be verified: job state unknown/, + "Owned GDB Windows Job Object settlement could not be verified: job state unknown", ], ] as const)( "retains an exited debugger when output settlement throws (%s)", @@ -451,7 +451,7 @@ it.each([ { platform }, ); - expect(result).toMatchObject({ + expect(result).toEqual({ status: "incomplete", reason: expectedReason, }); @@ -940,38 +940,6 @@ it("uses the host platform when an exited debugger is stopped without options", expect(outputUnrefs).toBe(process.platform === "win32" ? 0 : 1); }); -it.each([ - ["linux", "inherited output stream"], - ["win32", "Windows Job Object"], -] as const)( - "retains a non-Error output-settlement failure on %s", - async (platform, resource) => { - const result = await stopOwnedDebuggerProcess( - { - id: `output-settlement-${platform}`, - exited: true, - launched: { process: { kill: () => true } }, - supervisor: { - waitForExit: async () => true, - waitForOutputClose: async () => { - throw "output status unavailable"; - }, - limitAdditionalOutputBytes: () => {}, - dispose: () => {}, - unrefOutputStreams: () => {}, - }, - }, - "GDB", - { platform }, - ); - - expect(result).toEqual({ - status: "incomplete", - reason: `Owned GDB ${resource} settlement could not be verified: output status unavailable`, - }); - }, -); - it.skipIf(process.platform === "win32")( "keeps supervision until a late-writing descendant closes inherited output after leader exit", async () => { diff --git a/tests/boundary/providers/adb/adbInputSnapshotOwnership.test.ts b/tests/boundary/providers/adb/adbInputSnapshotOwnership.test.ts index 623b3c2194..d716d7ce2e 100644 --- a/tests/boundary/providers/adb/adbInputSnapshotOwnership.test.ts +++ b/tests/boundary/providers/adb/adbInputSnapshotOwnership.test.ts @@ -715,6 +715,7 @@ test("cancels after the input snapshot copy and before contacting the device", a if (result.ok) throw new Error("expected cancellation"); expect(result.error._tag).toBe("AnalysisCancelledError"); expect(execFileOutput).toHaveBeenCalledTimes(1); + expect(execFileBytes).not.toHaveBeenCalled(); expect(await readFile(f.source, "utf8")).toBe("immutable payload"); await f.provider.close(); }); @@ -742,5 +743,6 @@ test("reports input snapshot capacity failures before contacting the device", as expect(result.error._tag).toBe("AnalysisOutputError"); expect(result.error.cause).toBe(capacityFailure); expect(execFileOutput).toHaveBeenCalledTimes(1); + expect(execFileBytes).not.toHaveBeenCalled(); await f.provider.close(); }); diff --git a/tests/boundary/providers/hopper/hopperClient.cleanupRetry.test.ts b/tests/boundary/providers/hopper/hopperClient.cleanupRetry.test.ts index 6188f0a39b..897420af1c 100644 --- a/tests/boundary/providers/hopper/hopperClient.cleanupRetry.test.ts +++ b/tests/boundary/providers/hopper/hopperClient.cleanupRetry.test.ts @@ -230,6 +230,7 @@ it.each([false, true])( it("blocks a fresh launch while cleanup remains unverified", async () => { const launcher = new RetryCleanupLauncher(); const { client, child } = await failedStartup(launcher); + expect(client.operationHealth()).toMatchObject({ state: "not_started" }); await expect(client.start()).resolves.toMatchObject({ ok: false, error: { @@ -250,32 +251,6 @@ it("blocks a fresh launch while cleanup remains unverified", async () => { }); }); -it("preserves failed startup state through a blocked retry until cleanup succeeds", async () => { - const launcher = new RetryCleanupLauncher(); - const { client } = await failedStartup(launcher); - // The startup timeout leaves the client in not_started with retained - // resources; cleanup is still incomplete. - expect(client.operationHealth()).toMatchObject({ state: "not_started" }); - // An intervening retry is blocked by the retained resources, and now - // surfaces the recorded startup failure (upstream 069308bb) instead of - // a generic protocol error... - await expect(client.start()).resolves.toMatchObject({ - ok: false, - error: { - _tag: "ProviderAdapterError", - cleanupIncomplete: true, - cause: { cause: { _tag: "HopperTimeoutError" } }, - }, - }); - // ...but must not wipe the failed-startup state: the client still - // reports not_started (never idle) until cleanup succeeds, so a - // same-target open_binary keeps retiring the wedged client instead of - // fast-pathing it. - expect(client.operationHealth()).toMatchObject({ state: "not_started" }); - launcher.allowCleanup = true; - await expect(client.close()).resolves.toEqual({ ok: true, value: null }); -}); - it("permits a fresh launch after stopping an owned bridge-request provider without an acknowledgement", async () => { const fixture = new RetryCleanupLauncher(); fixture.allowCleanup = true; diff --git a/tests/boundary/providers/kuna/kunaProvider.test.ts b/tests/boundary/providers/kuna/kunaProvider.test.ts index 597f9c25d6..3b97364313 100644 --- a/tests/boundary/providers/kuna/kunaProvider.test.ts +++ b/tests/boundary/providers/kuna/kunaProvider.test.ts @@ -678,7 +678,11 @@ it("keeps answering pseudocode one function at a time when the export fails", as }); expect(pseudocode.rawResult).toMatchObject({ source: { kind: "single-function" }, - project_export: { state: "failed", failure_records: "not_exposed" }, + project_export: { + state: "failed", + failure_records: "not_exposed", + reason: expect.stringContaining("not in recognized object file format"), + }, }); expect(pseudocode.limitations).toEqual( expect.arrayContaining([ @@ -1058,25 +1062,6 @@ it("recovers when another server evicts the export this session adopted", async await client.close(); }); -it("reports a failed export as failed rather than building", async () => { - const { open } = await session("rea-kuna-failed-state-", { - project: "exit-failure", - }); - const client = open(); - await client.execute("procedure_assembly", { procedure: "main" }); - - expect( - (await run(client, "procedure_pseudo_code", { procedure: "main" })) - .rawResult, - ).toMatchObject({ - project_export: { - state: "failed", - reason: expect.stringContaining("not in recognized object file format"), - }, - }); - await client.close(); -}); - it.each([ { name: "export deadline", diff --git a/tests/boundary/providers/managed/managedMemberComparisonFromMetadata.test.ts b/tests/boundary/providers/managed/managedMemberComparisonFromMetadata.test.ts index 9fb44be6fd..78621bc869 100644 --- a/tests/boundary/providers/managed/managedMemberComparisonFromMetadata.test.ts +++ b/tests/boundary/providers/managed/managedMemberComparisonFromMetadata.test.ts @@ -2280,13 +2280,32 @@ describe("managed member comparison of raw-signature contention", () => { ...base, result: { ...base.result, methods: [], fields: [field] }, }; - for (const result of [ - compareManagedMembers(withOwnerless, resolvedOnly), - compareManagedMembers(resolvedOnly, withOwnerless), - ]) + for (const [result, leftTokens, rightTokens] of [ + [ + compareManagedMembers(withOwnerless, resolvedOnly), + [field.token, "0x04000002"], + [field.token], + ], + [ + compareManagedMembers(resolvedOnly, withOwnerless), + [field.token], + [field.token, "0x04000002"], + ], + ] as const) { expect( result.fields.filter(({ match }) => match.status === "matched"), ).toEqual([]); + expect(result.fields.length).toBeGreaterThan(0); + expect(comparedTokens(result, "fields", "left")).toEqual( + [...leftTokens].sort(), + ); + expect(comparedTokens(result, "fields", "right")).toEqual( + [...rightTokens].sort(), + ); + expect(result.fields.every((item) => item.status === "unknown")).toBe( + true, + ); + } }); }); @@ -2668,6 +2687,9 @@ describe("managed member comparison of unresolved owners", () => { }, }); const result = compareManagedMembers(side("Main"), side("Renamed")); + expect(result.methods.length).toBeGreaterThan(0); + expect(comparedTokens(result, "methods", "left")).toEqual([method.token]); + expect(comparedTokens(result, "methods", "right")).toEqual([method.token]); for (const item of result.methods) expect(item.match.status).not.toBe("matched"); @@ -2786,12 +2808,16 @@ describe("managed member comparison of unresolved owners", () => { }, }; - for (const result of [ - compareManagedMembers(left, right), - compareManagedMembers(right, left), - ]) + for (const [result, leftToken, rightToken] of [ + [compareManagedMembers(left, right), method.token, "0x06000002"], + [compareManagedMembers(right, left), "0x06000002", method.token], + ] as const) { + expect(result.methods.length).toBeGreaterThan(0); + expect(comparedTokens(result, "methods", "left")).toEqual([leftToken]); + expect(comparedTokens(result, "methods", "right")).toEqual([rightToken]); for (const item of result.methods) expect(item.match.status).not.toBe("matched"); + } }); }); @@ -3447,6 +3473,25 @@ const withTwin = ( }; }; +/** Every retained source token, including unresolved candidates with no selected identity. */ +const comparedTokens = ( + result: ReturnType, + kind: "methods" | "fields", + side: "left" | "right", +) => + [ + ...new Set( + result[kind] + .flatMap((item) => [ + item[side]?.token, + ...(side === "left" + ? item.match.candidate_left_tokens + : item.match.candidate_right_tokens), + ]) + .filter((token) => token !== undefined), + ), + ].sort(); + const inspect = (bytes: Buffer, path: string, typeNames?: string[]) => { const target = managedPeFixtureTarget(bytes, path); const result = inspectManagedMembersBytes( diff --git a/tests/composition/reverseEngineering.test.ts b/tests/composition/reverseEngineering.test.ts index daebd99d87..ff7fc18e01 100644 --- a/tests/composition/reverseEngineering.test.ts +++ b/tests/composition/reverseEngineering.test.ts @@ -146,12 +146,17 @@ describe("reverse engineering command services", () => { path: directory, operation: "file_headers", }); - expect(result.ok).toBe(false); - if (!result.ok) - expect(result.error.cause).toMatchObject({ - code: "EINVAL", - message: "Artifact digest requires a regular file", - }); + expect(result).toMatchObject({ + ok: false, + error: { + _tag: "AnalysisInputError", + operation: "inspect_with_objdump", + cause: { + code: "EINVAL", + message: "Artifact digest requires a regular file", + }, + }, + }); expect(invoked).toBe(false); } finally { await rm(directory, { recursive: true, force: true }); @@ -484,29 +489,38 @@ describe("Rizin command service", () => { } }); - it("rejects multiline Rizin command input before launching the provider", async () => { - const directory = await mkdtemp(join(tmpdir(), "rea-rizin-input-")); - const path = join(directory, "fixture.bin"); - await writeFile(path, "fixture"); - let invoked = false; - const service = new ReverseEngineeringService({ - run: async () => { - invoked = true; - return { stdout: "", stderr: "" }; - }, - }); - - try { - const result = await service.executeRizinCommand({ - path, - command: "iI\nq", + it.each(["iI\nq", "wx\u0000 00"])( + "rejects malformed Rizin command %j before launching the provider", + async (command) => { + const directory = await mkdtemp(join(tmpdir(), "rea-rizin-input-")); + const path = join(directory, "fixture.bin"); + await writeFile(path, "fixture"); + let invoked = false; + const service = new ReverseEngineeringService({ + run: async () => { + invoked = true; + return { stdout: "", stderr: "" }; + }, }); - expect(result.ok).toBe(false); - expect(invoked).toBe(false); - } finally { - await rm(directory, { recursive: true, force: true }); - } - }); + + try { + const result = await service.executeRizinCommand({ + path, + command, + }); + expect(result).toMatchObject({ + ok: false, + error: { + _tag: "AnalysisInputError", + operation: "execute_rizin_command", + }, + }); + expect(invoked).toBe(false); + } finally { + await rm(directory, { recursive: true, force: true }); + } + }, + ); it("preserves pre and post artifact digests when an unrestricted Rizin command changes the file", async () => { const directory = await mkdtemp(join(tmpdir(), "rea-rizin-mutation-")); diff --git a/tests/composition/reverseEngineeringEdgeCases.test.ts b/tests/composition/reverseEngineeringEdgeCases.test.ts index 2ed095f6df..0313c0fea7 100644 --- a/tests/composition/reverseEngineeringEdgeCases.test.ts +++ b/tests/composition/reverseEngineeringEdgeCases.test.ts @@ -385,27 +385,6 @@ it("rejects malformed Rizin requests before touching the provider", async () => expect(run).not.toHaveBeenCalled(); }); -it("rejects directories as artifacts before invoking the provider", async () => { - const { directory } = await createArtifact(); - const run = vi.fn(async () => ({ stdout: "", stderr: "" })); - const service = new ReverseEngineeringService({ run }); - - try { - const result = await service.inspectWithObjdump({ - path: directory, - operation: "file_headers", - }); - - expect(result).toMatchObject({ - ok: false, - error: { _tag: "AnalysisInputError", operation: "inspect_with_objdump" }, - }); - expect(run).not.toHaveBeenCalled(); - } finally { - await rm(directory, { recursive: true, force: true }); - } -}); - it("rejects new operations after service shutdown begins", async () => { const { directory, path } = await createArtifact(); const run = vi.fn(async () => ({ stdout: "", stderr: "" })); diff --git a/tests/composition/reverseEngineeringLocalInspectionEdgeCases.test.ts b/tests/composition/reverseEngineeringLocalInspectionEdgeCases.test.ts deleted file mode 100644 index 4915cf7437..0000000000 --- a/tests/composition/reverseEngineeringLocalInspectionEdgeCases.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { rm } from "node:fs/promises"; -import { createArtifact } from "../support/reverseEngineeringCompositionFixtures.js"; - -import { expect, it, vi } from "vitest"; - -import { TestReverseEngineeringService as ReverseEngineeringService } from "../support/reverseEngineeringService.js"; - -it("rejects NUL in a Rizin command before touching the provider", async () => { - const { directory, path } = await createArtifact(); - let invoked = false; - const service = new ReverseEngineeringService({ - run: async () => { - invoked = true; - return { stdout: "", stderr: "" }; - }, - }); - - try { - const result = await service.executeRizinCommand({ - path, - command: "wx\u0000 00", - }); - expect(result).toMatchObject({ - ok: false, - error: { _tag: "AnalysisInputError", operation: "execute_rizin_command" }, - }); - expect(invoked).toBe(false); - } finally { - await rm(directory, { recursive: true, force: true }); - } -}); diff --git a/tests/conformance/ida/providerContracts.test.ts b/tests/conformance/ida/providerContracts.test.ts index bfa4044d25..0e21b03dec 100644 --- a/tests/conformance/ida/providerContracts.test.ts +++ b/tests/conformance/ida/providerContracts.test.ts @@ -52,7 +52,7 @@ describe.each(["attached", "headless"] as const)( expect(operations).toContain("resolve_containing_procedure"); }); it.each(operations)( - "declares %s UI effects matching the upstream tools it reaches", + "serializes %s against its named advertised output contract", async (operation) => { const { root, target } = await createIdaTarget(); roots.push(root); @@ -69,50 +69,18 @@ describe.each(["attached", "headless"] as const)( target, producer, ); - try { - const execution = await client.execute( - operation, - parametersFor(operation), - ); - if (mode === "headless" && operation === "procedure_callers") - expect(execution).toMatchObject({ - ok: false, - error: { _tag: "AnalysisCapabilityUnavailableError" }, - }); - else if (!execution.ok) throw execution.error; - } finally { - expect((await client.close()).ok).toBe(true); - } - const descriptor = idaCapabilities( - IDA_COMPATIBILITY_PROFILES[mode === "attached" ? "legacy" : "modern"], - true, - ).find((capability) => capability.operation === operation); - expect(descriptor?.effects.mayShowUi).toBe( - producer.calls.some(({ name }) => GUI_NAVIGATING_TOOLS.has(name)), - ); - }, - ); - - it.each(operations)( - "serializes %s against its named advertised output contract", - async (operation) => { - const { root, target } = await createIdaTarget(); - roots.push(root); - const client = new IdaSessionClient( - { - command: "fixture", - args: [], - env: {}, - mode, - workspaceRoot: root, - timeoutMs: 1000, - }, - target, - new RecordingIdaMcp(target, mode), - ); try { const parameters = parametersFor(operation); const execution = await client.execute(operation, parameters); + const descriptor = idaCapabilities( + IDA_COMPATIBILITY_PROFILES[ + mode === "attached" ? "legacy" : "modern" + ], + true, + ).find((capability) => capability.operation === operation); + expect(descriptor?.effects.mayShowUi).toBe( + producer.calls.some(({ name }) => GUI_NAVIGATING_TOOLS.has(name)), + ); if (mode === "headless" && operation === "procedure_callers") { expect(execution.ok).toBe(false); if (!execution.ok) diff --git a/tests/conformance/package/packedTarListing.test.ts b/tests/conformance/package/packedTarListing.test.ts index 392d5a4951..d6be991876 100644 --- a/tests/conformance/package/packedTarListing.test.ts +++ b/tests/conformance/package/packedTarListing.test.ts @@ -51,11 +51,4 @@ describe("packed tar listing", () => { "package/b", ]); }); - - it("splits a bare newline listing the way the Windows bug appeared", () => { - // Guard the reported regression: the previous parse kept `\r` on members. - const naive = listing("\r\n").split("\n"); - expect(naive).not.toContain(packedBridges[0]); - expect(parseTarListing(listing("\r\n"))).toContain(packedBridges[0]); - }); }); diff --git a/tests/fixtures/fakeBuildComputerRea.ts b/tests/fixtures/fakeBuildComputerRea.ts index bed7642228..7a19b422e7 100644 --- a/tests/fixtures/fakeBuildComputerRea.ts +++ b/tests/fixtures/fakeBuildComputerRea.ts @@ -158,7 +158,10 @@ const FAKE_APT_GET = String.raw` import os import re import sys +import json +with open(os.path.join(os.environ["FAKE_REA_NOTES"], "apt.jsonl"), "a") as handle: + handle.write(json.dumps({"argv": sys.argv[1:]}) + "\n") with open(os.environ["APT_CONFIG"]) as handle: conf = handle.read() lists = re.search(r'Dir::State::Lists "([^"]+)"', conf).group(1) diff --git a/tests/fixtures/il2cpp/metadata.ts b/tests/fixtures/il2cpp/metadata.ts index cb2e490924..ad6fcea63d 100644 --- a/tests/fixtures/il2cpp/metadata.ts +++ b/tests/fixtures/il2cpp/metadata.ts @@ -1,11 +1,112 @@ -import { - il2cppMetadataSectionNames, - type IL2cppMetadataSectionName, -} from "../../../src/domain/il2cpp/il2cppMetadata.js"; +import type { IL2cppMetadataSectionName } from "../../../src/domain/il2cpp/il2cppMetadata.js"; type SectionName = IL2cppMetadataSectionName; type Width = 1 | 2 | 4; +/** Authored serialized header slots; never derive fixture bytes from the decoder catalog. */ +const V24_HEADER = [ + "stringLiterals", + "stringLiteralData", + "strings", + "events", + "properties", + "methods", + "parameterDefaultValues", + "fieldDefaultValues", + "fieldAndParameterDefaultValueData", + "fieldMarshaledSizes", + "parameters", + "fields", + "genericParameters", + "genericParameterConstraints", + "genericContainers", + "nestedTypes", + "interfaces", + "vtableMethods", + "interfaceOffsets", + "typeDefinitions", + "images", + "assemblies", + "metadataUsageLists", + "metadataUsagePairs", + "fieldRefs", + "referencedAssemblies", + "attributesInfo", + "attributeTypes", + "unresolvedVirtualCallParameterTypes", + "unresolvedVirtualCallParameterRanges", + "windowsRuntimeTypeNames", + "exportedTypeDefinitions", +] as const satisfies readonly SectionName[]; + +const V27_HEADER = [ + "stringLiterals", + "stringLiteralData", + "strings", + "events", + "properties", + "methods", + "parameterDefaultValues", + "fieldDefaultValues", + "fieldAndParameterDefaultValueData", + "fieldMarshaledSizes", + "parameters", + "fields", + "genericParameters", + "genericParameterConstraints", + "genericContainers", + "nestedTypes", + "interfaces", + "vtableMethods", + "interfaceOffsets", + "typeDefinitions", + "images", + "assemblies", + "fieldRefs", + "referencedAssemblies", + "attributesInfo", + "attributeTypes", + "unresolvedVirtualCallParameterTypes", + "unresolvedVirtualCallParameterRanges", + "windowsRuntimeTypeNames", + "windowsRuntimeStrings", + "exportedTypeDefinitions", +] as const satisfies readonly SectionName[]; + +const MODERN_HEADER = [ + "stringLiterals", + "stringLiteralData", + "strings", + "events", + "properties", + "methods", + "parameterDefaultValues", + "fieldDefaultValues", + "fieldAndParameterDefaultValueData", + "fieldMarshaledSizes", + "parameters", + "fields", + "genericParameters", + "genericParameterConstraints", + "genericContainers", + "nestedTypes", + "interfaces", + "vtableMethods", + "interfaceOffsets", + "typeDefinitions", + "images", + "assemblies", + "fieldRefs", + "referencedAssemblies", + "attributeData", + "attributeDataRanges", + "unresolvedIndirectCallParameterTypes", + "unresolvedIndirectCallParameterRanges", + "windowsRuntimeTypeNames", + "windowsRuntimeStrings", + "exportedTypeDefinitions", +] as const satisfies readonly SectionName[]; + /** Synthetic version 39 metadata small enough to read in a hex dump. */ interface Il2cppMetadataFixtureOptions { /** Serialized TypeIndex width. */ @@ -180,7 +281,8 @@ const assembleFile = ( ) => { const fixed = version !== 39; const entryBytes = fixed ? 8 : 12; - const names = il2cppMetadataSectionNames(version); + const names: readonly SectionName[] = + version === 24 ? V24_HEADER : version === 27 ? V27_HEADER : MODERN_HEADER; const headerBytes = 8 + names.length * entryBytes; const header = Buffer.alloc(headerBytes); header.writeUInt32LE(0xfab11baf, 0);