156 lines
5.1 KiB
JavaScript
156 lines
5.1 KiB
JavaScript
import { describe, expect, it } from "vitest";
|
|
import { applyValidatedProposal } from "@/lib/graph/apply-proposal.js";
|
|
import { formulateQuestion } from "@/lib/graph/question-formulator.js";
|
|
import { selectActiveUnknownCandidate } from "@/lib/graph/utils.js";
|
|
import { questionPriorityGeneralisationFixtures } from "@/tests/fixtures/question-priority-generalisation.js";
|
|
|
|
function clone(value) {
|
|
return JSON.parse(JSON.stringify(value));
|
|
}
|
|
|
|
function buildResolutionProposal(graph) {
|
|
const activeNode = graph.nodes.find(
|
|
(node) => node.id === graph.activeUnknownNodeId,
|
|
);
|
|
const placeholderCandidate = graph.nodes.find(
|
|
(node) => node.kind === "unknown" && node.id !== activeNode.id,
|
|
);
|
|
|
|
return {
|
|
addedNodes: [],
|
|
updatedNodes: [
|
|
{
|
|
nodeId: activeNode.id,
|
|
previousStatus: activeNode.status,
|
|
newStatus: "resolved",
|
|
previousValue: activeNode.value ?? null,
|
|
newValue: activeNode.value ?? "Resolved context answer",
|
|
reason:
|
|
"The resolved context unknown is treated as answered for fixture progression.",
|
|
},
|
|
],
|
|
addedEdges: [],
|
|
removedEdgeIds: [],
|
|
resolvedUnknownNodeIds: [activeNode.id],
|
|
affectedNodeIds: [],
|
|
selectedQuestion: {
|
|
nodeId: placeholderCandidate?.id,
|
|
question: "Placeholder candidate question?",
|
|
reason: "Candidate only; deterministic selector should override it.",
|
|
},
|
|
};
|
|
}
|
|
|
|
function assertQuestionStructure(question) {
|
|
expect(question.match(/\?/g) || []).toHaveLength(1);
|
|
expect(question).not.toMatch(/\?\s*(and|or)\b/i);
|
|
expect(question).not.toMatch(/^What is\s+/i);
|
|
expect(question).toMatch(/^(What|Who|When)\b/);
|
|
}
|
|
|
|
describe("question priority generalisation", () => {
|
|
for (const fixture of questionPriorityGeneralisationFixtures) {
|
|
it(`${fixture.scenario} selects a foundational unknown and singular answerable strategy`, () => {
|
|
const originalGraph = clone(fixture.graph);
|
|
const deterministicSelection = selectActiveUnknownCandidate(
|
|
fixture.graph,
|
|
[fixture.graph.activeUnknownNodeId],
|
|
);
|
|
|
|
const result = applyValidatedProposal({
|
|
situationGraph: fixture.graph,
|
|
proposal: buildResolutionProposal(fixture.graph),
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
expect(fixture.graph).toEqual(originalGraph);
|
|
expect(result.graphUpdate.selectedQuestion?.question).toBe(
|
|
"Placeholder candidate question?",
|
|
);
|
|
|
|
expect(deterministicSelection.nodeId).toBe(
|
|
result.selectedQuestion.nodeId,
|
|
);
|
|
expect(fixture.acceptableFoundationalUnknownNodeIds).toContain(
|
|
result.selectedQuestion.nodeId,
|
|
);
|
|
expect(result.selectedQuestion.nodeId).not.toBe(
|
|
fixture.graph.nodes[fixture.graph.nodes.length - 1].id,
|
|
);
|
|
|
|
expect(fixture.acceptableQuestionStrategies).toContain(
|
|
result.selectedQuestion.strategy,
|
|
);
|
|
assertQuestionStructure(result.selectedQuestion.question);
|
|
|
|
const lowerQuestion = result.selectedQuestion.question.toLowerCase();
|
|
for (const topic of fixture.prohibitedFirstTopics) {
|
|
expect(lowerQuestion).not.toContain(topic.toLowerCase());
|
|
}
|
|
|
|
const selectedNode = result.updatedSituationGraph.nodes.find(
|
|
(node) => node.id === result.selectedQuestion.nodeId,
|
|
);
|
|
const reformulated = formulateQuestion({
|
|
node: selectedNode,
|
|
graph: result.updatedSituationGraph,
|
|
context: {
|
|
resolvedValues: ["Resolved context answer"],
|
|
},
|
|
});
|
|
|
|
expect(reformulated.question).toBe(result.selectedQuestion.question);
|
|
expect(clone(result.updatedSituationGraph)).toEqual(
|
|
result.updatedSituationGraph,
|
|
);
|
|
});
|
|
}
|
|
|
|
it("reports all five selected unknowns and strategies", () => {
|
|
const summary = questionPriorityGeneralisationFixtures.map((fixture) => {
|
|
const result = applyValidatedProposal({
|
|
situationGraph: fixture.graph,
|
|
proposal: buildResolutionProposal(fixture.graph),
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
|
|
return {
|
|
scenario: fixture.scenario,
|
|
nodeId: result.selectedQuestion.nodeId,
|
|
strategy: result.selectedQuestion.strategy,
|
|
};
|
|
});
|
|
|
|
expect(summary).toMatchInlineSnapshot(`
|
|
[
|
|
{
|
|
"nodeId": "hire-success-criteria",
|
|
"scenario": "Should we hire another engineer?",
|
|
"strategy": "decision criterion",
|
|
},
|
|
{
|
|
"nodeId": "van-reliability-threshold",
|
|
"scenario": "Should we replace the delivery vans?",
|
|
"strategy": "decision criterion",
|
|
},
|
|
{
|
|
"nodeId": "country-value-threshold",
|
|
"scenario": "Should we launch in another country?",
|
|
"strategy": "actor/customer",
|
|
},
|
|
{
|
|
"nodeId": "project-benefit-threshold",
|
|
"scenario": "Should we continue a project that is over budget?",
|
|
"strategy": "decision criterion",
|
|
},
|
|
{
|
|
"nodeId": "support-value-threshold",
|
|
"scenario": "Should we introduce a paid support tier?",
|
|
"strategy": "actor/customer",
|
|
},
|
|
]
|
|
`);
|
|
});
|
|
});
|