test: generalise question priority across decisions
This commit is contained in:
@@ -0,0 +1,155 @@
|
||||
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",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user