Compare commits

...
Author SHA1 Message Date
morluto 45a3d03dbf style(test): format CLI help default annotations (#9280)
(cherry picked from commit ac30fbd658)
2026-11-07 23:16:19 +08:00
morluto 4e518b0f2b chore: sync main test updates 2026-11-07 22:50:57 +08:00
morluto 4983d30edf chore: sync main analysis changes 2026-11-07 22:50:54 +08:00
morluto eb94309098 fix(cli): reconcile source closure with main 2026-11-07 22:42:55 +08:00
morluto 67ca695caf Merge branch 'babysit-8870' into babysit-8873
# Conflicts:
#	src/application/DirectAnalysis.ts
#	src/cli/analysisEvidenceDelivery.ts
#	tests/boundary/filesystem/directAnalysisSnapshotReplay.test.ts
2026-11-07 15:52:27 +08:00
morluto 95dc5c0ad0 Merge remote-tracking branch 'origin/main' into babysit-8870
# Conflicts:
#	src/application/DirectAnalysis.ts
#	src/application/binary/AnalysisSnapshotCache.ts
#	src/application/binary/AnalysisSnapshotReplayBinding.ts
#	src/application/investigation/InvestigationEvidence.ts
#	tests/boundary/filesystem/directAnalysisSnapshotReplay.test.ts
#	tests/boundary/mcp/workflowSnapshotReplayMcp.test.ts
2026-11-07 15:51:36 +08:00
morluto 5df3e273ae fix: supervise function bundle verifier processes 2026-11-07 15:24:00 +08:00
morluto 196f0afbbc fix(cli): retain sources across incomplete finalization 2026-11-04 14:37:44 +08:00
morluto 3368a036ae test(ghidra): verify saved project evidence bundles 2026-11-04 14:34:19 +08:00
morluto 2e1e99283e fix(cli): preserve partially captured source closure 2026-11-04 14:26:17 +08:00
morluto 2e8d463400 fix(cli): retain function evidence sources in bundles 2026-11-04 14:24:09 +08:00
morluto e62484931c fix: bound and share source-closure checks 2026-11-04 14:17:15 +08:00
morluto 89d5bb7056 fix: revalidate restored snapshot sources lazily 2026-11-04 14:06:59 +08:00
morluto 75c7187290 fix: preserve source closure during snapshot replay 2026-11-04 13:55:45 +08:00
10 changed files with 538 additions and 45 deletions

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`,
);
+41 -31
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,
@@ -25,8 +28,10 @@ import {
retainAnalysisResultSources,
type AnalysisResultWithSources,
} from "./investigation/recordEvidenceSources.js";
import { resolveInvestigationEvidenceClosure } from "./investigation/InvestigationEvidence.js";
import { createInvestigationEvidenceClosureChecker } from "./investigation/InvestigationEvidence.js";
import {
createInvestigationEvidenceClosureChecker,
collectInvestigationEvidenceClosure,
} from "./investigation/InvestigationEvidence.js";
import type { Evidence } from "../domain/evidence.js";
import type { NativeToolName } from "../contracts/native/nativeToolContracts.js";
import type { EnhancedToolName } from "../contracts/enhancedInputs.js";
@@ -383,12 +388,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;
@@ -411,23 +416,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;
@@ -512,12 +513,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);
}
@@ -548,20 +549,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,
@@ -643,16 +654,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
@@ -447,7 +447,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",
@@ -55,10 +55,7 @@ const singleDefaultOptions: readonly {
{ command: "inspect-signal-decoders", options: ["--offset"] },
];
const optionLine = (
help: string,
option: string,
): string | undefined => {
const optionLine = (help: string, option: string): string | undefined => {
const line = help
.split(/\r?\n/u)
.find((candidate) => candidate.trimStart().startsWith(`${option} `));
@@ -80,7 +77,9 @@ describe("CLI help default annotations", () => {
}
const statements = line.match(/default/giu)?.length ?? 0;
if (statements !== 1)
repeated.push(`${command} ${option}: ${statements} default statements`);
repeated.push(
`${command} ${option}: ${statements} default statements`,
);
}
}
expect(repeated).toEqual([]);
@@ -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)}`] }
: {}),
}),
],
}
@@ -1880,7 +1886,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,
@@ -1888,8 +1897,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;
@@ -1901,9 +1914,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 = {
@@ -1918,7 +1952,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);
@@ -1930,6 +1968,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