diff --git a/tests/acceptance/applications/hopperPublisherRejectionCli.test.ts b/tests/acceptance/applications/hopperPublisherRejectionCli.test.ts new file mode 100644 index 0000000000..092ee9f6ab --- /dev/null +++ b/tests/acceptance/applications/hopperPublisherRejectionCli.test.ts @@ -0,0 +1,60 @@ +import { access, chmod, copyFile, readFile, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { expect } from "vitest"; +import { cliTest } from "../../support/cli/cliFixture.js"; +import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; +import { analysisCliErrorEnvelopeSchema } from "../../../src/contracts/errorSchemas.js"; + +cliTest.skipIf(process.platform !== "linux")( + "refuses an unauthenticated Hopper launcher through the public CLI before executing it", + async ({ cli }) => { + const home = await createTestTempDirectory("rea-hopper-publisher-cli-"); + const launcher = join(home, "foreign-hopper"); + const target = join(home, "sample"); + const marker = join(home, "launched"); + await writeFile( + launcher, + '#!/bin/sh\nprintf launched > "$REA_HOPPER_TEST_MARKER"\nexit 99\n', + ); + await chmod(launcher, 0o700); + await copyFile("/usr/bin/true", target); + const original = await readFile(target); + const run = await cli.run({ + arguments: [ + "search", + target, + "fixture", + "--provider", + "hopper", + "--kind", + "strings", + "--mode", + "regex", + "--json", + ], + environment: { + HOME: home, + TMPDIR: home, + HOPPER_LAUNCHER_PATH: launcher, + REA_HOPPER_TEST_MARKER: marker, + REA_LOG_LEVEL: "silent", + // Reject deterministically without pretending to authenticate a build. + NODE_OPTIONS: `--import=${new URL("../../fixtures/hopperRejectionFeed.mjs", import.meta.url).href}`, + }, + }); + expect(run.stderr).toBe(""); + expect(run.exitCode).toBe(1); + expect(analysisCliErrorEnvelopeSchema.parse(run.json)).toMatchObject({ + code: "provider_unavailable", + details: { + stage: "launch", + launch_rejection: { + boundary: "publisher_verification", + reason: "unsupported_hopper_build", + }, + }, + }); + await expect(access(marker)).rejects.toMatchObject({ code: "ENOENT" }); + expect(await readFile(target)).toEqual(original); + }, +); diff --git a/tests/acceptance/applications/hopperRegexFailureCli.test.ts b/tests/acceptance/applications/hopperRegexFailureCli.test.ts deleted file mode 100644 index 21a122e61f..0000000000 --- a/tests/acceptance/applications/hopperRegexFailureCli.test.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { chmod, copyFile, mkdir, readFile } from "node:fs/promises"; -import { dirname, join } from "node:path"; -import { fileURLToPath } from "node:url"; -import { expect } from "vitest"; -import { cliTest } from "../../support/cli/cliFixture.js"; -import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; -import { - analysisCliErrorEnvelopeSchema, - analysisErrorProjectionSchema, -} from "../../../src/contracts/errorSchemas.js"; -import { parseEvidence } from "../../../src/domain/evidence.js"; - -cliTest.skipIf(process.platform === "win32")( - "projects accepted Hopper regex operation through the public CLI with exact bridge request context", - async ({ cli }) => { - const root = await createTestTempDirectory("rea-hopper-regex-cli-"); - const launcher = join(root, "fake-hopper.mjs"), - target = join(root, "sample"); - await copyFile( - fileURLToPath( - new URL("../../fixtures/hopperRegexCliLauncher.mjs", import.meta.url), - ), - launcher, - ); - await chmod(launcher, 0o700); - await copyFile("/usr/bin/true", target); - for (const kind of ["strings", "procedures"] as const) - for (const mode of [ - "capability_unavailable", - "bridge_exception", - "valid", - ] as const) { - const home = join(root, `${kind}-${mode}`), - trace = join(home, "trace"); - await mkdir(home); - const run = await cli.run({ - arguments: [ - "search", - target, - "fixture", - "--provider", - "hopper", - "--kind", - kind, - "--mode", - "regex", - "--json", - ], - environment: { - HOME: home, - TMPDIR: home, - PATH: `${dirname(process.execPath)}:/usr/bin:/bin:/usr/sbin:/sbin`, - HOPPER_LAUNCHER_PATH: launcher, - REA_LOG_LEVEL: "silent", - REA_TEST_HOPPER_REGEX_MODE: mode, - REA_TEST_HOPPER_TRACE: trace, - }, - }); - expect(run.stderr).toBe(""); - expect((await readFile(trace, "utf8")).trim().split("\n")).toEqual([ - "health", - `list_${kind}`, - "shutdown_document", - ]); - if (mode === "valid") { - expect(run.exitCode).toBe(0); - const evidence = parseEvidence(run.json); - expect(evidence.operation).toBe(`search_${kind}`); - expect(evidence.normalized_result).toEqual([ - { address: "0x1000", value: "fixture" }, - ]); - } else { - expect(run.exitCode).toBe(1); - const { error: label, ...projection } = - analysisCliErrorEnvelopeSchema.parse(run.json); - expect(label).not.toBe(""); - const error = analysisErrorProjectionSchema.parse(projection); - expect(error.code, JSON.stringify(error)).toBe( - mode === "capability_unavailable" - ? "capability_unavailable" - : "execution_failure", - ); - expect(error.details).toMatchObject({ - operation: `search_${kind}`, - diagnostics: { - provider_request: { - operation: `list_${kind}`, - request_id: expect.any(Number), - provider_code: -32000, - diagnostic_type: mode, - }, - }, - }); - if (mode === "bridge_exception") - expect(error.message).toContain(`search_${kind}`); - } - } - }, -); diff --git a/tests/acceptance/providers/jebConfigurationIsolation.test.ts b/tests/acceptance/providers/jebConfigurationIsolation.test.ts index 7dba820509..f4452b060b 100644 --- a/tests/acceptance/providers/jebConfigurationIsolation.test.ts +++ b/tests/acceptance/providers/jebConfigurationIsolation.test.ts @@ -8,7 +8,8 @@ import { workspaceCliTest as test } from "../../support/cli/workspaceCliFixture. import type { TestWorkspace } from "../../support/workspace/workspaceFixture.js"; const isolatedEnvironment = (workspace: TestWorkspace) => ({ - PATH: "", + // Discovery still needs the host's process-ownership prerequisites, such as ps. + PATH: process.env.PATH ?? "", HOME: workspace.home, USERPROFILE: workspace.home, APPDATA: workspace.path("appdata"), diff --git a/tests/boundary/evm/evmoleInterfaceProvider.test.ts b/tests/boundary/evm/evmoleInterfaceProvider.test.ts index 5abd9fe3f4..36393f6577 100644 --- a/tests/boundary/evm/evmoleInterfaceProvider.test.ts +++ b/tests/boundary/evm/evmoleInterfaceProvider.test.ts @@ -649,6 +649,7 @@ it "retains captured warnings after a successful worker: %s", async (scenario) => { const { path } = await fixture(); + const source = await readFile(path); const controller = new AbortController(); const reason = { requested: "stop during root removal" }; let denyClose = @@ -726,10 +727,14 @@ it : { diagnostics: { captured_output: output } }, }); const evidence = parseEvidence(result.error.partialObservation); + expect(evidence.subject?.local_path).toBe(path); + expect(evidence.subject?.digest.sha256).toBe( + createHash("sha256").update(source).digest("hex"), + ); + expect(await readFile(path)).toEqual(source); expect(evidence).toMatchObject({ operation: "inspect_evm_interface", parameters: { path, encoding: "hex" }, - subject: { path, sha256: expect.any(String) }, provider: { id: "evmole-interface" }, raw_result: { functions: [] }, normalized_result: { diff --git a/tests/boundary/filesystem/ghidraJavaProbe.test.ts b/tests/boundary/filesystem/ghidraJavaProbe.test.ts index 3320388154..47b3004355 100644 --- a/tests/boundary/filesystem/ghidraJavaProbe.test.ts +++ b/tests/boundary/filesystem/ghidraJavaProbe.test.ts @@ -3,6 +3,7 @@ import { spawnOwnedProviderProcess } from "../../../src/process/ProviderProcess. import { createInspectionResources } from "../../fixtures/inspectionResources.js"; import { mkdir, writeFile, readFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, expect, it } from "vitest"; @@ -58,6 +59,13 @@ const probeHost = ...spawn, command: process.execPath, arguments: [`${command}.cjs`, ...spawn.arguments], + // The controlled Node prerequisite needs Windows runtime state. + hostEnvironment: { + SystemRoot: process.env.SystemRoot, + TEMP: tmpdir(), + TMP: tmpdir(), + ...spawn.hostEnvironment, + }, }), }), }; diff --git a/tests/boundary/filesystem/javascript-applications/javascriptModuleRelationships.test.ts b/tests/boundary/filesystem/javascript-applications/javascriptModuleRelationships.test.ts index c893dddd93..3e45278733 100644 --- a/tests/boundary/filesystem/javascript-applications/javascriptModuleRelationships.test.ts +++ b/tests/boundary/filesystem/javascript-applications/javascriptModuleRelationships.test.ts @@ -82,7 +82,7 @@ it.each([ ); describe("cross-version source module identity", () => { - it("keeps a source module unchanged when only a sibling file changes", async () => { + it("keeps exact source identity without claiming unchanged unresolved relationships", async () => { const root = await createTestTempDirectory("rea-source-module-sibling-"); const versions = await Promise.all( ["left", "right"].map(async (name) => { @@ -129,12 +129,11 @@ describe("cross-version source module identity", () => { rightNativeEvidence: [], }); - expect( - comparison.items.find( - ({ left_node_id }) => left_node_id === leftModule.node_id, - ), - ).toMatchObject({ - status: "unchanged", + const item = comparison.items.find( + ({ left_node_id }) => left_node_id === leftModule.node_id, + ); + expect(item).toMatchObject({ + status: "unknown", right_node_id: rightModule.node_id, match: { status: "matched", @@ -142,6 +141,15 @@ describe("cross-version source module identity", () => { confidence: "exact", }, }); + expect(item?.dimensions).not.toContain("content"); + expect(item?.dimensions).not.toContain("properties"); + expect(item?.limitations).toEqual( + expect.arrayContaining([ + expect.stringMatching( + /Incomplete graph coverage|Relationship target identity remains unresolved/, + ), + ]), + ); }); }); diff --git a/tests/fixtures/hopperRegexCliLauncher.mjs b/tests/fixtures/hopperRegexCliLauncher.mjs deleted file mode 100644 index 1c7efbad19..0000000000 --- a/tests/fixtures/hopperRegexCliLauncher.mjs +++ /dev/null @@ -1,72 +0,0 @@ -#!/usr/bin/env node -import { readFileSync, appendFileSync } from "node:fs"; -import { createServer } from "node:net"; - -const bootstrap = readFileSync( - process.argv[process.argv.indexOf("-Y") + 1], - "utf8", -); -const value = (name) => { - const line = bootstrap - .split("\n") - .find((line) => line.startsWith(`${name} = `)); - if (line === undefined) - throw new Error(`Missing fixture bootstrap field ${name}`); - return JSON.parse(line.slice(name.length + 3)); -}; -const token = value("REA_TOKEN"), - runId = value("REA_RUN_ID"); -const server = createServer((socket) => { - let pending = ""; - socket.setEncoding("utf8"); - socket.on("error", () => undefined); - socket.on("data", (chunk) => { - pending += chunk; - let end; - while ((end = pending.indexOf("\n")) >= 0) { - const request = JSON.parse(pending.slice(0, end)); - pending = pending.slice(end + 1); - if (request.token !== token) - throw new Error("Fixture authentication failed"); - appendFileSync(process.env.REA_TEST_HOPPER_TRACE, `${request.method}\n`); - let reply; - if (request.method === "health") - reply = { - result: { - name: "REA Hopper bridge", - version: "1.0.0", - run_id: runId, - process_id: process.pid, - }, - }; - else if (request.method === "shutdown_document") { - reply = { - result: { - shutdown: true, - analysis_stopped: true, - document_closed: true, - }, - }; - socket.once("close", () => server.close()); - } else if ( - request.method === "list_strings" || - request.method === "list_procedures" - ) { - reply = - process.env.REA_TEST_HOPPER_REGEX_MODE === "valid" - ? { result: [{ address: "0x1000", value: "fixture" }] } - : { - error: { - code: -32000, - message: "fixture inventory unavailable", - type: process.env.REA_TEST_HOPPER_REGEX_MODE, - }, - }; - } else if (request.method === "current_document") - reply = { result: "fixture" }; - else throw new Error(`Unexpected fixture operation ${request.method}`); - socket.write(`${JSON.stringify({ id: request.id, ...reply })}\n`); - } - }); -}); -server.listen(value("REA_SOCKET")); diff --git a/tests/process-global/hopperRegexFailureCli.test.ts b/tests/process-global/hopperRegexFailureCli.test.ts new file mode 100644 index 0000000000..7bca7ca7ff --- /dev/null +++ b/tests/process-global/hopperRegexFailureCli.test.ts @@ -0,0 +1,131 @@ +import { copyFile } from "node:fs/promises"; +import { join } from "node:path"; +import { afterEach, expect, it, onTestFinished, vi } from "vitest"; +import type { + BridgeLauncher, + BridgeSession, +} from "../../src/hopper/BridgeLauncher.js"; +import { analysisCliErrorEnvelopeSchema } from "../../src/contracts/errorSchemas.js"; +import { parseEvidence } from "../../src/domain/evidence.js"; +import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js"; +import { waitForExit } from "../support/process/processFixture.js"; +import { HopperFixtureLauncher } from "../boundary/providers/hopper/hopperClient.fixture.js"; +import { serveCli } from "../boundary/cli/cliServeHarness.js"; + +const originalExitCode = process.exitCode; +afterEach(() => { + process.exitCode = originalExitCode; +}); + +const fixture = vi.hoisted(() => ({ + launcher: undefined as BridgeLauncher | undefined, +})); + +// Substitute only process admission. CLI composition, provider, regex search, +// socket protocol, Evidence and error projection all use production code. +// The compiled CLI separately verifies refusal of an unauthenticated launcher. +vi.mock("../../src/hopper/BridgeLauncher.js", async (importOriginal) => ({ + ...(await importOriginal< + typeof import("../../src/hopper/BridgeLauncher.js") + >()), + HopperApplicationLauncher: class implements BridgeLauncher { + launch(session: BridgeSession) { + if (fixture.launcher === undefined) + throw new Error("Missing fixture launcher"); + return fixture.launcher.launch(session); + } + }, +})); + +it + .skipIf(process.platform === "win32") + .each( + (["strings", "procedures"] as const).flatMap((kind) => + (["capability_unavailable", "bridge_exception", "valid"] as const).map( + (mode) => ({ kind, mode }), + ), + ), + )( + "projects $kind regex $mode through CLI with exact wire identity", + async ({ kind, mode }) => { + process.exitCode = undefined; + const home = await createTestTempDirectory("rea-hopper-regex-cli-"); + const target = join(home, "sample"); + await copyFile("/usr/bin/true", target); + const launcher = new HopperFixtureLauncher( + undefined, + "quiet", + "ready", + "acknowledge", + mode === "valid" ? "none" : mode, + ); + fixture.launcher = launcher; + onTestFinished(async () => { + fixture.launcher = undefined; + for (const child of launcher.processes) { + if (child.exitCode === null && child.signalCode === null) + child.kill("SIGTERM"); + await waitForExit(child, 5_000); + } + }); + const run = await serveCli( + [ + "search", + target, + "fixture", + "--provider", + "hopper", + "--kind", + kind, + "--mode", + "regex", + "--json", + ], + { + HOME: home, + HOPPER_LAUNCHER_PATH: process.execPath, + REA_LOG_LEVEL: "silent", + }, + ); + const output: unknown = JSON.parse(run.stdout); + if (mode === "valid") { + expect(process.exitCode ?? run.exitCode, run.stdout).toBe(0); + const evidence = parseEvidence(output); + expect(evidence.operation).toBe(`search_${kind}`); + expect(evidence.normalized_result).toEqual([ + { address: "0x1000", value: "fixture" }, + ]); + } else { + expect(process.exitCode ?? run.exitCode, run.stdout).toBe(1); + const error = analysisCliErrorEnvelopeSchema.parse(output); + const request = await launcher.waitForRequest(`list_${kind}`); + expect(error.code, run.stdout).toBe( + mode === "capability_unavailable" + ? "capability_unavailable" + : "execution_failure", + ); + expect(error.details).toMatchObject({ + operation: `search_${kind}`, + diagnostics: { + provider_request: { + operation: request.method, + request_id: request.id, + provider_code: -32000, + diagnostic_type: mode, + }, + }, + }); + if (mode === "bridge_exception") + expect(error.message).toContain(`search_${kind}`); + } + expect(launcher.requests.map(({ method }) => method)).toEqual([ + "health", + `list_${kind}`, + "shutdown", + ]); + expect(launcher.processes).toHaveLength(1); + const child = launcher.processes[0]; + if (child === undefined) throw new Error("Missing fixture process"); + expect(child.exitCode !== null || child.signalCode !== null).toBe(true); + }, +);