Files
reverse-engineering-anything/tests/boundary/cli/traceNativeValuesCompletenessCli.test.ts
T

260 lines
8.4 KiB
TypeScript

import { readFile, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { Cli } from "incur";
import pino from "pino";
import { expect, it } from "vitest";
import { z } from "zod";
import {
createAnalysisExecution,
type AnalysisClient,
type AnalysisOperationPort,
type AnalysisProvider,
} from "../../../src/application/AnalysisProvider.js";
import { runDirectAnalysis } from "../../../src/application/DirectAnalysis.js";
import type { DirectAnalysisDependencies } from "../../../src/application/DirectAnalysisDependencies.js";
import { CLEANUP_STOPPED_VALUE_TRACE } from "../../../src/application/native/NativeValueTrace.js";
import {
failingValueTraceReads,
valueTraceDiamond,
valueTraceProvider,
VALUE_TRACE_DIAMOND_READS,
} from "../../../src/application/native/NativeValueTrace.fixture.js";
import { registerCoreNativeCommands } from "../../../src/cli/coreNativeCommands.js";
import { parseConfig } from "../../../src/config/parseConfig.js";
import { AnalysisCapabilityUnavailableError } from "../../../src/domain/analysisErrorCore.js";
import { createAnalysisProfile } from "../../../src/domain/analysisProfile.js";
import { parseEvidence } from "../../../src/domain/evidence.js";
import { nativeValueTraceSchema } from "../../../src/domain/native/nativeValueTrace.js";
import { ProviderCleanupError } from "../../../src/domain/providerCleanupError.js";
import { err, ok } from "../../../src/domain/result.js";
import { createTestBinarySession } from "../../fixtures/binarySession.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
const effects = {
mutatesArtifact: false,
launchesProcess: false,
mayShowUi: false,
mayAccessNetwork: false,
mayWriteFilesystem: false,
changesPermissions: false,
requiresRoot: false,
};
/** One-shot CLI analysis over a simulated value-trace provider; returns parsed stdout. */
const serveTraceCli = async (
port: AnalysisOperationPort,
close: AnalysisClient["close"] = async () => ok(null),
) => {
const directory = await createTestTempDirectory("rea-value-trace-cli-");
const path = join(directory, "fixture.hop");
await writeFile(path, "synthetic analysis database");
const provider: AnalysisProvider = {
identity: () => valueTraceProvider,
capabilities: () =>
(["analyze_function", "resolve_native_call_targets"] as const).map(
(operation) => ({
provider: valueTraceProvider,
operation,
available: true,
reason: null,
cachePolicy: "live",
limitations: [],
effects,
}),
),
resolveAnalysisProfile: async () =>
ok({
profile: createAnalysisProfile(valueTraceProvider, { fixture: true }),
}),
createClient: () => ({
execute: async (operation, parameters, options) =>
operation === "health"
? ok(createAnalysisExecution(null, valueTraceProvider))
: port.execute(operation, parameters, options),
close,
}),
};
const createSession = () => createTestBinarySession(provider);
const dependencies: DirectAnalysisDependencies = {
readConfiguration: () => parseConfig({}),
createBinarySession: createSession,
createNativeDispatchBinarySession: createSession,
createArtifactBinarySession: createSession,
createNativeBinarySession: createSession,
createManagedBinarySession: createSession,
};
const cli = Cli.create("rea-value-trace-cli-test", { sync: false });
registerCoreNativeCommands(
cli,
pino({ enabled: false }),
(target, tool, arguments_, options) =>
runDirectAnalysis(dependencies, target, tool, arguments_, options),
);
return async (options: readonly string[]) => {
let stdout = "";
await cli.serve(
["trace-native-values", path, "0x1000", "--max-depth", "1", ...options],
{
env: {},
stdout: (text) => {
stdout += text;
},
exit: () => undefined,
},
);
return { path, output: JSON.parse(stdout) as unknown };
};
};
const traceCli = async () => {
const run = await serveTraceCli(
valueTraceDiamond({ sessionSubject: true }).port,
);
return async (options: readonly string[]) =>
nativeValueTraceSchema.parse(
parseEvidence((await run(options)).output).normalized_result,
);
};
const cliFailureSchema = z.object({
code: z.string(),
details: z
.object({
resources: z.array(z.string()),
partial_observation: z.unknown(),
})
.loose(),
});
it("reports the CLI last page and its observed node budget separately", async () => {
const trace = await traceCli();
expect(
await trace(["--offset", "10", "--limit", "3", "--json"]),
).toMatchObject({
nodes: [{ id: "0x3000/parameter:0" }],
total_nodes: 11,
next_offset: null,
page_truncated: true,
traversal: { status: "complete" },
truncated: true,
unknowns: [],
});
const capped = await trace([
"--max-nodes",
"9",
"--offset",
"8",
"--limit",
"3",
"--json",
]);
expect(capped).toMatchObject({
nodes: [{ id: "0x3000/op-1" }],
total_nodes: 9,
next_offset: null,
page_truncated: true,
traversal: { status: "truncated", reasons: ["max_nodes"] },
truncated: true,
});
expect(capped.unknowns).toContainEqual({
procedure: "0x3000",
address: null,
reason: "Node budget reached",
});
});
it("preserves the completed trace's source closure when final close fails", async () => {
const run = await serveTraceCli(
valueTraceDiamond({ sessionSubject: true }).port,
async () =>
err(
new ProviderCleanupError(valueTraceProvider.id, ["owned session"], {
reason: "controlled final close failure",
}),
),
);
const directory = await createTestTempDirectory("rea-trace-close-sources-");
const destination = join(directory, "trace.json");
const { output } = await run(["--evidence_output", destination, "--json"]);
const failed = cliFailureSchema.parse(output);
expect(failed.code).toBe("cleanup_incomplete");
const trace = parseEvidence(failed.details.partial_observation);
expect(trace.evidence_links.length).toBeGreaterThan(0);
expect(failed.details).toMatchObject({
source_closure: {
publication: "not_published",
complete: true,
omitted_count: 0,
records: expect.arrayContaining([
expect.objectContaining({ evidence_id: trace.evidence_links[0] }),
]),
},
});
await expect(readFile(destination)).rejects.toThrow();
});
it.each([
{ close: "confirmed", resources: ["owned worker"] },
{ close: "unconfirmed", resources: ["owned session", "owned worker"] },
] as const)(
"stops a CLI trace at a callee's unconfirmed cleanup with $close final close",
async ({ close, resources }) => {
const failure = new AnalysisCapabilityUnavailableError(
valueTraceProvider.id,
"analyze_function",
"controlled child failure",
{
cleanup: {
reason: "cleanup proof refused",
resources: ["owned worker"],
},
},
);
const { port, reads } = failingValueTraceReads(
valueTraceDiamond({ sessionSubject: true }).port,
{ "analyze_function:0x2000": failure },
);
const run = await serveTraceCli(
port,
close === "confirmed"
? async () => ok(null)
: async () =>
err(
new ProviderCleanupError(
valueTraceProvider.id,
["owned session"],
{},
),
),
);
const { path, output } = await run(["--json"]);
expect(reads).toEqual(VALUE_TRACE_DIAMOND_READS.slice(0, 4));
const failed = cliFailureSchema.parse(output);
expect(failed.code).toBe("cleanup_incomplete");
expect([...failed.details.resources].sort()).toEqual(resources);
const partial = parseEvidence(failed.details.partial_observation);
expect(partial).toMatchObject({
operation: "trace_native_values",
caller_input_path: path,
parameters: { procedure: "0x1000", max_depth: 1 },
});
const admitted = nativeValueTraceSchema.parse(partial.normalized_result);
expect(admitted.procedures.map(({ address }) => address)).toEqual([
"0x1000",
]);
expect(admitted.traversal).toEqual({
status: "truncated",
reasons: ["cleanup_incomplete"],
});
expect(admitted.unknowns).toContainEqual({
procedure: "0x2000",
address: null,
reason: CLEANUP_STOPPED_VALUE_TRACE,
});
},
);