- Add answerMeaning schema with supportCategory and resolutionGuidance enums - Add pre-mutation guard that validates proposal alignment with answerMeaning - Update prompt builder to instruct the model on answerMeaning contract - Add tests for schema, guard logic, parsing defaults, and regression cases A-D
174 lines
5.0 KiB
JavaScript
174 lines
5.0 KiB
JavaScript
import { describe, expect, it } from "vitest";
|
|
import { parseGraphUpdateProposal } from "@/lib/graph/update-proposal.js";
|
|
|
|
function makeValidProposal(overrides = {}) {
|
|
return {
|
|
addedNodes: [],
|
|
updatedNodes: [
|
|
{
|
|
nodeId: "n-unknown",
|
|
previousStatus: "unknown",
|
|
newStatus: "resolved",
|
|
previousValue: null,
|
|
newValue: "1.9 complaints per 100 units",
|
|
reason: "The answer directly provides the normalized complaint rate.",
|
|
},
|
|
],
|
|
addedEdges: [],
|
|
removedEdgeIds: [],
|
|
resolvedUnknownNodeIds: ["n-unknown"],
|
|
affectedNodeIds: [],
|
|
selectedQuestion: null,
|
|
answerMeaning: {
|
|
userSupportedMeaning:
|
|
"The user directly provided the updated complaint rate.",
|
|
possibleInference: null,
|
|
supportCategory: "other",
|
|
resolutionGuidance: "may_resolve",
|
|
},
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe("parseGraphUpdateProposal", () => {
|
|
it("parses a valid proposal", () => {
|
|
const result = parseGraphUpdateProposal(makeValidProposal());
|
|
expect(result.success).toBe(true);
|
|
expect(result.proposal.updatedNodes).toHaveLength(1);
|
|
});
|
|
|
|
it("fails on malformed JSON", () => {
|
|
const result = parseGraphUpdateProposal("{not json");
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
it("fails when required update content is invalid", () => {
|
|
const result = parseGraphUpdateProposal({
|
|
updatedNodes: [{ nodeId: "n-unknown" }],
|
|
});
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
it("removes null array entries and logs them", () => {
|
|
const result = parseGraphUpdateProposal(
|
|
JSON.stringify({
|
|
...makeValidProposal(),
|
|
addedNodes: [null],
|
|
}),
|
|
);
|
|
expect(result.success).toBe(true);
|
|
expect(result.proposal.addedNodes).toEqual([]);
|
|
expect(result.normalisationsApplied).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({ change: "Removed null array entry" }),
|
|
]),
|
|
);
|
|
});
|
|
|
|
it("fills missing optional arrays with empty arrays", () => {
|
|
const result = parseGraphUpdateProposal({
|
|
updatedNodes: [],
|
|
});
|
|
expect(result.success).toBe(true);
|
|
expect(result.proposal.addedNodes).toEqual([]);
|
|
expect(result.proposal.addedEdges).toEqual([]);
|
|
expect(result.normalisationsApplied.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("normalises confirmed enum alias and preserves IDs", () => {
|
|
const result = parseGraphUpdateProposal({
|
|
...makeValidProposal(),
|
|
addedNodes: [
|
|
{
|
|
id: "n-new",
|
|
label: "Reported update",
|
|
description: "A new reported claim",
|
|
kind: "reported_statement",
|
|
status: "supported",
|
|
confidence: "medium",
|
|
value: null,
|
|
unit: null,
|
|
evidenceIds: [],
|
|
dependsOn: [],
|
|
affects: [],
|
|
parentId: null,
|
|
childIds: [],
|
|
},
|
|
],
|
|
});
|
|
expect(result.success).toBe(true);
|
|
expect(result.proposal.addedNodes[0].kind).toBe("reported_claim");
|
|
expect(result.proposal.addedNodes[0].id).toBe("n-new");
|
|
});
|
|
|
|
it("unknown enum values still fail", () => {
|
|
const result = parseGraphUpdateProposal({
|
|
...makeValidProposal(),
|
|
addedNodes: [
|
|
{
|
|
id: "n-new",
|
|
label: "Bad node",
|
|
description: "Bad node",
|
|
kind: "unsupported_kind",
|
|
status: "supported",
|
|
confidence: "medium",
|
|
value: null,
|
|
unit: null,
|
|
evidenceIds: [],
|
|
dependsOn: [],
|
|
affects: [],
|
|
parentId: null,
|
|
childIds: [],
|
|
},
|
|
],
|
|
});
|
|
expect(result.success).toBe(false);
|
|
});
|
|
|
|
it("defaults missing selectedQuestion to null", () => {
|
|
const result = parseGraphUpdateProposal({
|
|
addedNodes: [],
|
|
updatedNodes: [],
|
|
addedEdges: [],
|
|
removedEdgeIds: [],
|
|
resolvedUnknownNodeIds: [],
|
|
affectedNodeIds: [],
|
|
});
|
|
expect(result.success).toBe(true);
|
|
expect(result.proposal.selectedQuestion).toBeNull();
|
|
});
|
|
|
|
it("defaults missing answerMeaning to null", () => {
|
|
const result = parseGraphUpdateProposal({
|
|
addedNodes: [],
|
|
updatedNodes: [],
|
|
addedEdges: [],
|
|
removedEdgeIds: [],
|
|
resolvedUnknownNodeIds: [],
|
|
affectedNodeIds: [],
|
|
selectedQuestion: null,
|
|
});
|
|
expect(result.success).toBe(true);
|
|
expect(result.proposal.answerMeaning).toBeNull();
|
|
});
|
|
|
|
it("parses a valid selectedQuestion", () => {
|
|
const result = parseGraphUpdateProposal(
|
|
makeValidProposal({
|
|
selectedQuestion: {
|
|
nodeId: "n-follow-up",
|
|
question: "How should commercial value be defined for this decision?",
|
|
reason: "A consequential unknown remains unresolved.",
|
|
},
|
|
}),
|
|
);
|
|
expect(result.success).toBe(true);
|
|
expect(result.proposal.selectedQuestion?.nodeId).toBe("n-follow-up");
|
|
});
|
|
|
|
it("does not invent a next question field outside the contract", () => {
|
|
const result = parseGraphUpdateProposal(makeValidProposal());
|
|
expect(result.proposal.nextQuestion).toBeUndefined();
|
|
});
|
|
});
|