fix(functions): name the rejected side and supplied kind in function Evidence errors (#6969)

fix(functions): name the rejected side and supplied kind in Evidence errors

Function comparison previously reported only that analyze_function/rea.analysis Evidence was required, leaving callers to discover which side was wrong and what was supplied. Include the side, its operation and predicate, and the expected pair in the input issue.

Co-authored-by: dhruvac29 <dhruvac29@users.noreply.github.com>
This commit is contained in:
Dhruvilkumar Ashokbhai Chodvadiya
2026-10-24 00:44:39 -07:00
committed by GitHub
parent 72ba1cd92b
commit e00ee310f5
2 changed files with 139 additions and 44 deletions
+19 -4
View File
@@ -2,6 +2,7 @@ import type { Evidence } from "./evidence.js";
import { AnalysisInputError } from "./analysisErrorCore.js";
import type { EvidenceIntegrityError } from "./evidenceErrors.js";
import { readEvidence, readEvidencePayload } from "./evidenceValidation.js";
import type { JsonValue } from "./jsonValue.js";
import { err, ok, type Result } from "./result.js";
import { compareUnicodeCodePoints } from "./unicodeCodePointOrder.js";
import { functionDossierSchema, type FunctionDossier } from "./hopperValues.js";
@@ -29,19 +30,33 @@ export const parseFunctionEvidence = (
const parsed = readEvidence(input);
if (!parsed.ok) return parsed;
const evidence = parsed.value;
const invalidSelection = (message: string) =>
const invalidSelection = (message: string, expected?: JsonValue) =>
err(
new AnalysisInputError(selection.operation, { userMessage: message }, [
{ path: selection.path, reason: "invalid_value", message },
{
path: selection.path,
reason: "invalid_value",
message,
...(expected === undefined ? {} : { expected }),
},
]),
);
if (
evidence.operation !== "analyze_function" ||
evidence.predicate_type !== "rea.analysis"
)
) {
const location = selection.path.map(String).join(".");
const subject =
location === ""
? "supplied Evidence"
: selection.path.length === 1
? `${location} Evidence`
: `Evidence at ${location}`;
return invalidSelection(
"Function analysis requires analyze_function Evidence with the rea.analysis predicate",
`Function analysis requires analyze_function Evidence with the rea.analysis predicate; the ${subject} has operation ${evidence.operation} and predicate ${evidence.predicate_type}.`,
{ operation: "analyze_function", predicate_type: "rea.analysis" },
);
}
if (evidence.subject === null)
return invalidSelection(
"Function analysis requires artifact-bound Evidence",
+120 -40
View File
@@ -258,6 +258,20 @@ it.each([
},
);
const wrongKindFunctionEvidence = (mismatch: "operation" | "predicate") =>
createEvidence(
undefined,
{ id: "fixture", name: "Fixture", version: "1" },
{
operation:
mismatch === "operation" ? "inventory_artifact" : "analyze_function",
predicateType:
mismatch === "predicate" ? "rea.artifact-inventory" : "rea.analysis",
parameters: {},
result: {},
},
);
describe("function comparison MCP integration", () => {
it("records linked comparison Evidence and its residual unknown", async () => {
const session = createTestBinarySession(() => ({
@@ -353,50 +367,116 @@ describe("function comparison MCP integration", () => {
]);
}
});
});
it("rejects inline Evidence with the wrong operation", async () => {
const session = createTestBinarySession(() => ({
health: () => Promise.resolve(),
execute: () => Promise.resolve(observed(null)),
close: () => Promise.resolve(resultOk(null)),
}));
const wrong = createEvidence(
undefined,
{ id: "fixture", name: "Fixture", version: "1" },
{
operation: "inventory_artifact",
parameters: {},
result: {},
},
);
session.recordEvidence(wrong);
session.recordEvidence(FUNCTION_COMPARISON_EXAMPLE.right);
const server = createServer({ kind: "session", session });
const client = new Client({
name: "function-authority-test",
version: "1",
});
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
try {
await server.connect(serverTransport);
await client.connect(clientTransport);
const result = await client.callTool({
describe("function comparison input-kind diagnostics", () => {
it.each([
{ side: "left", mismatch: "operation" },
{ side: "right", mismatch: "operation" },
{ side: "left", mismatch: "predicate" },
{ side: "right", mismatch: "predicate" },
] as const)(
"rejects inline $side Evidence with the wrong $mismatch as an input-selection error",
async ({ side, mismatch }) => {
const session = createTestBinarySession(() => {
throw new Error("Wrong-kind function input must not launch a provider");
});
const harness = await createApplicationMcpHarness({ session });
onTestFinished(harness.close);
const wrong = wrongKindFunctionEvidence(mismatch);
const good =
FUNCTION_COMPARISON_EXAMPLE[side === "left" ? "right" : "left"];
const result = await harness.client.callTool({
name: "compare_functions",
arguments: {
left: wrong,
right: FUNCTION_COMPARISON_EXAMPLE.right,
arguments:
side === "left"
? { left: wrong, right: good }
: { left: good, right: wrong },
});
expect(parseMcpToolError(result).error).toMatchObject({
code: "invalid_request",
category: "invalid_input",
recovery: {
kind: "inspect_input_contract",
operation: "compare_functions",
},
details: {
operation: "compare_functions",
issues: [
{
path: [side],
reason: "invalid_value",
message: `Function analysis requires analyze_function Evidence with the rea.analysis predicate; the ${side} Evidence has operation ${wrong.operation} and predicate ${wrong.predicate_type}.`,
expected: {
operation: "analyze_function",
predicate_type: "rea.analysis",
},
},
],
},
});
expect(result.isError).toBe(true);
expect(session.exportEvidenceBundle().records).toHaveLength(2);
} finally {
await Promise.allSettled([
client.close(),
server.close(),
session.close(),
]);
}
},
);
it("names the retained wrong-kind Evidence side", async () => {
const session = createTestBinarySession(() => {
throw new Error("Wrong-kind retained input must not launch a provider");
});
const wrong = wrongKindFunctionEvidence("operation");
expect(session.recordEvidence(wrong).ok).toBe(true);
expect(session.recordEvidence(FUNCTION_COMPARISON_EXAMPLE.right).ok).toBe(
true,
);
const harness = await createApplicationMcpHarness({ session });
onTestFinished(harness.close);
const result = await harness.client.callTool({
name: "compare_functions",
arguments: {
left: { kind: "retained-evidence", evidence_id: wrong.evidence_id },
right: {
kind: "retained-evidence",
evidence_id: FUNCTION_COMPARISON_EXAMPLE.right.evidence_id,
},
},
});
expect(parseMcpToolError(result).error).toMatchObject({
code: "invalid_request",
category: "invalid_input",
details: {
issues: [
{
path: ["left"],
message: expect.stringContaining(
"the left Evidence has operation inventory_artifact",
),
},
],
},
});
expect(session.exportEvidenceBundle().records).toHaveLength(2);
});
it("keeps tampered function Evidence an integrity failure", async () => {
const session = createTestBinarySession(() => {
throw new Error("Tampered function input must not launch a provider");
});
const harness = await createApplicationMcpHarness({ session });
onTestFinished(harness.close);
const tampered = {
...FUNCTION_COMPARISON_EXAMPLE.left,
limitations: [
...FUNCTION_COMPARISON_EXAMPLE.left.limitations,
"tampered",
],
};
const result = await harness.client.callTool({
name: "compare_functions",
arguments: { left: tampered, right: FUNCTION_COMPARISON_EXAMPLE.right },
});
expect(parseMcpToolError(result).error).toMatchObject({
code: "evidence_integrity_mismatch",
category: "integrity_mismatch",
});
});
});