From 3c9b5d6d029ed09f8f19287187ab969b9a72016d Mon Sep 17 00:00:00 2001 From: Ysopwnable <161784452+N0zoM1z0@users.noreply.github.com> Date: Sat, 17 Oct 2026 14:20:23 +0800 Subject: [PATCH] test(ci): align fixtures with runtime and evidence contracts Restore test fixture runtime prerequisites and align assertions with current Evidence and uncertainty contracts. Separate actual CLI publisher refusal from explicitly admitted Hopper socket/CLI fixtures, retaining exact wire identity and cleanup coverage. Validate retained EVM Evidence against exact unchanged source-carrier bytes. Validation: 88 targeted local test passes across three completed selections, two platform skips, full check:ci; hosted Windows probes and complete current-head CI. No production authority, authentication or schema change. Controlled fixtures do not qualify actual disassemblers or workers. Fixes #4690. --- .../hopperPublisherRejectionCli.test.ts | 60 ++++++++ .../hopperRegexFailureCli.test.ts | 99 ------------- .../jebConfigurationIsolation.test.ts | 3 +- .../evm/evmoleInterfaceProvider.test.ts | 7 +- .../filesystem/ghidraJavaProbe.test.ts | 8 ++ .../javascriptModuleRelationships.test.ts | 22 ++- tests/fixtures/hopperRegexCliLauncher.mjs | 72 ---------- .../hopperRegexFailureCli.test.ts | 131 ++++++++++++++++++ 8 files changed, 222 insertions(+), 180 deletions(-) create mode 100644 tests/acceptance/applications/hopperPublisherRejectionCli.test.ts delete mode 100644 tests/acceptance/applications/hopperRegexFailureCli.test.ts delete mode 100644 tests/fixtures/hopperRegexCliLauncher.mjs create mode 100644 tests/process-global/hopperRegexFailureCli.test.ts 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); + }, +);