import { describe, expect, it } from "vitest"; // ── Simulated rendering structure extracted from reasoning-workspace.jsx ─────── // Mirrors the exact DOM order within the workspace grid container (lg:grid-cols-3). // Tests the invariant: Open Questions MUST always appear before Possible Interpretations. // Simulated grid-section ordering for each state function getGridSectionOrder(state) { const { postAnalyseStatus, hasGraph, hasAssumptions } = state; if (!hasGraph) return []; let sections = []; // Section A: Current Understanding (only when not in initial reflection) if (postAnalyseStatus !== "success") { sections.push("current-understanding"); } // Section B: Investigation + Open Questions (always when not in initial reflection) if (postAnalyseStatus !== "success") { sections.push("investigation"); sections.push("open-questions"); // OQ is nested inside investigation container } // Section C: Possible Interpretations (only when assumptions exist and not in initial reflection) if (postAnalyseStatus !== "success" && hasAssumptions) { sections.push("possible-interpretations"); } return sections; } // Simulated initial reflection surface ordering (space-y-6 flex-wrap layout) function getInitialReflectionSectionOrder(postAnalyseStatus, hasGraph) { if (postAnalyseStatus !== "success") return []; let sections = []; // In initial reflection: Current Understanding + Situation render side by side sections.push("current-understanding"); sections.push("situation"); // Then in separate space-y-6 container: Open Questions before Possible Interpretations sections.push("open-questions"); sections.push("possible-interpretations"); return sections; } describe("presentation-order-invariant", () => { describe("workspace grid ordering (post-Analyse states)", () => { it("Open Questions always renders before Possible Interpretations during focused investigation", () => { const sections = getGridSectionOrder({ postAnalyseStatus: null, hasGraph: true, hasAssumptions: true, }); const oqIndex = sections.indexOf("open-questions"); const piIndex = sections.indexOf("possible-interpretations"); expect(oqIndex).toBeGreaterThan(-1); expect(piIndex).toBeGreaterThan(-1); expect(oqIndex).toBeLessThan(piIndex); }); it("Open Questions always renders before Possible Interpretations during post-contribution state", () => { const sections = getGridSectionOrder({ postAnalyseStatus: null, hasGraph: true, hasAssumptions: true, }); const oqIndex = sections.indexOf("open-questions"); const piIndex = sections.indexOf("possible-interpretations"); expect(oqIndex).toBeLessThan(piIndex); }); it("Post-Analyse Completion: Open Questions still before Possible Interpretations", () => { const sections = getGridSectionOrder({ postAnalyseStatus: null, hasGraph: true, hasAssumptions: true, }); // Even when all unknowns are resolved (completion state), // the DOM order within the workspace grid preserves OQ before PI const oqIndex = sections.indexOf("open-questions"); const piIndex = sections.indexOf("possible-interpretations"); expect(oqIndex).toBeLessThan(piIndex); }); it("No Possible Interpretations when no assumptions exist", () => { const sections = getGridSectionOrder({ postAnalyseStatus: null, hasGraph: true, hasAssumptions: false, }); expect(sections).not.toContain("possible-interpretations"); expect(sections).toContain("open-questions"); }); it("All three sections exist during normal focused investigation with assumptions", () => { const sections = getGridSectionOrder({ postAnalyseStatus: null, hasGraph: true, hasAssumptions: true, }); expect(sections).toContain("current-understanding"); expect(sections).toContain("investigation"); expect(sections).toContain("open-questions"); expect(sections).toContain("possible-interpretations"); }); it("grid row positions enforce OQ-before-PI even if DOM order changes", () => { // The CSS Grid row-start values create an additional safeguard: // Investigation + Open Questions = row-start-2, Situation = row-start-2 col 3, // Possible Interpretations = row-start-4 (below everything) // This means OQ is at rows 2-3 while PI is at row 4 — structurally enforced. const positions = { "current-understanding": { rowStart: 1 }, "investigation": { rowStart: 2, rowEnd: 4, colStart: 1, colSpan: 2 }, "open-questions": { rowStart: 2, rowEnd: 4, colStart: 1, colSpan: 2 }, "situation": { rowStart: 2, rowEnd: 4, colStart: 3 }, "possible-interpretations": { rowStart: 4 }, }; // Investigation+OQ occupy rows 2-3; PI occupies row 4 — never overlapping expect(positions["investigation"].rowStart).toBeLessThan(positions["possible-interpretations"].rowStart); expect(positions["open-questions"].rowStart).toBeLessThan(positions["possible-interpretations"].rowStart); // Situation occupies same rows as Investigation (parallel lane) expect(positions["situation"].rowStart).toBe(positions["investigation"].rowStart); expect(positions["situation"].rowEnd).toBe(positions["investigation"].rowEnd); }); }); describe("initial reflection surface ordering", () => { it("Open Questions renders before Possible Interpretations in initial post-Analyse state", () => { const sections = getInitialReflectionSectionOrder("success", true); const oqIndex = sections.indexOf("open-questions"); const piIndex = sections.indexOf("possible-interpretations"); expect(oqIndex).toBeGreaterThan(-1); expect(piIndex).toBeGreaterThan(-1); expect(oqIndex).toBeLessThan(piIndex); }); it("Current Understanding and Situation render before Open Questions in initial state", () => { const sections = getInitialReflectionSectionOrder("success", true); const cuIndex = sections.indexOf("current-understanding"); const oqIndex = sections.indexOf("open-questions"); const piIndex = sections.indexOf("possible-interpretations"); expect(cuIndex).toBeLessThan(oqIndex); // Situation is side-by-side with Current Understanding (flex row) // Both are in the first flex-row container, before space-y-6 OQ+PI container }); it("initial reflection has NO grid-based positioning — uses natural DOM flow", () => { // Initial reflection surface uses `space-y-6` (vertical stacking), NOT CSS Grid. // Ordering is purely determined by DOM order within the flex-wrap and space-y-6 containers. const sections = getInitialReflectionSectionOrder("success", true); // No row-start/col-start values in initial reflection expect(sections).toEqual([ "current-understanding", "situation", "open-questions", "possible-interpretations", ]); }); }); describe("cross-state consistency", () => { it("canonical order is the same in all states: OQ before PI", () => { const gridOrder = getGridSectionOrder({ postAnalyseStatus: null, hasGraph: true, hasAssumptions: true, }); const initialOrder = getInitialReflectionSectionOrder("success", true); // Verify OQ always precedes PI in both paths expect(gridOrder.indexOf("open-questions")).toBeLessThan(gridOrder.indexOf("possible-interpretations")); expect(initialOrder.indexOf("open-questions")).toBeLessThan(initialOrder.indexOf("possible-interpretations")); }); it("no state renders Possible Interpretations without also rendering Open Questions (when graph exists)", () => { const states = [ { postAnalyseStatus: null, hasGraph: true, hasAssumptions: true }, { postAnalyseStatus: null, hasGraph: true, hasAssumptions: false }, { postAnalyseStatus: "success", hasGraph: true, hasAssumptions: true }, ]; for (const state of states) { const order = getGridSectionOrder(state); const hasPI = order.includes("possible-interpretations"); const hasOQ = order.includes("open-questions"); // When PI exists (graph+assumptions+not-initial), OQ also exists if (hasPI) { expect(hasOQ).toBe(true); } } }); }); describe("grid position integrity", () => { it("no two sections occupy the same row-start without column separation", () => { // Situation and Investigation share row-start-2 but are in different columns (3 vs 1-2) const positions = [ { name: "current-understanding", rowStart: 1, colStart: 1, colSpan: 3 }, { name: "investigation", rowStart: 2, rowEnd: 4, colStart: 1, colSpan: 2 }, { name: "possible-interpretations", rowStart: 4, colStart: 1, colSpan: 3 }, ]; // Check for overlapping sections that share a row but don't have column separation for (let i = 0; i < positions.length; i++) { for (let j = i + 1; j < positions.length; j++) { const a = positions[i]; const b = positions[j]; const aRowEnd = a.rowEnd || (a.rowStart || 0) + 1; const bRowEnd = b.rowEnd || (b.rowStart || 0) + 1; const rowsOverlap = !(aRowEnd <= (b.rowStart || 0) || (bRowEnd || 0) <= a.rowStart); if (rowsOverlap) { // If they share a row, they must not overlap in columns expect(a.colStart + a.colSpan).toBeLessThanOrEqual(b.colStart); } } } }); it("Investigation+OQ and Situation have identical row spans (parallel lanes)", () => { const investigationRowStart = 2; const investigationRowEnd = 4; const situationRowStart = 2; const situationRowEnd = 4; expect(investigationRowStart).toBe(situationRowStart); expect(investigationRowEnd).toBe(situationRowEnd); }); it("Current Understanding row (1) is above Investigation row (2)", () => { const cuRow = 1; const invRow = 2; expect(cuRow).toBeLessThan(invRow); }); it("Possible Interpretations row (4) is below all others", () => { const piRow = 4; expect(piRow).toBeGreaterThan(3); // Must be > max of other rows (Investigation ends at 4, but starts at 2) }); }); describe("edge cases", () => { it("no graph produces empty grid sections", () => { const sections = getGridSectionOrder({ postAnalyseStatus: null, hasGraph: false, hasAssumptions: true, }); expect(sections).toHaveLength(0); }); it("initial reflection state does not render grid sections", () => { const sections = getGridSectionOrder({ postAnalyseStatus: "success", hasGraph: true, hasAssumptions: true, }); // postAnalyseStatus === "success" suppresses all grid sections (handled by initial reflection surface) expect(sections).toHaveLength(0); }); it("transition from focused investigation to completion preserves OQ-before-PI ordering", () => { const preCompletion = getGridSectionOrder({ postAnalyseStatus: null, hasGraph: true, hasAssumptions: true, }); const oqBeforePi = preCompletion.indexOf("open-questions") < preCompletion.indexOf("possible-interpretations"); expect(oqBeforePi).toBe(true); }); }); });