fix(ghidra): make checkpoint workflows discoverable (#9246)

Expose the existing opt-in Ghidra checkpoint lifecycle in tool descriptions and summary capabilities. Distinguish confirmed initial analysis from restored Programs, preserving the checkpoint path and digest through shared CLI/MCP results.

Remove prose-driven checks and their scaffolding; retain canonical effects and lifecycle/source-fact coverage. Validated with 445 focused tests, 13 real Ghidra CLI/MCP scenarios and three isolated local Codex CLI tasks covering disabled setup, saving edits and fresh-agent restoration.
This commit is contained in:
gensokyo-dev authored and GitHub committed 2026-11-07 16:56:33 +08:00
1 parent 441ba94086
commit 8eff651270
12 files changed
+61 -204

No files matched your search

@@ -406,6 +406,10 @@ describe possible providers, while top-level session capabilities describe the
current route and `analysis_provider_binding` names the selected provider. Do
not treat those scopes as interchangeable.
For editable Ghidra analysis that must survive a later agent process, select
the [owned Program checkpoint workflow](references/native-and-artifacts.md#continue-editable-ghidra-analysis-across-processes)
before making metadata edits. It is separate from Evidence/Unknown persistence.
For a Ghidra startup timeout on a large native target, follow
[deadline recovery](references/native-and-artifacts.md#ghidra-startup-deadline-recovery)
before abandoning deep analysis or repeating the same cold import. The default
@@ -381,6 +381,28 @@ operation's full ordered list. Candidate capabilities, returned with complete
detail, describe possible providers. Top-level session capabilities describe the current route, and
`analysis_provider_binding` identifies the selected provider.
## Continue editable Ghidra analysis across processes
For a task spanning agent sessions, check `binary_session` after selecting
Ghidra: the current provider's `capabilities[].shared_limitations` states whether
Program checkpoints are enabled or the session is ephemeral. Successful native
queries distinguish initial analysis from an actually restored packed Program;
do not infer that restoration happened merely because checkpoint mode is enabled.
In ephemeral mode, choose a private absolute directory and set
`REA_GHIDRA_CHECKPOINT_DIR` in the environment starting REA. Restart the MCP server
after changing that launch configuration; no current tool call or runtime
analysis-setting update enables it. Stock ELF/PE/Mach-O imports on Linux/macOS
are supported; see the [checkpoint setup and limitations](https://github.com/morluto/rea/blob/main/docs/installation.md#automatically-checkpoint-an-owned-ghidra-program).
With the setting enabled, normal opens restore compatible saved Programs
automatically. Initial completed analysis is saved, and `close_binary` saves
later metadata edits; no separate save/restore tool is needed. Repair a reported
save failure and retry close. Cancellation or process failure can lose edits
after the last successful save; unfinished initial analysis cannot resume.
Keep the same checkpoint configuration for later processes. Evidence/Unknown
history and immutable REA query snapshots have separate persistence workflows.
## Reuse caller-selected Ghidra analysis
When the caller supplies an expanded project, configure `REA_GHIDRA_PROJECT_PATH`
@@ -404,7 +426,7 @@ When the error names `ghidra.startup_timeout_ms`, call
`update_analysis_settings` with `action: "set"` and a larger value within the
reported bounds. Resolve retained cleanup before the next startup attempt.
No REA restart is needed; an already accepted startup keeps its original
budget. A new attempt repeats import. Preserve useful Evidence before closing
budget. Follow the error's session-specific restart guidance, and preserve useful Evidence before closing
and reopening the target when remediation requires that lifecycle step.
For a per-function timeout, update `ghidra.decompile_timeout_seconds` and retry
@@ -660,7 +682,7 @@ numbering gaps leave coverage unknown; a DfuSe suffix CRC mismatch follows
### Function annotations in Ghidra
Fresh-import sessions admit annotations; existing-project reuse is read-only.
Use `annotate_native_function` for one function name and/or entry comments, with at least one explicit change. Review its annotation readback and refreshed dossier inline. Changes are atomic and session-scoped; empty comments clear them, omitted fields preserve them. Later MCP calls observe edits until close. CLI `annotate-native-function` returns the updated analysis before discarding the session. Original executable bytes are unchanged; edits invalidate immutable snapshots. Windows P0 does not admit database mutations. For naming only, `set_address_name` / `set_addresses_names` also work with Ghidra: they name function entries or data addresses (primary labels) and return compact per-address success booleans instead of a dossier. Ghidra rejects batch addresses that request the same name or a name another selected address holds; `raw_result.failures` gives each failure's reason.
Use `annotate_native_function` for one function name and/or entry comments, with at least one explicit change. Review its annotation readback and refreshed dossier inline. Changes are atomic; empty comments clear them, omitted fields preserve them. Later MCP calls observe edits. Normal close saves them when owned Program checkpoint mode is enabled; ephemeral mode discards them. CLI `annotate-native-function` follows the same setting when closing after its updated result. Select the [checkpoint workflow](#continue-editable-ghidra-analysis-across-processes) before edits that must survive a later process. Original executable bytes are unchanged; edits invalidate immutable snapshots. Windows P0 does not admit database mutations. For naming only, `set_address_name` / `set_addresses_names` also work with Ghidra: they name function entries or data addresses (primary labels) and return compact per-address success booleans instead of a dossier. Ghidra rejects batch addresses that request the same name or a name another selected address holds; `raw_result.failures` gives each failure's reason.
For `compare_functions`, obtain `analyze_function` Evidence before and after the
edit while the MCP session is open; annotation Evidence is not a comparison
+7
View File
@@ -1449,6 +1449,13 @@ and analysis into a private temporary project with `-noanalysis`. Normal close
saves metadata changes; unchanged queries do not rewrite the checkpoint.
The target executable is never executed by this workflow.
For agents, `open_binary` advertises this startup setting. After selecting Ghidra,
the default `binary_session` summary reports enabled or ephemeral mode in the
provider's `capabilities[].shared_limitations`. Successful native results state
whether initial analysis was created or a verified packed Program was restored.
No separate save/restore tool or runtime analysis-setting update is required;
enabling checkpoint mode requires the startup environment and an MCP restart.
Storage is separated by target SHA-256 and analysis-profile identity, including
the supported Ghidra version. Identical executable bytes at another path can
reuse the Program. One live REA session holds the checkpoint's socket lease;
+1 -1
View File
@@ -50,7 +50,7 @@ const official = <
export const OFFICIAL_TOOL_CONTRACTS = [
official(
"annotate_native_function",
"Atomically update one function name and/or entry comments, read them back, and return a refreshed complete function dossier. Ghidra changes its ephemeral session analysis database only; original executable bytes remain unchanged. Empty comment text clears that comment. Edits remain available to subsequent MCP calls until close; CLI returns the updated dossier before discarding its session. Invalid edits roll back the entire operation. Windows P0 and providers without this capability are unsupported.",
"Atomically update one function name and/or entry comments, read them back, and return a refreshed complete function dossier. Ghidra edits owned Program metadata; original executable bytes remain unchanged. Empty comment text clears that comment. With REA_GHIDRA_CHECKPOINT_DIR configured at startup, normal close saves edits for the next process; ephemeral mode discards them. CLI follows the same setting after returning its updated dossier. Check binary_session capabilities for the current mode. Invalid edits roll back the entire operation. Windows P0 and providers without this capability are unsupported.",
nativeFunctionAnnotationsInputSchema,
),
official(
+1 -1
View File
@@ -25,7 +25,7 @@ export const binarySessionInputSchema = z.strictObject({
.enum(BINARY_SESSION_DETAILS)
.default("summary")
.describe(
"summary (default) keeps open state, the binding, current-route capabilities, each provider candidate's availability and target-support status, code and reason, and every tool's availability, reason and remediation; omitted_fields lists what it leaves out. complete adds each unavailable candidate's remediation, provider diagnostics, candidate capabilities and static tool surface, effect and annotation flags.",
"Start with summary (default): it keeps open state, the binding, current-route capabilities (including selected Ghidra checkpoint mode), each provider candidate's availability and target-support status, code and reason, and every tool's availability, reason and remediation; omitted_fields lists what it leaves out. Request complete only when you need unavailable-candidate remediation, provider diagnostics, candidate capabilities or static tool surface, effect and annotation flags.",
),
expected_package_version: z.string().min(1).optional(),
expected_catalog_digest: z
+3 -3
View File
@@ -68,7 +68,7 @@ export const exportEvidenceBundleInputSchema = z.strictObject({
export const SESSION_TOOL_CONTRACTS = [
session(
"open_binary",
"Open a local executable, application bundle, archive, JavaScript, source map, plist, or analysis database after validation. format=dos-com explicitly interprets 1..65280 headerless bytes as a DOS COM analysis image. format=raw-binary interprets headerless bytes with a required architecture and entry_point; file offset 0 maps at that address. Omission preserves header-based detection. provider_id selects one deep provider or deterministic auto selection; the binding remains stable until close or an explicit switch, with no failure fallback. Reopening the same target and profile retains its active database and metadata edits. An optional analysis snapshot is imported atomically and must match the binary identity, concrete provider, and canonical analysis profile exactly; importing into a session with metadata mutations requires closing and reopening it first. Switching targets preserves investigation records. A failed admitted open reports error.details.selection_at_failure, target_at_open_completion, and provider_id_at_open_completion. The target summary and provider id are the selected binary and bound provider when that attempt settled, or null when absent. Later calls can change the selection, so use binary_session for current state. Call close_binary to release engine resources; reset_investigation explicitly clears investigation records.",
"Open a local executable, application bundle, archive, JavaScript, source map, plist, or analysis database after validation. format=dos-com explicitly interprets 1..65280 headerless bytes as a DOS COM analysis image. format=raw-binary interprets headerless bytes with a required architecture and entry_point; file offset 0 maps at that address. Omission preserves header-based detection. provider_id selects one deep provider or deterministic auto selection; the binding remains stable until close or an explicit switch, with no failure fallback. Reopening the same target and profile retains its active database and metadata edits. For editable Ghidra persistence across processes, set REA_GHIDRA_CHECKPOINT_DIR to a private absolute directory in the startup environment (stock ELF/PE/Mach-O on Linux/macOS; restart MCP). Opens and normal close then restore/save automatically; no separate persistence tool or runtime setting update is needed. An optional analysis snapshot is imported atomically and must match the binary identity, concrete provider, and canonical analysis profile exactly; importing into a session with metadata mutations requires closing and reopening it first. Switching targets preserves investigation records. A failed admitted open reports error.details.selection_at_failure, target_at_open_completion, and provider_id_at_open_completion. The target summary and provider id are the selected binary and bound provider when that attempt settled, or null when absent. Later calls can change the selection, so use binary_session for current state. Call close_binary to release engine resources; reset_investigation explicitly clears investigation records.",
openBinaryInputSchema,
),
session(
@@ -78,7 +78,7 @@ export const SESSION_TOOL_CONTRACTS = [
),
session(
"close_binary",
"Drain earlier requests and release the active engine, replay cache and owned resources while retaining investigation Evidence references and complete Unknown history in memory. Optionally save a target-bound analysis snapshot before closing; resource-only sessions have no binary snapshot binding. Later requests wait for this transition. Existing files require overwrite; a failed snapshot save keeps the engine open. Metadata edits prevent snapshot saves until the engine is recreated. Use reset_investigation to clear records explicitly, optionally saving a complete cross-domain Evidence bundle for import in a new process. Closing does not automatically persist records.",
"Drain earlier requests and release the active engine, replay cache and owned resources while retaining investigation Evidence references and complete Unknown history in memory. Optionally save a target-bound analysis snapshot before closing; resource-only sessions have no binary snapshot binding. Later requests wait for this transition. Existing files require overwrite; a failed snapshot save keeps the engine open. Metadata edits prevent snapshot saves until the engine is recreated. Use reset_investigation to clear records explicitly, optionally saving a complete cross-domain Evidence bundle for import in a new process. With Ghidra checkpoint mode enabled, normal close saves Program metadata; a failed save retains the live Program for close retry. Closing does not automatically persist investigation Evidence/Unknown records.",
closeBinaryInputSchema,
),
session(
@@ -88,7 +88,7 @@ export const SESSION_TOOL_CONTRACTS = [
),
session(
"binary_session",
'Report the active binary target or resource-only app root, provider and capability availability when applicable, client-feature, analysis, and server-identity status without starting analysis. The default summary lists omitted_fields; detail: "complete" adds provider diagnostics, candidate capabilities and static tool flags.',
'Report the active binary target or resource-only app root, provider and capability availability when applicable, client-feature, analysis, and server-identity status without starting analysis. Start with the default summary: after selecting Ghidra, capabilities[].shared_limitations already reports enabled checkpoint or ephemeral mode. Request detail: "complete" only for provider diagnostics, candidate capabilities or static tool flags; omitted_fields lists what summary leaves out.',
binarySessionInputSchema,
),
session(
-145
View File
@@ -8,128 +8,6 @@ import {
type ToolEffects,
} from "./toolEffects.js";
/**
* Descriptions that name running, launching, executing, starting, installing,
* or uninstalling something. A matching tool must declare foreign execution or
* device mutation unless it is listed below with the reason the wording does
* not describe that effect.
*/
const EXECUTION_WORDING =
/\b(?:run|runs|running|launch|launches|launching|execute|executes|executing|execution|start|starts|install|installs|uninstall|uninstalls)\b/iu;
const EXECUTION_WORDING_EXCEPTIONS: Readonly<Record<string, string>> = {
inspect_artifact: "states that mapper execution remains unobserved",
guided_workflows:
"renders existing guidance and states that it does not start analysis",
inspect_analysis_settings:
"reports launch configuration without launching any process",
update_analysis_settings:
"resets defaults to launch configuration without launching any process",
inspect_with_objdump:
"runs a caller-selected inspection tool, not target code",
start_gdb_session:
"starts a debugger session; execution is attributed to gdb_console",
start_frida_session:
"spawned targets remain suspended until resume_frida_session",
inspect_native_load_image: "states that it does not execute the target",
resolve_native_call_targets: "states that it does not execute code",
inspect_byte_entropy: "states that it profiles the file without execution",
inspect_pe_resources: "states that it does not execute the image",
inspect_native_exports: "reads the export table without executing it",
inspect_devicetree: "states that it does not execute the image",
inspect_elf_notes: "states that it does not execute the image",
inspect_apple_ips: "states that it does not execute a target",
inspect_kernel_module: "states that it does not execute the module",
inspect_pe_load_config: "states that it does not execute the image",
inspect_binary_hardening: "states that it does not execute the image",
inspect_desktop_application:
"states that directory triage performs no target execution",
inspect_pe_certificates: "states that it does not execute the image",
inspect_pe_imports: "states that it does not execute the image",
inspect_go_binary: "reads embedded metadata without executing the target",
inspect_rust_binary:
"reads an embedded dependency list without executing the target",
inspect_python_pyc:
"states that it does not execute bytecode or invoke CPython",
inspect_erlang_beam: "states that it does not execute bytecode",
inspect_sqlite_database:
"uses a trusted inspector worker without executing application code or caller SQL",
inspect_binary_layout:
"parses the binary offline with operator-configured pwntools",
inspect_recorded_crash: "inspects a core file without launching its target",
inspect_macho: "runs host Mach-O inspection tools, not the target",
inspect_evm_interface:
"decodes bytecode in REA's bundled worker without executing it",
compare_wasm_interfaces:
"compares retained inspection Evidence without executing modules",
inspect_wasm_artifact:
"WABT validates and decompiles the module without executing it",
identify_flutter_build:
"identifies the build through execution-free APK parsing",
inspect_dart_aot: "inspects snapshot bytes without executing them",
inspect_analysis_view:
"reports retained WASM execution/network states without performing execution",
inspect_managed_artifact: "reads metadata without executing target code",
inspect_managed_members: "reads metadata without executing target code",
inspect_managed_native_boundaries:
"reads metadata without executing target code",
verify_managed_native_boundaries: "states it does not execute managed code",
project_managed_application_graph: "states it does not execute managed code",
inspect_android_package: "states it does not execute the APK",
search_android_classes: "states it does not execute the APK",
inspect_android_jni_bindings:
"reads DEX and ELF bytes without executing target code",
inspect_apktool_client:
"inspects the operator-selected apktool launcher; never installs it",
inspect_pcap_client:
"probes the operator-selected tshark and capinfos; never installs them",
inspect_jeb_client: "talks to a caller-started JEB; never launches it",
open_jeb_project: "the engine analyzes the file; REA does not execute it",
inspect_adb_client:
"inspects the operator-selected adb client; never starts its server",
list_adb_devices: "may start the local adb server, which REA does not own",
inspect_api_contract:
"states there is no target execution during offline contract parsing",
inspect_firmware_regions: "states there is no target execution",
inspect_gpu_code_objects:
"reads static declarations without executing GPU code",
inspect_model_artifact:
"states that model code and inference are never executed",
extract_firmware: "states that extracted code is never executed",
analyze_javascript_application: "reconstructs the graph without executing",
reconcile_javascript_runtime: "never invents execution claims",
observe_javascript_runtime: "never evaluates or resumes the target",
trace_application_feature: "never executes the application",
trace_javascript_semantics: "static reachability never claims execution",
compare_javascript_export_shapes: "states it does not execute JavaScript",
project_android_application_graph: "execution-free inventory",
project_apple_application_graph: "execution-free inventory",
project_harmony_application_graph: "execution-free inventory",
trace_web_source_location: "states there is no application execution",
trace_web_module_imports:
"runs only REA's resolver stub in operator-configured Chromium",
export_web_scripts: "states there is no refetch or execution",
observe_web_execution:
"instruments an externally driven page; it does not run code itself",
recover_javascript_sources: "states there is no application code execution",
goto_address: "start names an object boundary, not a launch",
prev_address: "start names an object boundary, not a launch",
trace_call_path: "start names the traversal origin",
observe_native_ui: "observes an already-running app without launching it",
inspect_adb_package: "reports install times without changing the device",
list_adb_processes: "lists running processes without changing the device",
inspect_web_event_listeners: "states it does not evaluate JavaScript",
inspect_apk_signature: "states that no Android code executes",
};
const declaresForeignAuthority = (name: string): boolean => {
const effects = TOOL_EFFECTS[name];
return (
effects !== undefined &&
(effects.executesForeignCode || effects.mutatesDevice)
);
};
describe("tool effects", () => {
it("declares the web bundle worker's private file and child process effects", () => {
const contract = TOOL_CONTRACTS.find(
@@ -150,29 +28,6 @@ describe("tool effects", () => {
);
});
it("keeps execution and device wording aligned with declared effects", () => {
const undeclared = TOOL_CONTRACTS.filter(
({ name, description }) =>
EXECUTION_WORDING.test(description) &&
!declaresForeignAuthority(name) &&
EXECUTION_WORDING_EXCEPTIONS[name] === undefined,
).map(({ name }) => name);
expect(undeclared).toEqual([]);
const staleExceptions = Object.keys(EXECUTION_WORDING_EXCEPTIONS).filter(
(name) => {
const contract = TOOL_CONTRACTS.find(
(candidate) => candidate.name === name,
);
return (
contract === undefined ||
!EXECUTION_WORDING.test(contract.description) ||
declaresForeignAuthority(name)
);
},
);
expect(staleExceptions).toEqual([]);
});
it("marks foreign execution and device mutation as destructive", () => {
const destructive = TOOL_CONTRACTS.filter(
({ effects }) => effects.executesForeignCode || effects.mutatesDevice,
+5 -3
View File
@@ -739,9 +739,11 @@ export class GhidraClient {
}
}
/** Actual restore source, including a restart within the same REA session. */
checkpointRestore(): GhidraPackedProgram | undefined {
return this.#checkpointRestore;
/** Verified source after startup: null for initial checkpointed analysis; undefined if disabled or unconfirmed. */
checkpointRestore(): GhidraPackedProgram | null | undefined {
if (this.#checkpoint === undefined || !this.#startupSucceeded())
return undefined;
return this.#checkpointRestore ?? null;
}
/** Read completion from the current attempt; no independent readiness flag. */
@@ -3,7 +3,6 @@ import {
EXISTING_PROJECT_WINDOWS_REMEDIATION,
EXISTING_PROJECT_WINDOWS_UNAVAILABLE,
existingProjectCapabilities,
checkpointCapabilities,
ghidraEmptyIncomingReferencesLimitation,
ghidraEmptyDossierIncomingLimitation,
ghidraEmptyExternalOutgoingLimitation,
@@ -110,48 +109,6 @@ import { createEvidence } from "../domain/evidence.js";
import { createEvidenceBundle } from "../domain/evidenceBundle.js";
const INSTALL = "/opt/ghidra_12.1.4_PUBLIC";
it.skipIf(process.platform === "win32")(
"health describes checkpoint recovery without ephemeral reimport advice",
async () => {
const directory = await mkdtemp(join(tmpdir(), "rea-checkpoint-health-"));
const ghidra = provider(
installationHost(),
() => ({
start: async () => ok(sessionInfo()),
callTool: async () => ok([]),
close: async () => ok(null),
checkpointRestore: () => ({
path: join(directory, "verified.gzf"),
sha256: "a".repeat(64),
}),
}),
{ REA_GHIDRA_CHECKPOINT_DIR: directory },
);
const client = await createElfClient(ghidra);
try {
const health = await client.execute("health", {});
if (!health.ok) throw health.error;
expect(health.value.limitations.join("\n")).toContain(
"last successfully saved owned Program",
);
expect(health.value.limitations.join("\n")).toContain(
"auto-analysis was not rerun",
);
expect(health.value.limitations.join("\n")).not.toMatch(
/imports the original artifact again|discards session annotations/,
);
for (const capability of checkpointCapabilities()) {
expect(capability.limitations.join("\n")).not.toMatch(
/discarded on close|imports the original artifact again/,
);
}
} finally {
await client.close();
await ghidra.closeOwnedResources();
await rm(directory, { recursive: true, force: true });
}
},
);
afterEach(() => vi.restoreAllMocks());
const installationHost = (
platform: NodeJS.Platform = "linux",
+3 -3
View File
@@ -22,7 +22,7 @@ const providerOperations = Object.freeze([
/** Health limitations shared by every Ghidra-backed capability. */
export const healthLimitations = Object.freeze([
"The session serves operations only after Ghidra reports default auto-analysis complete; incomplete analysis does not expose partial results.",
"The imported Program and temporary project are ephemeral and deleted on close. Annotation operations edit session database metadata; original executable bytes are never written.",
"Ghidra Program checkpoint mode is disabled: closing discards metadata edits; original executable bytes are unchanged. For stock ELF/PE/Mach-O on Linux/macOS, set REA_GHIDRA_CHECKPOINT_DIR to a private absolute directory before starting REA (restart MCP after changing its startup environment).",
"Cancelling an active Ghidra request terminates its ephemeral database and discards session annotations. The selected target remains open; the next provider query imports the original artifact again.",
]);
@@ -50,8 +50,8 @@ export const ghidraHealthLimitations = (
]
: source === "owned-checkpoint"
? [
"Opt-in Ghidra checkpoints retain the last successfully saved owned Program, including its names, comments and types. Restoring a checkpoint skips auto-analysis; its analysis is retained state, not a fresh discovery.",
"REA saves initial completed analysis and saves metadata changes on normal close. A failed save retains the live session for retry. Cancellation or process failure can lose changes after the last successful save; unfinished initial auto-analysis is not resumable.",
"Ghidra Program checkpoint mode is enabled (REA_GHIDRA_CHECKPOINT_DIR). Opening the same target/profile automatically restores saved analysis, names, comments and types; no separate save or restore tool is needed. Restored analysis is retained state, not a fresh discovery.",
"REA saves initial completed analysis and metadata changes on normal close (close_binary in MCP). A failed save retains the live session for close retry. Cancellation or process failure can lose changes after the last successful save; unfinished initial auto-analysis is not resumable.",
"The live project is a temporary working copy. Checkpoints persist in REA_GHIDRA_CHECKPOINT_DIR; source executables and caller-selected source projects are not modified.",
]
: healthLimitations;
+7 -3
View File
@@ -821,9 +821,13 @@ export const createGhidraProviderClient = (input: {
return [
...(restored === undefined
? []
: [
`Restored owned Program ${restored.path} (SHA-256 ${restored.sha256}); auto-analysis was not rerun.`,
]),
: restored === null
? [
"Created the initial owned Ghidra Program from the executable and saved its analysis checkpoint; metadata edits are saved on normal close.",
]
: [
`Restored owned Program ${restored.path} (SHA-256 ${restored.sha256}); auto-analysis was not rerun.`,
]),
...providerLimitations,
...targetLimitations,
...(psp ? GHIDRA_PSP_LIMITATIONS : []),
@@ -106,10 +106,13 @@ it.skipIf(process.platform === "win32")(
"saves completed initial analysis, leaves an unchanged checkpoint alone, and releases its lease",
async () => {
const f = await fixture();
expect(f.client.checkpointRestore()).toBeUndefined();
expect((await f.client.start()).ok).toBe(true);
expect(f.client.checkpointRestore()).toBeNull();
const path = join(f.selection.directory, "current.json"),
before = await readFile(path, "utf8");
expect((await f.client.close()).ok).toBe(true);
expect(f.client.checkpointRestore()).toBeUndefined();
expect(await readFile(path, "utf8")).toEqual(before);
expect(f.launcher.requests).toContain("checkpoint_program");
const selection = await resolveGhidraCheckpoint(
@@ -129,6 +132,9 @@ it.skipIf(process.platform === "win32")(
});
clients.push(resumed);
expect((await resumed.start()).ok).toBe(true);
expect(resumed.checkpointRestore()).toMatchObject({
sha256: JSON.parse(await readFile(path, "utf8")).source.sha256,
});
expect(f.launcher.packedPrograms.at(-1)?.sha256).toBe(
JSON.parse(await readFile(path, "utf8")).source.sha256,
);