fix(oracle-r2): same-origin baseUrl + refresh actually re-fetches
Oracle round-2 review caught two real bugs: 1. Production baseUrl bypassed the nginx /api proxy - api.ts defaulted to https://demo.flow-master.ai in prod - Browser would hit cross-origin and CORS-fail - Now: baseUrl='' everywhere; nginx.conf already reverse-proxies /api/* - vite.config.ts proxy still handles dev 2. Refresh button didn't refresh - setMode('live') early-returned when already in live mode - Now: setMode() and refreshLive() share runLiveFetch(); refreshLive ignores the same-mode guard and always re-runs - 4 new vitest regressions in state/store.test.ts cover the contract - Smoke now asserts /api/ea2/work-items is called twice after Refresh Also: - buildScenarios.ts parallelized: cap N=6 candidates, Promise.all per- candidate fetches → live mode now ~3s instead of 30s - CommandBar + LeftRail preview toasts now name the exact endpoint (/api/runtime/transactions/{id}/actions) in the visible text - Landing 'Go live' button rebound to refreshLive() when already live; copy changed to 'Live · refresh' - README: scenario table now renders (added separator row); deploy section points at the real ops PR + the actual overlay path (overlays/demo, not overlays/mc.flow-master.ai); CORS doc clarifies same-origin requirement Constraint: browsers reject cross-origin → same-origin /api/* required Rejected: dev/prod baseUrl divergence | created production bug Confidence: high Scope-risk: narrow Not-tested: production image actually built + served by ops PR (gated by trusted updater + DNS)
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
// 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: "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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user