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:
morlutoandCursor Agent authored and GitHub committed 2026-11-05 00:19:50 +08:00
1 parent 25b62db838
commit 52bca6de41
22 files changed
+403 -37

No files matched your search

+7
View File
@@ -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
+32
View File
@@ -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,
+13
View File
@@ -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(
{
+24
View File
@@ -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 {
+4 -2
View File
@@ -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";
+2
View File
@@ -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
+3 -1
View File
@@ -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 }),
});
+93
View File
@@ -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 = () => {
+22 -3
View File
@@ -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;
+1 -1
View File
@@ -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")
+20 -9
View File
@@ -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,
});
+1 -1
View File
@@ -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,
});