test(ui): make initial branch selection user owned
This commit is contained in:
@@ -0,0 +1,208 @@
|
||||
/**
|
||||
* RTO.27F — Primary lifecycle surface invariant
|
||||
*
|
||||
* Tests that exactly one primary surface renders in every meaningful state:
|
||||
* no zero, no two. Exercises the exported predicates from
|
||||
* scenario-form.jsx directly to avoid SSR/React-Testing-Library complications.
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { hasValidInvestigationContext, derivePrimarySurface } from "@/components/scenario-form";
|
||||
|
||||
/* ── Helpers ─────────────────────────────────────────────────────── */
|
||||
|
||||
const EMPTY_RESULT = null;
|
||||
const RESULT_WITH_GRAPH = { situationGraph: {}, selectedQuestion: "q1" };
|
||||
const SCENARIO_EMPTY = "";
|
||||
const SCENARIO_SET = "Some situation description";
|
||||
|
||||
/* ── hasValidInvestigationContext tests ─────────────────────────── */
|
||||
|
||||
describe("hasValidInvestigationContext", () => {
|
||||
it("rejects idle + no result + empty scenario", () => {
|
||||
expect(hasValidInvestigationContext(EMPTY_RESULT, "idle", SCENARIO_EMPTY)).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects loading state", () => {
|
||||
expect(hasValidInvestigationContext(EMPTY_RESULT, "loading", SCENARIO_SET)).toBe(false);
|
||||
});
|
||||
|
||||
it("accepts result with situationGraph", () => {
|
||||
expect(hasValidInvestigationContext(RESULT_WITH_GRAPH, "success", SCENARIO_EMPTY)).toBe(true);
|
||||
});
|
||||
|
||||
it("accepts success + non-empty scenario (session restoration)", () => {
|
||||
expect(hasValidInvestigationContext(EMPTY_RESULT, "success", SCENARIO_SET)).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects success + empty scenario (no investigation data)", () => {
|
||||
expect(hasValidInvestigationContext(EMPTY_RESULT, "success", SCENARIO_EMPTY)).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects error state even with graph (graph is valid but surface is ERROR)", () => {
|
||||
// Note: hasGraph returns true — the derivePrimarySurface function handles this by routing to ERROR_SURFACE first.
|
||||
expect(hasValidInvestigationContext(RESULT_WITH_GRAPH, "error", SCENARIO_SET)).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects false scenario input (empty string)", () => {
|
||||
expect(hasValidInvestigationContext(EMPTY_RESULT, "success", "")).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects whitespace-only scenario", () => {
|
||||
expect(hasValidInvestigationContext(EMPTY_RESULT, "success", " ")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
/* ── derivePrimarySurface tests (7 required cases) ─────────────── */
|
||||
|
||||
describe("derivePrimarySurface — CASE 1: idle / no result / experiment=false", () => {
|
||||
it("returns SCENARIO_ENTRY", () => {
|
||||
const surface = derivePrimarySurface(EMPTY_RESULT, "idle", false, SCENARIO_EMPTY);
|
||||
expect(surface).toBe("SCENARIO_ENTRY");
|
||||
expect(surface).not.toBeNull();
|
||||
expect(surface).not.toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
describe("derivePrimarySurface — CASE 2: idle / no result / experiment=true (stale flag)", () => {
|
||||
it("returns SCENARIO_ENTRY (not blank) — this is the RTO.27F critical fix", () => {
|
||||
const surface = derivePrimarySurface(EMPTY_RESULT, "idle", true, SCENARIO_EMPTY);
|
||||
expect(surface).toBe("SCENARIO_ENTRY");
|
||||
expect(surface).not.toBeNull();
|
||||
expect(surface).not.toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
describe("derivePrimarySurface — CASE 3: valid investigation / experiment=true / branch selected", () => {
|
||||
it("returns EXPERIMENT_NOTEBOOK when a branch is active", () => {
|
||||
const surface = derivePrimarySurface(RESULT_WITH_GRAPH, "success", true, SCENARIO_SET, "branch-a");
|
||||
expect(surface).toBe("EXPERIMENT_NOTEBOOK");
|
||||
expect(surface).not.toBeNull();
|
||||
expect(surface).not.toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
describe("derivePrimarySurface — RTO.28A: branch-selection surface", () => {
|
||||
it("returns BRANCH_SELECTION when valid investigation exists but activeBranchId is null", () => {
|
||||
const surface = derivePrimarySurface(RESULT_WITH_GRAPH, "success", true, SCENARIO_SET, null);
|
||||
expect(surface).toBe("BRANCH_SELECTION");
|
||||
expect(surface).not.toBeNull();
|
||||
expect(surface).not.toBe("");
|
||||
});
|
||||
|
||||
it("returns EXPERIMENT_NOTEBOOK when valid investigation exists and activeBranchId is set", () => {
|
||||
const surface = derivePrimarySurface(RESULT_WITH_GRAPH, "success", true, SCENARIO_SET, "branch-a");
|
||||
expect(surface).toBe("EXPERIMENT_NOTEBOOK");
|
||||
expect(surface).not.toBeNull();
|
||||
expect(surface).not.toBe("");
|
||||
});
|
||||
|
||||
it("returns EXPERIMENT_NOTEBOOK when valid investigation exists and activeBranchId is branch-b", () => {
|
||||
const surface = derivePrimarySurface(RESULT_WITH_GRAPH, "success", true, SCENARIO_SET, "branch-b");
|
||||
expect(surface).toBe("EXPERIMENT_NOTEBOOK");
|
||||
expect(surface).not.toBeNull();
|
||||
expect(surface).not.toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
describe("derivePrimarySurface — CASE 4: valid investigation / experiment=false", () => {
|
||||
it("returns NORMAL_WORKSPACE", () => {
|
||||
const surface = derivePrimarySurface(RESULT_WITH_GRAPH, "success", false, SCENARIO_SET);
|
||||
expect(surface).toBe("NORMAL_WORKSPACE");
|
||||
expect(surface).not.toBeNull();
|
||||
expect(surface).not.toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
describe("derivePrimarySurface — CASE 5: loading", () => {
|
||||
it("returns LOADING regardless of other flags", () => {
|
||||
const surface = derivePrimarySurface(RESULT_WITH_GRAPH, "loading", true, SCENARIO_SET);
|
||||
expect(surface).toBe("LOADING");
|
||||
expect(surface).not.toBeNull();
|
||||
|
||||
// Even with empty scenario or no graph — loading always wins
|
||||
const surface2 = derivePrimarySurface(null, "loading", false, "");
|
||||
expect(surface2).toBe("LOADING");
|
||||
expect(surface2).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("derivePrimarySurface — CASE 6: error / valid context", () => {
|
||||
it("returns ERROR_SURFACE when graph exists", () => {
|
||||
const surface = derivePrimarySurface(RESULT_WITH_GRAPH, "error", false, SCENARIO_SET);
|
||||
expect(surface).toBe("ERROR_SURFACE");
|
||||
expect(surface).not.toBeNull();
|
||||
});
|
||||
|
||||
it("returns ERROR_SURFACE even with no graph (safe fallback)", () => {
|
||||
const surface = derivePrimarySurface(null, "error", false, SCENARIO_SET);
|
||||
expect(surface).toBe("ERROR_SURFACE");
|
||||
expect(surface).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("derivePrimarySurface — CASE 7: partial/stale restored session", () => {
|
||||
it("returns SCENARIO_ENTRY when no graph and idle (stale experiment flag)", () => {
|
||||
const surface = derivePrimarySurface(EMPTY_RESULT, "idle", true, SCENARIO_EMPTY);
|
||||
expect(surface).toBe("SCENARIO_ENTRY");
|
||||
expect(surface).not.toBeNull();
|
||||
});
|
||||
|
||||
it("returns BRANCH_SELECTION when session has scenario + success (valid context, no branch)", () => {
|
||||
const surface = derivePrimarySurface(EMPTY_RESULT, "success", true, SCENARIO_SET);
|
||||
expect(surface).toBe("BRANCH_SELECTION");
|
||||
expect(surface).not.toBeNull();
|
||||
});
|
||||
|
||||
it("returns SCENARIO_ENTRY for stale experiment preference without any session data", () => {
|
||||
const surface = derivePrimarySurface(EMPTY_RESULT, "idle", true, "");
|
||||
expect(surface).toBe("SCENARIO_ENTRY");
|
||||
expect(surface).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
/* ── Exhaustive invariant: every state maps to exactly one surface ─ */
|
||||
|
||||
describe("PRIMARY_SURFACE_COUNT — exhaustive invariant (zero allowed, two forbidden)", () => {
|
||||
const testCases = [
|
||||
// idle variants
|
||||
{ result: null, status: "idle", exp: false, scenario: "", ab: null, expected: "SCENARIO_ENTRY" },
|
||||
{ result: null, status: "idle", exp: true, scenario: "", ab: null, expected: "SCENARIO_ENTRY" },
|
||||
{ result: null, status: "idle", exp: false, scenario: "x", ab: null, expected: "SCENARIO_ENTRY" },
|
||||
// loading variants
|
||||
{ result: null, status: "loading", exp: false, scenario: "", ab: null, expected: "LOADING" },
|
||||
{ result: null, status: "loading", exp: true, scenario: "x", ab: null, expected: "LOADING" },
|
||||
{ result: RESULT_WITH_GRAPH, status: "loading", exp: false, scenario: "x", ab: null, expected: "LOADING" },
|
||||
// error variants
|
||||
{ result: null, status: "error", exp: false, scenario: "x", ab: null, expected: "ERROR_SURFACE" },
|
||||
{ result: RESULT_WITH_GRAPH, status: "error", exp: true, scenario: "x", ab: null, expected: "ERROR_SURFACE" },
|
||||
// success — no graph, empty scenario (no valid context)
|
||||
{ result: null, status: "success", exp: false, scenario: "", ab: null, expected: "SCENARIO_ENTRY" },
|
||||
{ result: null, status: "success", exp: true, scenario: "", ab: null, expected: "SCENARIO_ENTRY" },
|
||||
// success — no graph, non-empty scenario (session restored without graph)
|
||||
{ result: null, status: "success", exp: false, scenario: "x", ab: null, expected: "NORMAL_WORKSPACE" },
|
||||
{ result: null, status: "success", exp: true, scenario: "x", ab: null, expected: "BRANCH_SELECTION" },
|
||||
// success — with graph, no branch → BRANCH_SELECTION
|
||||
{ result: RESULT_WITH_GRAPH, status: "success", exp: true, scenario: "x", ab: null, expected: "BRANCH_SELECTION" },
|
||||
// success — with graph, branch-a selected → EXPERIMENT_NOTEBOOK
|
||||
{ result: RESULT_WITH_GRAPH, status: "success", exp: true, scenario: "x", ab: "branch-a", expected: "EXPERIMENT_NOTEBOOK" },
|
||||
// success — with graph, branch-b selected → EXPERIMENT_NOTEBOOK
|
||||
{ result: RESULT_WITH_GRAPH, status: "success", exp: true, scenario: "x", ab: "branch-b", expected: "EXPERIMENT_NOTEBOOK" },
|
||||
// success — with graph, experiment=false (production)
|
||||
{ result: RESULT_WITH_GRAPH, status: "success", exp: false, scenario: "x", ab: null, expected: "NORMAL_WORKSPACE" },
|
||||
];
|
||||
|
||||
for (const tc of testCases) {
|
||||
it(
|
||||
`${tc.status}|exp=${tc.exp}|graph=${!!tc.result?.situationGraph}|ab=${tc.ab}|"${tc.scenario}" → ${tc.expected}`,
|
||||
() => {
|
||||
const surface = derivePrimarySurface(tc.result, tc.status, tc.exp, tc.scenario, tc.ab);
|
||||
expect(surface).toBe(tc.expected);
|
||||
// The invariant: exactly one surface, never null/undefined/blank
|
||||
expect(surface).not.toBeNull();
|
||||
expect(surface).not.toBe("");
|
||||
expect(surface).toBeDefined();
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user