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:
2026-06-14 00:09:32 +04:00
commit 3ffd0e68a7
45 changed files with 15603 additions and 0 deletions
+108
View File
@@ -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
});
});