// Regression: refreshLive() must re-fetch when already in live mode, // and setMode("live") repeated must not early-return. import { describe, it, expect, vi, beforeEach } from "vitest"; import { useApp } from "./store"; function stubFetch() { const calls: string[] = []; const store = new Map(); globalThis.sessionStorage = { getItem: (k) => store.get(k) ?? null, setItem: (k, v) => { store.set(k, String(v)); }, removeItem: (k) => { store.delete(k); }, clear: () => store.clear(), key: () => null, length: 0, } as Storage; globalThis.fetch = vi.fn(async (input: RequestInfo | URL) => { const url = typeof input === "string" ? input : input.toString(); calls.push(url); if (url.endsWith("/api/v1/auth/dev-login")) { return new Response(JSON.stringify({ access_token: "T" }), { status: 200 }); } if (url.endsWith("/api/v1/auth/me")) { return new Response(JSON.stringify({ user_id: "u", tenant_id: "t", email: "dev@flow-master.ai" }), { status: 200 }); } if (url.includes("/api/ea2/work-items")) { return new Response(JSON.stringify({ items: [] }), { status: 200 }); } return new Response("not stubbed", { status: 404 }); }) as unknown as typeof fetch; return calls; } beforeEach(() => { useApp.setState({ mode: "snapshot", liveLoading: false, liveError: null, liveFetchedAt: null }); }); describe("store live-mode + refresh", () => { it("setMode('live') performs a network fetch", async () => { const calls = stubFetch(); await useApp.getState().setMode("live"); const meCalls = calls.filter((u) => u.endsWith("/api/v1/auth/me")).length; const wiCalls = calls.filter((u) => u.includes("/api/ea2/work-items")).length; expect(meCalls).toBeGreaterThanOrEqual(1); expect(wiCalls).toBeGreaterThanOrEqual(1); expect(useApp.getState().mode).toBe("live"); expect(useApp.getState().liveFetchedAt).toBeTruthy(); }); it("refreshLive() triggers a second fetch and bumps liveFetchedAt", async () => { const calls = stubFetch(); await useApp.getState().setMode("live"); const initialFetched = useApp.getState().liveFetchedAt!; const initialWi = calls.filter((u) => u.includes("/api/ea2/work-items")).length; await new Promise((r) => setTimeout(r, 5)); await useApp.getState().refreshLive(); const afterWi = calls.filter((u) => u.includes("/api/ea2/work-items")).length; expect(afterWi).toBeGreaterThan(initialWi); expect(useApp.getState().liveFetchedAt!).toBeGreaterThanOrEqual(initialFetched); }); it("refreshLive() in snapshot mode is a no-op (does NOT fetch)", async () => { const calls = stubFetch(); await useApp.getState().refreshLive(); expect(calls.length).toBe(0); expect(useApp.getState().mode).toBe("snapshot"); }); it("setMode('snapshot') when already snapshot does not re-toast", async () => { stubFetch(); const before = useApp.getState().toasts.length; await useApp.getState().setMode("snapshot"); expect(useApp.getState().toasts.length).toBe(before); }); it("refreshLive() resets selectedStepId to defaultStepId when the prior step no longer exists in the merged catalog", async () => { stubFetch(); await useApp.getState().setMode("live"); const scenario = useApp.getState().scenarios[0]; if (!scenario) throw new Error("no scenario"); useApp.setState({ scenarioId: scenario.id, selectedStepId: "definitely-not-a-real-step-id" }); await useApp.getState().refreshLive(); const after = useApp.getState(); const sc = after.scenarios.find((s) => s.id === after.scenarioId); expect(sc).toBeDefined(); expect(sc!.steps.some((st) => st.id === after.selectedStepId)).toBe(true); }); it("refreshLive() preserves a still-valid selectedStepId", async () => { stubFetch(); await useApp.getState().setMode("live"); const scenario = useApp.getState().scenarios[0]; if (!scenario) throw new Error("no scenario"); const validStep = scenario.steps[scenario.steps.length - 1]; useApp.setState({ scenarioId: scenario.id, selectedStepId: validStep.id }); await useApp.getState().refreshLive(); expect(useApp.getState().selectedStepId).toBe(validStep.id); }); });