Turn {c.sequence || idx + 1} — contribution ({getHistoricalPropositions(c, findings).length ?? 0} observations, {c.uncertainties?.length ?? 0} unclear)
@@ -564,12 +571,16 @@ function SecondaryPreviousLearning({ nodeId, contributions, findings }) {
const priorContribs = threadContribs.slice(0, -1);
if (!priorContribs.length) return null;
+ // Presentation-reversal: render newest prior turn first so user sees what was learned most recently at the top.
+ // This is a presentation-only decision; chronological order in data is preserved elsewhere.
+ const reversedPrior = [...priorContribs].reverse();
+
return (
Previous learning
- {priorContribs.map((c, idx) => (
+ {reversedPrior.map((c, idx) => (
Turn {c.sequence || idx + 1} — contribution ({getHistoricalPropositions(c, findings).length ?? 0} observations, {c.uncertainties?.length ?? 0} unclear)
diff --git a/tests/open-questions-vs-assumptions.test.jsx b/tests/open-questions-vs-assumptions.test.jsx
index c2aa09f..552e7b2 100644
--- a/tests/open-questions-vs-assumptions.test.jsx
+++ b/tests/open-questions-vs-assumptions.test.jsx
@@ -1604,9 +1604,173 @@ describe("Narrow textarea rendering condition — v0.49 completed-result repair"
});
});
- // ── v0.49 — completed-result provenance narrative ──────────────
+ // ── v0.50: IN-PLACE FOLLOW-UP CONTINUATION (defect: follow-up selection clears hasAnswer, causing "Question" header to reappear) ─
- describe("v0.49 Case A — completed turn provenance narrative", () => {
+// Simulated FQB condition for determining whether to render completed-narrative framing
+function shouldRenderCompletedFraming(focused) {
+ const hasAnswer = Boolean(focused?.answer);
+ // OLD (buggy): only checks answer
+ return hasAnswer && Boolean(focused?.question?.trim());
+}
+
+// FIXED condition: also treats a non-null result as "completed context exists"
+function shouldRenderCompletedFraming_FIXED(focused) {
+ const hasAnswer = Boolean(focused?.answer);
+ const hasCompletedContext = Boolean(focused?.result);
+ return (hasAnswer || hasCompletedContext) && Boolean(focused?.question?.trim());
+}
+
+// ── Simulated reverse rendering for Previous Learning chronology ────────
+
+function renderPriorContribs(chronological) {
+ // Mirrors PriorContributionsSummary / SecondaryPreviousLearning: slice(0, -1) preserves order
+ const prior = chronological.slice(0, -1);
+ return prior.map((c, idx) => ({
+ label: `Turn ${c.sequence || idx + 1}`,
+ sequence: c.sequence,
+ id: c.id,
+ }));
+}
+
+function renderPriorContribs_REVERSED(chronological) {
+ // Reversed at presentation boundary only
+ const prior = chronological.slice(0, -1);
+ return [...prior].reverse().map((c, idx) => ({
+ label: `Turn ${c.sequence || idx + 1}`,
+ sequence: c.sequence,
+ id: c.id,
+ }));
+}
+
+describe("v0.50 Case A — selected follow-up continues in place with completed context", () => {
+ it("OLD: selecting follow-up (answer=null, result exists) does NOT render completed framing → proves defect", () => {
+ const beforeSelect = {
+ question: "Turn 2 question",
+ answer: "Turn 2 answer", // non-null = hasAnswer = true
+ status: "formulated",
+ result: { observations: ["Finding A"], uncertainties: [], possibleFollowUpQuestions: ["Should we explore X?"] },
+ };
+ expect(shouldRenderCompletedFraming(beforeSelect)).toBe(true);
+
+ const afterSelect = {
+ ...beforeSelect,
+ question: "Should we explore X?", // follow-up question
+ answer: null, // setFollowUpQuestion clears answer
+ status: "formulated",
+ result: beforeSelect.result, // result stays (causal context)
+ };
+
+ // BUG: after selecting a follow-up, completed framing is lost
+ expect(shouldRenderCompletedFraming(afterSelect)).toBe(false);
+ });
+
+ it("FIXED: after follow-up selection with result, completed framing IS preserved → proves repair", () => {
+ const afterSelect = {
+ question: "Should we explore X?",
+ answer: null,
+ status: "formulated",
+ result: { observations: ["Finding A"], uncertainties: [], possibleFollowUpQuestions: ["Another option?"] },
+ };
+
+ expect(shouldRenderCompletedFraming_FIXED(afterSelect)).toBe(true);
+ });
+});
+
+// ── Simulated reverse rendering for Previous Learning ──────────────
+
+function renderPriorContribs(contributions) {
+ // PriorContributionsSummary and SecondaryPreviousLearning use: slice(0, -1)
+ const prior = contributions.slice(0, -1);
+ return prior.map((c, idx) => ({
+ label: `Turn ${c.sequence || idx + 1}`,
+ sequence: c.sequence,
+ id: c.id,
+ }));
+}
+
+function renderPriorContribs_REVERSED(contributions) {
+ const prior = contributions.slice(0, -1);
+ // Reverse at presentation boundary only
+ return [...prior].reverse().map((c, idx) => ({
+ label: `Turn ${c.sequence || idx + 1}`,
+ sequence: c.sequence,
+ id: c.id,
+ }));
+}
+
+describe("v0.50 Prior contribs display order — reversed at presentation boundary", () => {
+ const threeContributions = [
+ { id: "contrib-turn-1", sequence: 1, targetNodeId: "node-A", observations: ["Turn 1 finding"] },
+ { id: "contrib-turn-2", sequence: 2, targetNodeId: "node-A", observations: ["Turn 2 finding"] },
+ { id: "contrib-turn-3", sequence: 3, targetNodeId: "node-A", observations: ["Turn 3 finding"] }, // latest = excluded as current result
+ ];
+
+ it("chronological order: slice(0,-1) preserves input sequence [1, 2]", () => {
+ const ordered = renderPriorContribs(threeContributions);
+ expect(ordered.map((c) => c.sequence)).toEqual([1, 2]);
+ });
+
+ it("REVERSED display order: newest prior turn first [2, 1]", () => {
+ const reversed = renderPriorContribs_REVERSED(threeContributions);
+ expect(reversed.map((c) => c.sequence)).toEqual([2, 1]);
+ });
+
+ it("reversal puts Turn 2 (newest prior) at top of the list for the user", () => {
+ const reversed = renderPriorContribs_REVERSED(threeContributions);
+ expect(reversed[0].label).toBe("Turn 2");
+ expect(reversed[1].label).toBe("Turn 1");
+ });
+
+ it("data order is unchanged — only the presentation layer reverses", () => {
+ const ordered = renderPriorContribs(threeContributions);
+ const reversed = renderPriorContribs_REVERSED(threeContributions);
+ // The data order (ordered) is unaffected by reversing
+ expect(ordered.map((c) => c.sequence)).toEqual([1, 2]);
+ // Reversal produces the opposite visual order for the user
+ expect(reversed.map((c) => c.sequence)).toEqual([2, 1]);
+ // Both refer to the same underlying contributions (same IDs)
+ const orderedIds = new Set(ordered.map((c) => c.id));
+ const reversedIds = new Set(reversed.map((c) => c.id));
+ expect(orderedIds).toEqual(reversedIds);
+ });
+
+ it("two-turn thread: reversal still works", () => {
+ const twoContributions = [
+ { id: "contrib-turn-1", sequence: 1, targetNodeId: "node-B" },
+ { id: "contrib-turn-2", sequence: 2, targetNodeId: "node-B" }, // excluded as current
+ ];
+
+ const reversed = renderPriorContribs_REVERSED(twoContributions);
+ expect(reversed).toHaveLength(1);
+ expect(reversed[0].sequence).toBe(1);
+ });
+
+ it("single prior turn: reversal is no-op (one element)", () => {
+ const single = [
+ { id: "contrib-turn-1", sequence: 1, targetNodeId: "node-C" },
+ { id: "contrib-turn-2", sequence: 2, targetNodeId: "node-C" }, // excluded as current
+ ];
+
+ const reversed = renderPriorContribs_REVERSED(single);
+ expect(reversed).toHaveLength(1);
+ expect(reversed[0].sequence).toBe(1);
+ });
+
+ it("reversal with non-sequential sequences preserves correct chronological reverse", () => {
+ const irregular = [
+ { id: "contrib-A", sequence: 5, targetNodeId: "node-D" },
+ { id: "contrib-B", sequence: 10, targetNodeId: "node-D" },
+ { id: "contrib-C", sequence: 15, targetNodeId: "node-D" }, // excluded as current
+ ];
+
+ const reversed = renderPriorContribs_REVERSED(irregular);
+ expect(reversed.map((c) => c.sequence)).toEqual([10, 5]);
+ });
+});
+
+// ── v0.50 INVESTIGATING cue on normal Open Questions buttons (actual render path) ───
+
+describe("v0.49 Case A — completed turn provenance narrative", () => {
it("completed turn renders distinct user response and derived findings sections", () => {
const focused = {
question: "Completed question",