Mission Control demo v2
Polished command-center for FlowMaster with two data modes:
- SNAPSHOT: bundled src/scenarios.json from demo.flow-master.ai
- LIVE: in-browser fetch via src/lib/api.ts (dev-login + bearer)
Scenarios:
- procurement, extra-1, extra-2 (live from EA2)
- ar, hcm, gl, service (industry blueprints, same typed shell)
Honesty pass after Oracle review:
- No invented numbers (Telemetry derives SLA + agent acceptance from real data)
- Preview-only actions fire toasts naming the endpoint to wire them
- Blueprint tours framed as 'industry blueprint', not 'we don't have this yet'
- Mode pill + last-fetch age + refresh in topbar
- Dev CORS dodged via vite proxy; production deploys same-origin
18 vitest tests + 26 playwright smoke assertions + DOM layout audit.
Constraint: cross-origin live mode rejected by browser → fall back to snapshot
Rejected: hardcoded SLA % | dishonest demo metrics
Directive: wire preview-only action handlers to /api/runtime/transactions/{id}/actions to ship them for real
Confidence: high
Scope-risk: narrow
Not-tested: production deployment via flowmaster-ops overlay
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
// Tests the in-browser API client against a stubbed fetch.
|
||||
// We don't hit the real backend here — that's covered by the smoke test
|
||||
// (`pnpm qa:smoke` toggles live mode and asserts the network call resolves).
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { api } from "./api";
|
||||
|
||||
function mockFetch(handlers: Array<(url: string, init: RequestInit) => Response | undefined>) {
|
||||
const calls: Array<{ url: string; init: RequestInit }> = [];
|
||||
globalThis.fetch = vi.fn(async (input: RequestInfo | URL, init: RequestInit = {}) => {
|
||||
const url = typeof input === "string" ? input : input.toString();
|
||||
calls.push({ url, init });
|
||||
for (const h of handlers) {
|
||||
const r = h(url, init);
|
||||
if (r) return r;
|
||||
}
|
||||
return new Response("not stubbed", { status: 500 });
|
||||
}) as unknown as typeof fetch;
|
||||
// sessionStorage stub
|
||||
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;
|
||||
return calls;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
// reset between tests
|
||||
api.clearToken();
|
||||
});
|
||||
|
||||
describe("api client", () => {
|
||||
it("ping() returns ok with email on success", async () => {
|
||||
mockFetch([
|
||||
(url) => url.endsWith("/api/v1/auth/dev-login")
|
||||
? new Response(JSON.stringify({ access_token: "T1" }), { status: 200, headers: { "Content-Type": "application/json" } })
|
||||
: undefined,
|
||||
(url) => url.endsWith("/api/v1/auth/me")
|
||||
? new Response(JSON.stringify({ user_id: "u1", tenant_id: "t1", email: "dev@flow-master.ai" }), { status: 200, headers: { "Content-Type": "application/json" } })
|
||||
: undefined,
|
||||
]);
|
||||
const r = await api.ping();
|
||||
expect(r.ok).toBe(true);
|
||||
expect(r.user).toBe("dev@flow-master.ai");
|
||||
});
|
||||
|
||||
it("ping() returns ok=false with reason when login fails", async () => {
|
||||
mockFetch([
|
||||
(url) => url.endsWith("/api/v1/auth/dev-login")
|
||||
? new Response("nope", { status: 401 })
|
||||
: undefined,
|
||||
]);
|
||||
const r = await api.ping();
|
||||
expect(r.ok).toBe(false);
|
||||
expect(r.reason).toMatch(/401/);
|
||||
});
|
||||
|
||||
it("workItems() returns the items array", async () => {
|
||||
mockFetch([
|
||||
(url) => url.endsWith("/api/v1/auth/dev-login")
|
||||
? new Response(JSON.stringify({ access_token: "T2" }), { status: 200 })
|
||||
: undefined,
|
||||
(url) => url.includes("/api/ea2/work-items")
|
||||
? new Response(JSON.stringify({ items: [{ transaction_id: "tx1", status: "running", definition_key: "k1" }] }), { status: 200 })
|
||||
: undefined,
|
||||
]);
|
||||
const items = await api.workItems();
|
||||
expect(items.length).toBe(1);
|
||||
expect(items[0].transaction_id).toBe("tx1");
|
||||
});
|
||||
|
||||
it("graph() returns null on 404 (typed-safe failure mode)", async () => {
|
||||
mockFetch([
|
||||
(url) => url.endsWith("/api/v1/auth/dev-login")
|
||||
? new Response(JSON.stringify({ access_token: "T3" }), { status: 200 })
|
||||
: undefined,
|
||||
(url) => url.includes("/api/ea2/process-definitions/")
|
||||
? new Response("not found", { status: 404 })
|
||||
: undefined,
|
||||
]);
|
||||
const g = await api.graph("does-not-exist");
|
||||
expect(g).toBeNull();
|
||||
});
|
||||
|
||||
it("re-logs in after 401 then succeeds", async () => {
|
||||
let metHits = 0;
|
||||
const calls = mockFetch([
|
||||
(url) => url.endsWith("/api/v1/auth/dev-login")
|
||||
? new Response(JSON.stringify({ access_token: `T${metHits + 1}` }), { status: 200 })
|
||||
: undefined,
|
||||
(url) => {
|
||||
if (url.endsWith("/api/v1/auth/me")) {
|
||||
metHits += 1;
|
||||
if (metHits === 1) return new Response("expired", { status: 401 });
|
||||
return new Response(JSON.stringify({ user_id: "u", tenant_id: "t", email: "dev@flow-master.ai" }), { status: 200 });
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
]);
|
||||
const r = await api.ping();
|
||||
expect(r.ok).toBe(true);
|
||||
expect(calls.filter((c) => c.url.endsWith("/dev-login")).length).toBe(2); // initial + retry login
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user