mirror of
https://github.com/morluto/rea.git
synced 2026-11-03 05:16:16 +00:00
fix!: preserve feature traces when owner lookup is unavailable (#8368)
* fix!: preserve feature traces when owner lookup is unavailable * test: strengthen feature trace cancellation assertions * fix: preserve feature trace provider readiness errors * test: verify all feature trace reference sources * test: verify IDA guidance at its owning boundary * style: format feature trace regression
This commit is contained in:
1 parent
f72638f6b1
commit
6e9ee97a69
16 files changed
+440
-37
No files matched your search
@@ -210,8 +210,13 @@ unresolved relationship reads. Cancellation remains a tool error.
|
||||
|
||||
`trace_feature` validates each reference's containing-procedure resolution using
|
||||
the same contract as navigation context. Explicit absence remains a successful
|
||||
negative observation; malformed provider replies return `unreadable_output`
|
||||
for `resolve_containing_procedure` before successful-result projection.
|
||||
negative observation. If the provider reports a non-retryable capability refusal
|
||||
attributed to the owner lookup without incomplete cleanup, the reference remains visible
|
||||
with `containing_procedure.status: "unavailable"` and a residual unknown. That
|
||||
unknown does not imply references were truncated.
|
||||
Malformed provider replies return `unreadable_output` for
|
||||
`resolve_containing_procedure` before successful-result projection;
|
||||
cancellation and incomplete cleanup remain errors.
|
||||
|
||||
REA prepares complete MCP results within the pinned stdio client's 10 MiB
|
||||
receive-buffer budget, including both text and structured representations and
|
||||
|
||||
@@ -21,6 +21,8 @@ export const verifyKunaReferences = async ({
|
||||
address,
|
||||
expectedCaller,
|
||||
nativeIncoming,
|
||||
verifyNamedTrace = false,
|
||||
minimumTraceReferences = 1,
|
||||
}) => {
|
||||
const before = await Promise.all([
|
||||
entries(join(cacheRoot, "projects")),
|
||||
@@ -80,7 +82,44 @@ export const verifyKunaReferences = async ({
|
||||
direction: "forward",
|
||||
});
|
||||
assert.equal(path.value.goal_status, "reached");
|
||||
composed = { graph_ms: graph.ms, path_ms: path.ms };
|
||||
let traceMs;
|
||||
if (verifyNamedTrace) {
|
||||
const trace = await call("trace_feature", { query: name });
|
||||
traceMs = trace.ms;
|
||||
assert.ok(
|
||||
trace.value.matches.some(
|
||||
(match) => match.type === "procedure" && match.address === address,
|
||||
),
|
||||
`${name} was not retained in its feature trace`,
|
||||
);
|
||||
assert.ok(
|
||||
incoming.value.length >= minimumTraceReferences,
|
||||
`${name} fixture did not produce the expected incoming references`,
|
||||
);
|
||||
const references = trace.value.references.filter(
|
||||
(row) => row.target_address === address,
|
||||
);
|
||||
assert.deepEqual(
|
||||
references.map((row) => row.source_address).sort(),
|
||||
[...incoming.value].sort(),
|
||||
`${name} lost or duplicated an observed incoming reference`,
|
||||
);
|
||||
for (const reference of references) {
|
||||
assert.equal(reference.containing_procedure.status, "unavailable");
|
||||
assert.equal(reference.containing_procedure.provider_id, "kuna");
|
||||
assert.ok(
|
||||
trace.value.residual_unknowns.some((unknown) =>
|
||||
unknown.includes(reference.source_address),
|
||||
),
|
||||
);
|
||||
}
|
||||
assert.equal(trace.value.truncated, false);
|
||||
}
|
||||
composed = {
|
||||
graph_ms: graph.ms,
|
||||
path_ms: path.ms,
|
||||
...(traceMs === undefined ? {} : { feature_trace_ms: traceMs }),
|
||||
};
|
||||
}
|
||||
assert.deepEqual(
|
||||
await Promise.all([
|
||||
|
||||
@@ -115,7 +115,9 @@ __attribute__((noinline)) int rea_classify(int c) {
|
||||
}
|
||||
int main(int argc, char **argv) {
|
||||
if (argc < 2) return 2;
|
||||
printf("%u %d\n", rea_checksum((const unsigned char *)argv[1], strlen(argv[1])),
|
||||
size_t length = strlen(argv[1]);
|
||||
printf("%u %u %d\n", rea_checksum((const unsigned char *)argv[1], length),
|
||||
rea_checksum((const unsigned char *)argv[1], length ? length - 1 : 0),
|
||||
rea_classify(argv[1][0]));
|
||||
return 0;
|
||||
}
|
||||
@@ -282,8 +284,12 @@ const quality = (text) => ({
|
||||
const verifyKuna = async (
|
||||
path,
|
||||
functions,
|
||||
expectedCaller,
|
||||
expectedStrings = [],
|
||||
{
|
||||
expectedCaller,
|
||||
expectedStrings = [],
|
||||
verifyNamedTrace = false,
|
||||
minimumTraceReferences = {},
|
||||
} = {},
|
||||
) => {
|
||||
const sha256 = createHash("sha256")
|
||||
.update(await readFile(path))
|
||||
@@ -320,6 +326,8 @@ const verifyKuna = async (
|
||||
name,
|
||||
address,
|
||||
expectedCaller,
|
||||
verifyNamedTrace,
|
||||
minimumTraceReferences: minimumTraceReferences[name],
|
||||
nativeIncoming: (targetAddress) =>
|
||||
nativeKunaIncomingSites({
|
||||
executable: kuna,
|
||||
@@ -549,10 +557,12 @@ try {
|
||||
for (const binary of [built, stripped])
|
||||
results.push({
|
||||
binary: binary.slice(workspace.length + 1),
|
||||
kuna: await verifyKuna(binary, [...fixtureFunctions], fixtureMain, [
|
||||
"_φ( °-°)/ REA unicode marker",
|
||||
"REA wide marker",
|
||||
]),
|
||||
kuna: await verifyKuna(binary, [...fixtureFunctions], {
|
||||
expectedCaller: fixtureMain,
|
||||
expectedStrings: ["_φ( °-°)/ REA unicode marker", "REA wide marker"],
|
||||
verifyNamedTrace: binary === built,
|
||||
minimumTraceReferences: { rea_checksum: 2 },
|
||||
}),
|
||||
});
|
||||
|
||||
// Caller-built matrix.
|
||||
|
||||
@@ -776,6 +776,23 @@ describe("trace_feature requirements", () => {
|
||||
reason: null,
|
||||
}));
|
||||
|
||||
it("remains available when owner lookup is unavailable", () => {
|
||||
expect(
|
||||
entry(
|
||||
"trace_feature",
|
||||
status({
|
||||
open: true,
|
||||
kind: "executable",
|
||||
capabilities: capabilities([
|
||||
"search_strings",
|
||||
"search_procedures",
|
||||
"xrefs",
|
||||
]),
|
||||
}),
|
||||
),
|
||||
).toMatchObject({ available: true, reason: "available" });
|
||||
});
|
||||
|
||||
it("needs the search operations the workflow actually calls", () => {
|
||||
expect(
|
||||
entry(
|
||||
|
||||
@@ -215,12 +215,7 @@ const ENHANCED_REQUIREMENTS: ReadonlyMap<string, ProviderComposition> = new Map(
|
||||
],
|
||||
analyze_function: ["analyze_function"],
|
||||
inspect_native_api: ["analyze_function"],
|
||||
trace_feature: [
|
||||
"search_strings",
|
||||
"search_procedures",
|
||||
"xrefs",
|
||||
"resolve_containing_procedure",
|
||||
],
|
||||
trace_feature: ["search_strings", "search_procedures", "xrefs"],
|
||||
} satisfies Readonly<Record<EnhancedContractName, ProviderComposition>>),
|
||||
);
|
||||
|
||||
|
||||
@@ -4,12 +4,16 @@ import type {
|
||||
AnalysisExecution,
|
||||
AnalysisOperation,
|
||||
} from "./AnalysisProvider.js";
|
||||
import { AnalysisOutputError } from "../domain/analysisErrorCore.js";
|
||||
import {
|
||||
AnalysisCancelledError,
|
||||
AnalysisCapabilityUnavailableError,
|
||||
AnalysisOutputError,
|
||||
} from "../domain/analysisErrorCore.js";
|
||||
import type { AnalysisError } from "../domain/analysisErrorBase.js";
|
||||
import type { JsonValue } from "../domain/jsonValue.js";
|
||||
import {
|
||||
parseContainingProcedureResolution,
|
||||
type ContainingProcedureResolution,
|
||||
type TraceContainingProcedure,
|
||||
} from "../domain/native/nativeObservation.js";
|
||||
import { err, ok, type Result } from "../domain/result.js";
|
||||
import type { EnhancedResult, TraceReference } from "./EnhancedToolTypes.js";
|
||||
@@ -70,7 +74,7 @@ export const traceLiteralFeature = async (
|
||||
search_mode: "literal",
|
||||
matches: searched.value.matches,
|
||||
references: traced.value.references,
|
||||
truncated: traced.value.residual.length > 0,
|
||||
truncated: traced.value.truncated,
|
||||
residual_unknowns: traced.value.residual,
|
||||
});
|
||||
};
|
||||
@@ -121,14 +125,18 @@ const traceReferences = async (
|
||||
observe: ObserveCall,
|
||||
request: ReferenceTraceRequest,
|
||||
): Promise<
|
||||
Result<{ references: TraceReference[]; residual: string[] }, AnalysisError>
|
||||
Result<
|
||||
{ references: TraceReference[]; residual: string[]; truncated: boolean },
|
||||
AnalysisError
|
||||
>
|
||||
> => {
|
||||
const references: TraceReference[] = [];
|
||||
const residual: string[] = [];
|
||||
let truncated = false;
|
||||
// References identify targets by address, independently of search alternatives.
|
||||
const tracedTargets = new Set<string>();
|
||||
// A source address is resolved once however many matches reference it.
|
||||
const containing = new Map<string, ContainingProcedureResolution>();
|
||||
const containing = new Map<string, TraceContainingProcedure>();
|
||||
for (const match of request.matches) {
|
||||
if (tracedTargets.has(match.address)) continue;
|
||||
tracedTargets.add(match.address);
|
||||
@@ -140,10 +148,12 @@ const traceReferences = async (
|
||||
if (!observed.ok) return observed;
|
||||
// Only an explicit producer report of truncation is a coverage gap; a
|
||||
// provider that reports nothing keeps its previous, unqualified result.
|
||||
if (observed.value.referencesTruncated === true)
|
||||
if (observed.value.referencesTruncated === true) {
|
||||
truncated = true;
|
||||
residual.push(
|
||||
`References to ${match.address} were truncated by the provider; additional references may exist.`,
|
||||
);
|
||||
}
|
||||
const sources = observed.value.result;
|
||||
if (!Array.isArray(sources))
|
||||
return err(
|
||||
@@ -167,10 +177,34 @@ const traceReferences = async (
|
||||
{ address: source },
|
||||
request.signal,
|
||||
);
|
||||
if (!resolved.ok) return resolved;
|
||||
const parsed = parseContainingProcedureResolution(resolved.value);
|
||||
if (!parsed.ok) return parsed;
|
||||
containingProcedure = parsed.value;
|
||||
if (!resolved.ok) {
|
||||
if (
|
||||
!(resolved.error instanceof AnalysisCapabilityUnavailableError) ||
|
||||
resolved.error.operation !== "resolve_containing_procedure" ||
|
||||
resolved.error.cleanupIncomplete ||
|
||||
resolved.error.retryable
|
||||
)
|
||||
return resolved;
|
||||
if (request.signal?.aborted === true)
|
||||
return err(
|
||||
new AnalysisCancelledError("trace_feature", {
|
||||
cause: request.signal.reason,
|
||||
}),
|
||||
);
|
||||
containingProcedure = {
|
||||
query_address: source,
|
||||
status: "unavailable",
|
||||
provider_id: resolved.error.providerId,
|
||||
reason: resolved.error.reason,
|
||||
};
|
||||
residual.push(
|
||||
`Containing procedure for ${source} is unavailable: ${resolved.error.reason}`,
|
||||
);
|
||||
} else {
|
||||
const parsed = parseContainingProcedureResolution(resolved.value);
|
||||
if (!parsed.ok) return parsed;
|
||||
containingProcedure = parsed.value;
|
||||
}
|
||||
containing.set(source, containingProcedure);
|
||||
}
|
||||
references.push({
|
||||
@@ -180,5 +214,5 @@ const traceReferences = async (
|
||||
});
|
||||
}
|
||||
}
|
||||
return ok({ references, residual });
|
||||
return ok({ references, residual, truncated });
|
||||
};
|
||||
@@ -3,7 +3,7 @@ import type { z } from "zod";
|
||||
import { analysisInputErrorFromIssues } from "../domain/inputIssueProjection.js";
|
||||
import type { AnalysisError } from "../domain/analysisErrorBase.js";
|
||||
import type { JsonValue } from "../domain/jsonValue.js";
|
||||
import type { ContainingProcedureResolution } from "../domain/native/nativeObservation.js";
|
||||
import type { TraceContainingProcedure } from "../domain/native/nativeObservation.js";
|
||||
import { err, type Result } from "../domain/result.js";
|
||||
import {
|
||||
enhancedInputSchemas,
|
||||
@@ -14,7 +14,7 @@ export type EnhancedResult = Promise<Result<JsonValue, AnalysisError>>;
|
||||
export type TraceReference = {
|
||||
readonly target_address: string;
|
||||
readonly source_address: string;
|
||||
readonly containing_procedure: ContainingProcedureResolution;
|
||||
readonly containing_procedure: TraceContainingProcedure;
|
||||
};
|
||||
|
||||
/** One enhanced call whose input was parsed at its owning adapter boundary. */
|
||||
|
||||
@@ -17,7 +17,9 @@ export const workflowAnalysisProfile = (
|
||||
? { workflow_revision: 5 }
|
||||
: operation === "batch_decompile"
|
||||
? { workflow_revision: 4 }
|
||||
: operation === "get_call_graph" || operation === "trace_feature"
|
||||
? { workflow_revision: 3 }
|
||||
: { workflow_revision: 1 }),
|
||||
: operation === "trace_feature"
|
||||
? { workflow_revision: 4 }
|
||||
: operation === "get_call_graph"
|
||||
? { workflow_revision: 3 }
|
||||
: { workflow_revision: 1 }),
|
||||
});
|
||||
@@ -1248,6 +1248,11 @@ describe("executions that report truncated references", () => {
|
||||
it("revises the trace_feature workflow profile so earlier results are not replayed", () => {
|
||||
const profile = ANALYSIS_SNAPSHOT_PROFILE;
|
||||
|
||||
expect(
|
||||
workflowAnalysisProfile(profile, "trace_feature").parameters
|
||||
.workflow_revision,
|
||||
).toBe(4);
|
||||
|
||||
expect(workflowAnalysisProfile(profile, "trace_feature")).not.toEqual(
|
||||
workflowAnalysisProfile(profile),
|
||||
);
|
||||
|
||||
@@ -80,7 +80,7 @@ export const ENHANCED_TOOL_CONTRACTS = [
|
||||
...FUNCTION_WORKFLOW_TOOL_CONTRACTS,
|
||||
enhanced(
|
||||
"trace_feature",
|
||||
"Trace the complete literal query, preserving spaces, through every matching string and procedure, their xrefs, and truthful containing-procedure resolution. Returns observations and residual unknowns without inferring reference kinds.",
|
||||
"Trace the complete literal query, preserving spaces, through every matching string and procedure and their xrefs. Preserve an explicit unavailable owner for a non-retryable containment capability refusal. Returns observations and residual unknowns without inferring reference kinds; truncated only reports an observed reference cap.",
|
||||
enhancedInputSchemas.trace_feature,
|
||||
),
|
||||
enhanced(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
nativeAddressListSchema,
|
||||
nativeObservationSourceSchemas,
|
||||
containingProcedureResolutionSchema,
|
||||
traceContainingProcedureSchema,
|
||||
} from "../domain/native/nativeObservation.js";
|
||||
import { overviewInventoryCoverageSchema } from "../domain/native/inventoryCoverage.js";
|
||||
import {
|
||||
@@ -261,7 +261,7 @@ const literalTraceOutput = evidenceResultOf(
|
||||
z.object({
|
||||
target_address: z.string(),
|
||||
source_address: z.string(),
|
||||
containing_procedure: containingProcedureResolutionSchema,
|
||||
containing_procedure: traceContainingProcedureSchema,
|
||||
}),
|
||||
),
|
||||
truncated: z.boolean(),
|
||||
|
||||
@@ -27,6 +27,20 @@ export type ContainingProcedureResolution = z.infer<
|
||||
typeof containingProcedureResolutionSchema
|
||||
>;
|
||||
|
||||
/** A trace can retain a reference when its provider cannot resolve ownership. */
|
||||
export const traceContainingProcedureSchema = z.union([
|
||||
containingProcedureResolutionSchema,
|
||||
z.strictObject({
|
||||
query_address: z.string(),
|
||||
status: z.literal("unavailable"),
|
||||
provider_id: z.string(),
|
||||
reason: z.string(),
|
||||
}),
|
||||
]);
|
||||
export type TraceContainingProcedure = z.infer<
|
||||
typeof traceContainingProcedureSchema
|
||||
>;
|
||||
|
||||
/** Validate provider procedure facts before a workflow interprets absence. */
|
||||
export const parseContainingProcedureResolution = (
|
||||
value: JsonValue,
|
||||
|
||||
@@ -3,7 +3,10 @@ import { afterEach, describe, expect, it } from "vitest";
|
||||
import type { AnalysisOperationPort } from "../../../src/application/AnalysisProvider.js";
|
||||
import { createAnalysisExecution } from "../../../src/application/AnalysisProvider.js";
|
||||
import { EnhancedTools } from "../../../src/application/EnhancedTools.js";
|
||||
import { AnalysisOutputError } from "../../../src/domain/analysisErrorCore.js";
|
||||
import {
|
||||
AnalysisCapabilityUnavailableError,
|
||||
AnalysisOutputError,
|
||||
} from "../../../src/domain/analysisErrorCore.js";
|
||||
import { err, ok } from "../../../src/domain/result.js";
|
||||
|
||||
import { closeEnhancedToolResources, connect } from "./enhancedToolsHarness.js";
|
||||
@@ -137,6 +140,195 @@ const trace = async (analysis: AnalysisOperationPort) => {
|
||||
return result.structuredContent as { normalized_result: unknown };
|
||||
};
|
||||
|
||||
it("preserves references and explicitly unknown owners without claiming truncation", async () => {
|
||||
const baseline = provider();
|
||||
const output = await trace({
|
||||
execute: (operation, input, options) =>
|
||||
operation === "resolve_containing_procedure" && input.address === "0x20"
|
||||
? Promise.resolve(
|
||||
err(
|
||||
new AnalysisCapabilityUnavailableError(
|
||||
"fixture",
|
||||
operation,
|
||||
"not supported",
|
||||
),
|
||||
),
|
||||
)
|
||||
: baseline.execute(operation, input, options),
|
||||
});
|
||||
expect(output.normalized_result).toMatchObject({
|
||||
truncated: false,
|
||||
residual_unknowns: [expect.stringContaining("0x20")],
|
||||
references: [
|
||||
{
|
||||
target_address: "0x10",
|
||||
source_address: "0x20",
|
||||
containing_procedure: {
|
||||
query_address: "0x20",
|
||||
status: "unavailable",
|
||||
provider_id: "fixture",
|
||||
reason: "not supported",
|
||||
},
|
||||
},
|
||||
{
|
||||
target_address: "0x11",
|
||||
source_address: "0x21",
|
||||
containing_procedure: containing("0x21"),
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(output.normalized_result).toMatchObject({
|
||||
residual_unknowns: [expect.stringContaining("not supported")],
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps distinct targets when a capped reference source has an unavailable owner", async () => {
|
||||
const calls: string[] = [];
|
||||
const baseline = provider({ duplicateProcedure: true, calls });
|
||||
const output = await trace({
|
||||
execute: (operation, input, options) => {
|
||||
if (operation === "xrefs") {
|
||||
calls.push(`xrefs:${String(input.address)}`);
|
||||
return Promise.resolve(
|
||||
ok(
|
||||
createAnalysisExecution(["0x20"], PROVIDER, {
|
||||
referencesTruncated: input.address === "0x10",
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (operation === "resolve_containing_procedure") {
|
||||
calls.push(`resolve_containing_procedure:${String(input.address)}`);
|
||||
return Promise.resolve(
|
||||
err(
|
||||
new AnalysisCapabilityUnavailableError(
|
||||
"fixture",
|
||||
operation,
|
||||
"not supported",
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return baseline.execute(operation, input, options);
|
||||
},
|
||||
});
|
||||
|
||||
expect(calls.filter((call) => call.startsWith("xrefs:"))).toEqual([
|
||||
"xrefs:0x10",
|
||||
"xrefs:0x11",
|
||||
]);
|
||||
expect(
|
||||
calls.filter((call) => call.startsWith("resolve_containing_procedure:")),
|
||||
).toEqual(["resolve_containing_procedure:0x20"]);
|
||||
expect(output.normalized_result).toMatchObject({
|
||||
truncated: true,
|
||||
residual_unknowns: [
|
||||
expect.stringContaining("0x10"),
|
||||
expect.stringContaining("0x20"),
|
||||
],
|
||||
references: [
|
||||
{
|
||||
target_address: "0x10",
|
||||
source_address: "0x20",
|
||||
containing_procedure: {
|
||||
status: "unavailable",
|
||||
reason: "not supported",
|
||||
},
|
||||
},
|
||||
{
|
||||
target_address: "0x11",
|
||||
source_address: "0x20",
|
||||
containing_procedure: {
|
||||
status: "unavailable",
|
||||
reason: "not supported",
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("does not swallow an unavailable owner with incomplete cleanup", async () => {
|
||||
const baseline = provider();
|
||||
const result = await new EnhancedTools({
|
||||
execute: (operation, input, options) =>
|
||||
operation === "resolve_containing_procedure"
|
||||
? Promise.resolve(
|
||||
err(
|
||||
new AnalysisCapabilityUnavailableError(
|
||||
"fixture",
|
||||
operation,
|
||||
"cleanup failed",
|
||||
{
|
||||
cleanup: {
|
||||
reason: "cleanup failed",
|
||||
resources: ["owned worker"],
|
||||
},
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
: baseline.execute(operation, input, options),
|
||||
}).execute("trace_feature", { query: "hello" });
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok) expect(result.error.cleanupIncomplete).toBe(true);
|
||||
});
|
||||
|
||||
it("preserves cancellation that arrives during an unavailable owner lookup", async () => {
|
||||
const baseline = provider();
|
||||
const controller = new AbortController();
|
||||
const reason = { request: "cancel during owner lookup" };
|
||||
const result = await new EnhancedTools({
|
||||
execute: (operation, input, options) => {
|
||||
if (operation !== "resolve_containing_procedure")
|
||||
return baseline.execute(operation, input, options);
|
||||
controller.abort(reason);
|
||||
return Promise.resolve(
|
||||
err(
|
||||
new AnalysisCapabilityUnavailableError(
|
||||
"fixture",
|
||||
operation,
|
||||
"not supported",
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
}).execute("trace_feature", { query: "hello" }, controller.signal);
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok) {
|
||||
expect(result.error._tag).toBe("AnalysisCancelledError");
|
||||
expect(result.error.cause).toBe(reason);
|
||||
}
|
||||
});
|
||||
|
||||
it("retains incomplete cleanup when cancellation arrives during owner lookup", async () => {
|
||||
const baseline = provider();
|
||||
const controller = new AbortController();
|
||||
const result = await new EnhancedTools({
|
||||
execute: (operation, input, options) => {
|
||||
if (operation !== "resolve_containing_procedure")
|
||||
return baseline.execute(operation, input, options);
|
||||
controller.abort();
|
||||
return Promise.resolve(
|
||||
err(
|
||||
new AnalysisCapabilityUnavailableError(
|
||||
"fixture",
|
||||
operation,
|
||||
"cleanup failed",
|
||||
{
|
||||
cleanup: {
|
||||
reason: "cleanup failed",
|
||||
resources: ["owned worker"],
|
||||
},
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
}).execute("trace_feature", { query: "hello" }, controller.signal);
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok) expect(result.error.cleanupIncomplete).toBe(true);
|
||||
});
|
||||
|
||||
describe("trace_feature reference coverage", () => {
|
||||
it("marks the trace truncated for the target whose references the provider capped", async () => {
|
||||
const output = await trace(
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import { rm } from "node:fs/promises";
|
||||
import { afterEach, expect, it } from "vitest";
|
||||
import { AnalysisCapabilityUnavailableError } from "../../../src/domain/analysisErrorCore.js";
|
||||
import { IdaSessionClient } from "../../../src/ida/IdaSessionClient.js";
|
||||
import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
|
||||
import { createIdaTarget, RecordingIdaMcp } from "../../fixtures/idaMcp.js";
|
||||
import { closeEnhancedToolResources, connect } from "./enhancedToolsHarness.js";
|
||||
|
||||
afterEach(closeEnhancedToolResources);
|
||||
|
||||
it.each([2, 3])(
|
||||
"preserves unknown IDA readiness at owner health check %s",
|
||||
async (failureCheck) => {
|
||||
const { root, target } = await createIdaTarget("arm64");
|
||||
const producer = new RecordingIdaMcp(target, "headless");
|
||||
let xrefsSeen = false;
|
||||
let healthAfterXrefs = 0;
|
||||
producer.overrideCall = (name) => {
|
||||
if (name === "xrefs_to") xrefsSeen = true;
|
||||
if (
|
||||
name === "server_health" &&
|
||||
xrefsSeen &&
|
||||
++healthAfterXrefs >= failureCheck
|
||||
)
|
||||
return {
|
||||
status: "ok",
|
||||
input_path: producer.inputPath,
|
||||
idb_path: `${producer.inputPath}.i64`,
|
||||
module: "target.elf",
|
||||
imagebase: producer.imageBase,
|
||||
auto_analysis_ready: null,
|
||||
};
|
||||
return undefined;
|
||||
};
|
||||
const ida = new IdaSessionClient(
|
||||
{
|
||||
command: "fixture",
|
||||
args: [],
|
||||
env: {},
|
||||
mode: "headless",
|
||||
timeoutMs: 1000,
|
||||
workspaceRoot: root,
|
||||
},
|
||||
target,
|
||||
producer,
|
||||
);
|
||||
let refusal: AnalysisCapabilityUnavailableError | undefined;
|
||||
const mcp = await connect({
|
||||
execute: async (operation, input, options) => {
|
||||
const result = await ida.execute(operation, input, options);
|
||||
if (
|
||||
operation === "resolve_containing_procedure" &&
|
||||
!result.ok &&
|
||||
result.error instanceof AnalysisCapabilityUnavailableError
|
||||
)
|
||||
refusal = result.error;
|
||||
return result;
|
||||
},
|
||||
});
|
||||
try {
|
||||
const result = await mcp.callTool({
|
||||
name: "trace_feature",
|
||||
arguments: { query: "a.b" },
|
||||
});
|
||||
const error = parseMcpToolError(result).error;
|
||||
expect(error).toMatchObject({
|
||||
code: "capability_unavailable",
|
||||
details: {
|
||||
operation: "server_health",
|
||||
diagnostics: { auto_analysis_ready: null },
|
||||
},
|
||||
});
|
||||
expect(refusal?.remediationAction).toBeDefined();
|
||||
expect(error.remediation?.action).toBe(refusal?.remediationAction);
|
||||
expect(healthAfterXrefs).toBe(failureCheck);
|
||||
if (failureCheck === 3) {
|
||||
expect(error.details?.partial_observation).toMatchObject({
|
||||
operation: "resolve_containing_procedure",
|
||||
provider: { id: "ida" },
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
expect((await ida.close()).ok).toBe(true);
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -71,6 +71,8 @@ export interface FakeKunaBehavior {
|
||||
/** Bytes of an extra root member in the graph JSON. */
|
||||
readonly graphPadding?: number;
|
||||
readonly xrefs?: "ok" | "hang" | "not-json";
|
||||
/** Additional exact-target incoming call-site addresses. */
|
||||
readonly extraXrefSources?: readonly number[];
|
||||
readonly referenceDelayMs?: number;
|
||||
readonly referencePadding?: number;
|
||||
/** `invalid-then-hang` writes a contradictory first row, then never finishes. */
|
||||
@@ -155,7 +157,7 @@ if (args[0] === "--version") {
|
||||
const identity = (name, value) => ({ name, address: value, address_hex: "0x" + value.toString(16) });
|
||||
const aliases = target === "0x2000" ? [identity("puts.slot", 12288)] : [];
|
||||
const row = (from, to, kind) => ({ address: from, address_hex: "0x" + from.toString(16), from_address: from, from_address_hex: "0x" + from.toString(16), to_address: to, to_address_hex: "0x" + to.toString(16), kind, from_function: identity("main", 4096), to_function: null, instruction: "fixture reference" + "x".repeat(behavior.referencePadding ?? 0) });
|
||||
const rows = [row(4100, parseInt(target, 16), "call")];
|
||||
const rows = [4100, ...(behavior.extraXrefSources ?? [])].map((source) => row(source, parseInt(target, 16), "call"));
|
||||
if (aliases.length) rows.push(row(4164, 12288, "call"));
|
||||
setTimeout(() => process.stdout.write(JSON.stringify({ binary: args[1], direction: "to", count: rows.length, target: { ...identity(null, parseInt(target, 16)), aliases }, xrefs: rows })), behavior.referenceDelayMs ?? 0);
|
||||
}
|
||||
|
||||
@@ -146,6 +146,7 @@ it.each([
|
||||
async ({ rejectClose, rejectRead, wrongXrefs, wrongStrings }) => {
|
||||
const root = await createTestTempDirectory("rea-kuna-verifier-cleanup-");
|
||||
const kuna = await writeFakeKuna(root, {
|
||||
extraXrefSources: [4104],
|
||||
stringRows: [
|
||||
{ address: 12288, text: "REA fixture prompt" },
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user