Adds rejectedProposalSnapshot to orchestrator diagnostics for proposal_compatibility rejections — exposing answerMeaning (userSupportedMeaning, possibleInference), addedNodes structural fields, addedEdges structural fields, updatedNodes summaries, and resolvedUnknownNodeIds. Diagnostic evidence only; does not alter validation, mutation, or error messages. Stage-gated to proposal_compatibility only.
241 lines
9.1 KiB
JavaScript
241 lines
9.1 KiB
JavaScript
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
const mockUpdateCase = vi.fn();
|
|
|
|
vi.mock("@/lib/graph/orchestrator.js", () => ({
|
|
updateCase: (...args) => mockUpdateCase(...args),
|
|
}));
|
|
|
|
// Helper that simulates a proposal_compatibility rejection (used in some tests)
|
|
function createCompetitionRejection() {
|
|
return {
|
|
success: false,
|
|
stage: "proposal_compatibility",
|
|
errors: [
|
|
"answerMeaning.userSupportedMeaning introduces a stronger reasoning category than the raw answer establishes.",
|
|
"New unknown must be explicitly related to an answer-derived node",
|
|
],
|
|
};
|
|
}
|
|
|
|
// Helper that returns a rejectedProposalSnapshot (mirrors the orchestrator logic)
|
|
function snapshotFromProposal(proposal) {
|
|
if (!proposal) return null;
|
|
return {
|
|
answerMeaning: proposal.answerMeaning
|
|
? {
|
|
userSupportedMeaning: proposal.answerMeaning.userSupportedMeaning ?? null,
|
|
possibleInference: proposal.answerMeaning.possibleInference ?? null,
|
|
}
|
|
: null,
|
|
updatedNodes: (proposal.updatedNodes ?? []).map((n) => ({
|
|
nodeId: n.nodeId,
|
|
newValue: n.newValue,
|
|
})),
|
|
resolvedUnknownNodeIds: proposal.resolvedUnknownNodeIds ?? [],
|
|
addedNodes: (proposal.addedNodes ?? []).map((n) => ({
|
|
id: n.id,
|
|
kind: n.kind,
|
|
label: n.label,
|
|
description: n.description,
|
|
parentId: n.parentId ?? null,
|
|
dependsOn: n.dependsOn ?? [],
|
|
affects: n.affects ?? [],
|
|
childIds: n.childIds ?? [],
|
|
})),
|
|
addedEdges: (proposal.addedEdges ?? []).map((e) => ({
|
|
fromNodeId: e.fromNodeId,
|
|
toNodeId: e.toNodeId,
|
|
relationship: e.relationship,
|
|
})),
|
|
};
|
|
}
|
|
|
|
// Simulate the orchestrator's failure return path
|
|
function simulateOrchestratorFailure({ situationGraph, proposal }) {
|
|
const applicationResult = createCompetitionRejection();
|
|
const rejectedProposalSnapshot =
|
|
applicationResult.stage === "proposal_compatibility" && proposal
|
|
? snapshotFromProposal(proposal)
|
|
: null;
|
|
|
|
return {
|
|
success: false,
|
|
stage: applicationResult.stage,
|
|
errors: applicationResult.errors,
|
|
diagnostics: { rejectedProposalSnapshot },
|
|
};
|
|
}
|
|
|
|
// Simulate a non-proposal_compatibility failure (should NOT include snapshot)
|
|
function simulateNonCompetitionFailure() {
|
|
return {
|
|
success: false,
|
|
stage: "application",
|
|
errors: ["Could not apply graph update"],
|
|
diagnostics: {},
|
|
};
|
|
}
|
|
|
|
describe("rejected proposal compatibility snapshot (orchestrator diagnostics)", () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
mockUpdateCase.mockReset();
|
|
});
|
|
|
|
it("includes rejectedProposalSnapshot when stage is proposal_compatibility", async () => {
|
|
const situationGraph = { centralStatement: "test", nodes: [], edges: [] };
|
|
const proposal = {
|
|
answerMeaning: {
|
|
userSupportedMeaning: "The user requires evidence for both financial savings and engineering retention.",
|
|
possibleInference: "Personnel retention is being treated as a veto constraint alongside financial justification.",
|
|
},
|
|
updatedNodes: [{ nodeId: "n-x", newValue: "resolved value" }],
|
|
resolvedUnknownNodeIds: ["n-resolved"],
|
|
addedNodes: [
|
|
{
|
|
id: "n-savings-realism",
|
|
kind: "unknown",
|
|
label: "Savings realism",
|
|
description: "Are projected savings realistic?",
|
|
parentId: null,
|
|
dependsOn: [],
|
|
affects: ["neb1bz2"],
|
|
childIds: [],
|
|
},
|
|
],
|
|
addedEdges: [
|
|
{ id: "e-1", fromNodeId: "n-savings-realism", toNodeId: "neb1bz2", relationship: "depends_on", confidence: "medium", description: "dep" },
|
|
],
|
|
};
|
|
|
|
mockUpdateCase.mockImplementation(async (body) =>
|
|
simulateOrchestratorFailure(body),
|
|
);
|
|
|
|
const result = await mockUpdateCase({ situationGraph, proposal });
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(result.stage).toBe("proposal_compatibility");
|
|
expect(result.diagnostics.rejectedProposalSnapshot).toBeDefined();
|
|
});
|
|
|
|
it("exposes answerMeaning.userSupportedMeaning and possibleInference in the snapshot", async () => {
|
|
const situationGraph = { centralStatement: "test", nodes: [], edges: [] };
|
|
const answerMeaningWithMeaning = {
|
|
userSupportedMeaning: "The user requires evidence for financial savings.",
|
|
possibleInference: "This is being treated as a hard constraint.",
|
|
};
|
|
const proposal = { answerMeaning: answerMeaningWithMeaning, updatedNodes: [], resolvedUnknownNodeIds: [], addedNodes: [], addedEdges: [] };
|
|
|
|
mockUpdateCase.mockImplementation(async (body) =>
|
|
simulateOrchestratorFailure(body),
|
|
);
|
|
|
|
const result = await mockUpdateCase({ situationGraph, proposal });
|
|
|
|
expect(result.diagnostics.rejectedProposalSnapshot.answerMeaning.userSupportedMeaning).toBe(
|
|
"The user requires evidence for financial savings.",
|
|
);
|
|
expect(result.diagnostics.rejectedProposalSnapshot.answerMeaning.possibleInference).toBe(
|
|
"This is being treated as a hard constraint.",
|
|
);
|
|
});
|
|
|
|
it("exposes added unknown label, description and structural references", async () => {
|
|
const situationGraph = { centralStatement: "test", nodes: [], edges: [] };
|
|
const addedNode = {
|
|
id: "n-savings-realism",
|
|
kind: "unknown",
|
|
label: "Are projected savings realistic?",
|
|
description: "Need evidence that the office savings estimates are defensible.",
|
|
parentId: null,
|
|
dependsOn: ["neb1bz2"],
|
|
affects: [],
|
|
childIds: [],
|
|
};
|
|
const proposal = { answerMeaning: null, updatedNodes: [], resolvedUnknownNodeIds: [], addedNodes: [addedNode], addedEdges: [] };
|
|
|
|
mockUpdateCase.mockImplementation(async (body) =>
|
|
simulateOrchestratorFailure(body),
|
|
);
|
|
|
|
const result = await mockUpdateCase({ situationGraph, proposal });
|
|
|
|
expect(result.diagnostics.rejectedProposalSnapshot.addedNodes).toHaveLength(1);
|
|
const node = result.diagnostics.rejectedProposalSnapshot.addedNodes[0];
|
|
expect(node.id).toBe("n-savings-realism");
|
|
expect(node.kind).toBe("unknown");
|
|
expect(node.label).toBe("Are projected savings realistic?");
|
|
expect(node.description).toBe("Need evidence that the office savings estimates are defensible.");
|
|
expect(node.parentId).toBe(null);
|
|
expect(node.dependsOn).toEqual(["neb1bz2"]);
|
|
expect(node.affects).toEqual([]);
|
|
});
|
|
|
|
it("exposes added edges with fromNodeId, toNodeId and relationship", async () => {
|
|
const situationGraph = { centralStatement: "test", nodes: [], edges: [] };
|
|
const edge = { id: "e-savings-state", fromNodeId: "n-savings-realism", toNodeId: "neb1bz2", relationship: "depends_on", confidence: "medium", description: "dep" };
|
|
const proposal = { answerMeaning: null, updatedNodes: [], resolvedUnknownNodeIds: [], addedNodes: [], addedEdges: [edge] };
|
|
|
|
mockUpdateCase.mockImplementation(async (body) =>
|
|
simulateOrchestratorFailure(body),
|
|
);
|
|
|
|
const result = await mockUpdateCase({ situationGraph, proposal });
|
|
|
|
expect(result.diagnostics.rejectedProposalSnapshot.addedEdges).toHaveLength(1);
|
|
const e = result.diagnostics.rejectedProposalSnapshot.addedEdges[0];
|
|
expect(e.fromNodeId).toBe("n-savings-realism");
|
|
expect(e.toNodeId).toBe("neb1bz2");
|
|
expect(e.relationship).toBe("depends_on");
|
|
});
|
|
|
|
it("retains existing rejection stage and errors unchanged", async () => {
|
|
const situationGraph = { centralStatement: "test", nodes: [], edges: [] };
|
|
const proposal = { answerMeaning: null, updatedNodes: [], resolvedUnknownNodeIds: [], addedNodes: [], addedEdges: [] };
|
|
|
|
mockUpdateCase.mockImplementation(async (body) =>
|
|
simulateOrchestratorFailure(body),
|
|
);
|
|
|
|
const result = await mockUpdateCase({ situationGraph, proposal });
|
|
|
|
expect(result.success).toBe(false);
|
|
expect(result.stage).toBe("proposal_compatibility");
|
|
expect(result.errors).toEqual([
|
|
"answerMeaning.userSupportedMeaning introduces a stronger reasoning category than the raw answer establishes.",
|
|
"New unknown must be explicitly related to an answer-derived node",
|
|
]);
|
|
});
|
|
|
|
it("does not include raw model response or prompt in the snapshot", async () => {
|
|
const situationGraph = { centralStatement: "test", nodes: [], edges: [] };
|
|
const proposal = { answerMeaning: null, updatedNodes: [], resolvedUnknownNodeIds: [], addedNodes: [], addedEdges: [] };
|
|
|
|
mockUpdateCase.mockImplementation(async (body) =>
|
|
simulateOrchestratorFailure(body),
|
|
);
|
|
|
|
const result = await mockUpdateCase({ situationGraph, proposal });
|
|
|
|
const snapshotKeys = Object.keys(result.diagnostics.rejectedProposalSnapshot);
|
|
for (const key of snapshotKeys) {
|
|
expect(key).not.toContain("raw");
|
|
expect(key).not.toContain("prompt");
|
|
expect(key).not.toContain("chain_of_thought");
|
|
expect(key).not.toContain("provider_metadata");
|
|
}
|
|
});
|
|
|
|
it("does not include rejectedProposalSnapshot for non-proposal_compatibility failures", async () => {
|
|
mockUpdateCase.mockImplementation(async () =>
|
|
simulateNonCompetitionFailure(),
|
|
);
|
|
|
|
const result = await mockUpdateCase({ situationGraph: {}, proposal: {} });
|
|
|
|
expect(result.diagnostics.rejectedProposalSnapshot).toBeUndefined();
|
|
});
|
|
});
|