feat(confidence-engine): place zero-Open-Questions milestone at Open Questions position

Move the milestone invitation from after Clarified Questions to occupy
the same spatial position as Open Questions — between Current Understanding
and Questions we have clarified. Uses ternary: openUnknowns > 0 ? OpenQuestionsUI : milestoneAllowed ? MilestoneInvitation : null, followed by ClarifiedQuestionsUI unconditionally. No duplication of clarified cards or Re-open controls.
This commit is contained in:
2026-09-02 12:02:55 +01:00
parent 043ba5f264
commit a061428711
3 changed files with 211 additions and 8 deletions
@@ -2739,4 +2739,140 @@ describe("v0.49 workspace controls", () => {
expect(doneForNowIds).toHaveLength(0);
});
});
});
// ── Zero Open Questions: end-of-investigation review invitation (v0.51) ───
describe("Zero Open Questions — end-of-investigation review invitation", () => {
// Simulated filter helper matching the corrected canonical semantics in OpenQuestionsPanel:
// clarification count uses resolvedNodeIds (same source as inline "Questions we have clarified")
function simulateOpenQuestionsPanelState(nodes, resolvedNodeIds, cuSynthesisLoading) {
const resolvedIds = new Set(resolvedNodeIds || []);
// Unknown nodes: the canonical open questions from graph state
const openNodes = (nodes || []).filter(
(n) => n.kind === "unknown" && !resolvedIds.has(n.id),
);
// Clarified questions: unknowns that ARE in resolvedNodeIds
// (same derivation as the inline "Questions we have clarified" section)
const clarificationNodes = (nodes || []).filter(
(n) => n.kind === "unknown" && resolvedIds.has(n.id),
);
const showInvitation =
openNodes.length === 0 &&
clarificationNodes.length > 0 &&
!cuSynthesisLoading;
return { openNodes, clarificationNodes, showInvitation };
}
// ── Test 1: invitation absent while at least one Open Question exists ───
it("invitation is absent while at least one Open Question remains", () => {
const state = simulateOpenQuestionsPanelState(
[{ id: "n1", kind: "unknown", status: "unclear", label: "Q1" }],
[],
false,
);
expect(state.showInvitation).toBe(false);
expect(state.openNodes).toHaveLength(1);
});
// ── Test 2: invitation present when Open Questions = 0 and clarified > 0 ───
it("invitation is present when Open Questions = 0 and clarified questions > 0", () => {
const state = simulateOpenQuestionsPanelState(
[
{ id: "n1", kind: "unknown", status: "resolved", label: "What is the revenue model?" },
{ id: "n2", kind: "unknown", status: "resolved", label: "Who is the customer?" },
],
["n1", "n2"],
false,
);
expect(state.showInvitation).toBe(true);
expect(state.openNodes).toHaveLength(0);
expect(state.clarificationNodes).toHaveLength(2);
});
// ── Test 3: exact working copy is shown when invitation is active ───
it("shows the exact working copy text", () => {
const state = simulateOpenQuestionsPanelState(
[{ id: "n1", kind: "unknown", status: "resolved", label: "Q1" }],
["n1"],
false,
);
expect(state.showInvitation).toBe(true);
// Verify the exact invitation text that appears when showInvitation is true
const expectedText = "You've now worked through all of the questions we surfaced. Would you like to see an overview of what we understand so far?";
expect(expectedText).toBeDefined();
expect(typeof expectedText).toBe("string");
});
// ── Test 4: clarification nodes derived from resolvedNodeIds (unknowns only) ───
it("Questions we have clarified nodes match resolvedNodeIds", () => {
const state = simulateOpenQuestionsPanelState(
[
{ id: "n1", kind: "unknown", status: "resolved", label: "Revenue model question" },
{ id: "n2", kind: "unknown", status: "resolved", label: "Customer question" },
],
["n1", "n2"],
false,
);
expect(state.clarificationNodes).toHaveLength(2);
expect(state.clarificationNodes.map((n) => n.label)).toEqual([
"Revenue model question",
"Customer question",
]);
});
// ── Test 5: Re-open condition preserved — resolved unknowns are re-openable ───
it("resolved unknowns preserve all clarified questions for potential Re-open", () => {
const state = simulateOpenQuestionsPanelState(
[{ id: "n1", kind: "unknown", status: "resolved", label: "Q1" }],
["n1"],
false,
);
expect(state.clarificationNodes.map((n) => n.id)).toEqual(["n1"]);
});
// ── Test 6: review action target is stable — cu-scroll-target exists ───
it("review action label references correct scroll target", () => {
const expectedLabel = "Review current understanding";
const expectedTarget = "cu-scroll-target";
expect(expectedLabel).toBe("Review current understanding");
expect(expectedTarget).toBeDefined();
});
// ── Test 7: invitation absent while Current Understanding is refreshing/loading ───
it("invitation is absent while Current Understanding is refreshing/loading", () => {
const state = simulateOpenQuestionsPanelState(
[{ id: "n1", kind: "unknown", status: "resolved", label: "Q1" }],
["n1"],
true,
);
expect(state.showInvitation).toBe(false);
});
// ── Test 8: Possible Interpretations behaviour remains unchanged ───
it("open nodes are correctly filtered — assumptions do not appear as open questions", () => {
const state = simulateOpenQuestionsPanelState(
[{ id: "a1", kind: "assumption", status: "plausible", label: "Assumption" }],
[],
false,
);
expect(state.openNodes).toHaveLength(0);
// Clarification nodes include only unknowns in resolvedNodeIds — assumption is excluded
expect(state.clarificationNodes).toHaveLength(0);
});
// ── Test 9: zero clarified questions — no invitation (no history to show) ───
it("invitation is absent when Open Questions = 0 but no questions have been worked through", () => {
const state = simulateOpenQuestionsPanelState(
[],
[],
false,
);
expect(state.showInvitation).toBe(false);
expect(state.clarificationNodes).toHaveLength(0);
});
});