diff --git a/docs/v0.7-question-simplicity-experiment.md b/docs/v0.7-question-simplicity-experiment.md index bdeca1b..80865f0 100644 --- a/docs/v0.7-question-simplicity-experiment.md +++ b/docs/v0.7-question-simplicity-experiment.md @@ -1,3 +1,28 @@ +## Post-update selection invariant + +After every successful graph update, the full deterministic question-selection pipeline must run again whenever eligible unresolved unknowns remain. + +That means the update path must not stop at graph mutation, child resolution, emergent unknown creation, decomposition, or upward propagation. It must continue through: + +```text +updated graph +→ rebuild reasoning state +→ identify unresolved candidates +→ select active unknown +→ atomicity assessment +→ answerability assessment +→ decompose if required +→ reselect +→ reasoning-pattern selection +→ investigation-strategy selection +→ question-family selection +→ question formulation +→ complexity validation +→ selectedQuestion +``` + +Returning no question is only valid when no eligible unresolved candidate remains, the case is complete, ambiguity cannot be safely resolved, or question formulation fails validation with an explicit deterministic reason. + # v0.7 Question Simplicity Experiment ## Observed failure diff --git a/lib/graph/apply-proposal.js b/lib/graph/apply-proposal.js index 88a995e..c08786f 100644 --- a/lib/graph/apply-proposal.js +++ b/lib/graph/apply-proposal.js @@ -1407,15 +1407,24 @@ function buildDecompositionTemplates(parentNode, graph, depth = 0) { dependsOnLabels: [], }, { - label: "What happens when this problem is not resolved", + label: "Whether other people experience this problem", description: - "Need to know what happens when this problem is not resolved, because that is needed before judging whether the problem matters.", + "Need to know whether other people experience this problem, because that must be established before deciding whether the problem is broadly important.", dependsOnLabels: ["Who experiences this problem"], }, { label: "How often this problem happens", description: "Need to know how often this problem happens, because that helps judge whether it is a real recurring problem.", + dependsOnLabels: [ + "Who experiences this problem", + "Whether other people experience this problem", + ], + }, + { + label: "What happens when this problem is not resolved", + description: + "Need to know what happens when this problem is not resolved, because that is needed before judging whether the problem matters.", dependsOnLabels: ["Who experiences this problem"], }, { @@ -1424,6 +1433,7 @@ function buildDecompositionTemplates(parentNode, graph, depth = 0) { "Need to know how people deal with this problem today, because that is needed before comparing alternatives or value.", dependsOnLabels: [ "Who experiences this problem", + "Whether other people experience this problem", "What happens when this problem is not resolved", "How often this problem happens", ], @@ -1435,6 +1445,7 @@ function buildDecompositionTemplates(parentNode, graph, depth = 0) { "Need to know whether people actively look for help with this problem, because that is needed before judging demand or willingness to pay.", dependsOnLabels: [ "Who experiences this problem", + "Whether other people experience this problem", "What happens when this problem is not resolved", "How often this problem happens", "How people deal with this problem today", @@ -1446,6 +1457,7 @@ function buildDecompositionTemplates(parentNode, graph, depth = 0) { "Need to know whether people would pay to solve this problem, because that can only be judged after the problem itself is established.", dependsOnLabels: [ "Who experiences this problem", + "Whether other people experience this problem", "What happens when this problem is not resolved", "How often this problem happens", "How people deal with this problem today", @@ -1635,6 +1647,81 @@ function isSelectableUnresolvedUnknown(graph, nodeId) { ); } +function listUnresolvedUnknownCandidates( + graph, + resolvedCurrentTurnNodeIds = [], +) { + const resolvedCurrentTurnSet = new Set(resolvedCurrentTurnNodeIds || []); + + return (graph.nodes || []).filter( + (node) => + node.kind === "unknown" && + !["resolved", "contradicted"].includes(node.status) && + !(graph.resolvedNodeIds || []).includes(node.id) && + !resolvedCurrentTurnSet.has(node.id), + ); +} + +function listEligibleUnknownCandidates(graph, resolvedCurrentTurnNodeIds = []) { + return listUnresolvedUnknownCandidates( + graph, + resolvedCurrentTurnNodeIds, + ).filter( + (node) => + (scoreUnknownCandidate(graph, node, graph.resolvedNodeIds || []) + .unresolvedParentUnknownCount ?? 0) === 0, + ); +} + +function selectOrderedSiblingCandidate( + graph, + candidateNodeIds = [], + resolvedCurrentTurnNodeIds = [], +) { + const candidates = candidateNodeIds + .map((nodeId) => findNodeById(graph, nodeId)) + .filter(Boolean); + + if (candidates.length < 2) { + return null; + } + + const parentId = candidates[0]?.parentId ?? null; + if (!parentId || !candidates.every((node) => node.parentId === parentId)) { + return null; + } + + const parentNode = findNodeById(graph, parentId); + const orderedIds = (parentNode?.childIds || []).filter((nodeId) => + candidateNodeIds.includes(nodeId), + ); + const fallbackOrderedIds = (graph.nodes || []) + .filter((node) => candidateNodeIds.includes(node.id)) + .map((node) => node.id); + const orderedCandidateIds = + orderedIds.length > 0 ? orderedIds : fallbackOrderedIds; + + const eligibleIds = new Set( + listEligibleUnknownCandidates(graph, resolvedCurrentTurnNodeIds).map( + (node) => node.id, + ), + ); + + const selectedId = orderedCandidateIds.find((nodeId) => + eligibleIds.has(nodeId), + ); + if (!selectedId) { + return null; + } + + return { + status: "selected", + nodeId: selectedId, + reason: + "Resolved a sibling tie using the deterministic decomposition order after the update left multiple equally scored follow-up children.", + }; +} + function selectedQuestionBelongsToChild(graph, selectedQuestion) { if (!selectedQuestion?.nodeId) return false; return Boolean(findNodeById(graph, selectedQuestion.nodeId)?.parentId); @@ -2356,6 +2443,9 @@ export function applyValidatedProposal({ reasoningResolution.reasoningStateOverride, ); updatedSituationGraph.reasoningState = nextReasoningState; + const resolvedCurrentTurnNodeIds = [ + ...new Set(proposalSnapshot.resolvedUnknownNodeIds || []), + ]; deterministicSelection = isSelectableUnresolvedUnknown( updatedSituationGraph, decompositionResult.selectedChildNodeId, @@ -2371,6 +2461,27 @@ export function applyValidatedProposal({ updatedSituationGraph.resolvedNodeIds, ); + if (deterministicSelection?.status === "ambiguous") { + const orderedSiblingSelection = selectOrderedSiblingCandidate( + updatedSituationGraph, + deterministicSelection.tiedCandidateIds || [], + resolvedCurrentTurnNodeIds, + ); + + if (orderedSiblingSelection) { + deterministicSelection = orderedSiblingSelection; + } + } + + const unresolvedCandidates = listUnresolvedUnknownCandidates( + updatedSituationGraph, + resolvedCurrentTurnNodeIds, + ); + const eligibleCandidates = listEligibleUnknownCandidates( + updatedSituationGraph, + resolvedCurrentTurnNodeIds, + ); + const atomicityAssessment = decompositionResult.atomicityAssessment; const answerabilityAssessment = decompositionResult.answerabilityAssessment; const decompositionDepth = decompositionResult.decompositionDepth; @@ -2500,6 +2611,15 @@ export function applyValidatedProposal({ ) ? finalSelectedQuestion?.nodeId : null); + const noQuestionReason = finalSelectedQuestion?.question + ? null + : deterministicSelection?.status === "ambiguous" + ? "Eligible unresolved candidates remain tied after update-time reselection." + : eligibleCandidates.length === 0 + ? unresolvedCandidates.length === 0 + ? "No unresolved unknown candidates remain after this update." + : "Unresolved unknowns remain, but none are currently eligible for direct investigation." + : "Question formulation did not produce a valid next question despite eligible unresolved candidates."; const resultGraphValidation = situationGraphSchema.safeParse( updatedSituationGraph, @@ -2580,6 +2700,11 @@ export function applyValidatedProposal({ decompositionResult.decompositionTriggeredByAnswerability ?? false, previousQuestion, finalQuestion: finalSelectedQuestion?.question ?? null, + unresolvedCandidateCount: unresolvedCandidates.length, + eligibleCandidateCount: eligibleCandidates.length, + candidateNodeIds: eligibleCandidates.map((node) => node.id), + resolvedCurrentTurnNodeIds, + noQuestionReason, plainLanguageNormalisations, propagationPerformed, resolvedChildNodeId, diff --git a/lib/graph/orchestrator.js b/lib/graph/orchestrator.js index 7a34ff9..731d2d6 100644 --- a/lib/graph/orchestrator.js +++ b/lib/graph/orchestrator.js @@ -195,6 +195,11 @@ function buildUpdateDiagnostics({ rejectedQuestionFamilies, selectedQuestionTemplate, reasoningPatternReason, + unresolvedCandidateCount, + eligibleCandidateCount, + candidateNodeIds, + resolvedCurrentTurnNodeIds, + noQuestionReason, }) { return { promptVersion: promptVersion ?? "v0.4", @@ -281,6 +286,11 @@ function buildUpdateDiagnostics({ rejectedQuestionFamilies: rejectedQuestionFamilies ?? [], selectedQuestionTemplate: selectedQuestionTemplate ?? null, reasoningPatternReason: reasoningPatternReason ?? null, + unresolvedCandidateCount: unresolvedCandidateCount ?? 0, + eligibleCandidateCount: eligibleCandidateCount ?? 0, + candidateNodeIds: candidateNodeIds ?? [], + resolvedCurrentTurnNodeIds: resolvedCurrentTurnNodeIds ?? [], + noQuestionReason: noQuestionReason ?? null, unknownSelectionExplanation: unknownSelectionExplanation ?? null, }; } @@ -660,6 +670,11 @@ async function updateCaseWithDependencies(body, dependencies = {}) { finalQuestion: null, selectedUnknownBefore: null, selectedUnknownAfter: null, + unresolvedCandidateCount: 0, + eligibleCandidateCount: 0, + candidateNodeIds: [], + resolvedCurrentTurnNodeIds: [], + noQuestionReason: null, plainLanguageNormalisations: [], unknownSelectionExplanation: explainUnknownSelection( situationGraph, @@ -759,6 +774,12 @@ async function updateCaseWithDependencies(body, dependencies = {}) { finalQuestion: applicationResult.finalQuestion, selectedUnknownBefore: applicationResult.selectedUnknownBefore, selectedUnknownAfter: applicationResult.selectedUnknownAfter, + unresolvedCandidateCount: applicationResult.unresolvedCandidateCount, + eligibleCandidateCount: applicationResult.eligibleCandidateCount, + candidateNodeIds: applicationResult.candidateNodeIds, + resolvedCurrentTurnNodeIds: + applicationResult.resolvedCurrentTurnNodeIds, + noQuestionReason: applicationResult.noQuestionReason, plainLanguageNormalisations: applicationResult.plainLanguageNormalisations, reasoningPattern: @@ -850,6 +871,11 @@ async function updateCaseWithDependencies(body, dependencies = {}) { finalQuestion: null, selectedUnknownBefore: null, selectedUnknownAfter: null, + unresolvedCandidateCount: 0, + eligibleCandidateCount: 0, + candidateNodeIds: [], + resolvedCurrentTurnNodeIds: [], + noQuestionReason: null, plainLanguageNormalisations: [], reasoningPattern: null, questionFamily: null, diff --git a/lib/graph/question-formulator.js b/lib/graph/question-formulator.js index 111f7f8..e7b2db3 100644 --- a/lib/graph/question-formulator.js +++ b/lib/graph/question-formulator.js @@ -504,6 +504,9 @@ function buildFoundationalDirectQuestion(node) { if (/^who experiences this problem$/i.test(label)) { return "Who experiences this problem?"; } + if (/^whether other people experience this problem$/i.test(label)) { + return "What makes you think other people experience this problem too?"; + } if (/^what happens when this problem is not resolved$/i.test(label)) { return "What happens when this problem is not resolved?"; } diff --git a/tests/graph/apply-proposal.test.js b/tests/graph/apply-proposal.test.js index 6fcde75..33400e6 100644 --- a/tests/graph/apply-proposal.test.js +++ b/tests/graph/apply-proposal.test.js @@ -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/, + ); + }); }); diff --git a/tests/graph/orchestrator.test.js b/tests/graph/orchestrator.test.js index 74574d2..128b124 100644 --- a/tests/graph/orchestrator.test.js +++ b/tests/graph/orchestrator.test.js @@ -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"); diff --git a/tests/ui/scenario-form.test.jsx b/tests/ui/scenario-form.test.jsx index d93caf3..9a879f8 100644 --- a/tests/ui/scenario-form.test.jsx +++ b/tests/ui/scenario-form.test.jsx @@ -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( + , + ); + + 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."); + }); }); \ No newline at end of file