diff --git a/docs/cli.md b/docs/cli.md index bff60650c4..415c8d693d 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -436,6 +436,10 @@ keeps that target's analysis commitments with the replacement client. This includes the native dispatch workflow profile on a profileless route, so its existing snapshot and fresh observations retain the same provenance. +For native dispatch, the snapshot binding retains the reader profile; workflow +Evidence and `workflow_entries` retain the composed profile, which commits that +reader and the workflow's analysis caveats. Restoration preserves both profiles. + The MCP save receipt reports `primitive_entries`, `workflow_entries`, and `evidence_records` separately. Zero primitive bindings can still accompany retained workflow results and Evidence. These are cached observations, not a saved provider diff --git a/src/ghidra/GhidraInstallation.test.ts b/src/ghidra/GhidraInstallation.test.ts index 1735404b07..105192ba93 100644 --- a/src/ghidra/GhidraInstallation.test.ts +++ b/src/ghidra/GhidraInstallation.test.ts @@ -1847,7 +1847,7 @@ it.each([ async (platform, javaHome, pathJava, expected) => { const checked: string[] = []; const probed: string[] = []; - await inspectGhidraInstallation( + const inspection = await inspectGhidraInstallation( { resourceOwner: inspectionResources.owner, environment: { PATH: "/selected-bin" }, @@ -1857,17 +1857,37 @@ it.each([ ...(javaHome === undefined ? {} : { javaHome }), }, host({ + readText: () => + `application.version=${SUPPORTED_GHIDRA_VERSION}\napplication.java.min=21\n`, executable: (candidate) => { checked.push(candidate); return candidate === pathJava || host().executable(candidate); }, probeJava: (command) => { probed.push(command); - return missingJavaProbe; + // A running JVM remains selected even when outside Ghidra's bounds. + // An absent PATH runtime instead permits macOS fallback discovery. + return javaProbeOk({ + ...JAVA, + home: "/selected-jdk", + major: 17, + version: "17.0.20.1", + }); }, }), ); expect(probed).toEqual([expected]); + expect(inspection).toMatchObject({ + status: "unavailable", + javaCommand: expected, + javaVersion: "17.0.20.1", + }); + expect(inspection.checks.find(({ name }) => name === "java")).toMatchObject( + { + status: "failed", + code: "unsupported_version", + }, + ); expect( checked.some((candidate) => candidate.includes("/opt/openjdk/")), ).toBe(false); diff --git a/tests/acceptance/nativeDispatchSnapshot.test.ts b/tests/acceptance/nativeDispatchSnapshot.test.ts index 8321ac3333..da6018e489 100644 --- a/tests/acceptance/nativeDispatchSnapshot.test.ts +++ b/tests/acceptance/nativeDispatchSnapshot.test.ts @@ -5,6 +5,7 @@ import { expect } from "vitest"; import { toolContract } from "../../src/contracts/toolContracts.js"; import { parseEvidence } from "../../src/domain/evidence.js"; import { readAnalysisSnapshot } from "../../src/application/binary/AnalysisSnapshotFiles.js"; +import { workflowAnalysisProfile } from "../../src/application/WorkflowAnalysisProfile.js"; import { APPLE_DISPATCH_METADATA_ANALYSIS_PROFILE } from "../../src/native/NativeMacOSProviderMetadata.js"; import { CPU, @@ -66,10 +67,21 @@ cliTest.skipIf(process.platform !== "darwin")( .structuredContent, ); expect(original.analysis_profile).toEqual( - APPLE_DISPATCH_METADATA_ANALYSIS_PROFILE, + workflowAnalysisProfile( + APPLE_DISPATCH_METADATA_ANALYSIS_PROFILE, + "inspect_native_dispatch_metadata", + ), ); await good("record_unknown", { ...unknown, severity: "medium" }); await good("close_binary", { snapshot_path: saved, overwrite: true }); + const savedSnapshot = await readAnalysisSnapshot(saved); + if (!savedSnapshot.ok) throw savedSnapshot.error; + expect(savedSnapshot.value.binding.analysis_profile).toEqual( + APPLE_DISPATCH_METADATA_ANALYSIS_PROFILE, + ); + expect( + savedSnapshot.value.workflow_entries[0]?.execution.analysis_profile, + ).toEqual(original.analysis_profile); await good("open_binary", { path: first, snapshot_path: saved }); const rejected = await call("open_binary", { path: second, @@ -94,7 +106,12 @@ cliTest.skipIf(process.platform !== "darwin")( const snapshot = await readAnalysisSnapshot(after); if (!snapshot.ok) throw snapshot.error; expect(snapshot.value.binding.analysis_profile).toEqual( - original.analysis_profile, + savedSnapshot.value.binding.analysis_profile, + ); + expect( + snapshot.value.workflow_entries[0]?.execution.analysis_profile, + ).toEqual( + savedSnapshot.value.workflow_entries[0]?.execution.analysis_profile, ); expect(await readFile(first)).toEqual(Buffer.from(bytes)); expect(await readFile(second)).toEqual(Buffer.from(bytes));