181 lines
6.6 KiB
JavaScript
181 lines
6.6 KiB
JavaScript
import { describe, expect, it } from "vitest";
|
|
import { buildGraphUpdatePrompt } from "@/lib/graph/prompt-builder.js";
|
|
import { makeEdge, makeGraph, makeNode } from "@/lib/graph/schema.js";
|
|
|
|
function makeContext() {
|
|
const unknown = makeNode({
|
|
id: "n-unknown",
|
|
label: "Complaint rate denominator",
|
|
description: "Need the denominator to compare complaint rates",
|
|
kind: "unknown",
|
|
status: "unknown",
|
|
confidence: "high",
|
|
});
|
|
const observation = makeNode({
|
|
id: "n-obs",
|
|
label: "Complaints up 35%",
|
|
description: "Complaints increased by 35%",
|
|
kind: "observation",
|
|
status: "supported",
|
|
confidence: "high",
|
|
});
|
|
|
|
return {
|
|
situationGraph: makeGraph({
|
|
centralStatement: "Complaints increased while production increased.",
|
|
nodes: [unknown, observation],
|
|
edges: [
|
|
makeEdge({
|
|
id: "e1",
|
|
fromNodeId: observation.id,
|
|
toNodeId: unknown.id,
|
|
relationship: "supports",
|
|
confidence: "high",
|
|
description: "Observation informs the unknown",
|
|
}),
|
|
],
|
|
activeUnknownNodeId: unknown.id,
|
|
resolvedNodeIds: [],
|
|
currentSummary:
|
|
"Nodes: 1 observation, 1 unknown | Edges: 1 total | Unknowns: 1 unresolved",
|
|
}),
|
|
previousQuestion: "What denominator is being used for the complaint rate?",
|
|
answer:
|
|
"The complaint rate fell from 2.0 complaints per 100 units to 1.9 complaints per 100 units.",
|
|
};
|
|
}
|
|
|
|
describe("buildGraphUpdatePrompt", () => {
|
|
it("includes the current graph", () => {
|
|
const prompt = buildGraphUpdatePrompt(makeContext());
|
|
expect(prompt).toContain(
|
|
"Complaints increased while production increased.",
|
|
);
|
|
expect(prompt).toContain("Complaint rate denominator");
|
|
});
|
|
|
|
it("includes previous question and answer", () => {
|
|
const prompt = buildGraphUpdatePrompt(makeContext());
|
|
expect(prompt).toContain(
|
|
"What denominator is being used for the complaint rate?",
|
|
);
|
|
expect(prompt).toContain(
|
|
"The complaint rate fell from 2.0 complaints per 100 units to 1.9 complaints per 100 units.",
|
|
);
|
|
});
|
|
|
|
it("contains exact schema keys", () => {
|
|
const prompt = buildGraphUpdatePrompt(makeContext());
|
|
expect(prompt).toContain("addedNodes");
|
|
expect(prompt).toContain("updatedNodes");
|
|
expect(prompt).toContain("addedEdges");
|
|
expect(prompt).toContain("removedEdgeIds");
|
|
expect(prompt).toContain("resolvedUnknownNodeIds");
|
|
expect(prompt).toContain("affectedNodeIds");
|
|
expect(prompt).toContain("selectedQuestion");
|
|
expect(prompt).toContain("answerMeaning");
|
|
});
|
|
|
|
it("lists enum values", () => {
|
|
const prompt = buildGraphUpdatePrompt(makeContext());
|
|
expect(prompt).toContain(
|
|
"observation | reported_claim | metric | state | transition | relationship | assumption | unknown | conclusion",
|
|
);
|
|
expect(prompt).toContain(
|
|
"known | unknown | provisional | supported | weakened | contradicted | resolved",
|
|
);
|
|
expect(prompt).toContain(
|
|
"supports | weakens | contradicts | depends_on | causes | may_cause | measures | compares_with | updates | other",
|
|
);
|
|
});
|
|
|
|
it("forbids full-graph replacement", () => {
|
|
const prompt = buildGraphUpdatePrompt(makeContext());
|
|
expect(prompt).toContain("Never return a replacement graph");
|
|
expect(prompt).toContain("Propose changes only");
|
|
});
|
|
|
|
it("requires JSON only", () => {
|
|
const prompt = buildGraphUpdatePrompt(makeContext());
|
|
expect(prompt).toContain("Return JSON only");
|
|
expect(prompt).toContain("Return one JSON object only");
|
|
});
|
|
|
|
it("describes controlled emergent unknown rules", () => {
|
|
const prompt = buildGraphUpdatePrompt(makeContext());
|
|
expect(prompt).toContain("Add at most 3 new unknown nodes");
|
|
expect(prompt).toContain("Resolve the answered unknown first");
|
|
expect(prompt).toContain(
|
|
"selectedQuestion.question must be one narrow non-compound question",
|
|
);
|
|
expect(prompt).toContain(
|
|
"the engine will deterministically choose final priority after validation",
|
|
);
|
|
});
|
|
|
|
it("instructs the model to preserve user-supported meaning separately from inference", () => {
|
|
const prompt = buildGraphUpdatePrompt(makeContext());
|
|
expect(prompt).toContain(
|
|
"answerMeaning.userSupportedMeaning must state only what the user's answer directly supports",
|
|
);
|
|
expect(prompt).toContain(
|
|
"Put any stronger interpretation in answerMeaning.possibleInference",
|
|
);
|
|
expect(prompt).toContain(
|
|
"supportCategory and resolutionGuidance are optional descriptive hints only",
|
|
);
|
|
expect(prompt).toContain(
|
|
"leave them null rather than inventing rigid category labels",
|
|
);
|
|
});
|
|
});
|
|
|
|
// ── Semantic-to-mutation contract (57J.39) ──────────────
|
|
|
|
describe("buildGraphUpdatePrompt — semantic-to-mutation MUST rule", () => {
|
|
it("contains the explicit structural-materialization MUST rule", () => {
|
|
const prompt = buildGraphUpdatePrompt(makeContext());
|
|
expect(prompt).toContain("MUST express its effect through structural mutation");
|
|
});
|
|
|
|
it("rule permits update/refine of existing structure", () => {
|
|
const prompt = buildGraphUpdatePrompt(makeContext());
|
|
expect(prompt).toContain("update/refinement of existing structure");
|
|
});
|
|
|
|
it("rule permits resolving an existing unknown", () => {
|
|
const prompt = buildGraphUpdatePrompt(makeContext());
|
|
expect(prompt).toContain("resolution of an existing unknown");
|
|
});
|
|
|
|
it("rule permits genuinely new unknown when needed", () => {
|
|
const prompt = buildGraphUpdatePrompt(makeContext());
|
|
expect(prompt).toContain("a genuinely new unknown");
|
|
});
|
|
|
|
it("rule explicitly states answerMeaning alone is not sufficient", () => {
|
|
const prompt = buildGraphUpdatePrompt(makeContext());
|
|
expect(prompt).toContain("answerMeaning alone is not sufficient");
|
|
});
|
|
|
|
it("rule does NOT force adding a new node", () => {
|
|
const prompt = buildGraphUpdatePrompt(makeContext());
|
|
// The rule should be silent about forcing new nodes — this is preserved by existing rule #7.
|
|
// Verify the MUST rule exists but doesn't contain "must add a new node" or similar.
|
|
const mustRuleMatch = prompt.match(
|
|
/6\..*?(?=\n7\.)/s,
|
|
);
|
|
expect(mustRuleMatch).not.toBe(null);
|
|
expect(mustRuleMatch[0]).not.toContain("must add a new node");
|
|
});
|
|
|
|
it("does not imply possibleInference alone triggers mutation", () => {
|
|
const prompt = buildGraphUpdatePrompt(makeContext());
|
|
// The rule must reference userSupportedMeaning specifically, not possibleInference as a trigger.
|
|
const mustRuleMatch = prompt.match(
|
|
/6\..*?(?=\n7\.)/s,
|
|
);
|
|
expect(mustRuleMatch[0]).toContain("userSupportedMeaning");
|
|
});
|
|
});
|