Files
rea/tests/boundary/cli/analyzeFunctionDetail.test.ts
morluto 82fd223638 refactor(native): consolidate provider-neutral analysis contracts
Move shared function, dossier, inventory, reference and output semantics from the legacy Hopper module to native domain owners. Migrate provider, adapter, test and verifier consumers directly.
2026-10-27 06:43:53 +08:00

114 lines
4.5 KiB
TypeScript

import { Cli } from "incur";
import pino from "pino";
import { stat } from "node:fs/promises";
import { join } from "node:path";
import { expect, it, vi } from "vitest";
import { inspectAnalysisView } from "../../../src/application/analysisView/AnalysisViewService.js";
import { registerCoreBinaryCommands } from "../../../src/cli/coreBinaryCommands.js";
import type { CliInstance } from "../../../src/cli/types.js";
import { CLI_COMMANDS } from "../../../src/cliCommandNames.js";
import { createEvidence } from "../../../src/domain/evidence.js";
import { functionDossierSchema } from "../../../src/domain/native/functionDossier.js";
import { ghidraFunctionDossier } from "../../../src/domain/ghidraValues.fixture.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
// A complete analyze_function Evidence as runDirectAnalysis would return it:
// subject-bound (so the summary view has an artifact) with a schema-valid dossier.
const cannedCompleteEvidence = () =>
createEvidence(
{ path: "/fixtures/native.exe", format: "pe", sha256: "a".repeat(64) },
{ id: "ghidra", name: "Ghidra", version: "12.1.4" },
{
operation: "analyze_function",
parameters: { procedure: "main" },
result: functionDossierSchema.parse(ghidraFunctionDossier()),
rawResult: null,
},
);
const harness = () => {
const runDirectAnalysis = vi.fn(
async () => cannedCompleteEvidence() as unknown,
);
const cli = Cli.create("rea-fn-detail-test", { version: "1", sync: false });
registerCoreBinaryCommands(
cli as unknown as CliInstance,
pino({ enabled: false }),
runDirectAnalysis as never,
{},
);
const command = Cli.toCommands.get(cli)?.get(CLI_COMMANDS.function);
if (command === undefined)
throw new Error("function command was not registered");
const argsSchema = Reflect.get(command, "args");
const optionsSchema = Reflect.get(command, "options");
const execute = Reflect.get(command, "run");
if (typeof execute !== "function")
throw new Error("function command has no run callback");
const invoke = (options: Record<string, unknown>): Promise<unknown> =>
execute({
args: (argsSchema as { parse(v: unknown): unknown }).parse({
path: "/fixtures/native.exe",
address: "main",
}),
options: (optionsSchema as { parse(v: unknown): unknown }).parse(options),
});
return { runDirectAnalysis, invoke };
};
it("defaults to the complete dossier and runs analysis", async () => {
const h = harness();
const out = (await h.invoke({})) as { operation?: string };
expect(h.runDirectAnalysis).toHaveBeenCalledOnce();
expect(out.operation).toBe("analyze_function");
expect(
(out as { normalized_result?: unknown }).normalized_result,
).toBeDefined();
});
it("rejects summary delivery without --evidence_output before any analysis", async () => {
const h = harness();
const out = await h.invoke({ detail: "summary" });
expect(h.runDirectAnalysis).not.toHaveBeenCalled();
expect(JSON.stringify(out)).toContain("evidence_output");
});
it("summary delivery returns the compact view and writes a bundle inspect-analysis-view pages", async () => {
const h = harness();
const directory = await createTestTempDirectory("rea-fn-detail-");
const destination = join(directory, "complete.json");
const out = (await h.invoke({
detail: "summary",
evidence_output: destination,
})) as {
summary: {
normalized_result: { kind: string; parent_evidence_id: string };
};
evidence_bundle: { path: string; bytes: number };
};
expect(h.runDirectAnalysis).toHaveBeenCalledOnce();
expect(out.summary.normalized_result.kind).toBe("summary");
expect(typeof out.summary.normalized_result.parent_evidence_id).toBe(
"string",
);
expect(out.evidence_bundle.bytes).toBeGreaterThan(0);
expect((await stat(destination)).size).toBeGreaterThan(0);
// The saved bundle is the evidence-file source inspect-analysis-view reads back.
const parentId = out.summary.normalized_result.parent_evidence_id;
const page = await inspectAnalysisView({
source: { kind: "evidence-file", path: destination, evidence_id: parentId },
view: { kind: "native", facet: "pseudocode", offset: 0, limit: 16 },
});
if (!page.ok) throw page.error;
expect(page.value.parameters).toMatchObject({ parent_evidence_id: parentId });
expect(page.value.normalized_result).toMatchObject({
kind: "native",
item: {
text: functionDossierSchema
.parse(ghidraFunctionDossier())
.pseudocode.slice(0, 16),
},
});
});