mirror of
https://github.com/morluto/rea.git
synced 2026-11-09 16:26:36 +00:00
* fix: retain address context cleanup failures * fix!: preserve address context input errors BREAKING CHANGE: Strict clients that cached the previous full inspect_address_context output schema must refresh tools/list and accept the typed error field on unavailable facets. * test: use session contract for address context * fix!: preserve address context resource recovery BREAKING CHANGE: Clients validating unavailable context facets against a cached strict full output schema must refresh tools/list to accept resource_constraint errors. * fix: expose authenticated records refused by the evidence ledger (#8426) * fix: preserve completed evidence on retention refusal * fix: preserve context when unretained evidence exceeds delivery limits * fix(mcp): recover oversized context from session evidence * fix(evidence): preserve completed atomic write results * fix: retain completed results when source publication fails * test: cover bounded recovery with unknown retention * fix(evidence): preserve recovery identity and finalization facts * fix(evidence): clarify retention of omitted observations * test(evidence): compare omission guidance with its owner * fix(evidence): preserve completed composed workflows * fix(evidence): clarify unretained recovery export * test(evidence): cover mixed recovery guidance * fix(evidence): preserve caller scope in secondary failures * refactor(evidence): remove superseded cleanup projection * test(evidence): align recovery checks with owners * fix: preserve cleanup on partial recovery limits * test: use owners for diagnostic wording * fix: admit completed analysis before evidence construction * fix: preserve selected paths when reusing evidence * fix(cli): preserve caller path in unretained evidence * fix(evidence): preserve recovery metadata and admission bounds * refactor: consolidate evidence closure imports * style(test): format CLI help default annotations (#9280) * test(mcp): keep recovery checks focused on contract facts * fix(evidence): preserve feature trace replay revision * test: align evidence recovery checks with ownership * test: align partial recovery and cleanup diagnostics * fix(evidence): preserve nested partials during cleanup * fix(evidence): respect outer recovery ownership * fix(capture): discard rejected nested observations * test(frida): assert inline evidence recovery --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com>
965 lines
31 KiB
TypeScript
965 lines
31 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
||
import { z } from "zod";
|
||
|
||
import {
|
||
recordWorkflowUnknowns,
|
||
createWorkflowEvidence,
|
||
executeComposedWorkflow,
|
||
retainComposedWorkflowSources,
|
||
retainPartialWorkflowEvidence,
|
||
workflowSnapshotRecord,
|
||
} from "./WorkflowEvidence.js";
|
||
import { InvestigationRecords } from "./investigation/InvestigationRecords.js";
|
||
import { UnknownRegistryError } from "../domain/unknownRegistryError.js";
|
||
import { err, ok } from "../domain/result.js";
|
||
import {
|
||
createAnalysisExecution,
|
||
type AnalysisOperationPort,
|
||
} from "./AnalysisProvider.js";
|
||
import {
|
||
AnalysisCancelledError,
|
||
AnalysisResourceConstraintError,
|
||
} from "../domain/analysisErrorCore.js";
|
||
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
|
||
import { estimateJsonStorage } from "./JsonAuthenticationHeadroom.js";
|
||
import { nativeApiBoundarySchema } from "../domain/native/nativeApiBoundary.js";
|
||
import { projectNativeApiInspection } from "./native/NativeApiInspection.js";
|
||
import { functionDossierSchema } from "../domain/native/functionDossier.js";
|
||
import { ghidraFunctionDossier } from "../domain/ghidraValues.fixture.js";
|
||
import { ANALYSIS_SNAPSHOT_PROFILE } from "../domain/analysisSnapshot.fixture.js";
|
||
import { createEvidence } from "../domain/evidence.js";
|
||
|
||
it("preserves cleanup and reports an omitted partial when authentication headroom is unavailable", () => {
|
||
const error = new AnalysisCancelledError("get_call_graph", {
|
||
cleanup: { reason: "close failed", resources: ["session:fixture"] },
|
||
});
|
||
const outcome = {
|
||
...err(error),
|
||
partial: { result: { nodes: ["observed"] }, limitations: [] },
|
||
sourceEvidence: [],
|
||
};
|
||
retainPartialWorkflowEvidence(
|
||
outcome,
|
||
{
|
||
target: undefined,
|
||
operation: "get_call_graph",
|
||
parameters: {},
|
||
upstreamProfile: undefined,
|
||
},
|
||
() =>
|
||
err(
|
||
new AnalysisResourceConstraintError(
|
||
"get_call_graph",
|
||
"capacity",
|
||
"no headroom",
|
||
null,
|
||
),
|
||
),
|
||
);
|
||
expect(projectAnalysisError(error)).toMatchObject({
|
||
code: "cleanup_incomplete",
|
||
details: {
|
||
cleanup: "incomplete",
|
||
resources: ["session:fixture"],
|
||
partial_observation: {
|
||
kind: "omitted-observation",
|
||
reason: "insufficient-authentication-headroom",
|
||
},
|
||
},
|
||
});
|
||
});
|
||
|
||
it.each([0, 1, 100])(
|
||
"admits enough recovery representation for %i completed batch items",
|
||
(count) => {
|
||
const provider = { id: "fixture", name: "Fixture", version: "1" };
|
||
const source = createEvidence(undefined, provider, {
|
||
operation: "health",
|
||
parameters: {},
|
||
result: "source",
|
||
});
|
||
const items = Array.from({ length: count }, (_, index) => ({
|
||
address: `input-${index}`,
|
||
procedure: {
|
||
status: "resolved",
|
||
address: `0x${(index + 1).toString(16)}`,
|
||
name: null,
|
||
},
|
||
status: "ok",
|
||
pseudocode: 'escaped " and Unicode α'.repeat((index % 3) + 1),
|
||
}));
|
||
const completed = {
|
||
...ok({
|
||
result: { items, total: count, succeeded: count, failed: 0 },
|
||
limitations: [],
|
||
evidenceLinks: [source.evidence_id],
|
||
}),
|
||
sourceEvidence: [source],
|
||
};
|
||
let admitted: ReturnType<typeof estimateJsonStorage> | undefined;
|
||
const outcome = retainComposedWorkflowSources(
|
||
() =>
|
||
err(
|
||
new AnalysisResourceConstraintError(
|
||
"record_evidence",
|
||
"capacity",
|
||
"full",
|
||
null,
|
||
),
|
||
),
|
||
completed,
|
||
{
|
||
target: {
|
||
path: "/tmp/example",
|
||
sha256: "a".repeat(64),
|
||
kind: "executable",
|
||
format: "mach-o",
|
||
architecture: "arm64",
|
||
availableArchitectures: ["arm64"],
|
||
},
|
||
operation: "batch_decompile",
|
||
parameters: { addresses: items.map((item) => item.address) },
|
||
upstreamProfile: ANALYSIS_SNAPSHOT_PROFILE,
|
||
},
|
||
() => undefined,
|
||
(input, _operation, additional) => {
|
||
const base = estimateJsonStorage(input);
|
||
admitted = {
|
||
valueBytes: base.valueBytes + additional.valueBytes,
|
||
serializedUtf16Bytes:
|
||
base.serializedUtf16Bytes + additional.serializedUtf16Bytes,
|
||
};
|
||
return ok(undefined);
|
||
},
|
||
);
|
||
expect(outcome.ok).toBe(false);
|
||
if (outcome.ok || admitted === undefined)
|
||
throw new Error("Expected admitted recovery");
|
||
const error = projectAnalysisError(outcome.error);
|
||
const completedEvidence = z
|
||
.object({ completed_observation: z.object({ evidence: z.unknown() }) })
|
||
.parse(error.details).completed_observation.evidence;
|
||
const actual = estimateJsonStorage(completedEvidence);
|
||
expect(admitted.valueBytes).toBeGreaterThanOrEqual(actual.valueBytes);
|
||
expect(admitted.serializedUtf16Bytes).toBeGreaterThanOrEqual(
|
||
actual.serializedUtf16Bytes,
|
||
);
|
||
},
|
||
);
|
||
|
||
it("links shared provider source Evidence once without copying its payload into the workflow", async () => {
|
||
const source = createEvidence(
|
||
undefined,
|
||
{ id: "fixture", name: "Fixture", version: "1" },
|
||
{
|
||
operation: "health",
|
||
parameters: { facet: "saved-analysis-report" },
|
||
result: { report: "historical content ".repeat(1_000) },
|
||
},
|
||
{ immutable: true },
|
||
);
|
||
const result = await executeComposedWorkflow(
|
||
{
|
||
execute: async () =>
|
||
ok(
|
||
createAnalysisExecution(
|
||
"observed",
|
||
{ id: "fixture", name: "Fixture", version: "1" },
|
||
{ sourceEvidence: [source] },
|
||
),
|
||
),
|
||
},
|
||
async (observed) => {
|
||
await observed.execute("list_procedures", {});
|
||
await observed.execute("procedure_pseudo_code", { procedure: "0x1000" });
|
||
return ok({ count: 1 });
|
||
},
|
||
);
|
||
expect(result).toEqual({
|
||
...ok({
|
||
result: { count: 1 },
|
||
limitations: [],
|
||
evidenceLinks: [source.evidence_id],
|
||
}),
|
||
sourceEvidence: [source],
|
||
});
|
||
expect(result.ok && JSON.stringify(result.value)).not.toContain(
|
||
"historical content",
|
||
);
|
||
});
|
||
|
||
it.each([false, true])(
|
||
"propagates transient=%s through composed snapshot decisions while retaining Evidence",
|
||
async (transient) => {
|
||
let calls = 0;
|
||
const composed = await executeComposedWorkflow(
|
||
{
|
||
execute: async () =>
|
||
ok(
|
||
createAnalysisExecution(
|
||
"observed",
|
||
provider,
|
||
transient && calls++ === 0
|
||
? { cachePolicy: "live" as const }
|
||
: {},
|
||
),
|
||
),
|
||
},
|
||
async (observed) => {
|
||
await observed.execute("analyze_function", { procedure: "0x1000" });
|
||
const last = await observed.execute("procedure_info", {
|
||
procedure: "0x1000",
|
||
});
|
||
return last.ok ? ok(last.value.result) : last;
|
||
},
|
||
);
|
||
if (!composed.ok) throw composed.error;
|
||
const evidence = createWorkflowEvidence({
|
||
target: undefined,
|
||
operation: "inspect_native_api",
|
||
parameters: {},
|
||
result: composed.value.result,
|
||
upstreamProfile: ANALYSIS_SNAPSHOT_PROFILE,
|
||
upstreamLimitations: composed.value.limitations,
|
||
});
|
||
expect(new InvestigationRecords().recordEvidence(evidence).ok).toBe(true);
|
||
expect(
|
||
workflowSnapshotRecord(
|
||
evidence,
|
||
"inspect_native_api",
|
||
ANALYSIS_SNAPSHOT_PROFILE,
|
||
composed.value.cachePolicy,
|
||
) === undefined,
|
||
).toBe(transient);
|
||
expect(evidence.normalized_result).toBe("observed");
|
||
},
|
||
);
|
||
|
||
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 = () => {
|
||
let resolve: () => void = () => {
|
||
throw new Error("gate not initialized");
|
||
};
|
||
const promise = new Promise<void>((release) => {
|
||
resolve = release;
|
||
});
|
||
return { promise, resolve };
|
||
};
|
||
const parents = [gate(), gate()];
|
||
const children = [gate(), gate()];
|
||
const analysis: AnalysisOperationPort = {
|
||
execute: async (operation, parameters) => {
|
||
const index = Number(parameters.index);
|
||
if (operation === "procedure_address") {
|
||
await parents[index]!.promise;
|
||
return ok(createAnalysisExecution(String(index), provider));
|
||
}
|
||
children[index]!.resolve();
|
||
return ok(
|
||
createAnalysisExecution(String(index), provider, {
|
||
limitations: [
|
||
index === 0 ? "first child caveat" : "second child caveat",
|
||
],
|
||
}),
|
||
);
|
||
},
|
||
};
|
||
const composed = await executeComposedWorkflow(
|
||
analysis,
|
||
async (observed) => {
|
||
const branches = Promise.all(
|
||
[0, 1].map(async (index) => {
|
||
await observed.execute("procedure_address", { index });
|
||
const child = await observed.execute("address_name", { index });
|
||
if (!child.ok) throw child.error;
|
||
return child.value.result;
|
||
}),
|
||
);
|
||
for (const index of order) {
|
||
parents[index]!.resolve();
|
||
await children[index]!.promise;
|
||
}
|
||
return ok(await branches);
|
||
},
|
||
);
|
||
if (!composed.ok) throw composed.error;
|
||
return createWorkflowEvidence({
|
||
target: undefined,
|
||
operation: "batch_decompile",
|
||
parameters: {},
|
||
result: composed.value.result,
|
||
upstreamProfile: undefined,
|
||
upstreamLimitations: composed.value.limitations,
|
||
});
|
||
};
|
||
const left = await run([0, 1]);
|
||
const right = await run([1, 0]);
|
||
expect(left.normalized_result).toEqual(right.normalized_result);
|
||
expect(left.limitations).toEqual(right.limitations);
|
||
expect(left.evidence_id).toBe(right.evidence_id);
|
||
});
|
||
|
||
it("retains unique upstream limits in canonical order across concurrent observations", async () => {
|
||
let release: () => void = () => {
|
||
throw new Error("gate not initialized");
|
||
};
|
||
const gate = new Promise<void>((resolve) => {
|
||
release = resolve;
|
||
});
|
||
let calls = 0;
|
||
const options = { signal: new AbortController().signal };
|
||
const analysis: AnalysisOperationPort = {
|
||
execute: async (operation, parameters, forwarded) => {
|
||
expect(parameters).toEqual({ marker: true });
|
||
expect(forwarded).toMatchObject(options);
|
||
expect(forwarded?.workScope).toBeDefined();
|
||
const call = calls++;
|
||
if (call === 0) await gate;
|
||
else release();
|
||
const limitation =
|
||
operation === "list_strings"
|
||
? ["first", "shared"]
|
||
: ["second", "shared"];
|
||
return ok(
|
||
createAnalysisExecution([], provider, { limitations: limitation }),
|
||
);
|
||
},
|
||
};
|
||
const result = await executeComposedWorkflow(analysis, async (observed) => {
|
||
await Promise.all([
|
||
observed.execute("list_strings", { marker: true }, options),
|
||
observed.execute("list_procedures", { marker: true }, options),
|
||
]);
|
||
return ok({ count: 0 });
|
||
});
|
||
expect(calls).toBe(2);
|
||
expect(result).toEqual(
|
||
ok({
|
||
result: { count: 0 },
|
||
limitations: [
|
||
"Fixture (fixture 1) list_procedures, list_strings: shared",
|
||
"Fixture (fixture 1) list_procedures: second",
|
||
"Fixture (fixture 1) list_strings: first",
|
||
],
|
||
}),
|
||
);
|
||
});
|
||
|
||
it("deduplicates exact source caveats without merging providers or operations", async () => {
|
||
const firstProvider = { id: "first", name: "First", version: "1" };
|
||
const secondProvider = { id: "second", name: "Second", version: "1" };
|
||
const result = await executeComposedWorkflow(
|
||
{
|
||
execute: async (operation, parameters) =>
|
||
ok(
|
||
createAnalysisExecution(
|
||
null,
|
||
parameters.provider === "second" ? secondProvider : firstProvider,
|
||
{ limitations: ["same source caveat"] },
|
||
),
|
||
),
|
||
},
|
||
async (observed) => {
|
||
await observed.execute("list_strings", { provider: "first" });
|
||
await observed.execute("list_strings", { provider: "first" });
|
||
await observed.execute("list_strings", { provider: "second" });
|
||
await observed.execute("list_procedures", { provider: "first" });
|
||
return ok({ count: 0 });
|
||
},
|
||
);
|
||
|
||
expect(result).toEqual(
|
||
ok({
|
||
result: { count: 0 },
|
||
limitations: [
|
||
"First (first 1) list_procedures, list_strings: same source caveat",
|
||
"Second (second 1) list_strings: same source caveat",
|
||
],
|
||
}),
|
||
);
|
||
});
|
||
|
||
it("bounds per-item caveat retention while preserving inventory caveats", async () => {
|
||
const result = await executeComposedWorkflow(
|
||
{
|
||
execute: async (operation, parameters) =>
|
||
ok(
|
||
createAnalysisExecution(null, provider, {
|
||
limitations:
|
||
operation === "list_procedures"
|
||
? ["inventory caveat"]
|
||
: [
|
||
"shared caveat",
|
||
`caveat at ${String(parameters.procedure)}`,
|
||
],
|
||
}),
|
||
),
|
||
},
|
||
async (observed) => {
|
||
await observed.execute("list_procedures", {});
|
||
for (const procedure of ["0x1", "0x2", "0x3"])
|
||
await observed.execute("procedure_pseudo_code", { procedure });
|
||
return ok({ count: 3 });
|
||
},
|
||
{
|
||
perItemOperations: new Set(["procedure_pseudo_code"]),
|
||
maxDistinct: 2,
|
||
retainedIn: "the files keep them",
|
||
},
|
||
);
|
||
expect(result).toEqual(
|
||
ok({
|
||
result: { count: 3 },
|
||
limitations: [
|
||
"Fixture (fixture 1) list_procedures: inventory caveat",
|
||
"Fixture (fixture 1) procedure_pseudo_code: caveat at 0x1",
|
||
"Fixture (fixture 1) procedure_pseudo_code: shared caveat",
|
||
"2 per-item provider limitation observations were omitted from this answer; the files keep them.",
|
||
],
|
||
}),
|
||
);
|
||
});
|
||
|
||
it("spends one budget slot on a caveat that two per-item operations share", async () => {
|
||
const result = await executeComposedWorkflow(
|
||
{
|
||
execute: async (operation, parameters) =>
|
||
ok(
|
||
createAnalysisExecution(null, provider, {
|
||
limitations: [
|
||
"shared caveat",
|
||
`${operation} at ${String(parameters.procedure)}`,
|
||
],
|
||
}),
|
||
),
|
||
},
|
||
async (observed) => {
|
||
await observed.execute("procedure_info", { procedure: "0x1" });
|
||
await observed.execute("procedure_pseudo_code", { procedure: "0x1" });
|
||
return ok(null);
|
||
},
|
||
{
|
||
perItemOperations: new Set(["procedure_info", "procedure_pseudo_code"]),
|
||
maxDistinct: 2,
|
||
retainedIn: "the files keep them",
|
||
},
|
||
);
|
||
expect(result).toEqual(
|
||
ok({
|
||
result: null,
|
||
limitations: [
|
||
"Fixture (fixture 1) procedure_info, procedure_pseudo_code: shared caveat",
|
||
"Fixture (fixture 1) procedure_info: procedure_info at 0x1",
|
||
"1 per-item provider limitation observation was omitted from this answer; the files keep them.",
|
||
],
|
||
}),
|
||
);
|
||
});
|
||
|
||
it("counts repeated omitted observations without retaining their text for ranking", async () => {
|
||
const result = await executeComposedWorkflow(
|
||
{
|
||
execute: async (_operation, parameters) =>
|
||
ok(
|
||
createAnalysisExecution(null, provider, {
|
||
limitations: [
|
||
parameters.procedure === "first"
|
||
? "first caveat"
|
||
: "later caveat",
|
||
],
|
||
}),
|
||
),
|
||
},
|
||
async (observed) => {
|
||
await observed.execute("procedure_pseudo_code", { procedure: "first" });
|
||
for (let index = 0; index < 30; index += 1)
|
||
await observed.execute("procedure_pseudo_code", { procedure: "later" });
|
||
return ok(null);
|
||
},
|
||
{
|
||
perItemOperations: new Set(["procedure_pseudo_code"]),
|
||
maxDistinct: 1,
|
||
retainedIn: "the index keeps them",
|
||
},
|
||
);
|
||
expect(result).toEqual(
|
||
ok({
|
||
result: null,
|
||
limitations: [
|
||
"Fixture (fixture 1) procedure_pseudo_code: first caveat",
|
||
"30 per-item provider limitation observations were omitted from this answer; the index keeps them.",
|
||
],
|
||
}),
|
||
);
|
||
});
|
||
|
||
it("bounds retained caveat text while preserving smaller later observations", async () => {
|
||
const result = await executeComposedWorkflow(
|
||
{
|
||
execute: async () =>
|
||
ok(
|
||
createAnalysisExecution(null, provider, {
|
||
limitations: [
|
||
"x".repeat(9_000),
|
||
"first",
|
||
"second",
|
||
"x".repeat(9_000),
|
||
"ok",
|
||
],
|
||
}),
|
||
),
|
||
},
|
||
async (observed) => {
|
||
await observed.execute("procedure_pseudo_code", {});
|
||
return ok(null);
|
||
},
|
||
{
|
||
perItemOperations: new Set(["procedure_pseudo_code"]),
|
||
maxDistinct: 8,
|
||
maxBytes: 12,
|
||
retainedIn: "the index keeps them",
|
||
},
|
||
);
|
||
expect(result).toEqual(
|
||
ok({
|
||
result: null,
|
||
limitations: [
|
||
"Fixture (fixture 1) procedure_pseudo_code: first",
|
||
"Fixture (fixture 1) procedure_pseudo_code: ok",
|
||
"3 per-item provider limitation observations were omitted from this answer; the index keeps them.",
|
||
],
|
||
}),
|
||
);
|
||
});
|
||
|
||
it("preserves a failed composed operation without an extra provider read", async () => {
|
||
const failure = err(new AnalysisCancelledError("list_strings"));
|
||
let calls = 0;
|
||
const result = await executeComposedWorkflow(
|
||
{
|
||
execute: async () => {
|
||
calls++;
|
||
return failure;
|
||
},
|
||
},
|
||
async (observed) => {
|
||
const observedResult = await observed.execute("list_strings", {});
|
||
return observedResult.ok
|
||
? ok(observedResult.value.result)
|
||
: observedResult;
|
||
},
|
||
);
|
||
expect(result).toBe(failure);
|
||
expect(calls).toBe(1);
|
||
});
|
||
|
||
describe("workflow residual question retention", () => {
|
||
it("keeps questions linked to workflow Evidence and records repeats idempotently", () => {
|
||
const records = new InvestigationRecords();
|
||
const result = {
|
||
residual_unknowns: ["Which provider resolves this boundary?"],
|
||
};
|
||
const evidence = createWorkflowEvidence({
|
||
target: undefined,
|
||
operation: "inspect_native_api",
|
||
parameters: { procedure: "0x1000" },
|
||
result,
|
||
upstreamProfile: undefined,
|
||
upstreamLimitations: [],
|
||
});
|
||
expect(records.recordEvidence(evidence).ok).toBe(true);
|
||
const input = {
|
||
name: "inspect_native_api",
|
||
result,
|
||
evidenceId: evidence.evidence_id,
|
||
recordUnknown: (
|
||
unknown: Parameters<InvestigationRecords["recordUnknown"]>[0],
|
||
) => records.recordUnknown(unknown),
|
||
};
|
||
expect(recordWorkflowUnknowns(input).ok).toBe(true);
|
||
expect(recordWorkflowUnknowns(input).ok).toBe(true);
|
||
expect(records.listUnknowns()).toMatchObject([
|
||
{
|
||
question: `${result.residual_unknowns[0]} (Evidence ${evidence.evidence_id})`,
|
||
domain: "native-api",
|
||
supporting_evidence_ids: [evidence.evidence_id],
|
||
required_authority: "shipped-artifact",
|
||
},
|
||
]);
|
||
});
|
||
|
||
it("retains one identical residual question per distinct workflow Evidence", () => {
|
||
const records = new InvestigationRecords();
|
||
const result = {
|
||
residual_unknowns: ["Which provider resolves this boundary?"],
|
||
};
|
||
const evidence = ["0x1000", "0x2000"].map((procedure) =>
|
||
createWorkflowEvidence({
|
||
target: undefined,
|
||
operation: "inspect_native_api",
|
||
parameters: { procedure },
|
||
result,
|
||
upstreamProfile: undefined,
|
||
upstreamLimitations: [],
|
||
}),
|
||
);
|
||
expect(evidence[0]?.evidence_id).not.toBe(evidence[1]?.evidence_id);
|
||
|
||
for (const item of evidence) {
|
||
expect(records.recordEvidence(item).ok).toBe(true);
|
||
const input = {
|
||
name: "inspect_native_api",
|
||
result,
|
||
evidenceId: item.evidence_id,
|
||
recordUnknown: (
|
||
unknown: Parameters<InvestigationRecords["recordUnknown"]>[0],
|
||
) => records.recordUnknown(unknown),
|
||
};
|
||
expect(recordWorkflowUnknowns(input).ok).toBe(true);
|
||
expect(recordWorkflowUnknowns(input).ok).toBe(true);
|
||
}
|
||
|
||
expect(records.listUnknowns()).toHaveLength(2);
|
||
expect(
|
||
records
|
||
.listUnknowns()
|
||
.map(({ supporting_evidence_ids }) => supporting_evidence_ids),
|
||
).toEqual(
|
||
expect.arrayContaining(evidence.map(({ evidence_id }) => [evidence_id])),
|
||
);
|
||
});
|
||
|
||
it.each([
|
||
["spaces", " Which boundary? "],
|
||
["line feed and tab", "\nWhich boundary?\t"],
|
||
["byte order mark", "\uFEFFWhich boundary?\uFEFF"],
|
||
])(
|
||
"normalizes edge whitespace from %s without changing workflow Evidence",
|
||
(_, question) => {
|
||
const records = new InvestigationRecords();
|
||
const result = { residual_unknowns: [question] };
|
||
const evidence = createWorkflowEvidence({
|
||
target: undefined,
|
||
operation: "inspect_native_api",
|
||
parameters: {},
|
||
result,
|
||
upstreamProfile: undefined,
|
||
upstreamLimitations: [],
|
||
});
|
||
|
||
expect(records.recordEvidence(evidence).ok).toBe(true);
|
||
expect(
|
||
recordWorkflowUnknowns({
|
||
name: "inspect_native_api",
|
||
result,
|
||
evidenceId: evidence.evidence_id,
|
||
recordUnknown: (unknown) => records.recordUnknown(unknown),
|
||
}).ok,
|
||
).toBe(true);
|
||
expect(records.listUnknowns()).toMatchObject([
|
||
{ question: `Which boundary? (Evidence ${evidence.evidence_id})` },
|
||
]);
|
||
expect(evidence.normalized_result).toEqual(result);
|
||
},
|
||
);
|
||
|
||
it("rejects questions that are empty after whitespace normalization", () => {
|
||
const records = new InvestigationRecords();
|
||
const result = { residual_unknowns: [" \n\t "] };
|
||
const evidence = createWorkflowEvidence({
|
||
target: undefined,
|
||
operation: "inspect_native_api",
|
||
parameters: {},
|
||
result,
|
||
upstreamProfile: undefined,
|
||
upstreamLimitations: [],
|
||
});
|
||
|
||
expect(records.recordEvidence(evidence).ok).toBe(true);
|
||
expect(
|
||
recordWorkflowUnknowns({
|
||
name: "inspect_native_api",
|
||
result,
|
||
evidenceId: evidence.evidence_id,
|
||
recordUnknown: (unknown) => records.recordUnknown(unknown),
|
||
}).ok,
|
||
).toBe(false);
|
||
expect(records.listUnknowns()).toEqual([]);
|
||
expect(evidence.normalized_result).toEqual(result);
|
||
});
|
||
|
||
it("preserves registry failures rather than presenting a completed workflow", () => {
|
||
const error = new UnknownRegistryError("limit");
|
||
expect(
|
||
recordWorkflowUnknowns({
|
||
name: "trace_feature",
|
||
result: { residual_unknowns: ["Which branch handles the request?"] },
|
||
evidenceId: "unretained",
|
||
recordUnknown: () => err(error),
|
||
}),
|
||
).toEqual(err(error));
|
||
});
|
||
});
|
||
|
||
it("reports a retained workflow and the committed prefix when Unknown publication fails", () => {
|
||
const records = new InvestigationRecords();
|
||
const result = { residual_unknowns: ["First?", "First?", "Second?"] };
|
||
const evidence = createWorkflowEvidence({
|
||
target: undefined,
|
||
operation: "trace_feature",
|
||
parameters: {},
|
||
result,
|
||
upstreamProfile: undefined,
|
||
upstreamLimitations: [],
|
||
});
|
||
expect(records.recordEvidence(evidence).ok).toBe(true);
|
||
const failure = new AnalysisResourceConstraintError(
|
||
"record_unknown",
|
||
"capacity",
|
||
"full",
|
||
null,
|
||
);
|
||
const outcome = recordWorkflowUnknowns({
|
||
name: "trace_feature",
|
||
result,
|
||
evidenceId: evidence.evidence_id,
|
||
evidenceRetained: true,
|
||
recordUnknown: (input) =>
|
||
input.question.startsWith("Second?")
|
||
? err(failure)
|
||
: records.recordUnknown(input),
|
||
});
|
||
if (outcome.ok) throw new Error("expected failure");
|
||
const projected = projectAnalysisError(outcome.error);
|
||
expect(projected.details?.workflow_unknown_publication).toEqual({
|
||
evidence_id: evidence.evidence_id,
|
||
evidence_retention: "retained",
|
||
total_questions: 3,
|
||
created_questions: 1,
|
||
existing_questions: 1,
|
||
next_question_index: 2,
|
||
});
|
||
expect(projected.remediation.action).toContain("get_evidence_bundle");
|
||
const invocation = projectAnalysisError(
|
||
outcome.error.retainPartialObservation(evidence),
|
||
"invocation",
|
||
);
|
||
expect(invocation.details?.workflow_unknown_publication).toMatchObject({
|
||
evidence_retention: "invocation-only",
|
||
});
|
||
expect(invocation.details?.partial_observation).toEqual(evidence);
|
||
expect(invocation.remediation.action).not.toContain("get_evidence_bundle");
|
||
expect(records.listUnknowns()).toHaveLength(1);
|
||
expect(records.evidenceById(evidence.evidence_id)?.normalized_result).toEqual(
|
||
result,
|
||
);
|
||
expect(
|
||
recordWorkflowUnknowns({
|
||
name: "trace_feature",
|
||
result,
|
||
evidenceId: evidence.evidence_id,
|
||
evidenceRetained: true,
|
||
recordUnknown: (input) => records.recordUnknown(input),
|
||
}).ok,
|
||
).toBe(true);
|
||
expect(records.listUnknowns()).toHaveLength(2);
|
||
});
|
||
|
||
it.each([
|
||
" Which boundary? ",
|
||
"\nWhich boundary?\t",
|
||
"\uFEFFWhich boundary?\uFEFF",
|
||
])(
|
||
"retains contract-accepted question %j without rewriting workflow Evidence",
|
||
(question) => {
|
||
const boundary = nativeApiBoundarySchema.parse({
|
||
available: false,
|
||
reason: "Synthetic provider unavailable boundary",
|
||
residual_unknowns: [question],
|
||
});
|
||
const dossier = functionDossierSchema.parse(ghidraFunctionDossier());
|
||
const result = projectNativeApiInspection({
|
||
...dossier,
|
||
native_api: boundary,
|
||
});
|
||
const evidence = createWorkflowEvidence({
|
||
target: undefined,
|
||
operation: "inspect_native_api",
|
||
parameters: { procedure: "0x1000" },
|
||
result,
|
||
upstreamProfile: undefined,
|
||
upstreamLimitations: [],
|
||
});
|
||
const records = new InvestigationRecords();
|
||
expect(records.recordEvidence(evidence).ok).toBe(true);
|
||
expect(
|
||
recordWorkflowUnknowns({
|
||
name: "inspect_native_api",
|
||
result,
|
||
evidenceId: evidence.evidence_id,
|
||
recordUnknown: (input) => records.recordUnknown(input),
|
||
}).ok,
|
||
).toBe(true);
|
||
expect(records.listUnknowns()).toMatchObject([
|
||
{ question: `Which boundary? (Evidence ${evidence.evidence_id})` },
|
||
]);
|
||
expect(records.evidenceById(evidence.evidence_id)).toEqual(evidence);
|
||
expect(evidence.normalized_result).toMatchObject({
|
||
residual_unknowns: [question],
|
||
});
|
||
},
|
||
);
|
||
|
||
it("shares temporary provider state only within a single completed workflow", async () => {
|
||
const owner = {};
|
||
const scopes: Array<
|
||
NonNullable<import("./AnalysisProvider.js").ExecutionOptions["workScope"]>
|
||
> = [];
|
||
const provider = { id: "fixture", name: "Fixture", version: "1" };
|
||
const analysis: AnalysisOperationPort = {
|
||
execute: async (_operation, _parameters, options) => {
|
||
const scope = options?.workScope;
|
||
if (scope === undefined) throw new Error("workflow scope missing");
|
||
scopes.push(scope);
|
||
scope.set(owner, (scope.get<number>(owner) ?? 0) + 1);
|
||
return ok(createAnalysisExecution(null, provider));
|
||
},
|
||
};
|
||
const run = () =>
|
||
executeComposedWorkflow(analysis, async (observed) => {
|
||
await observed.execute("health", {});
|
||
await observed.execute("health", {});
|
||
expect(scopes.at(-1)?.get(owner)).toBe(2);
|
||
return ok(null);
|
||
});
|
||
await run();
|
||
expect(scopes[0]?.get(owner)).toBeUndefined();
|
||
scopes[0]?.set(owner, "late completion");
|
||
expect(scopes[0]?.get(owner)).toBeUndefined();
|
||
await run();
|
||
expect(scopes[0]).toBe(scopes[1]);
|
||
expect(scopes[0]).not.toBe(scopes[2]);
|
||
expect(scopes[2]).toBe(scopes[3]);
|
||
});
|
||
|
||
it("clears workflow state when its callback throws", async () => {
|
||
const owner = {};
|
||
let captured: import("./AnalysisProvider.js").ExecutionOptions["workScope"];
|
||
const analysis: AnalysisOperationPort = {
|
||
execute: async (_operation, _parameters, options) => {
|
||
captured = options?.workScope;
|
||
captured?.set(owner, "in flight");
|
||
return ok(
|
||
createAnalysisExecution(null, {
|
||
id: "fixture",
|
||
name: "Fixture",
|
||
version: "1",
|
||
}),
|
||
);
|
||
},
|
||
};
|
||
await expect(
|
||
executeComposedWorkflow(analysis, async (observed) => {
|
||
await observed.execute("health", {});
|
||
throw new Error("failed workflow");
|
||
}),
|
||
).rejects.toThrow("failed workflow");
|
||
expect(captured?.get(owner)).toBeUndefined();
|
||
});
|