296 lines
11 KiB
React
296 lines
11 KiB
React
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);
|
|
});
|
|
});
|
|
});
|