mirror of
https://github.com/morluto/rea.git
synced 2026-11-14 18:52:48 +00:00
fix(native): retain value trace source evidence (#8384)
* fix(native): retain value trace source evidence * fix(native): build trace error evidence only on failure * test(mcp): expect unowned value trace refusal * test(mcp): avoid freezing trace error wording * test(native): keep value-trace stop coverage with source evidence Main's stop and continuation cases now meet the trace's evidence-owner precondition: direct calls supply a source id, and a completed MCP trace runs in a session that can retain it. --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com>
This commit is contained in:
1 parent
25b62db838
commit
52bca6de41
22 files changed
+403
-37
No files matched your search
@@ -434,6 +434,13 @@ nodes and edges. Node payloads are bounded to 8 MiB and serialized results to
|
||||
32 MiB. Pagination returns edges whose source is on the node page and preserves
|
||||
stable IDs for endpoints outside that page.
|
||||
|
||||
Procedure, node and edge `evidence_id` values identify the source function
|
||||
observations used by the trace. In an MCP session, retrieve them from the
|
||||
Evidence bundle for inspection or export. A replayed source keeps the original
|
||||
observation's identity; the trace also reports that replay occurred. Older
|
||||
cached value traces are not replayed under the revised workflow profile, while
|
||||
their historical Evidence remains available.
|
||||
|
||||
The one-shot CLI accepts `--evidence_output <new-path>` to save the trace and
|
||||
the transitive source Evidence closure named by its links. The destination is
|
||||
never overwritten. `--detail summary` requires `--evidence_output`; the bundle
|
||||
|
||||
@@ -215,6 +215,19 @@ async function verifyNativeValueAdapters(
|
||||
);
|
||||
assert.deepEqual(mcpEvidence.normalized_result, mcpGraph);
|
||||
assert.deepEqual(mcpGraph, cliGraph);
|
||||
const warmReply = await client.callTool(
|
||||
{
|
||||
name: "trace_native_values",
|
||||
arguments: { procedure, max_depth: 2, max_functions: 8, limit: 3 },
|
||||
},
|
||||
{ timeout: 180000 },
|
||||
);
|
||||
const warmEvidence = parseEvidence(
|
||||
requireMcpOutput(warmReply, "trace_native_values"),
|
||||
);
|
||||
const warmGraph = nativeValueTraceSchema.parse(
|
||||
warmEvidence.normalized_result,
|
||||
);
|
||||
const analyzed = await client.callTool(
|
||||
{
|
||||
name: "analyze_function",
|
||||
@@ -276,6 +289,25 @@ async function verifyNativeValueAdapters(
|
||||
"MCP omitted structured Evidence bundle",
|
||||
);
|
||||
const bundle = requireMcpEvidenceBundle(bundleReply);
|
||||
for (const [evidence, graph] of [
|
||||
[mcpEvidence, mcpGraph],
|
||||
[warmEvidence, warmGraph],
|
||||
]) {
|
||||
for (const source of [
|
||||
...graph.procedures,
|
||||
...graph.nodes,
|
||||
...graph.edges,
|
||||
]) {
|
||||
assert.ok(evidence.evidence_links.includes(source.evidence_id));
|
||||
assert.equal(
|
||||
bundle.records.find(
|
||||
(record) => record.evidence_id === source.evidence_id,
|
||||
)?.operation,
|
||||
"analyze_function",
|
||||
`Missing source Evidence ${source.evidence_id}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
for (const evidence of [
|
||||
mcpEvidence,
|
||||
functionEvidence,
|
||||
|
||||
@@ -504,6 +504,9 @@ export async function verifyGhidraBoundaries(
|
||||
// Prime both inventories before editing so stale names cannot hide in caches.
|
||||
await call("list_names");
|
||||
await call("list_procedures");
|
||||
const traceInput = { procedure: address, max_depth: 0, max_functions: 1 };
|
||||
const traceBefore = await evidence("trace_native_values", traceInput);
|
||||
const sourceBefore = traceBefore.normalized_result.procedures[0].evidence_id;
|
||||
const changes = {
|
||||
procedure: address,
|
||||
name: "dead",
|
||||
@@ -517,6 +520,16 @@ export async function verifyGhidraBoundaries(
|
||||
comment: changes.comment,
|
||||
inline_comment: changes.inline_comment,
|
||||
});
|
||||
const traceAfter = await evidence("trace_native_values", traceInput);
|
||||
const sourceAfter = traceAfter.normalized_result.procedures[0].evidence_id;
|
||||
assert.notEqual(sourceBefore, sourceAfter);
|
||||
const sourceProcedure = (evidenceId) =>
|
||||
call("inspect_analysis_view", {
|
||||
source: { kind: "retained-evidence", evidence_id: evidenceId },
|
||||
view: { kind: "native", facet: "procedure", offset: 0, limit: 1 },
|
||||
});
|
||||
assert.equal((await sourceProcedure(sourceBefore)).item.name, name);
|
||||
assert.equal((await sourceProcedure(sourceAfter)).item.name, changes.name);
|
||||
const regexPattern = "^(a|aa)*b$";
|
||||
const regexFailure = await client.callTool(
|
||||
{
|
||||
|
||||
@@ -511,6 +511,30 @@ const verifyKuna = async (
|
||||
references,
|
||||
});
|
||||
}
|
||||
if (functions.length > 0) {
|
||||
const trace = await cold.call("trace_native_values", {
|
||||
procedure: functions[0][1],
|
||||
max_depth: 0,
|
||||
max_functions: 1,
|
||||
});
|
||||
const bundle = await cold.call("get_evidence_bundle", {
|
||||
detail: "complete",
|
||||
});
|
||||
for (const source of [
|
||||
...trace.value.procedures,
|
||||
...trace.value.nodes,
|
||||
...trace.value.edges,
|
||||
]) {
|
||||
assert.ok(trace.structured.evidence_links.includes(source.evidence_id));
|
||||
assert.equal(
|
||||
bundle.value.records.find(
|
||||
(record) => record.evidence_id === source.evidence_id,
|
||||
)?.operation,
|
||||
"analyze_function",
|
||||
`Missing source Evidence ${source.evidence_id}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (cause) {
|
||||
coldFailure = cause;
|
||||
} finally {
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import type { RetainedResourceScope } from "../application/AnalysisProvider.js";
|
||||
import type {
|
||||
RetainedResourceScope,
|
||||
ProviderAvailability,
|
||||
} from "../application/AnalysisProvider.js";
|
||||
import type {
|
||||
DoctorProviderCheck,
|
||||
DoctorProviderInspection,
|
||||
} from "../application/Doctor.js";
|
||||
import type { ProviderAvailability } from "../application/AnalysisProvider.js";
|
||||
import type { JsonValue } from "../domain/jsonValue.js";
|
||||
import {
|
||||
inspectJadxAvailability,
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
import {
|
||||
createAnalysisExecution,
|
||||
type AnalysisOperationPort,
|
||||
type AnalysisExecution,
|
||||
} from "./AnalysisProvider.js";
|
||||
import {
|
||||
AnalysisCapabilityUnavailableError,
|
||||
@@ -21,7 +22,6 @@ import {
|
||||
primaryAnalysisError,
|
||||
type AnalysisError,
|
||||
} from "../domain/analysisErrorBase.js";
|
||||
import type { AnalysisExecution } from "./AnalysisProvider.js";
|
||||
import type { Result } from "../domain/result.js";
|
||||
import type { JsonValue } from "../domain/jsonValue.js";
|
||||
import { err, ok } from "../domain/result.js";
|
||||
|
||||
@@ -34,6 +34,8 @@ export interface ExecutionOptions {
|
||||
readonly snapshotContext?: "one-shot-target";
|
||||
readonly signal?: AbortSignal;
|
||||
readonly progress?: ProgressReporter;
|
||||
/** The composed workflow will cite this observation and needs its Evidence. */
|
||||
readonly retainEvidence?: true;
|
||||
/**
|
||||
* Set when a procedure selector came from provider output, such as a
|
||||
* revisited callee, rather than from the caller. A provider-derived
|
||||
|
||||
@@ -21,5 +21,7 @@ export const workflowAnalysisProfile = (
|
||||
? { workflow_revision: 4 }
|
||||
: operation === "get_call_graph"
|
||||
? { workflow_revision: 3 }
|
||||
: { workflow_revision: 1 }),
|
||||
: operation === "trace_native_values"
|
||||
? { workflow_revision: 2 }
|
||||
: { workflow_revision: 1 }),
|
||||
});
|
||||
@@ -111,6 +111,99 @@ it.each([false, true])(
|
||||
|
||||
const provider = { id: "fixture", name: "Fixture", version: "1" };
|
||||
|
||||
it("retains requested live observations and links their original producer sources", async () => {
|
||||
const upstream = createEvidence(undefined, provider, {
|
||||
operation: "health",
|
||||
parameters: {},
|
||||
result: "source",
|
||||
});
|
||||
const result = await executeComposedWorkflow(
|
||||
{
|
||||
execute: async () =>
|
||||
ok(
|
||||
createAnalysisExecution("live observation", provider, {
|
||||
cachePolicy: "live",
|
||||
sourceEvidence: [upstream],
|
||||
}),
|
||||
),
|
||||
},
|
||||
async (observed) => {
|
||||
const value = await observed.execute(
|
||||
"analyze_function",
|
||||
{ procedure: "0x1" },
|
||||
{
|
||||
retainEvidence: true,
|
||||
},
|
||||
);
|
||||
return value.ok ? ok({ source: value.value.evidenceId ?? null }) : value;
|
||||
},
|
||||
);
|
||||
if (!result.ok) throw result.error;
|
||||
const source = result.sourceEvidence?.find(
|
||||
(record) => record.operation === "analyze_function",
|
||||
);
|
||||
expect(source).toBeDefined();
|
||||
expect(source?.evidence_links).toEqual([upstream.evidence_id]);
|
||||
expect(result.value.result).toEqual({ source: source?.evidence_id });
|
||||
expect(result.value.evidenceLinks).toContain(source?.evidence_id);
|
||||
expect(result.value.cachePolicy).toBe("live");
|
||||
});
|
||||
|
||||
it("keeps an existing session-issued source identity on replay", async () => {
|
||||
const original = createEvidence(undefined, provider, {
|
||||
operation: "analyze_function",
|
||||
parameters: { procedure: "0x1" },
|
||||
result: "original",
|
||||
});
|
||||
const result = await executeComposedWorkflow(
|
||||
{
|
||||
execute: async () =>
|
||||
ok({
|
||||
...createAnalysisExecution("original", provider, {
|
||||
limitations: ["replayed"],
|
||||
}),
|
||||
evidenceId: original.evidence_id,
|
||||
}),
|
||||
},
|
||||
async (observed) => {
|
||||
const value = await observed.execute(
|
||||
"analyze_function",
|
||||
{ procedure: "0x1" },
|
||||
{
|
||||
retainEvidence: true,
|
||||
},
|
||||
);
|
||||
return value.ok ? ok({ source: value.value.evidenceId ?? null }) : value;
|
||||
},
|
||||
);
|
||||
if (!result.ok) throw result.error;
|
||||
expect(result.value.result).toEqual({ source: original.evidence_id });
|
||||
expect(result.value.evidenceLinks).toEqual([original.evidence_id]);
|
||||
expect(result.sourceEvidence).toBeUndefined();
|
||||
});
|
||||
|
||||
it("retains a completed requested source if the composed workflow is cancelled", async () => {
|
||||
const result = await executeComposedWorkflow(
|
||||
{
|
||||
execute: async () => ok(createAnalysisExecution("observed", provider)),
|
||||
},
|
||||
async (observed) => {
|
||||
await observed.execute(
|
||||
"analyze_function",
|
||||
{ procedure: "0x1" },
|
||||
{
|
||||
retainEvidence: true,
|
||||
},
|
||||
);
|
||||
return err(new AnalysisCancelledError("trace_native_values"));
|
||||
},
|
||||
);
|
||||
expect(result.ok).toBe(false);
|
||||
expect(result.sourceEvidence?.map((record) => record.operation)).toEqual([
|
||||
"analyze_function",
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps Evidence identity stable when dependent branches finish in opposite orders", async () => {
|
||||
const run = async (order: readonly number[]) => {
|
||||
const gate = () => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { AnalysisWorkScope } from "./AnalysisWorkScope.js";
|
||||
import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
|
||||
import { jsonStringByteLength } from "../domain/jsonStringByteLength.js";
|
||||
import { createAnalysisExecutionEvidence } from "../domain/analysisObservation.js";
|
||||
import { REA_WORKFLOW_PROVIDER } from "../domain/investigationProviders.js";
|
||||
import { workflowAnalysisProfile } from "./WorkflowAnalysisProfile.js";
|
||||
import type {
|
||||
@@ -101,12 +102,10 @@ export const executeComposedWorkflow = async (
|
||||
const observed: AnalysisOperationPort = {
|
||||
resourceScope: analysis.resourceScope,
|
||||
execute: async (operation, parameters, options) => {
|
||||
const execution = await analysis.execute(operation, parameters, {
|
||||
let execution = await analysis.execute(operation, parameters, {
|
||||
...options,
|
||||
workScope,
|
||||
});
|
||||
if (execution.ok && execution.value.evidenceId !== undefined)
|
||||
evidenceLinks.add(execution.value.evidenceId);
|
||||
if (execution.ok && execution.value.sourceEvidence !== undefined) {
|
||||
const recorded = recordEvidenceSources(
|
||||
(source) => sourceLedger.record(source),
|
||||
@@ -114,6 +113,26 @@ export const executeComposedWorkflow = async (
|
||||
);
|
||||
if (!recorded.ok) return recorded;
|
||||
}
|
||||
if (
|
||||
execution.ok &&
|
||||
options?.retainEvidence === true &&
|
||||
execution.value.evidenceId === undefined
|
||||
) {
|
||||
const source = createAnalysisExecutionEvidence(
|
||||
operation,
|
||||
parameters,
|
||||
execution.value,
|
||||
"observed",
|
||||
);
|
||||
const recorded = recordEvidenceSources(
|
||||
(evidence) => sourceLedger.record(evidence),
|
||||
[source],
|
||||
);
|
||||
if (!recorded.ok) return recorded;
|
||||
execution = ok({ ...execution.value, evidenceId: source.evidence_id });
|
||||
}
|
||||
if (execution.ok && execution.value.evidenceId !== undefined)
|
||||
evidenceLinks.add(execution.value.evidenceId);
|
||||
if (execution.ok && execution.value.cachePolicy === "live")
|
||||
cachePolicy = "live";
|
||||
if (execution.ok)
|
||||
|
||||
@@ -1182,6 +1182,71 @@ it("does not replay pre-envelope call graphs under the revised workflow profile"
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("keeps older value-trace Evidence as history without replaying its source IDs", () => {
|
||||
const cache = new AnalysisSnapshotCache();
|
||||
const target = ANALYSIS_SNAPSHOT_TARGET;
|
||||
const profile = ANALYSIS_SNAPSHOT_PROFILE;
|
||||
const operation = "trace_native_values" as const;
|
||||
const parameters = { procedure: "0x1000" };
|
||||
const oldProfile = workflowAnalysisProfile(profile);
|
||||
const oldResult = {
|
||||
seed: "0x1000",
|
||||
target_sha256: target.sha256,
|
||||
nodes: [],
|
||||
edges: [],
|
||||
procedures: [
|
||||
{
|
||||
address: "0x1000",
|
||||
depth: 0,
|
||||
evidence_id: `ev_${"a".repeat(64)}`,
|
||||
provider: profile.provider.id,
|
||||
analysis_profile_digest: profile.digest,
|
||||
},
|
||||
],
|
||||
unknowns: [],
|
||||
total_nodes: 0,
|
||||
total_edges: 0,
|
||||
decompilations: 1,
|
||||
next_offset: null,
|
||||
page_truncated: false,
|
||||
traversal: { status: "complete" },
|
||||
truncated: false,
|
||||
limitations: [],
|
||||
};
|
||||
const execution = {
|
||||
...createAnalysisExecution(oldResult, REA_WORKFLOW_PROVIDER, {
|
||||
subject: target,
|
||||
}),
|
||||
analysisProfile: oldProfile,
|
||||
};
|
||||
cache.recordWorkflow({ target, profile, operation, parameters, execution });
|
||||
const evidence = createEvidence(target, REA_WORKFLOW_PROVIDER, {
|
||||
operation,
|
||||
parameters,
|
||||
result: oldResult,
|
||||
confidence: "derived",
|
||||
analysisProfile: oldProfile,
|
||||
});
|
||||
const exported = cache.export(
|
||||
target,
|
||||
profile,
|
||||
createEvidenceBundle([evidence]),
|
||||
);
|
||||
if (!exported.ok) throw exported.error;
|
||||
const snapshot = parseAnalysisSnapshot(exported.value);
|
||||
expect(snapshot.evidence_bundle.records).toContainEqual(evidence);
|
||||
expect(
|
||||
snapshotEvidenceForQuery(snapshot, {
|
||||
target,
|
||||
bindingProfile: profile,
|
||||
operation,
|
||||
parameters,
|
||||
provider: REA_WORKFLOW_PROVIDER,
|
||||
evidenceProfile: workflowAnalysisProfile(profile, operation),
|
||||
}),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it("declines source-bearing snapshot entries while replaying omitted and empty source controls", () => {
|
||||
const cache = new AnalysisSnapshotCache();
|
||||
const target = ANALYSIS_SNAPSHOT_TARGET;
|
||||
|
||||
@@ -12,8 +12,8 @@ import {
|
||||
snapshotMatchesTarget,
|
||||
snapshotTarget,
|
||||
type SnapshotBindingMismatch,
|
||||
type AnalysisSnapshot,
|
||||
} from "../../domain/analysisSnapshot.js";
|
||||
import type { AnalysisSnapshot } from "../../domain/analysisSnapshot.js";
|
||||
import { parseBinaryTarget } from "../BinaryTargetResolver.js";
|
||||
import type { ArtifactResourceScope } from "../../artifacts/ArtifactResourceScope.js";
|
||||
import {
|
||||
|
||||
@@ -10,7 +10,10 @@ import {
|
||||
} from "../../contracts/native/functionMatchingContracts.js";
|
||||
import { createEvidence, type Evidence } from "../../domain/evidence.js";
|
||||
import { readFunctionInventory } from "../../domain/native/functionInventory.js";
|
||||
import { nativeFunctionMatchingSchema } from "../../domain/native/functionMatching.js";
|
||||
import {
|
||||
nativeFunctionMatchingSchema,
|
||||
NATIVE_FUNCTION_MATCHING_PREDICATE,
|
||||
} from "../../domain/native/functionMatching.js";
|
||||
import { AnalysisError } from "../../domain/analysisErrorBase.js";
|
||||
import { AnalysisCancelledError } from "../../domain/analysisErrorCore.js";
|
||||
import { analysisInputErrorFromIssues } from "../../domain/inputIssueProjection.js";
|
||||
@@ -20,7 +23,6 @@ import { resolveNativeMatchingSources } from "./NativeMatchingSources.js";
|
||||
import type { ExecutionOptions } from "../AnalysisProvider.js";
|
||||
import type { NativeMatchingAllowance } from "../../config/nativeMatchingAllowance.js";
|
||||
import { FUNCTION_COMPARISON_PROVIDER } from "../../domain/comparisonEvidenceIdentity.js";
|
||||
import { NATIVE_FUNCTION_MATCHING_PREDICATE } from "../../domain/native/functionMatching.js";
|
||||
|
||||
const OPERATION = "match_native_functions";
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createAnalysisExecutionEvidence } from "../../domain/analysisObservation.js";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { traceNativeValues } from "./NativeValueTrace.js";
|
||||
import { traceNativeValues as traceNativeValuesCore } from "./NativeValueTrace.js";
|
||||
import { executeComposedWorkflow } from "../WorkflowEvidence.js";
|
||||
import {
|
||||
createAnalysisExecution,
|
||||
type AnalysisOperationPort,
|
||||
@@ -26,6 +27,27 @@ import {
|
||||
valueTraceProvider as provider,
|
||||
valueTraceSubject as subject,
|
||||
} from "./NativeValueTrace.fixture.js";
|
||||
|
||||
const traceNativeValues: typeof traceNativeValuesCore = async (
|
||||
source,
|
||||
parameters,
|
||||
signal,
|
||||
) => {
|
||||
const result = await executeComposedWorkflow(source, (observed) =>
|
||||
traceNativeValuesCore(observed, parameters, signal),
|
||||
);
|
||||
return result.ok ? ok(result.value.result) : result;
|
||||
};
|
||||
|
||||
it("does not return a source ID without an Evidence owner", async () => {
|
||||
const result = await traceNativeValuesCore(valueTraceDiamond().port, {
|
||||
procedure: "0x1000",
|
||||
});
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok)
|
||||
expect(result.error._tag).toBe("AnalysisCapabilityUnavailableError");
|
||||
});
|
||||
|
||||
const analysis: AnalysisOperationPort = {
|
||||
execute: async (operation, parameters) => {
|
||||
if (operation === "resolve_native_call_targets")
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
import { createAnalysisExecutionEvidence } from "../../domain/analysisObservation.js";
|
||||
import {
|
||||
AnalysisCancelledError,
|
||||
AnalysisCapabilityUnavailableError,
|
||||
AnalysisOutputError,
|
||||
} from "../../domain/analysisErrorCore.js";
|
||||
import { err, ok } from "../../domain/result.js";
|
||||
@@ -116,6 +117,7 @@ export const traceNativeValues = async (
|
||||
{
|
||||
...(signal === undefined ? {} : { signal }),
|
||||
...(next.depth > 0 ? { selectorOrigin: "provider" as const } : {}),
|
||||
retainEvidence: true,
|
||||
},
|
||||
);
|
||||
decompilations++;
|
||||
@@ -129,12 +131,13 @@ export const traceNativeValues = async (
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const evidence = createAnalysisExecutionEvidence(
|
||||
"analyze_function",
|
||||
{ procedure: next.procedure },
|
||||
execution.value,
|
||||
"observed",
|
||||
);
|
||||
const partialEvidence = () =>
|
||||
createAnalysisExecutionEvidence(
|
||||
"analyze_function",
|
||||
{ procedure: next.procedure },
|
||||
execution.value,
|
||||
"observed",
|
||||
);
|
||||
const parsed = functionDossierSchema.safeParse(execution.value.result);
|
||||
if (!parsed.success || execution.value.subject == null)
|
||||
return err(
|
||||
@@ -142,7 +145,7 @@ export const traceNativeValues = async (
|
||||
"trace_native_values",
|
||||
"Function dossier lacks validated facts or artifact identity",
|
||||
parsed.success ? undefined : { cause: parsed.error },
|
||||
).retainPartialObservation(evidence),
|
||||
).retainPartialObservation(partialEvidence()),
|
||||
);
|
||||
const dossier = parsed.data;
|
||||
const address = dossier.procedure.address;
|
||||
@@ -155,9 +158,17 @@ export const traceNativeValues = async (
|
||||
new AnalysisOutputError(
|
||||
"trace_native_values",
|
||||
"Cross-function target digest mismatch",
|
||||
).retainPartialObservation(evidence),
|
||||
).retainPartialObservation(partialEvidence()),
|
||||
);
|
||||
const evidenceId = execution.value.evidenceId;
|
||||
if (evidenceId === undefined)
|
||||
return err(
|
||||
new AnalysisCapabilityUnavailableError(
|
||||
"rea",
|
||||
"trace_native_values",
|
||||
"The function observation has no Evidence retention owner.",
|
||||
).retainPartialObservation(partialEvidence()),
|
||||
);
|
||||
const evidenceId = evidence.evidence_id;
|
||||
procedures.push({
|
||||
address,
|
||||
depth: next.depth,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { AnalysisOperationPort } from "../AnalysisProvider.js";
|
||||
import { ok } from "../../domain/result.js";
|
||||
import {
|
||||
AnalysisCancelledError,
|
||||
AnalysisCapabilityUnavailableError,
|
||||
@@ -34,6 +35,21 @@ describe("native value trace child stops", () => {
|
||||
0,
|
||||
VALUE_TRACE_DIAMOND_READS.indexOf(read) + 1,
|
||||
);
|
||||
// The trace cites each completed dossier. These cases cover stop behavior,
|
||||
// so the fixture only has to satisfy that precondition.
|
||||
const withSourceEvidence = (
|
||||
port: AnalysisOperationPort,
|
||||
): AnalysisOperationPort => ({
|
||||
execute: async (operation, parameters, options) => {
|
||||
const execution = await port.execute(operation, parameters, options);
|
||||
if (!execution.ok || execution.value.evidenceId !== undefined)
|
||||
return execution;
|
||||
return ok({
|
||||
...execution.value,
|
||||
evidenceId: `ev_${"ab".repeat(32)}`,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
it.each(childReads)(
|
||||
"returns a cancelled %s read without a partial trace",
|
||||
@@ -43,7 +59,7 @@ describe("native value trace child stops", () => {
|
||||
[read]: cancelled,
|
||||
});
|
||||
|
||||
const result = await traceNativeValues(port, {
|
||||
const result = await traceNativeValues(withSourceEvidence(port), {
|
||||
procedure: "0x1000",
|
||||
max_depth: 1,
|
||||
});
|
||||
@@ -86,7 +102,7 @@ describe("native value trace child stops", () => {
|
||||
};
|
||||
|
||||
const result = await traceNativeValues(
|
||||
aborting,
|
||||
withSourceEvidence(aborting),
|
||||
{ procedure: "0x1000", max_depth: 1 },
|
||||
controller.signal,
|
||||
);
|
||||
@@ -114,7 +130,7 @@ describe("native value trace child stops", () => {
|
||||
"resolve_native_call_targets:0x1008": failure,
|
||||
});
|
||||
|
||||
const result = await traceNativeValues(port, {
|
||||
const result = await traceNativeValues(withSourceEvidence(port), {
|
||||
procedure: "0x1000",
|
||||
max_depth: 1,
|
||||
});
|
||||
|
||||
@@ -10,8 +10,8 @@ import {
|
||||
NODE_CAPTURE_HEAP_ENFORCED,
|
||||
captureDecoderProcessMemoryLimitBytes,
|
||||
} from "./CaptureRelease.js";
|
||||
import { QLOG_CAPTURE_PROVIDER_IDENTITY } from "./CaptureProviderIdentities.js";
|
||||
import {
|
||||
QLOG_CAPTURE_PROVIDER_IDENTITY,
|
||||
HAR_CAPTURE_PROVIDER_IDENTITY,
|
||||
SAZ_CAPTURE_PROVIDER_IDENTITY,
|
||||
} from "./CaptureProviderIdentities.js";
|
||||
|
||||
@@ -440,14 +440,14 @@ describe("enhanced MCP tools", () => {
|
||||
]),
|
||||
},
|
||||
});
|
||||
// A fixed adapter has no session to retain source Evidence for the trace.
|
||||
expect(results[13]).toMatchObject({
|
||||
target_sha256: "a".repeat(64),
|
||||
decompilations: 1,
|
||||
unknowns: [
|
||||
expect.objectContaining({
|
||||
reason: "Provider has no native def-use model",
|
||||
}),
|
||||
],
|
||||
error: {
|
||||
code: "capability_unavailable",
|
||||
details: {
|
||||
operation: "trace_native_values",
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(results[14]).toMatchObject({
|
||||
counts: { selected: 6, written: 6, failed: 0, excluded: 0 },
|
||||
|
||||
@@ -159,10 +159,15 @@ describe("trace_native_values after a validated seed", () => {
|
||||
"keeps an ordinary failure from $read as a located unknown and continues",
|
||||
async ({ read, stop, ordinaryReads }) => {
|
||||
const operation = read.split(":")[0] ?? "";
|
||||
const { port, reads } = failingDiamond({
|
||||
[read]: childFailure(operation, false),
|
||||
});
|
||||
const client = await connect(port);
|
||||
// A completed trace cites source Evidence, so continuation needs a
|
||||
// session that can retain it. A fixed adapter still refuses that result.
|
||||
const { port, reads } = failingDiamond(
|
||||
{
|
||||
[read]: childFailure(operation, false),
|
||||
},
|
||||
{ sessionSubject: true },
|
||||
);
|
||||
const { client } = await openSession(port);
|
||||
|
||||
const response = await client.callTool(trace);
|
||||
|
||||
|
||||
@@ -2,9 +2,17 @@ import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
|
||||
import { afterEach, expect, it } from "vitest";
|
||||
import { valueTraceDiamond } from "../../../src/application/native/NativeValueTrace.fixture.js";
|
||||
import { parseEvidence } from "../../../src/domain/evidence.js";
|
||||
import { evidenceBundleSchema } from "../../../src/domain/evidenceBundle.js";
|
||||
import { z } from "zod";
|
||||
import { nativeValueTraceSchema } from "../../../src/domain/native/nativeValueTrace.js";
|
||||
import { advertisedMcpToolSchemas } from "../../fixtures/advertisedMcpTool.js";
|
||||
import { createServer } from "../../../src/server/createServer.js";
|
||||
import { ok } from "../../../src/domain/result.js";
|
||||
import {
|
||||
createBinarySessionTargets,
|
||||
createTestBinarySession,
|
||||
} from "../../fixtures/binarySession.js";
|
||||
import { observed } from "../../fixtures/analysisExecution.js";
|
||||
import {
|
||||
closeEnhancedToolResources,
|
||||
resources,
|
||||
@@ -15,8 +23,19 @@ afterEach(closeEnhancedToolResources);
|
||||
it.each(["full", "compact"] as const)(
|
||||
"separates page omission from named traversal budgets through the advertised %s MCP contract",
|
||||
async (profile) => {
|
||||
const [target] = await createBinarySessionTargets();
|
||||
const analysis = valueTraceDiamond({ sessionSubject: true }).port;
|
||||
const session = createTestBinarySession(() => ({
|
||||
execute: (operation, parameters, options) =>
|
||||
operation === "health"
|
||||
? Promise.resolve(observed(null))
|
||||
: analysis.execute(operation, parameters, options),
|
||||
close: async () => ok(null),
|
||||
}));
|
||||
const opened = await session.open(target);
|
||||
expect(opened.ok).toBe(true);
|
||||
const server = createServer(
|
||||
{ kind: "fixed", analysis: valueTraceDiamond().port },
|
||||
{ kind: "session", session },
|
||||
{ environment: { REA_MCP_OUTPUT_SCHEMA_PROFILE: profile } },
|
||||
);
|
||||
const client = new Client({ name: "value-trace-test", version: "1.0.0" });
|
||||
@@ -59,6 +78,24 @@ it.each(["full", "compact"] as const)(
|
||||
traversal: { status: "complete" },
|
||||
truncated: false,
|
||||
});
|
||||
const bundleReply = await client.callTool({
|
||||
name: "get_evidence_bundle",
|
||||
arguments: { detail: "complete" },
|
||||
});
|
||||
expect(bundleReply.isError).not.toBe(true);
|
||||
const bundle = z
|
||||
.object({ result: evidenceBundleSchema })
|
||||
.parse(bundleReply.structuredContent).result;
|
||||
for (const source of [
|
||||
...complete.procedures,
|
||||
...complete.nodes,
|
||||
...complete.edges,
|
||||
])
|
||||
expect(
|
||||
bundle.records.some(
|
||||
(record) => record.evidence_id === source.evidence_id,
|
||||
),
|
||||
).toBe(true);
|
||||
const pages = [];
|
||||
for (let offset: number | null = 0; offset !== null;) {
|
||||
const page = await trace({ ...request, offset, limit: 4 });
|
||||
|
||||
@@ -3,10 +3,24 @@ import { AnalysisInputError } from "../../../src/domain/analysisErrorCore.js";
|
||||
import { NoBinaryOpenError } from "../../../src/domain/configurationErrors.js";
|
||||
import { err } from "../../../src/domain/result.js";
|
||||
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
|
||||
import { valueTraceDiamond } from "../../../src/application/native/NativeValueTrace.fixture.js";
|
||||
import { closeEnhancedToolResources, connect } from "./enhancedToolsHarness.js";
|
||||
|
||||
afterEach(closeEnhancedToolResources);
|
||||
|
||||
it("refuses a completed trace when the adapter has no source Evidence owner", async () => {
|
||||
const client = await connect(valueTraceDiamond().port);
|
||||
const result = await client.callTool({
|
||||
name: "trace_native_values",
|
||||
arguments: { procedure: "0x1000" },
|
||||
});
|
||||
expect(result.isError).toBe(true);
|
||||
expect(parseMcpToolError(result).error).toMatchObject({
|
||||
code: "capability_unavailable",
|
||||
message: expect.stringContaining("source Evidence"),
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
failure: new AnalysisInputError("analyze_function", {
|
||||
|
||||
@@ -95,7 +95,7 @@ const vendor = (
|
||||
return { fetch };
|
||||
};
|
||||
const failed = (status: number, cancel?: () => void) => () =>
|
||||
new Response(new ReadableStream({ ...(cancel ? { cancel } : {}) }), {
|
||||
new Response(new ReadableStream(cancel ? { cancel } : {}), {
|
||||
status,
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user