mirror of
https://github.com/morluto/rea.git
synced 2026-11-05 14:26:07 +00:00
fix(mcp): guide retained evidence input recovery (#8761)
* fix(mcp): guide retained evidence input recovery * style(mcp): format catalog and server types * test(mcp): verify HTTP capacity recovery guidance * test(mcp): model catalog recorder imports in cleanup fixtures record-mcp-catalog.mjs loads tool contracts and the Evidence-input contract before it registers cleanup owners. The cleanup fixture still sentineled only the previous imports, so those loads failed before any owner wrote cleanup.jsonl. Track the script's read() calls and replace the new modules with the empty contract list this fault injection uses. --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com>
This commit is contained in:
1 parent
93295c3df3
commit
4afb6b79ae
15 files changed
+308
-98
No files matched your search
@@ -15,12 +15,16 @@ const [
|
||||
{ parseConfig },
|
||||
{ silentLogger },
|
||||
{ PROMPT_CONTRACTS },
|
||||
{ TOOL_CONTRACTS },
|
||||
{ getEvidenceInputContract },
|
||||
] = await Promise.all([
|
||||
read("server/createServer.js"),
|
||||
read("composition/binary.js"),
|
||||
read("config/parseConfig.js"),
|
||||
read("logger.js"),
|
||||
read("contracts/promptContracts.js"),
|
||||
read("contracts/toolContracts.js"),
|
||||
read("domain/evidenceInputContract.js"),
|
||||
]);
|
||||
const recordCatalog = async (profile) => {
|
||||
// Catalog generation is independent of the host account and user configuration.
|
||||
@@ -71,6 +75,11 @@ const recordCatalog = async (profile) => {
|
||||
instructions: client.getInstructions(),
|
||||
identity: server.catalogIdentity,
|
||||
tools,
|
||||
evidenceReferenceAlternatives: TOOL_CONTRACTS.filter(
|
||||
(contract) =>
|
||||
getEvidenceInputContract(contract.inputSchema)
|
||||
.hasEvidenceReferenceAlternative,
|
||||
).map((contract) => contract.name),
|
||||
prompts,
|
||||
};
|
||||
const encoded = JSON.stringify(catalog);
|
||||
|
||||
@@ -230,6 +230,34 @@ describe("public Evidence input declarations", () => {
|
||||
expect(declarationIssues(schema)).toEqual([]);
|
||||
});
|
||||
|
||||
it("distinguishes replaceable inline Evidence from reference-only sources", () => {
|
||||
for (const schema of [
|
||||
evidenceInputSchema,
|
||||
evidenceSourceInputSchema,
|
||||
analysisViewSourceSchema,
|
||||
z.array(evidenceInputSchema.optional()),
|
||||
])
|
||||
expect(
|
||||
getEvidenceInputContract(schema).hasEvidenceReferenceAlternative,
|
||||
).toBe(true);
|
||||
for (const schema of [
|
||||
retainedEvidenceReferenceSchema,
|
||||
evidenceFileReferenceSchema,
|
||||
exportEnvironmentBundleInputSchema.shape.source,
|
||||
z.object({ address: z.string() }),
|
||||
])
|
||||
expect(
|
||||
getEvidenceInputContract(schema).hasEvidenceReferenceAlternative,
|
||||
).toBe(false);
|
||||
// Reference guidance must not change the separately owned source parser.
|
||||
expect(
|
||||
getEvidenceInputContract(evidenceSourceInputSchema).hasEvidence,
|
||||
).toBe(false);
|
||||
expect(getEvidenceInputContract(analysisViewSourceSchema).hasEvidence).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects skipping defaults above Evidence-bearing request unions", () => {
|
||||
const choice = z.union([
|
||||
z.strictObject({
|
||||
|
||||
@@ -32,6 +32,7 @@ import { desktopViewRequestSchema } from "./desktopApplicationView.js";
|
||||
import { javascriptModuleReferencesRequestSchema } from "./javascriptModuleReferences.js";
|
||||
import { javascriptFactsViewRequestSchema } from "./javascriptFactsView.js";
|
||||
import { evidenceSchema } from "../evidence.js";
|
||||
import { declareEvidenceReferenceAlternative } from "../evidenceInputContract.js";
|
||||
import { apiSurfaceViewSchema } from "./webNetworkApiSurfaceView.js";
|
||||
import { evmViewRequestSchema } from "./evmInterfaceView.js";
|
||||
import { artifactInventoryViewRequestSchema } from "./artifactInventoryView.js";
|
||||
@@ -117,14 +118,16 @@ inlineEvidenceInputSchema._zod.processJSONSchema =
|
||||
evidenceSchema._zod.processJSONSchema;
|
||||
|
||||
/** Unauthenticated inline input, an exact same-session retained reference, or one local Evidence file. */
|
||||
export const analysisViewSourceSchema = z.discriminatedUnion("kind", [
|
||||
retainedEvidenceReferenceSchema,
|
||||
z.strictObject({
|
||||
kind: z.literal("inline"),
|
||||
evidence: inlineEvidenceInputSchema,
|
||||
}),
|
||||
evidenceFileReferenceSchema,
|
||||
]);
|
||||
export const analysisViewSourceSchema = declareEvidenceReferenceAlternative(
|
||||
z.discriminatedUnion("kind", [
|
||||
retainedEvidenceReferenceSchema,
|
||||
z.strictObject({
|
||||
kind: z.literal("inline"),
|
||||
evidence: inlineEvidenceInputSchema,
|
||||
}),
|
||||
evidenceFileReferenceSchema,
|
||||
]),
|
||||
);
|
||||
|
||||
/** Public inspect_analysis_view input. */
|
||||
export const inspectAnalysisViewInputSchema = z.strictObject({
|
||||
|
||||
@@ -11,6 +11,7 @@ type EvidenceInputDeclaration =
|
||||
// Zod registries carry owner metadata through describe/meta clones. This is a
|
||||
// schema declaration registry, never a store of caller Evidence or references.
|
||||
const declarations = z.registry<EvidenceInputDeclaration>();
|
||||
const referenceAlternatives = z.registry<{ readonly retained: true }>();
|
||||
const borrowedDeclarations = new WeakMap<object, z.ZodType>();
|
||||
|
||||
/** Inspect authored declarations, including described clones, for catalog checks. */
|
||||
@@ -25,6 +26,14 @@ export const declareEvidenceInput = <Schema extends z.ZodType>(
|
||||
return schema;
|
||||
};
|
||||
|
||||
/** Mark a source union that accepts both inline Evidence and a retained reference. */
|
||||
export const declareEvidenceReferenceAlternative = <Schema extends z.ZodType>(
|
||||
schema: Schema,
|
||||
): Schema => {
|
||||
referenceAlternatives.add(schema, { retained: true });
|
||||
return schema;
|
||||
};
|
||||
|
||||
/** Keep an SDK-borrowed selection associated with its owning declaration. */
|
||||
export const declareBorrowedEvidenceInput = <Schema extends z.ZodType>(
|
||||
schema: Schema,
|
||||
@@ -94,6 +103,8 @@ type Visitor<Failure> = (
|
||||
export interface EvidenceInputContract<Frame = unknown> {
|
||||
readonly frameSchema: z.ZodType<Frame>;
|
||||
readonly hasEvidence: boolean;
|
||||
/** A declared field can replace inline Evidence with a retained reference. */
|
||||
readonly hasEvidenceReferenceAlternative: boolean;
|
||||
resolve<Failure>(
|
||||
input: unknown,
|
||||
visit: Visitor<Failure>,
|
||||
@@ -102,9 +113,13 @@ export interface EvidenceInputContract<Frame = unknown> {
|
||||
}
|
||||
|
||||
const contracts = new WeakMap<z.ZodType, EvidenceInputContract>();
|
||||
const identity = (schema: z.ZodType): EvidenceInputContract => ({
|
||||
const identity = (
|
||||
schema: z.ZodType,
|
||||
hasEvidenceReferenceAlternative = false,
|
||||
): EvidenceInputContract => ({
|
||||
frameSchema: schema,
|
||||
hasEvidence: false,
|
||||
hasEvidenceReferenceAlternative,
|
||||
resolve: (input) => ok(input),
|
||||
});
|
||||
|
||||
@@ -129,11 +144,14 @@ export const getEvidenceInputContract = <Schema extends z.ZodType>(
|
||||
};
|
||||
|
||||
const compile = (schema: z.ZodType): EvidenceInputContract => {
|
||||
if (referenceAlternatives.get(schema) !== undefined)
|
||||
return identity(schema, true);
|
||||
const declared = declarations.get(schema);
|
||||
if (declared?.kind === "evidence")
|
||||
return {
|
||||
frameSchema: z.unknown().nonoptional(),
|
||||
hasEvidence: true,
|
||||
hasEvidenceReferenceAlternative: true,
|
||||
resolve: (input, visit, path = []) => visit(input, path),
|
||||
};
|
||||
if (declared?.kind === "borrowed") {
|
||||
@@ -156,9 +174,11 @@ const compile = (schema: z.ZodType): EvidenceInputContract => {
|
||||
schema instanceof z.ZodReadonly
|
||||
) {
|
||||
const child = getEvidenceInputContract(schema.unwrap() as z.ZodType);
|
||||
if (!child.hasEvidence) return identity(schema);
|
||||
if (!child.hasEvidence)
|
||||
return identity(schema, child.hasEvidenceReferenceAlternative);
|
||||
return {
|
||||
hasEvidence: true,
|
||||
hasEvidenceReferenceAlternative: child.hasEvidenceReferenceAlternative,
|
||||
frameSchema: replaceWrappedSchema(schema, child.frameSchema),
|
||||
resolve: (input, visit, path = []) =>
|
||||
input === undefined ||
|
||||
@@ -185,9 +205,14 @@ const objectContract = (schema: z.ZodObject): EvidenceInputContract => {
|
||||
[key, getEvidenceInputContract(child as z.ZodType)] as const,
|
||||
);
|
||||
const selected = children.filter(([, child]) => child.hasEvidence);
|
||||
if (selected.length === 0) return identity(schema);
|
||||
const hasEvidenceReferenceAlternative = children.some(
|
||||
([, child]) => child.hasEvidenceReferenceAlternative,
|
||||
);
|
||||
if (selected.length === 0)
|
||||
return identity(schema, hasEvidenceReferenceAlternative);
|
||||
return {
|
||||
hasEvidence: true,
|
||||
hasEvidenceReferenceAlternative,
|
||||
frameSchema: schema.clone(
|
||||
{
|
||||
...schema.def,
|
||||
@@ -215,9 +240,11 @@ const objectContract = (schema: z.ZodObject): EvidenceInputContract => {
|
||||
|
||||
const arrayContract = (schema: z.ZodArray): EvidenceInputContract => {
|
||||
const child = getEvidenceInputContract(schema.element as z.ZodType);
|
||||
if (!child.hasEvidence) return identity(schema);
|
||||
if (!child.hasEvidence)
|
||||
return identity(schema, child.hasEvidenceReferenceAlternative);
|
||||
return {
|
||||
hasEvidence: true,
|
||||
hasEvidenceReferenceAlternative: child.hasEvidenceReferenceAlternative,
|
||||
frameSchema: schema.clone(
|
||||
{ ...schema.def, element: child.frameSchema },
|
||||
{ parent: true },
|
||||
@@ -242,7 +269,11 @@ const unionContract = (schema: z.ZodUnion): EvidenceInputContract => {
|
||||
const options = schema.options.map((option) =>
|
||||
getEvidenceInputContract(option as z.ZodType),
|
||||
);
|
||||
if (!options.some((option) => option.hasEvidence)) return identity(schema);
|
||||
const hasEvidenceReferenceAlternative = options.some(
|
||||
(option) => option.hasEvidenceReferenceAlternative,
|
||||
);
|
||||
if (!options.some((option) => option.hasEvidence))
|
||||
return identity(schema, hasEvidenceReferenceAlternative);
|
||||
// Capture the successful branch while parsing; choosing it again later would
|
||||
// repeat defaults, overwrites and refinements. Keys are only parser results.
|
||||
const branches = new WeakMap<object, EvidenceInputContract>();
|
||||
@@ -257,6 +288,7 @@ const unionContract = (schema: z.ZodUnion): EvidenceInputContract => {
|
||||
);
|
||||
return {
|
||||
hasEvidence: true,
|
||||
hasEvidenceReferenceAlternative,
|
||||
frameSchema: frame.clone({
|
||||
...frame.def,
|
||||
...(schema.def.error === undefined ? {} : { error: schema.def.error }),
|
||||
|
||||
@@ -4,6 +4,7 @@ import { absoluteLocalPathStringSchema } from "./localPath.js";
|
||||
import {
|
||||
declareBorrowedEvidenceInput,
|
||||
declareEvidenceInput,
|
||||
declareEvidenceReferenceAlternative,
|
||||
} from "./evidenceInputContract.js";
|
||||
|
||||
/** Exact reference to Evidence already retained by the current session. */
|
||||
@@ -49,8 +50,7 @@ export const borrowEvidenceArray = () => {
|
||||
};
|
||||
|
||||
/** Evidence input, or one record read from a local Evidence file or bundle. */
|
||||
export const evidenceSourceInputSchema = z.union([
|
||||
...evidenceInputSchema.options,
|
||||
evidenceFileReferenceSchema,
|
||||
]);
|
||||
export const evidenceSourceInputSchema = declareEvidenceReferenceAlternative(
|
||||
z.union([...evidenceInputSchema.options, evidenceFileReferenceSchema]),
|
||||
);
|
||||
export type EvidenceSourceInput = z.output<typeof evidenceSourceInputSchema>;
|
||||
@@ -16,6 +16,8 @@ export interface RecordedMcpCatalog {
|
||||
readonly instructions: string;
|
||||
readonly identity: ReturnType<typeof createCatalogIdentity>;
|
||||
readonly tools: readonly Tool[];
|
||||
/** Tool names derived from canonical Evidence-input declarations at build time. */
|
||||
readonly evidenceReferenceAlternatives: readonly string[];
|
||||
readonly prompts: readonly (Prompt & {
|
||||
readonly parameters: Readonly<Record<string, PromptArgumentContract>>;
|
||||
})[];
|
||||
|
||||
@@ -51,6 +51,7 @@ const catalog: RecordedMcpCatalog = {
|
||||
outputSchema: z.toJSONSchema(output),
|
||||
},
|
||||
],
|
||||
evidenceReferenceAlternatives: [],
|
||||
prompts: [
|
||||
{
|
||||
name: "explain",
|
||||
@@ -78,6 +79,7 @@ const fixture = async (
|
||||
readonly invalidOutput?: boolean;
|
||||
readonly failAfterTool?: boolean;
|
||||
readonly maximumInputElements?: number;
|
||||
readonly evidenceReferenceAlternative?: boolean;
|
||||
} = {},
|
||||
) => {
|
||||
const started = barrier();
|
||||
@@ -85,7 +87,9 @@ const fixture = async (
|
||||
const closed = vi.fn();
|
||||
let loads = 0;
|
||||
const server = new DeferredEvidenceMcpServer(
|
||||
catalog,
|
||||
options.evidenceReferenceAlternative
|
||||
? { ...catalog, evidenceReferenceAlternatives: ["fixture"] }
|
||||
: catalog,
|
||||
new ToolResultDelivery(1024 * 1024),
|
||||
async (target) => {
|
||||
loads++;
|
||||
@@ -188,6 +192,26 @@ it("refuses excess input before loading a deferred runtime, then admits a valid
|
||||
expect(f.calls).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("uses recorded Evidence-reference guidance without loading the deferred runtime", async () => {
|
||||
const f = await fixture({
|
||||
maximumInputElements: 2,
|
||||
evidenceReferenceAlternative: true,
|
||||
});
|
||||
const result = await f.client.callTool({
|
||||
name: "fixture",
|
||||
arguments: { n: 3, other: [0] },
|
||||
});
|
||||
const text = result.content.find((part) => part.type === "text")?.text;
|
||||
expect(JSON.parse(text ?? "null")).toMatchObject({
|
||||
error: {
|
||||
code: "resource_constraint",
|
||||
remediation: { action: expect.stringContaining("retained-evidence") },
|
||||
},
|
||||
});
|
||||
expect(f.loads()).toBe(0);
|
||||
expect(f.calls).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("discovers without initialization, then shares one canonical runtime and preserves request metadata", async () => {
|
||||
const hold = barrier();
|
||||
const f = await fixture({ hold });
|
||||
|
||||
@@ -78,6 +78,8 @@ export class DeferredEvidenceMcpServer extends EvidenceMcpServerCore {
|
||||
"full",
|
||||
catalog.identity,
|
||||
);
|
||||
for (const name of catalog.evidenceReferenceAlternatives)
|
||||
this.registerEvidenceReferenceAlternative(name);
|
||||
for (const tool of catalog.tools) {
|
||||
const { name, inputSchema, outputSchema, ...metadata } = tool;
|
||||
let binding: DeferredTool;
|
||||
|
||||
@@ -62,6 +62,7 @@ import {
|
||||
} from "./toolResultEncoding.js";
|
||||
import { RequestTrackingMcpServer } from "./RequestTrackingMcpServer.js";
|
||||
import type { WithAdmittedAnalysis } from "./analysisAdmission.js";
|
||||
import { canReferenceInlineEvidence } from "./evidenceInputRegistration.js";
|
||||
import {
|
||||
currentMcpToolRequest,
|
||||
McpToolRequest,
|
||||
@@ -344,6 +345,11 @@ export class EvidenceMcpServerCore extends RequestTrackingMcpServer {
|
||||
| CompactInputSchemaPresentation
|
||||
| undefined;
|
||||
private readonly maximumInputElements: number;
|
||||
private readonly evidenceReferenceAlternatives = new Set<string>();
|
||||
|
||||
protected registerEvidenceReferenceAlternative(name: string): void {
|
||||
this.evidenceReferenceAlternatives.add(name);
|
||||
}
|
||||
|
||||
constructor(
|
||||
info: Implementation,
|
||||
@@ -392,6 +398,8 @@ export class EvidenceMcpServerCore extends RequestTrackingMcpServer {
|
||||
},
|
||||
callback: ToolCallback<InputArgs>,
|
||||
): RegisteredTool {
|
||||
if (canReferenceInlineEvidence(config.inputSchema))
|
||||
this.registerEvidenceReferenceAlternative(name);
|
||||
const budget = this.compactPresentation?.budgetBytes;
|
||||
const inputSchema =
|
||||
budget !== undefined &&
|
||||
@@ -467,6 +475,7 @@ export class EvidenceMcpServerCore extends RequestTrackingMcpServer {
|
||||
() => this.withAdmittedAnalysis,
|
||||
this.outputSchemaProfile,
|
||||
this.maximumInputElements,
|
||||
this.evidenceReferenceAlternatives,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -505,6 +514,7 @@ class ErrorEvidenceTransport implements Transport {
|
||||
private readonly currentAdmission: () => WithAdmittedAnalysis | undefined,
|
||||
private readonly outputSchemaProfile: McpOutputSchemaProfile,
|
||||
private readonly maximumInputElements: number,
|
||||
private readonly evidenceReferenceAlternatives: ReadonlySet<string>,
|
||||
) {
|
||||
if (transport.hasPerRequestStream !== undefined)
|
||||
this.hasPerRequestStream = transport.hasPerRequestStream;
|
||||
@@ -968,8 +978,9 @@ class ErrorEvidenceTransport implements Transport {
|
||||
"The MCP tool arguments exceed the configured input element allowance; schema validation and tool execution did not start.",
|
||||
{ max_input_elements: this.maximumInputElements },
|
||||
{
|
||||
remediationAction:
|
||||
"Reduce the combined array elements and object members in the arguments, or restart REA with a larger REA_MCP_MAX_INPUT_ELEMENTS allowance for this workload.",
|
||||
remediationAction: this.evidenceReferenceAlternatives.has(tool)
|
||||
? 'If the source Evidence is retained, use {"kind":"retained-evidence","evidence_id":"ev_..."} for declared Evidence fields; use a summary\'s parent_evidence_id for its complete source. Otherwise import the originating Evidence bundle when available. Reduce the arguments, or restart REA with a larger REA_MCP_MAX_INPUT_ELEMENTS allowance.'
|
||||
: "Reduce the combined array elements and object members in the arguments, or restart REA with a larger REA_MCP_MAX_INPUT_ELEMENTS allowance for this workload.",
|
||||
},
|
||||
);
|
||||
const result = this.delivery.toErrorToolResult(error, targetContent);
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
/** Internal registration facts derived from canonical Evidence input contracts. */
|
||||
const evidenceInputs = new WeakSet<object>();
|
||||
|
||||
export const markEvidenceInput = <T extends object>(schema: T): T => {
|
||||
evidenceInputs.add(schema);
|
||||
return schema;
|
||||
};
|
||||
|
||||
export const canReferenceInlineEvidence = (schema: unknown): boolean =>
|
||||
typeof schema === "object" && schema !== null && evidenceInputs.has(schema);
|
||||
@@ -9,6 +9,10 @@ import {
|
||||
toolInputSchemaWithMetadata,
|
||||
toolOutputSchemaWithMetadata,
|
||||
} from "../contracts/toolSchemaMetadata.js";
|
||||
import {
|
||||
canReferenceInlineEvidence,
|
||||
markEvidenceInput,
|
||||
} from "./evidenceInputRegistration.js";
|
||||
|
||||
/** Project the canonical Zod contracts directly into SDK registration. */
|
||||
export const toolRegistrationOptions = <Contract extends ToolContract>(
|
||||
@@ -40,7 +44,9 @@ export const toolRegistrationOptions = <Contract extends ToolContract>(
|
||||
return {
|
||||
title: contract.title,
|
||||
description: contract.description,
|
||||
inputSchema,
|
||||
inputSchema: evidenceInputs.hasEvidenceReferenceAlternative
|
||||
? markEvidenceInput(inputSchema)
|
||||
: inputSchema,
|
||||
outputSchema: toolOutputSchemaWithMetadata(contract),
|
||||
annotations: contract.annotations,
|
||||
};
|
||||
@@ -60,5 +66,10 @@ export const applicationToolRegistrationOptions = <
|
||||
validate: (value: unknown) => ({ value }),
|
||||
},
|
||||
};
|
||||
return { ...registration, inputSchema };
|
||||
return {
|
||||
...registration,
|
||||
inputSchema: canReferenceInlineEvidence(registration.inputSchema)
|
||||
? markEvidenceInput(inputSchema)
|
||||
: inputSchema,
|
||||
};
|
||||
};
|
||||
@@ -87,68 +87,113 @@ it("recovers a missing retained closure by importing the originating bundle", as
|
||||
expect(parseEvidence(retained.structuredContent)).toEqual(aggregate);
|
||||
});
|
||||
|
||||
it("aggregates selected retained observations within a caller's input budget", async () => {
|
||||
// Distinct authenticated observations may have identical derived contents.
|
||||
const comparisons = Array.from({ length: 12 }, (_, observation) =>
|
||||
createEvidence(undefined, FUNCTION_COMPARISON_EVIDENCE.provider, {
|
||||
operation: "compare_functions",
|
||||
predicateType: "rea.function-comparison",
|
||||
parameters: { ...FUNCTION_COMPARISON_EVIDENCE.parameters, observation },
|
||||
result: FUNCTION_COMPARISON_EVIDENCE.normalized_result,
|
||||
confidence: "derived",
|
||||
authority: "analyst-inference",
|
||||
limitations: FUNCTION_COMPARISON_EVIDENCE.limitations,
|
||||
evidenceLinks: FUNCTION_COMPARISON_EVIDENCE.evidence_links,
|
||||
}),
|
||||
);
|
||||
const directory = await createTestTempDirectory("rea-retained-comparisons-");
|
||||
const path = join(directory, "comparisons.json");
|
||||
await writeFile(
|
||||
path,
|
||||
JSON.stringify(
|
||||
createEvidenceBundle([
|
||||
FUNCTION_COMPARISON_EXAMPLE.left,
|
||||
FUNCTION_COMPARISON_EXAMPLE.right,
|
||||
...comparisons,
|
||||
]),
|
||||
),
|
||||
);
|
||||
const { call } = await connectLocalToolsMcp({
|
||||
REA_MCP_MAX_INPUT_ELEMENTS: "1024",
|
||||
});
|
||||
const imported = await call("import_evidence_bundle", { path });
|
||||
expect(imported.isError, JSON.stringify(imported)).not.toBe(true);
|
||||
const inline = await call("find_changed_behavior", { comparisons });
|
||||
expect(parseMcpToolError(inline).error).toMatchObject({
|
||||
code: "resource_constraint",
|
||||
details: { reported_limits: { max_input_elements: 1024 } },
|
||||
});
|
||||
const response = await call("find_changed_behavior", {
|
||||
comparisons: comparisons.map(({ evidence_id }) => ({
|
||||
kind: "retained-evidence",
|
||||
evidence_id,
|
||||
})),
|
||||
});
|
||||
expect(response.isError, JSON.stringify(response)).not.toBe(true);
|
||||
const aggregate = parseEvidence(response.structuredContent);
|
||||
const result = changedBehaviorResultSchema.parse(aggregate.normalized_result);
|
||||
expect(result.behavior_status).toBe("unknown");
|
||||
expect(result.summary.observed_changes).toBe(0);
|
||||
expect(aggregate.parameters.comparison_evidence_ids).toEqual(
|
||||
comparisons.map(({ evidence_id }) => evidence_id),
|
||||
);
|
||||
expect(
|
||||
new Set(
|
||||
result.findings.items.map(
|
||||
({ source_comparison_id }) => source_comparison_id,
|
||||
it.each(["compact", "full"] as const)(
|
||||
"aggregates retained observations within the %s input profile budget",
|
||||
async (profile) => {
|
||||
// Distinct authenticated observations may have identical derived contents.
|
||||
const comparisons = Array.from({ length: 12 }, (_, observation) =>
|
||||
createEvidence(undefined, FUNCTION_COMPARISON_EVIDENCE.provider, {
|
||||
operation: "compare_functions",
|
||||
predicateType: "rea.function-comparison",
|
||||
parameters: { ...FUNCTION_COMPARISON_EVIDENCE.parameters, observation },
|
||||
result: FUNCTION_COMPARISON_EVIDENCE.normalized_result,
|
||||
confidence: "derived",
|
||||
authority: "analyst-inference",
|
||||
limitations: FUNCTION_COMPARISON_EVIDENCE.limitations,
|
||||
evidenceLinks: FUNCTION_COMPARISON_EVIDENCE.evidence_links,
|
||||
}),
|
||||
);
|
||||
const directory = await createTestTempDirectory(
|
||||
"rea-retained-comparisons-",
|
||||
);
|
||||
const path = join(directory, "comparisons.json");
|
||||
await writeFile(
|
||||
path,
|
||||
JSON.stringify(
|
||||
createEvidenceBundle([
|
||||
FUNCTION_COMPARISON_EXAMPLE.left,
|
||||
FUNCTION_COMPARISON_EXAMPLE.right,
|
||||
...comparisons,
|
||||
]),
|
||||
),
|
||||
),
|
||||
).toEqual(new Set(comparisons.map(({ evidence_id }) => evidence_id)));
|
||||
expect(aggregate.evidence_links).toEqual(
|
||||
expect.arrayContaining([
|
||||
FUNCTION_COMPARISON_EXAMPLE.left.evidence_id,
|
||||
FUNCTION_COMPARISON_EXAMPLE.right.evidence_id,
|
||||
...comparisons.map(({ evidence_id }) => evidence_id),
|
||||
]),
|
||||
);
|
||||
});
|
||||
);
|
||||
const { call } = await connectLocalToolsMcp({
|
||||
REA_MCP_MAX_INPUT_ELEMENTS: "1024",
|
||||
REA_MCP_INPUT_SCHEMA_PROFILE: profile,
|
||||
});
|
||||
// Both refusals happen before any call initializes the deferred runtime.
|
||||
const inline = await call("find_changed_behavior", { comparisons });
|
||||
expect(parseMcpToolError(inline).error).toMatchObject({
|
||||
code: "resource_constraint",
|
||||
remediation: { action: expect.stringContaining("retained-evidence") },
|
||||
details: { reported_limits: { max_input_elements: 1024 } },
|
||||
});
|
||||
const unrelated = parseMcpToolError(
|
||||
await call("read_bytes", {
|
||||
address: "0x1000",
|
||||
length: 1,
|
||||
extra: Array.from({ length: 1024 }, () => 0),
|
||||
}),
|
||||
).error;
|
||||
expect(unrelated.code).toBe("resource_constraint");
|
||||
expect(unrelated.remediation.action).not.toContain("retained-evidence");
|
||||
const inlineView = parseMcpToolError(
|
||||
await call("inspect_analysis_view", {
|
||||
source: {
|
||||
kind: "inline",
|
||||
evidence: {
|
||||
...FUNCTION_COMPARISON_EXAMPLE.left,
|
||||
limitations: Array.from({ length: 1024 }, () => "limit"),
|
||||
},
|
||||
},
|
||||
view: { kind: "summary" },
|
||||
}),
|
||||
).error;
|
||||
expect(inlineView.code).toBe("resource_constraint");
|
||||
expect(inlineView.remediation.action).toContain("retained-evidence");
|
||||
const referenceOnly = parseMcpToolError(
|
||||
await call("export_environment_bundle", {
|
||||
source: {
|
||||
kind: "retained-evidence",
|
||||
evidence_id: FUNCTION_COMPARISON_EVIDENCE.evidence_id,
|
||||
},
|
||||
output_directory: "/unused",
|
||||
template_rules: Array.from({ length: 1024 }, () => "unused"),
|
||||
}),
|
||||
).error;
|
||||
expect(referenceOnly.code).toBe("resource_constraint");
|
||||
expect(referenceOnly.remediation.action).not.toContain("retained-evidence");
|
||||
const imported = await call("import_evidence_bundle", { path });
|
||||
expect(imported.isError, JSON.stringify(imported)).not.toBe(true);
|
||||
const response = await call("find_changed_behavior", {
|
||||
comparisons: comparisons.map(({ evidence_id }) => ({
|
||||
kind: "retained-evidence",
|
||||
evidence_id,
|
||||
})),
|
||||
});
|
||||
expect(response.isError, JSON.stringify(response)).not.toBe(true);
|
||||
const aggregate = parseEvidence(response.structuredContent);
|
||||
const result = changedBehaviorResultSchema.parse(
|
||||
aggregate.normalized_result,
|
||||
);
|
||||
expect(result.behavior_status).toBe("unknown");
|
||||
expect(result.summary.observed_changes).toBe(0);
|
||||
expect(aggregate.parameters.comparison_evidence_ids).toEqual(
|
||||
comparisons.map(({ evidence_id }) => evidence_id),
|
||||
);
|
||||
expect(
|
||||
new Set(
|
||||
result.findings.items.map(
|
||||
({ source_comparison_id }) => source_comparison_id,
|
||||
),
|
||||
),
|
||||
).toEqual(new Set(comparisons.map(({ evidence_id }) => evidence_id)));
|
||||
expect(aggregate.evidence_links).toEqual(
|
||||
expect.arrayContaining([
|
||||
FUNCTION_COMPARISON_EXAMPLE.left.evidence_id,
|
||||
FUNCTION_COMPARISON_EXAMPLE.right.evidence_id,
|
||||
...comparisons.map(({ evidence_id }) => evidence_id),
|
||||
]),
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -670,23 +670,41 @@ const copyCompiledPackage = async (prefix: string): Promise<string> => {
|
||||
return directory;
|
||||
};
|
||||
|
||||
/**
|
||||
* Modules `record-mcp-catalog.mjs` loads before it owns cleanup. The fixture
|
||||
* replaces every one of these; an import outside the list fails closed.
|
||||
*/
|
||||
const RECORDER_RUNTIME_MODULES = [
|
||||
"server/createServer.js",
|
||||
"composition/binary.js",
|
||||
"config/parseConfig.js",
|
||||
"logger.js",
|
||||
"contracts/promptContracts.js",
|
||||
"contracts/toolContracts.js",
|
||||
"domain/evidenceInputContract.js",
|
||||
] as const;
|
||||
|
||||
// Recorder cleanup tests replace every runtime import through their loader
|
||||
// hook. Keep only resolvable sentinels so an unmodeled import fails closed.
|
||||
const createRecorderCleanupFixture = async (): Promise<string> => {
|
||||
const directory = await createTestTempDirectory("rea-catalog-cleanup-");
|
||||
await mkdir(join(directory, "scripts"), { recursive: true });
|
||||
await cp(
|
||||
resolve("scripts/record-mcp-catalog.mjs"),
|
||||
join(directory, "scripts/record-mcp-catalog.mjs"),
|
||||
);
|
||||
const scriptPath = resolve("scripts/record-mcp-catalog.mjs");
|
||||
const imported = [
|
||||
...((await readFile(scriptPath, "utf8")).matchAll(/read\("([^"]+)"\)/g) ??
|
||||
[]),
|
||||
].map((match) => match[1]);
|
||||
if (
|
||||
imported.length !== RECORDER_RUNTIME_MODULES.length ||
|
||||
imported.some((name, index) => name !== RECORDER_RUNTIME_MODULES[index])
|
||||
) {
|
||||
throw new Error(
|
||||
`record-mcp-catalog.mjs runtime imports ${JSON.stringify(imported)} do not match the cleanup fixture ${JSON.stringify(RECORDER_RUNTIME_MODULES)}`,
|
||||
);
|
||||
}
|
||||
await cp(scriptPath, join(directory, "scripts/record-mcp-catalog.mjs"));
|
||||
await cp(resolve("package.json"), join(directory, "package.json"));
|
||||
for (const path of [
|
||||
"server/createServer.js",
|
||||
"composition/binary.js",
|
||||
"config/parseConfig.js",
|
||||
"logger.js",
|
||||
"contracts/promptContracts.js",
|
||||
]) {
|
||||
for (const path of RECORDER_RUNTIME_MODULES) {
|
||||
const file = join(directory, "dist", path);
|
||||
await mkdir(dirname(file), { recursive: true });
|
||||
await writeFile(
|
||||
@@ -771,6 +789,11 @@ it.each([
|
||||
"/dist/logger.js": "export const silentLogger = {};",
|
||||
"/dist/contracts/promptContracts.js":
|
||||
"export const PROMPT_CONTRACTS = [];",
|
||||
// The recorder derives Evidence-input names from these contracts. This
|
||||
// fault injection has no tools, so the derived list stays empty.
|
||||
"/dist/contracts/toolContracts.js": "export const TOOL_CONTRACTS = [];",
|
||||
"/dist/domain/evidenceInputContract.js":
|
||||
"export const getEvidenceInputContract = () => ({ hasEvidenceReferenceAlternative: false });",
|
||||
};
|
||||
const clientModule = `data:text/javascript,${encodeURIComponent(`
|
||||
export class Client {
|
||||
@@ -803,6 +826,9 @@ registerHooks({
|
||||
},
|
||||
});
|
||||
`)}`;
|
||||
expect(
|
||||
Object.keys(replacements).map((path) => path.slice("/dist/".length)),
|
||||
).toEqual([...RECORDER_RUNTIME_MODULES]);
|
||||
const child = spawnSync(
|
||||
process.execPath,
|
||||
[`--import=${hook}`, join(directory, "scripts/record-mcp-catalog.mjs")],
|
||||
|
||||
@@ -108,7 +108,10 @@ it("keeps input capacity limits while composing dossiers that cannot be resent i
|
||||
arguments: { ...selection, functions: [source] },
|
||||
});
|
||||
expect(parseMcpToolError(inline)).toMatchObject({
|
||||
error: { code: "resource_constraint" },
|
||||
error: {
|
||||
code: "resource_constraint",
|
||||
remediation: { action: expect.stringContaining("retained-evidence") },
|
||||
},
|
||||
});
|
||||
const retained = await client.callTool({
|
||||
name: "build_call_path",
|
||||
|
||||
@@ -136,6 +136,7 @@ processTest(
|
||||
});
|
||||
} else {
|
||||
expect(error.code).toBe("resource_constraint");
|
||||
expect(error.remediation.action).toContain("retained-evidence");
|
||||
expect(error.details).toMatchObject({
|
||||
resource: "transport",
|
||||
reported_limits: { max_input_elements: 1_024 },
|
||||
@@ -183,6 +184,9 @@ processTest(
|
||||
});
|
||||
const calibratedError = parseMcpToolError(calibration).error;
|
||||
expect(calibratedError.code).toBe("resource_constraint");
|
||||
expect(calibratedError.remediation.action).not.toContain(
|
||||
"retained-evidence",
|
||||
);
|
||||
expect(calibratedError.details).toMatchObject({
|
||||
resource: "transport",
|
||||
reported_limits: { max_input_elements: 1_024 },
|
||||
|
||||
Reference in new issue
Block a user