fix(cli): retain linked sources in function bundles (#8873)

* fix: preserve source closure during snapshot replay

* fix: revalidate restored snapshot sources lazily

* fix: bound and share source-closure checks

* fix(cli): retain function evidence sources in bundles

* fix(cli): preserve partially captured source closure

* test(ghidra): verify saved project evidence bundles

* fix(cli): retain sources across incomplete finalization

* fix: supervise function bundle verifier processes

* style(test): format CLI help default annotations (#9280)

(cherry picked from commit ac30fbd658)
This commit is contained in:
morluto authored and GitHub committed 2026-11-07 20:12:25 +00:00
1 parent f3c21665d8
commit 56fb5219ce
9 files changed
+531 -39

No files matched your search

@@ -0,0 +1,187 @@
#!/usr/bin/env node
// Exercise the public one-shot CLI on a caller-owned saved project. The report
// is intentionally synthetic; it is not a claim about Ghidra's analysis history.
import assert from "node:assert/strict";
import { createHash } from "node:crypto";
import {
mkdtemp,
mkdir,
readFile,
readdir,
rm,
writeFile,
} from "node:fs/promises";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { createVerifierCommands } from "./lib/verify-commands.mjs";
import { withVerifierCleanup } from "./lib/verifier-cleanup.mjs";
const installation = process.env.GHIDRA_INSTALL_DIR;
if (!installation || !process.env.JAVA_HOME)
throw new Error(
"Set GHIDRA_INSTALL_DIR and JAVA_HOME to caller-installed Ghidra and JDK 21.",
);
const commands = await createVerifierCommands();
let root;
let commandsClosed = false;
const targetHash = await withVerifierCleanup(async () => {
root = await mkdtemp(join(tmpdir(), "rea-function-bundle-"));
const target = join(root, "fixture");
const projectDir = join(root, "project");
const scriptDir = join(root, "scripts");
const source = join(root, "fixture.c");
const project = join(projectDir, "SourceControl.gpr");
const repo = resolve(process.cwd());
const hash = (data) => createHash("sha256").update(data).digest("hex");
const tree = async (directory, prefix = "") => {
const result = [];
for (const entry of (
await readdir(directory, { withFileTypes: true })
).sort((a, b) => a.name.localeCompare(b.name))) {
const path = join(directory, entry.name);
const relative = join(prefix, entry.name);
if (entry.isDirectory()) result.push(...(await tree(path, relative)));
else if (entry.isFile())
result.push([relative, hash(await readFile(path))]);
else throw new Error(`Unexpected project entry: ${relative}`);
}
return result;
};
const run = async (command, args, options = {}) =>
commands.exec(command, args, {
timeout: 120_000,
maxBuffer: 16 * 1024 * 1024,
...options,
});
await mkdir(projectDir);
await mkdir(scriptDir);
await writeFile(
source,
"__attribute__((noinline)) int leaf(int x) { return x + 1; }\nint main(void) { return leaf(2); }\n",
);
await writeFile(
join(scriptDir, "SetHistory.java"),
`import ghidra.app.script.GhidraScript;
import ghidra.program.model.listing.Program;
public class SetHistory extends GhidraScript {
public void run() throws Exception {
currentProgram.getOptions(Program.PROGRAM_INFO).setString("REA disabled analyzers", "{\\"format\\":\\"rea-ghidra-disabled-analyzers-v1\\",\\"analyzers\\":[{\\"name\\":\\"Synthetic historical control\\",\\"status\\":\\"disabled\\"}]}");
}
}
`,
);
await run(process.env.REA_CC ?? "cc", ["-O0", "-g", source, "-o", target]);
await run(join(installation, "support", "analyzeHeadless"), [
projectDir,
"SourceControl",
"-import",
target,
"-scriptPath",
scriptDir,
"-postScript",
"SetHistory.java",
]);
const before = await tree(projectDir);
const targetHash = hash(await readFile(target));
const env = {
...process.env,
REA_GHIDRA_PROJECT_PATH: project,
REA_GHIDRA_PROGRAM_NAME: "fixture",
};
const cli = join(repo, "scripts", "rea.mjs");
for (const detail of ["complete", "summary"]) {
const output = join(root, `${detail}.json`);
const { stdout } = await run(
process.execPath,
[
cli,
"function",
target,
"main",
"--provider",
"ghidra",
"--detail",
detail,
"--evidence_output",
output,
"--format",
"json",
"--full-output",
],
{ env },
);
assert.equal(JSON.parse(stdout).ok, true);
const bundle = JSON.parse(await readFile(output, "utf8"));
const parent = bundle.records.find(
(record) => record.operation === "analyze_function",
);
assert.ok(parent, "function observation missing from bundle");
assert.ok(
parent.evidence_links.length,
"fixture did not produce a linked historical source",
);
const records = new Map(
bundle.records.map((record) => [record.evidence_id, record]),
);
for (const record of bundle.records)
for (const link of record.evidence_links)
assert.ok(records.has(link), `bundle omits linked Evidence ${link}`);
assert.ok(
parent.evidence_links.some(
(id) => records.get(id)?.operation === "health",
),
"historical health source missing",
);
}
const existing = join(root, "existing.json");
await writeFile(existing, "caller-owned\n");
let failure;
try {
await run(
process.execPath,
[
cli,
"function",
target,
"main",
"--provider",
"ghidra",
"--detail",
"summary",
"--evidence_output",
existing,
"--format",
"json",
"--full-output",
],
{ env },
);
assert.fail("existing output unexpectedly overwritten");
} catch (error) {
if (error.code !== 1) throw error;
failure = JSON.parse(error.snapshot.stdout.text);
}
assert.equal(await readFile(existing, "utf8"), "caller-owned\n");
assert.equal(failure.ok, false);
assert.equal(failure.error.details.source_closure.complete, true);
assert.ok(
failure.error.details.source_closure.records.some(
(record) => record.operation === "health",
),
);
assert.equal(hash(await readFile(target)), targetHash);
assert.deepEqual(await tree(projectDir), before);
return targetHash;
}, [
async () => {
await commands.close();
commandsClosed = true;
},
async () => {
if (commandsClosed && root !== undefined)
await rm(root, { recursive: true, force: true });
},
]);
process.stdout.write(
`PASS Ghidra function bundles: complete, summary, no-overwrite; target and project unchanged (${targetHash})\n`,
);
+38 -30
View File
@@ -16,7 +16,10 @@ import {
import type { ResolvedSessionOpen } from "./binary/BinarySessionOpen.js";
import { silentLogger } from "../logger.js";
import type { Logger } from "pino";
import { withEvidenceCallerInputPath } from "../domain/evidence.js";
import {
tryParseEvidence,
withEvidenceCallerInputPath,
} from "../domain/evidence.js";
import {
createImmutableEvidenceBundle,
type EvidenceBundle,
@@ -26,8 +29,8 @@ import {
type AnalysisResultWithSources,
} from "./investigation/recordEvidenceSources.js";
import {
resolveInvestigationEvidenceClosure,
createInvestigationEvidenceClosureChecker,
collectInvestigationEvidenceClosure,
} from "./investigation/InvestigationEvidence.js";
import type { Evidence } from "../domain/evidence.js";
import type { NativeToolName } from "../contracts/native/nativeToolContracts.js";
@@ -388,12 +391,12 @@ const runAnalysis = async (
readonly evidenceParameters?: Readonly<Record<string, JsonValue>>;
readonly onEvidenceBundle?: (bundle: EvidenceBundle) => void;
readonly createSession?: () => BinarySession | Promise<BinarySession>;
readonly includeEvidenceSources?: boolean;
readonly providerId?: AnalysisProviderSelector;
readonly formatHint?: ExecutableFormatHint;
readonly architecture?: BinaryArchitecture;
readonly entryPoint?: string;
readonly espFlash?: EspFlashSelection;
readonly includeEvidenceSources?: boolean;
},
): Promise<AnalysisResultWithSources<Evidence, JsonValue>> => {
const { logger, signal, snapshotPath } = options;
@@ -416,23 +419,19 @@ const runAnalysis = async (
evidence: Evidence,
supplied: readonly Evidence[] = [],
): Result<null, AnalysisError> => {
if (options.includeEvidenceSources !== true) return ok(null);
const known = new Map(
if (!options.includeEvidenceSources) return ok(null);
const records = new Map(
supplied.map((record) => [record.evidence_id, record]),
);
const closure = resolveInvestigationEvidenceClosure(
const collected = collectInvestigationEvidenceClosure(
{
evidenceForAnalysis: (id) => {
const record = known.get(id) ?? session.evidenceForAnalysis(id);
if (record !== undefined) known.set(id, record);
return record;
},
evidenceForAnalysis: (id) =>
records.get(id) ?? session.evidenceForAnalysis(id),
},
evidence.evidence_links,
);
if (!closure.ok) return closure;
sourceEvidence = closure.value.map((id) => known.get(id) as Evidence);
return ok(null);
sourceEvidence = collected.records;
return collected.result;
};
const execute = async (): Promise<Result<Evidence, AnalysisError>> => {
let isDirectory = false;
@@ -518,12 +517,12 @@ const runAnalysis = async (
});
if (checkClosure(cached.evidence_links)) {
const evidence = withEvidenceCallerInputPath(cached, path);
const sources = captureSources(
const captured = captureSources(
evidence,
snapshot.evidence_bundle.records,
);
if (!sources.ok)
return err(sources.error.retainPartialObservation(evidence));
if (!captured.ok)
return err(captured.error.retainPartialObservation(evidence));
options.onEvidenceBundle?.(snapshot.evidence_bundle);
return ok(evidence);
}
@@ -554,20 +553,30 @@ const runAnalysis = async (
evidenceProfile,
saveSnapshot: snapshotPath !== undefined,
});
if (!executed.ok) return executed;
if (!executed.ok) {
if (options.includeEvidenceSources) {
const partial = tryParseEvidence(executed.error.partialObservation);
if (partial !== undefined) {
const captured = captureSources(partial);
if (!captured.ok)
executed.error.retainSecondaryFailure(captured.error);
}
}
return executed;
}
const { cachePolicy } = executed.value;
const evidence = withEvidenceCallerInputPath(executed.value.evidence, path);
// Provider execution may have already recorded the same semantic Evidence
// without this invocation's request path. Import replaces that metadata
// while retaining the authenticated result and snapshot binding.
const captured = captureSources(evidence);
if (!captured.ok)
return err(captured.error.retainPartialObservation(evidence));
const recorded = session.importEvidenceBundle(
createImmutableEvidenceBundle([evidence]),
);
if (!recorded.ok)
return err(recorded.error.retainPartialObservation(evidence));
const sources = captureSources(evidence);
if (!sources.ok)
return err(sources.error.retainPartialObservation(evidence));
if (isWorkflowEvidenceTool(tool)) {
const unknowns = recordWorkflowUnknowns({
name: tool,
@@ -649,16 +658,15 @@ const runAnalysis = async (
outcome.cleanupError.retainDiagnostics({
analysis_snapshot: { ...publishedSnapshot },
});
return err(
directAnalysisCleanupFailure(result, outcome.cleanupError, path),
);
return {
...err(directAnalysisCleanupFailure(result, outcome.cleanupError, path)),
...(sourceEvidence === undefined ? {} : { sourceEvidence }),
};
}
return result.ok
? {
...ok(result.value),
...(sourceEvidence === undefined ? {} : { sourceEvidence }),
}
: err(cliError(result.error, path));
return {
...(result.ok ? ok(result.value) : err(cliError(result.error, path))),
...(sourceEvidence === undefined ? {} : { sourceEvidence }),
};
};
const executeAnalysisTool = async (input: {
@@ -7,6 +7,7 @@ import { analysisErrorProjectionSchema } from "../../contracts/errorSchemas.js";
import { InvestigationRecords } from "./InvestigationRecords.js";
import {
createInvestigationEvidenceClosureChecker,
collectInvestigationEvidenceClosure,
resolveInvestigationEvidenceClosure,
} from "./InvestigationEvidence.js";
@@ -158,3 +159,25 @@ it("reuses a complete shared source graph without weakening missing-source check
expect(lookups).toBe(3);
expect(check([source.evidence_id, missing])).toBe(false);
});
it("collects other reachable sources after a missing ancestor", () => {
const missing = `ev_${"0".repeat(64)}`;
const first = observation("/reachable");
const incomplete = observation("/incomplete", [missing]);
const records = new InvestigationRecords();
for (const record of [first, incomplete]) {
const added = records.recordEvidence(record);
if (!added.ok) throw added.error;
}
const result = collectInvestigationEvidenceClosure(records, [
first.evidence_id,
incomplete.evidence_id,
]);
expect(result.result).toMatchObject({
ok: false,
error: { reason: "missing" },
});
expect(result.records.map((record) => record.evidence_id).sort()).toEqual(
[first.evidence_id, incomplete.evidence_id].sort(),
);
});
@@ -3,7 +3,7 @@ import type { EvidenceReader } from "./InvestigationRecordPort.js";
import { uniqueSorted } from "../../domain/canonicalOrdering.js";
import type { EvidenceIntegrityError } from "../../domain/evidenceErrors.js";
import type { Evidence } from "../../domain/evidence.js";
import { ok, type Result } from "../../domain/result.js";
import { err, ok, type Result } from "../../domain/result.js";
type Lookup = (id: string) => Evidence | undefined;
@@ -12,8 +12,10 @@ const visitClosure = (
lookup: Lookup,
seedIds: readonly string[],
knownComplete?: ReadonlySet<string>,
collect?: (record: Evidence) => void,
): Result<Set<string>, EvidenceIntegrityError> => {
const visited = new Set<string>();
let failure: EvidenceIntegrityError | undefined;
const pending = [...seedIds];
while (pending.length > 0) {
const evidenceId = pending.pop();
@@ -28,12 +30,36 @@ const visitClosure = (
lookup,
"retained-closure",
);
if (!evidence.ok) return evidence;
if (!evidence.ok) {
if (collect === undefined) return evidence;
failure ??= evidence.error;
visited.add(evidenceId);
continue;
}
visited.add(evidenceId);
collect?.(evidence.value);
for (const linkedId of evidence.value.evidence_links)
pending.push(linkedId);
}
return ok(visited);
return failure === undefined ? ok(visited) : err(failure);
};
/** Gather every reachable authenticated record while retaining the first failure. */
export const collectInvestigationEvidenceClosure = (
reader: Pick<EvidenceReader, "evidenceForAnalysis">,
seedIds: readonly string[],
): {
readonly records: Evidence[];
readonly result: Result<null, EvidenceIntegrityError>;
} => {
const records: Evidence[] = [];
const visited = visitClosure(
(id) => reader.evidenceForAnalysis(id),
seedIds,
undefined,
(record) => records.push(record),
);
return { records, result: visited.ok ? ok(null) : visited };
};
/** Follow exact session references without exporting unrelated records or Unknowns. */
+5 -1
View File
@@ -452,7 +452,11 @@ const registerFunctionCommand = (
"analyze_function",
),
},
{ ...directAnalysisOptionsFromCli(logger, options), signal },
{
...directAnalysisOptionsFromCli(logger, options),
signal,
includeEvidenceSources: options.evidence_output !== undefined,
},
);
return publishCliAnalysisEvidence(complete, delivery);
}),
@@ -89,6 +89,9 @@ it("summary delivery returns the compact view and writes a bundle inspect-analys
evidence_bundle: { path: string; bytes: number };
};
expect(h.runDirectAnalysis).toHaveBeenCalledOnce();
expect(h.runDirectAnalysis.mock.calls[0]?.[3]).toMatchObject({
includeEvidenceSources: true,
});
expect(out.summary.normalized_result.kind).toBe("summary");
expect(typeof out.summary.normalized_result.parent_evidence_id).toBe(
"string",
@@ -1,8 +1,10 @@
import { EvidenceIntegrityError } from "../../../src/domain/evidenceErrors.js";
import { ProviderCleanupError } from "../../../src/domain/providerCleanupError.js";
import { AnalysisCancelledError } from "../../../src/domain/analysisErrorCore.js";
import { projectAnalysisError } from "../../../src/domain/analysisErrorProjection.js";
import type { AnalysisError } from "../../../src/domain/analysisErrorBase.js";
import { projectCliAnalysisResult } from "../../../src/cli/projectCliAnalysisResult.js";
import { publishCliAnalysisEvidence } from "../../../src/cli/analysisEvidenceDelivery.js";
import { parseConfig } from "../../../src/config/parseConfig.js";
import { readFile, symlink, writeFile } from "node:fs/promises";
import { basename, dirname, join } from "node:path";
@@ -159,6 +161,7 @@ interface SnapshotProviderOptions {
readonly supportedOperations?: readonly CapabilityDescriptor["operation"][];
readonly limitations?: readonly string[];
readonly sourceEvidence?: boolean;
readonly sourceMissingLink?: boolean;
}
const makeProvider = (
@@ -263,6 +266,9 @@ const makeProvider = (
operation: "health",
parameters: {},
result: "fixture source",
...(options.sourceMissingLink
? { evidenceLinks: [`ev_${"0".repeat(64)}`] }
: {}),
}),
],
}
@@ -2036,7 +2042,10 @@ it("replays source-bearing direct analysis only while snapshot source Evidence i
});
const dependencies = fixture.dependencies(provider);
const parameters = { procedure: "0x1000", document: "fixture" };
const options = { snapshotPath: fixture.snapshotPath };
const options = {
snapshotPath: fixture.snapshotPath,
includeEvidenceSources: true,
};
const run = () =>
runDirectAnalysis(
dependencies,
@@ -2044,8 +2053,12 @@ it("replays source-bearing direct analysis only while snapshot source Evidence i
"procedure_pseudo_code",
parameters,
options,
).then(projectCliAnalysisResult);
const first = parseEvidence(await run());
);
const firstResult = await run();
const first = parseEvidence(projectCliAnalysisResult(firstResult));
expect(
firstResult.sourceEvidence?.map((record) => record.evidence_id),
).toEqual(first.evidence_links);
expect(first.evidence_links).toHaveLength(1);
const saved = await readAnalysisSnapshot(fixture.snapshotPath);
if (!saved.ok) throw saved.error;
@@ -2057,9 +2070,30 @@ it("replays source-bearing direct analysis only while snapshot source Evidence i
),
).toBe(true);
const initialStarts = fixture.starts.length;
expect(parseEvidence(await run()).evidence_links).toEqual(
const replayed = await run();
expect(
parseEvidence(projectCliAnalysisResult(replayed)).evidence_links,
).toEqual(first.evidence_links);
expect(replayed.sourceEvidence?.map((record) => record.evidence_id)).toEqual(
first.evidence_links,
);
const outputPath = join(
dirname(fixture.snapshotPath),
"replayed-evidence.json",
);
await publishCliAnalysisEvidence(replayed, {
providerId: IDENTITY.id,
operation: "procedure_pseudo_code",
signal: new AbortController().signal,
detail: "complete",
evidenceOutput: outputPath,
});
const published = JSON.parse(await readFile(outputPath, "utf8")) as {
records: Evidence[];
};
expect(published.records.map((record) => record.evidence_id).sort()).toEqual(
[first.evidence_id, ...first.evidence_links].sort(),
);
expect(fixture.starts).toHaveLength(initialStarts);
const withoutSource = {
@@ -2074,7 +2108,11 @@ it("replays source-bearing direct analysis only while snapshot source Evidence i
writeAnalysisSnapshot(withoutSource, fixture.snapshotPath, true, resources),
);
if (!written.ok) throw written.error;
const refreshed = parseEvidence(await run());
const refreshedResult = await run();
const refreshed = parseEvidence(projectCliAnalysisResult(refreshedResult));
expect(
refreshedResult.sourceEvidence?.map((record) => record.evidence_id),
).toEqual(first.evidence_links);
expect(fixture.starts).toHaveLength(initialStarts + 1);
expect(refreshed.evidence_links).toEqual(first.evidence_links);
const repaired = await readAnalysisSnapshot(fixture.snapshotPath);
@@ -2086,6 +2124,146 @@ it("replays source-bearing direct analysis only while snapshot source Evidence i
).toBe(true);
});
it("preserves captured sources when a transitive source is missing", async () => {
const fixture = await createSnapshotReplayFixture(
"rea-direct-partial-source-",
);
const provider = fixture.createProvider({
supportedOperations: ["procedure_pseudo_code"],
sourceEvidence: true,
sourceMissingLink: true,
});
const result = await runDirectAnalysis(
fixture.dependencies(provider),
fixture.path,
"procedure_pseudo_code",
{ procedure: "0x1000", document: "fixture" },
{ includeEvidenceSources: true },
);
expect(result.ok).toBe(false);
expect(result.sourceEvidence).toHaveLength(1);
const outputPath = join(dirname(fixture.snapshotPath), "missing-source.json");
const output = await publishCliAnalysisEvidence(result, {
providerId: IDENTITY.id,
operation: "procedure_pseudo_code",
signal: new AbortController().signal,
detail: "complete",
evidenceOutput: outputPath,
});
const failure = output as {
details: {
source_closure: {
records: Evidence[];
omitted: { evidence_id: string }[];
complete: boolean;
};
};
};
expect(failure.details.source_closure.complete).toBe(false);
expect(
failure.details.source_closure.records.map((record) => record.evidence_id),
).toEqual(result.sourceEvidence?.map((record) => record.evidence_id));
expect(failure.details.source_closure.omitted).toEqual([
{ evidence_id: `ev_${"0".repeat(64)}`, reason: "not_captured" },
]);
await expect(readFile(outputPath)).rejects.toMatchObject({ code: "ENOENT" });
});
it("retains completed function sources when cancellation arrives as execution returns", async () => {
const fixture = await createSnapshotReplayFixture(
"rea-direct-cancel-source-",
);
const provider = fixture.createProvider({
supportedOperations: ["analyze_function"],
sourceEvidence: true,
});
const session = createTestBinarySession(provider);
const execute = session.execute.bind(session);
const controller = new AbortController();
session.execute = async (...args) => {
const result = await execute(...args);
if (args[0] === "analyze_function") controller.abort();
return result;
};
const dependencies = {
...fixture.dependencies(provider),
createBinarySession: () => session,
};
const result = await runDirectAnalysis(
dependencies,
fixture.path,
"analyze_function",
{ procedure: "0x1000" },
{ signal: controller.signal, includeEvidenceSources: true },
);
expect(result.ok).toBe(false);
const outputPath = join(dirname(fixture.snapshotPath), "cancelled.json");
const output = await publishCliAnalysisEvidence(result, {
providerId: IDENTITY.id,
operation: "analyze_function",
signal: controller.signal,
detail: "complete",
evidenceOutput: outputPath,
});
const failure = output as {
code: string;
details: {
partial_observation: Evidence;
source_closure: { records: Evidence[]; complete: boolean };
};
};
expect(failure.code).toBe("cancelled");
expect(failure.details.source_closure.complete).toBe(true);
expect(
failure.details.source_closure.records.map((record) => record.evidence_id),
).toEqual(failure.details.partial_observation.evidence_links);
await expect(readFile(outputPath)).rejects.toMatchObject({ code: "ENOENT" });
});
it("retains function sources when completed parent admission fails", async () => {
const fixture = await createSnapshotReplayFixture(
"rea-direct-parent-admission-",
);
const provider = fixture.createProvider({
supportedOperations: ["analyze_function"],
sourceEvidence: true,
});
const session = createTestBinarySession(provider);
session.importEvidenceBundle = () =>
err(new EvidenceIntegrityError("controlled parent admission failure"));
const dependencies = {
...fixture.dependencies(provider),
createBinarySession: () => session,
};
const result = await runDirectAnalysis(
dependencies,
fixture.path,
"analyze_function",
{ procedure: "0x1000" },
{ includeEvidenceSources: true },
);
expect(result.ok).toBe(false);
const outputPath = join(dirname(fixture.snapshotPath), "refused.json");
const output = await publishCliAnalysisEvidence(result, {
providerId: IDENTITY.id,
operation: "analyze_function",
signal: new AbortController().signal,
detail: "complete",
evidenceOutput: outputPath,
});
const failure = output as {
details: {
partial_observation: Evidence;
source_closure: { records: Evidence[]; complete: boolean };
};
};
expect(failure.details.source_closure.complete).toBe(true);
expect(
failure.details.source_closure.records.map((record) => record.evidence_id),
).toEqual(failure.details.partial_observation.evidence_links);
await expect(readFile(outputPath)).rejects.toMatchObject({ code: "ENOENT" });
});
it("runs decompile export against the provider after loading a pseudocode snapshot", async () => {
const fixture = await createSnapshotReplayFixture("rea-directory-snapshot-");
const original = fixture.createProvider({
@@ -592,6 +592,63 @@ it.each(["interrupt", "cleanup-failure", "interrupt-cleanup-failure"] as const)(
},
);
it.each(["interrupt", "cleanup-failure"] as const)(
"retains captured source closure when direct finalization fails: %s",
async (scenario) => {
const workflow = await composedWorkflowFixture();
const controller = new AbortController();
const close = workflow.session.close.bind(workflow.session);
workflow.session.close = async () => {
workflow.session.close = close;
if (scenario === "interrupt") controller.abort();
const closed = await close();
if (scenario === "interrupt" || !closed.ok) return closed;
return err(
new ProviderCleanupError(
"fixture",
["owned-root"],
{ reason: "controlled session close failure" },
{ operation: "close_binary" },
),
);
};
const root = await createTestTempDirectory("rea-closure-direct-captured-");
const complete = await runDirectAnalysis(
workflow.dependencies,
workflow.path,
"binary_overview",
{},
{ signal: controller.signal, includeEvidenceSources: true },
);
const output = await publishCliAnalysisEvidence(complete, {
...delivery(root),
signal: controller.signal,
});
const failure = output as {
code: string;
details: { partial_observation: unknown };
};
expect(failure.code).toBe(
scenario === "interrupt" ? "cancelled" : "cleanup_incomplete",
);
const links = parseEvidence(
failure.details.partial_observation,
).evidence_links;
expect(links.length).toBeGreaterThan(0);
expect(closureOf(output)).toMatchObject({
publication: "not_published",
complete: true,
omitted_count: 0,
});
expect(
closureOf(output)
.records.map((record) => record.evidence_id)
.sort(),
).toEqual([...links].sort());
expect(await readdir(root)).toEqual([]);
},
);
it("reports through the shared one-shot delivery when cancellation arrives during close", async () => {
const root = await createTestTempDirectory("rea-closure-deliver-");
const held = source("held");
+6
View File
@@ -3,6 +3,12 @@
These source-owned fixtures are compiled at verification time; generated
binaries and Ghidra projects are never committed.
For the one-shot function bundle handoff, run `npm run build:cached` and then
`node scripts/verify-real-ghidra-function-bundle.mjs` with `GHIDRA_INSTALL_DIR`
and `JAVA_HOME` set. The verifier compiles its own target, creates a project
with a synthetic history report, and exercises complete, summary, and
no-overwrite CLI delivery. It checks the target and project files for changes.
- `../c/riscv.c` and `riscv-probe.S` produce source-owned RV32/RV64 ELF images
with double-float ABI declarations and an independently known instruction.
`../c/riscv-float-abi.c`, `riscv-probe.S` and `riscv-isa-probe.S` produce