Files
confidence-engine/tests/presentation-order-invariant.test.jsx

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);
});
});
});