Files
canvas-frontend/src/state/store.test.ts
T
shad d5a9c81b9c
build-and-publish / test (push) Has been cancelled
build-and-publish / image (push) Has been cancelled
feat(dogfood-wave1): Studio retries, Chat error, LLM gateway, kill snapshot, topbar cleanup (#14)
2026-06-15 14:30:04 +00:00

85 lines
3.3 KiB
TypeScript

// 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<string, string>();
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: "live", 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() before any fetch happens still works", async () => {
const calls = stubFetch();
await useApp.getState().refreshLive();
expect(useApp.getState().mode).toBe("live");
expect(calls.length).toBeGreaterThanOrEqual(0);
});
it("refreshLive() handles an empty EA2 response without throwing", async () => {
stubFetch();
await useApp.getState().setMode("live");
expect(useApp.getState().mode).toBe("live");
expect(Array.isArray(useApp.getState().scenarios)).toBe(true);
});
it("refreshLive() leaves liveError null when the stub returns success", async () => {
stubFetch();
await useApp.getState().setMode("live");
await useApp.getState().refreshLive();
expect(useApp.getState().liveError).toBeNull();
});
});