fix: continue question selection after graph updates
This commit is contained in:
@@ -229,6 +229,53 @@ function makeApplicationFixture() {
|
||||
};
|
||||
}
|
||||
|
||||
const COMMERCIAL_SCENARIO =
|
||||
"I have developed a new reasoning method that aims to help people determine whether they have enough justified confidence to make a decision. I believe it could become a commercial product, but I do not yet know whether it solves a genuine problem, whether people would value it enough to pay for it, or whether it is fundamentally different from existing AI tools. Before investing significant time and money into building it further, I want to determine whether continuing development is commercially justified.";
|
||||
|
||||
function makeCommercialUpdateFixture() {
|
||||
const parent = makeNode({
|
||||
id: "n-commercial-parent",
|
||||
label:
|
||||
"Commercial justification for whether continuing development is commercially justified",
|
||||
description:
|
||||
"Need to know whether this solves a genuine problem, whether people would value it enough to pay for it, and whether it is commercially justified before continuing development.",
|
||||
kind: "unknown",
|
||||
status: "unknown",
|
||||
confidence: "medium",
|
||||
});
|
||||
|
||||
return makeGraph({
|
||||
centralStatement: COMMERCIAL_SCENARIO,
|
||||
nodes: [parent],
|
||||
edges: [],
|
||||
activeUnknownNodeId: parent.id,
|
||||
resolvedNodeIds: [],
|
||||
currentSummary: "Commercial update fixture",
|
||||
});
|
||||
}
|
||||
|
||||
function makeMeaningfulNoOpProposal() {
|
||||
return {
|
||||
addedNodes: [
|
||||
makeNode({
|
||||
id: "n-anchor",
|
||||
label: "Update anchor",
|
||||
description:
|
||||
"Anchor state introduced by the answer because the update must contain a meaningful change.",
|
||||
kind: "state",
|
||||
status: "known",
|
||||
confidence: "low",
|
||||
}),
|
||||
],
|
||||
updatedNodes: [],
|
||||
addedEdges: [],
|
||||
removedEdgeIds: [],
|
||||
resolvedUnknownNodeIds: [],
|
||||
affectedNodeIds: [],
|
||||
selectedQuestion: null,
|
||||
};
|
||||
}
|
||||
|
||||
describe("applyValidatedProposal", () => {
|
||||
it("applies a valid proposal successfully", () => {
|
||||
const { graph, proposal, ids } = makeApplicationFixture();
|
||||
@@ -1318,4 +1365,74 @@ describe("applyValidatedProposal", () => {
|
||||
),
|
||||
).toHaveLength(firstResult.childNodeIds.length);
|
||||
});
|
||||
|
||||
it("reselects a remaining commercial sibling after resolving the first child", () => {
|
||||
const graph = makeCommercialUpdateFixture();
|
||||
|
||||
const firstResult = applyValidatedProposal({
|
||||
situationGraph: graph,
|
||||
proposal: makeMeaningfulNoOpProposal(),
|
||||
});
|
||||
|
||||
expect(firstResult.success).toBe(true);
|
||||
expect(firstResult.selectedQuestion?.question).toBe(
|
||||
"Who experiences this problem?",
|
||||
);
|
||||
|
||||
const secondResult = applyValidatedProposal({
|
||||
situationGraph: firstResult.updatedSituationGraph,
|
||||
proposal: {
|
||||
addedNodes: [],
|
||||
updatedNodes: [
|
||||
{
|
||||
nodeId: firstResult.selectedQuestion.nodeId,
|
||||
previousStatus: "unknown",
|
||||
newStatus: "resolved",
|
||||
previousValue: null,
|
||||
newValue:
|
||||
"I experience it myself when I am trying to decide whether a project, idea or investment is justified, but I do not yet know how common that problem is for other people.",
|
||||
reason: "The answer confirms a self-observed instance.",
|
||||
},
|
||||
],
|
||||
addedEdges: [],
|
||||
removedEdgeIds: [],
|
||||
resolvedUnknownNodeIds: [firstResult.selectedQuestion.nodeId],
|
||||
affectedNodeIds: [],
|
||||
selectedQuestion: null,
|
||||
},
|
||||
previousQuestion: firstResult.selectedQuestion.question,
|
||||
answer:
|
||||
"I experience it myself when I am trying to decide whether a project, idea or investment is justified, but I do not yet know how common that problem is for other people.",
|
||||
});
|
||||
|
||||
expect(secondResult.success).toBe(true);
|
||||
expect(secondResult.resolvedUnknownNodeIds).toContain(
|
||||
firstResult.selectedQuestion.nodeId,
|
||||
);
|
||||
expect(secondResult.newActiveUnknownNodeId).toBe(
|
||||
secondResult.selectedQuestion?.nodeId,
|
||||
);
|
||||
expect(secondResult.selectedQuestion?.question).toBe(
|
||||
"What makes you think other people experience this problem too?",
|
||||
);
|
||||
expect(secondResult.selectedQuestion?.reasoningPattern).toBe("decision");
|
||||
expect(secondResult.selectedQuestion?.questionFamily).toBe(
|
||||
"decision_foundation",
|
||||
);
|
||||
expect(secondResult.selectedQuestion?.nodeId).not.toBe(
|
||||
firstResult.selectedQuestion.nodeId,
|
||||
);
|
||||
expect(secondResult.unresolvedCandidateCount).toBeGreaterThan(0);
|
||||
expect(secondResult.eligibleCandidateCount).toBeGreaterThan(0);
|
||||
expect(secondResult.candidateNodeIds).toContain(
|
||||
secondResult.selectedQuestion?.nodeId,
|
||||
);
|
||||
expect(secondResult.resolvedCurrentTurnNodeIds).toContain(
|
||||
firstResult.selectedQuestion.nodeId,
|
||||
);
|
||||
expect(secondResult.noQuestionReason).toBeNull();
|
||||
expect(secondResult.selectedQuestion?.question.toLowerCase()).not.toMatch(
|
||||
/price|budget|market size|pilot metrics|benchmark|technical differentiation/,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { applyValidatedProposal } from "@/lib/graph/apply-proposal.js";
|
||||
import { validateGraphReferences } from "@/lib/graph/utils.js";
|
||||
import { makeGraph, makeNode } from "@/lib/graph/schema.js";
|
||||
|
||||
@@ -91,6 +92,29 @@ function makeCommercialAnalysisResult(overrides = {}) {
|
||||
});
|
||||
}
|
||||
|
||||
function makeCommercialUpdateGraph() {
|
||||
const parent = makeNode({
|
||||
id: "n-commercial-parent",
|
||||
label:
|
||||
"Commercial justification for whether continuing development is commercially justified",
|
||||
description:
|
||||
"Need to know whether this solves a genuine problem, whether people would value it enough to pay for it, and whether it is commercially justified before continuing development.",
|
||||
kind: "unknown",
|
||||
status: "unknown",
|
||||
confidence: "medium",
|
||||
});
|
||||
|
||||
return makeGraph({
|
||||
centralStatement:
|
||||
"I have developed a new reasoning method that aims to help people determine whether they have enough justified confidence to make a decision. I believe it could become a commercial product, but I do not yet know whether it solves a genuine problem, whether people would value it enough to pay for it, or whether it is fundamentally different from existing AI tools. Before investing significant time and money into building it further, I want to determine whether continuing development is commercially justified.",
|
||||
nodes: [parent],
|
||||
edges: [],
|
||||
activeUnknownNodeId: parent.id,
|
||||
resolvedNodeIds: [],
|
||||
currentSummary: "Commercial update scenario",
|
||||
});
|
||||
}
|
||||
|
||||
function makeUpdateGraph() {
|
||||
const unknown = makeNode({
|
||||
id: "n-unknown",
|
||||
@@ -1168,6 +1192,93 @@ describe("lib/graph/orchestrator startCase", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("reselects the other-people sibling after the first commercial child is answered", async () => {
|
||||
const { updateCase } = await import("@/lib/graph/orchestrator.js");
|
||||
|
||||
const seededGraph = applyValidatedProposal({
|
||||
situationGraph: makeCommercialUpdateGraph(),
|
||||
proposal: {
|
||||
addedNodes: [
|
||||
makeNode({
|
||||
id: "n-anchor",
|
||||
label: "Update anchor",
|
||||
description:
|
||||
"Anchor state introduced by the answer because the update must contain a meaningful change.",
|
||||
kind: "state",
|
||||
status: "known",
|
||||
confidence: "low",
|
||||
}),
|
||||
],
|
||||
updatedNodes: [],
|
||||
addedEdges: [],
|
||||
removedEdgeIds: [],
|
||||
resolvedUnknownNodeIds: [],
|
||||
affectedNodeIds: [],
|
||||
selectedQuestion: null,
|
||||
},
|
||||
});
|
||||
|
||||
expect(seededGraph.success).toBe(true);
|
||||
const resolvedFirstChildNodeId = seededGraph.selectedQuestion?.nodeId;
|
||||
const resolvedFirstQuestion = seededGraph.selectedQuestion?.question;
|
||||
|
||||
const initial = await updateCase(
|
||||
{
|
||||
situationGraph: seededGraph.updatedSituationGraph,
|
||||
previousQuestion: resolvedFirstQuestion,
|
||||
answer:
|
||||
"I experience it myself when I am trying to decide whether a project, idea or investment is justified, but I do not yet know how common that problem is for other people.",
|
||||
promptVersion: "v0.4",
|
||||
},
|
||||
{
|
||||
applyProposal: true,
|
||||
config: MOCK_CONFIG,
|
||||
provider: {
|
||||
generateReconstruction: vi.fn().mockResolvedValue({
|
||||
addedNodes: [],
|
||||
updatedNodes: [
|
||||
{
|
||||
nodeId: resolvedFirstChildNodeId,
|
||||
previousStatus: "unknown",
|
||||
newStatus: "resolved",
|
||||
previousValue: null,
|
||||
newValue:
|
||||
"I experience it myself when I am trying to decide whether a project, idea or investment is justified, but I do not yet know how common that problem is for other people.",
|
||||
reason: "The answer confirms a self-observed instance.",
|
||||
},
|
||||
],
|
||||
addedEdges: [],
|
||||
removedEdgeIds: [],
|
||||
resolvedUnknownNodeIds: [resolvedFirstChildNodeId],
|
||||
affectedNodeIds: [],
|
||||
selectedQuestion: null,
|
||||
}),
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(initial.success).toBe(true);
|
||||
expect(initial.selectedQuestion?.nodeId).toBe(
|
||||
initial.newActiveUnknownNodeId,
|
||||
);
|
||||
expect(initial.selectedQuestion?.question).toBe(
|
||||
"What makes you think other people experience this problem too?",
|
||||
);
|
||||
expect(initial.selectedQuestion?.reasoningPattern).toBe("decision");
|
||||
expect(initial.diagnostics.unresolvedCandidateCount).toBeGreaterThan(0);
|
||||
expect(initial.diagnostics.eligibleCandidateCount).toBeGreaterThan(0);
|
||||
expect(initial.diagnostics.candidateNodeIds).toContain(
|
||||
initial.selectedQuestion?.nodeId,
|
||||
);
|
||||
expect(initial.diagnostics.resolvedCurrentTurnNodeIds).toContain(
|
||||
resolvedFirstChildNodeId,
|
||||
);
|
||||
expect(initial.diagnostics.noQuestionReason).toBeNull();
|
||||
expect(initial.selectedQuestion?.question.toLowerCase()).not.toMatch(
|
||||
/price|budget|market size|pilot metrics|benchmark|technical differentiation/,
|
||||
);
|
||||
});
|
||||
|
||||
it("startCase no longer copies analysis nextQuestion directly when a graph-backed question exists", async () => {
|
||||
mockAnalyseScenario.mockResolvedValue(makeAnalysisResult());
|
||||
const { startCase } = await import("@/lib/graph/orchestrator.js");
|
||||
|
||||
@@ -275,6 +275,94 @@ function makeUpdateSuccess(overrides = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
function makeCommercialUpdateSuccess(overrides = {}) {
|
||||
return {
|
||||
success: true,
|
||||
stage: "update_applied",
|
||||
updatedSituationGraph: {
|
||||
centralStatement:
|
||||
"I have developed a new reasoning method that aims to help people determine whether they have enough justified confidence to make a decision. I believe it could become a commercial product, but I do not yet know whether it solves a genuine problem, whether people would value it enough to pay for it, or whether it is fundamentally different from existing AI tools. Before investing significant time and money into building it further, I want to determine whether continuing development is commercially justified.",
|
||||
currentSummary: "Commercial update summary",
|
||||
activeUnknownNodeId: "n-other-people",
|
||||
resolvedNodeIds: ["n-who"],
|
||||
nodes: [
|
||||
{
|
||||
id: "n-commercial-parent",
|
||||
label:
|
||||
"Commercial justification for whether continuing development is commercially justified",
|
||||
description:
|
||||
"Need to know whether this solves a genuine problem, whether people would value it enough to pay for it, and whether it is commercially justified before continuing development.",
|
||||
kind: "unknown",
|
||||
status: "provisional",
|
||||
confidence: "medium",
|
||||
},
|
||||
{
|
||||
id: "n-who",
|
||||
label: "Who experiences this problem",
|
||||
description:
|
||||
"Need to know who experiences this problem, because that must be clear before deciding whether it is commercially justified.",
|
||||
kind: "unknown",
|
||||
status: "resolved",
|
||||
confidence: "medium",
|
||||
value:
|
||||
"I experience it myself when I am trying to decide whether a project, idea or investment is justified, but I do not yet know how common that problem is for other people.",
|
||||
parentId: "n-commercial-parent",
|
||||
},
|
||||
{
|
||||
id: "n-other-people",
|
||||
label: "Whether other people experience this problem",
|
||||
description:
|
||||
"Need to know whether other people experience this problem, because that must be established before deciding whether the problem is broadly important.",
|
||||
kind: "unknown",
|
||||
status: "unknown",
|
||||
confidence: "medium",
|
||||
parentId: "n-commercial-parent",
|
||||
},
|
||||
],
|
||||
edges: [
|
||||
{
|
||||
id: "e-other-parent",
|
||||
fromNodeId: "n-other-people",
|
||||
toNodeId: "n-commercial-parent",
|
||||
relationship: "depends_on",
|
||||
confidence: "medium",
|
||||
description:
|
||||
"This child unknown must be investigated before the broader parent explanation can be resolved.",
|
||||
},
|
||||
],
|
||||
},
|
||||
proposal: {
|
||||
addedNodes: [],
|
||||
updatedNodes: [{ nodeId: "n-who", newStatus: "resolved", reason: "answered" }],
|
||||
addedEdges: [],
|
||||
removedEdgeIds: [],
|
||||
resolvedUnknownNodeIds: ["n-who"],
|
||||
affectedNodeIds: ["n-commercial-parent"],
|
||||
selectedQuestion: null,
|
||||
},
|
||||
selectedQuestion: {
|
||||
nodeId: "n-other-people",
|
||||
question: "What makes you think other people experience this problem too?",
|
||||
reason:
|
||||
"Formulated as a direct foundational question because this child unknown should be answered one step at a time.",
|
||||
reasoningPattern: "decision",
|
||||
questionFamily: "decision_foundation",
|
||||
},
|
||||
previousActiveUnknownNodeId: "n-who",
|
||||
newActiveUnknownNodeId: "n-other-people",
|
||||
affectedNodeIds: ["n-commercial-parent"],
|
||||
resolvedUnknownNodeIds: ["n-who"],
|
||||
diagnostics: {
|
||||
unresolvedCandidateCount: 2,
|
||||
eligibleCandidateCount: 1,
|
||||
candidateNodeIds: ["n-other-people"],
|
||||
resolvedCurrentTurnNodeIds: ["n-who"],
|
||||
noQuestionReason: null,
|
||||
},
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("scenario-form UI helpers", () => {
|
||||
it("submits to /api/cases/start", async () => {
|
||||
const fetchImpl = vi.fn().mockResolvedValue({ ok: true });
|
||||
@@ -700,4 +788,16 @@ describe("graph-backed UI rendering", () => {
|
||||
|
||||
expect(html).toContain("Proposal details");
|
||||
});
|
||||
|
||||
it("does not show the no-question fallback when a commercial follow-up sibling exists", () => {
|
||||
const html = renderToStaticMarkup(
|
||||
<GraphUpdateView updateResult={makeCommercialUpdateSuccess()} />,
|
||||
);
|
||||
|
||||
expect(html).toContain(
|
||||
"What makes you think other people experience this problem too?",
|
||||
);
|
||||
expect(html).toContain("New active unknown");
|
||||
expect(html).not.toContain("No next question selected yet.");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user