mirror of
https://github.com/morluto/rea.git
synced 2026-11-04 13:56:06 +00:00
After a validated seed, trace_native_values turned a cleanup-incomplete failure from a call-target read or a followed callee dossier into a plain-text unknown, kept dispatching reads, and returned successful Evidence without the cleanup_incomplete status, cleanup reason or owned resources. The trace now classifies child failures with the shared stopsTraversal predicate that call graph traversals use. A cancelled child still ends the trace with its cancellation. A child whose provider cannot confirm cleanup stops the trace: the provider's own error is the outcome, so its resources and bound cleanup retry stay intact, and the work admitted before the stop is returned as the trace's normal shape, with traversal truncated by cleanup_incomplete and a located unknown at the stopping read. That stop stays primary over a concurrent caller abort and over the output-size check. Ordinary child failures remain unknowns. MCP already retains such partials as workflow Evidence. The one-shot CLI dropped them, so runDirectAnalysis now retains the partial workflow Evidence on the failure through the same shared owner. Fixes #8371 Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
229 lines
7.3 KiB
TypeScript
229 lines
7.3 KiB
TypeScript
import { 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.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,
|
|
});
|
|
},
|
|
);
|