150 lines
5.3 KiB
TypeScript
150 lines
5.3 KiB
TypeScript
import { expect, it } from "vitest";
|
|
import { createAnalysisExecution } from "./AnalysisProvider.js";
|
|
import { executeFunctionAnalysisEvidence } from "./FunctionAnalysisEvidence.js";
|
|
import { AnalysisOutputError } from "../domain/analysisErrorCore.js";
|
|
import { parseEvidence } from "../domain/evidence.js";
|
|
import { createAnalysisProfile } from "../domain/analysisProfile.js";
|
|
import { ghidraFunctionDossier } from "../domain/ghidraValues.fixture.js";
|
|
import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
|
|
import { err, ok } from "../domain/result.js";
|
|
|
|
const provider = {
|
|
id: "dossier-fixture",
|
|
name: "Synthetic dossier",
|
|
version: "1",
|
|
};
|
|
const target: BinaryTarget = {
|
|
path: "/selected/application.elf",
|
|
sha256: "a".repeat(64),
|
|
kind: "executable",
|
|
format: "elf",
|
|
architecture: "x86_64",
|
|
availableArchitectures: ["x86_64"],
|
|
};
|
|
const parameters = { procedure: "fixture_main" };
|
|
|
|
it("does not start function work when admission is already cancelled", async () => {
|
|
const reason = { request: "cancelled before provider admission" };
|
|
let calls = 0;
|
|
const analysis = {
|
|
execute: async () => {
|
|
calls++;
|
|
return ok(createAnalysisExecution(ghidraFunctionDossier(), provider));
|
|
},
|
|
};
|
|
const result = await executeFunctionAnalysisEvidence(
|
|
analysis,
|
|
parameters,
|
|
target,
|
|
{ signal: AbortSignal.abort(reason) },
|
|
);
|
|
if (result.ok) throw new Error("Expected pre-admission cancellation");
|
|
expect(result.error).toMatchObject({ _tag: "AnalysisCancelledError" });
|
|
expect(result.error.cause).toBe(reason);
|
|
expect(calls).toBe(0);
|
|
});
|
|
|
|
it.each(["selected-target", "no-subject", "provider-subject"] as const)(
|
|
"retains the completed dossier and its %s binding after cancellation",
|
|
async (binding) => {
|
|
const dossier = ghidraFunctionDossier();
|
|
const reason = { request: "cancelled after provider observation" };
|
|
const analysisProfile = createAnalysisProfile(provider, {
|
|
language: "x86:LE:64",
|
|
});
|
|
const relationshipCoverage = {
|
|
status: "partial" as const,
|
|
limitations: ["Indirect callers remain unknown"],
|
|
limited_directions: ["callers"] as ["callers"],
|
|
};
|
|
const subject = {
|
|
...target,
|
|
path: "/observed/provider.elf",
|
|
sha256: "b".repeat(64),
|
|
};
|
|
for (const aborted of [false, true]) {
|
|
const controller = new AbortController();
|
|
const execution = createAnalysisExecution(dossier, provider, {
|
|
...(binding === "selected-target"
|
|
? {}
|
|
: { subject: binding === "no-subject" ? null : subject }),
|
|
analysisProfile,
|
|
relationshipCoverage,
|
|
rawResult: { provider_text: "preserved" },
|
|
limitations: ["Synthetic provider observation"],
|
|
locations: [{ kind: "address", address: "0x401000" }],
|
|
});
|
|
const result = await executeFunctionAnalysisEvidence(
|
|
{
|
|
execute: async () => {
|
|
if (aborted) controller.abort(reason);
|
|
return ok(execution);
|
|
},
|
|
},
|
|
parameters,
|
|
target,
|
|
{ signal: controller.signal },
|
|
);
|
|
if (aborted) {
|
|
if (result.ok) throw new Error("Expected late cancellation");
|
|
expect(result.error).toMatchObject({ _tag: "AnalysisCancelledError" });
|
|
expect(result.error.cause).toBe(reason);
|
|
} else if (!result.ok) throw result.error;
|
|
const evidence = parseEvidence(
|
|
result.ok ? result.value : result.error.partialObservation,
|
|
);
|
|
expect(evidence.operation).toBe("analyze_function");
|
|
expect(evidence.parameters).toEqual(parameters);
|
|
expect(evidence.normalized_result).toEqual(dossier);
|
|
expect(evidence.raw_result).toEqual({ provider_text: "preserved" });
|
|
expect(evidence.provider).toEqual(provider);
|
|
expect(evidence.analysis_profile).toEqual(analysisProfile);
|
|
expect(evidence.relationship_coverage).toEqual(relationshipCoverage);
|
|
expect(evidence.limitations).toContain("Synthetic provider observation");
|
|
expect(evidence.locations).toContainEqual({
|
|
kind: "address",
|
|
address: "0x401000",
|
|
});
|
|
if (binding === "no-subject") expect(evidence.subject).toBeNull();
|
|
else
|
|
expect(evidence.subject?.local_path).toBe(
|
|
binding === "selected-target" ? target.path : subject.path,
|
|
);
|
|
}
|
|
},
|
|
);
|
|
|
|
it.each(["provider-failure", "invalid-dossier"] as const)(
|
|
"keeps %s primary if cancellation arrives before result consumption",
|
|
async (kind) => {
|
|
const controller = new AbortController();
|
|
const failure = new AnalysisOutputError(
|
|
"analyze_function",
|
|
"Provider failure",
|
|
);
|
|
const result = await executeFunctionAnalysisEvidence(
|
|
{
|
|
execute: async () => {
|
|
controller.abort({ request: "unrelated late cancellation" });
|
|
return kind === "provider-failure"
|
|
? err(failure)
|
|
: ok(
|
|
createAnalysisExecution({ procedure: "incomplete" }, provider),
|
|
);
|
|
},
|
|
},
|
|
parameters,
|
|
target,
|
|
{ signal: controller.signal },
|
|
);
|
|
if (result.ok) throw new Error("Expected the producer/output failure");
|
|
if (kind === "provider-failure") expect(result.error).toBe(failure);
|
|
else
|
|
expect(result.error).toMatchObject({
|
|
_tag: "AnalysisOutputError",
|
|
operation: "analyze_function",
|
|
});
|
|
expect(result.error.partialObservation).toBeUndefined();
|
|
},
|
|
);
|