Files
confidence-engine/tests/graph/focused-investigation-boundaries.test.js
T

233 lines
10 KiB
JavaScript

import { describe, expect, it } from "vitest";
import { makeNode, makeGraph } from "@/lib/graph/schema.js";
import { formulateQuestionForTarget, buildFocusedDeconstructPrompt, validateFocusedDeconstructSchema } from "@/lib/graph/focused-investigation.js";
// ── helpers ──────────────────────────────────────────────────────────────
function makeTestGraph() {
return makeGraph({
centralStatement: "Should we launch the product now?",
currentSummary: "initial summary",
nodes: [
makeNode({ id: "a", label: "Market demand signal", description: "Evidence that customers want this.", kind: "unknown", status: "unknown" }),
makeNode({ id: "b", label: "Competitor activity", description: "What others are doing.", kind: "observation", status: "supported" }),
],
edges: [],
resolvedNodeIds: ["b"],
});
}
// ── Boundary 1: explicit formulation ─────────────────────────────────────
describe("formulateQuestionForTarget — explicit node selection", () => {
it("validates that the target node exists in the graph", () => {
const graph = makeTestGraph();
const result = formulateQuestionForTarget({ situationGraph: graph, targetNodeId: "nonexistent" });
expect(result.success).toBe(false);
expect(result.error).toContain("not found");
});
it("validates that the target node is an unknown", () => {
const graph = makeTestGraph();
const result = formulateQuestionForTarget({ situationGraph: graph, targetNodeId: "b" });
expect(result.success).toBe(false);
expect(result.error).toContain("not an unknown");
});
it("validates that the target node is not resolved", () => {
const graph = makeTestGraph();
// Create a resolved unknown node
graph.nodes.push(makeNode({ id: "c", label: "Done", description: "Completed item", kind: "unknown", status: "resolved" }));
const result = formulateQuestionForTarget({ situationGraph: graph, targetNodeId: "c" });
expect(result.success).toBe(false);
expect(result.error).toContain("resolved");
});
it("uses the supplied targetNodeId in the formulation output", () => {
const graph = makeTestGraph();
const result = formulateQuestionForTarget({ situationGraph: graph, targetNodeId: "a" });
expect(result.success).toBe(true);
expect(result.targetNodeId).toBe("a");
});
it("does not mutate activeUnknownNodeId on the graph", () => {
const graph = makeTestGraph();
const before = graph.activeUnknownNodeId;
formulateQuestionForTarget({ situationGraph: graph, targetNodeId: "a" });
expect(graph.activeUnknownNodeId).toBe(before);
});
it("does not mutate SituationGraph in any way", () => {
const graph = makeTestGraph();
const snapshot = JSON.stringify(graph);
formulateQuestionForTarget({ situationGraph: graph, targetNodeId: "a" });
expect(JSON.stringify(graph)).toBe(snapshot);
});
it("does not invoke global selection (selectActiveUnknownCandidate / determineGraphBackedQuestion)", () => {
const graph = makeTestGraph();
const result = formulateQuestionForTarget({ situationGraph: graph, targetNodeId: "a" });
expect(result.success).toBe(true);
expect(result.targetNodeId).toBe("a");
expect(result.question).toBeDefined();
expect(typeof result.question).toBe("string");
expect(result.question.length).toBeGreaterThan(0);
});
});
// ── Boundary 2: focused answer deconstruction ────────────────────────────
describe("buildFocusedDeconstructPrompt", () => {
it("produces a prompt containing the target label", () => {
const prompt = buildFocusedDeconstructPrompt({
targetLabel: "Market demand signal",
targetDescription: "Evidence that customers want this.",
centralStatement: "Should we launch?",
question: "What evidence would clarify market demand?",
answer: "Some evidence suggests demand.",
});
expect(prompt).toContain("Market demand signal");
});
it("produces a prompt containing the answer text", () => {
const prompt = buildFocusedDeconstructPrompt({
targetLabel: "X",
targetDescription: "Y",
centralStatement: "Z",
question: "Q?",
answer: "Some evidence suggests demand.",
});
expect(prompt).toContain("Some evidence suggests demand.");
});
it("does NOT output graph-mutation fields (only lists them as forbidden in the instructions)", () => {
const prompt = buildFocusedDeconstructPrompt({
targetLabel: "X",
targetDescription: "Y",
centralStatement: "Z",
question: "Q?",
answer: "A.",
});
// The prompt instructs the model NOT to output these fields.
// It mentions them only as forbidden outputs, not as required outputs.
expect(prompt).toContain("Do NOT include any of these fields");
expect(prompt).not.toContain("- addedNodes");
expect(prompt).not.toContain("- updatedNodes");
expect(prompt).not.toContain("- removedNodes");
expect(prompt).not.toContain("- resolvedNodeIds");
});
});
describe("buildFocusedDeconstructPrompt — relationship rule boundary lock", () => {
it("relationship instruction explicitly prohibits observation restatement", () => {
const prompt = buildFocusedDeconstructPrompt({
targetLabel: "X",
targetDescription: "Y",
centralStatement: "Z",
question: "Q?",
answer: "A.",
});
expect(prompt).toContain("Do not create a relationship by merely restating");
});
it("relationship instruction explicitly prohibits promotion of tentative/conditional meaning", () => {
const prompt = buildFocusedDeconstructPrompt({
targetLabel: "X",
targetDescription: "Y",
centralStatement: "Z",
question: "Q?",
answer: "A.",
});
expect(prompt).toContain("Tentative, speculative, or conditional language must not be promoted into an established relationship");
});
it("relationship instruction requires direct link between two distinct propositions", () => {
const prompt = buildFocusedDeconstructPrompt({
targetLabel: "X",
targetDescription: "Y",
centralStatement: "Z",
question: "Q?",
answer: "A.",
});
expect(prompt).toContain("must connect two distinct propositions that the user's answer itself links");
});
it("relationship instruction explicitly permits relationships: [] when no direct link exists", () => {
const prompt = buildFocusedDeconstructPrompt({
targetLabel: "X",
targetDescription: "Y",
centralStatement: "Z",
question: "Q?",
answer: "A.",
});
expect(prompt).toContain("relationships: []");
});
});
describe("validateFocusedDeconstructSchema", () => {
it("passes when all required fields are present", () => {
const result = { targetNodeId: "a", observations: [], uncertainties: [], assumptions: [], relationships: [], possibleFollowUpQuestions: [] };
expect(validateFocusedDeconstructSchema(result)).toEqual([]);
});
it("fails when a required field is missing", () => {
const result = { targetNodeId: "a", observations: [], uncertainties: [], assumptions: [] };
const errors = validateFocusedDeconstructSchema(result);
expect(errors.length).toBeGreaterThan(0);
expect(errors.some((e) => e.includes("relationships") || e.includes("possibleFollowUpQuestions"))).toBeTruthy();
});
it("rejects results that contain graph-mutation fields", () => {
const result = {
targetNodeId: "a", observations: [], uncertainties: [], assumptions: [], relationships: [], possibleFollowUpQuestions: [],
addedNodes: [{ id: "x" }],
};
const errors = validateFocusedDeconstructSchema(result);
expect(errors.some((e) => e.includes("addedNodes"))).toBeTruthy();
});
it("rejects results that contain resolvedNodeIds", () => {
const result = {
targetNodeId: "a", observations: [], uncertainties: [], assumptions: [], relationships: [], possibleFollowUpQuestions: [],
resolvedNodeIds: ["x"],
};
const errors = validateFocusedDeconstructSchema(result);
expect(errors.some((e) => e.includes("resolvedNodeIds"))).toBeTruthy();
});
it("rejects results that contain activeUnknownNodeId", () => {
const result = {
targetNodeId: "a", observations: [], uncertainties: [], assumptions: [], relationships: [], possibleFollowUpQuestions: [],
activeUnknownNodeId: "x",
};
const errors = validateFocusedDeconstructSchema(result);
expect(errors.some((e) => e.includes("activeUnknownNodeId"))).toBeTruthy();
});
it("rejects results that contain selectedQuestion", () => {
const result = {
targetNodeId: "a", observations: [], uncertainties: [], assumptions: [], relationships: [], possibleFollowUpQuestions: [],
selectedQuestion: "some question",
};
const errors = validateFocusedDeconstructSchema(result);
expect(errors.some((e) => e.includes("selectedQuestion"))).toBeTruthy();
});
it("all required fields are observations, uncertainties, assumptions, relationships, possibleFollowUpQuestions", () => {
const fullResult = { targetNodeId: "a", observations: [], uncertainties: [], assumptions: [], relationships: [], possibleFollowUpQuestions: [] };
const errors = validateFocusedDeconstructSchema(fullResult);
expect(errors.length).toBe(0);
});
it("focused result contains no graph-mutation fields when valid", () => {
const fullResult = { targetNodeId: "a", observations: [], uncertainties: [], assumptions: [], relationships: [], possibleFollowUpQuestions: [] };
const errors = validateFocusedDeconstructSchema(fullResult);
expect(errors.length).toBe(0);
// Confirm no forbidden keys are in the result
const forbidden = ["addedNodes", "updatedNodes", "removedNodes", "addedEdges", "removedEdges", "resolvedNodeIds", "activeUnknownNodeId", "selectedQuestion"];
for (const key of forbidden) {
expect(key in fullResult).toBe(false);
}
});
});