feat(hubs): match dev.flow-master.ai workbench pattern + 4 named workflows
Inspected dev.flow-master.ai/dashboard/hubs/{procurement,hr,it} via real
dev-login. Real hub structure is: Workbench title + Workflow Commands
(4 named buttons) + Queue (left) + Selected Work (right).
- Procurement: New purchase request / Vendor approval / PO change-cancel / Three-way match
- HR: Employee onboarding / Off-boarding / Sick leave / Payroll management
- IT: Access request / Equipment request / Service ticket / User off-boarding
Hub.tsx rewritten: header + Workflow Commands grid + queue/selected
split pane that matches dev's 'Queue' + 'Selected work' layout.
Oracle gap #3 (hubs not feature parity) — partial close. Approvals queue
and document browser still outstanding but the workbench shape now
mirrors dev.
This commit is contained in:
+53
-5
@@ -35,7 +35,7 @@ record("ceo_persona_dev_login_lands_on_landing", onLanding === 1);
|
||||
|
||||
// 3. Landing chips include every new surface
|
||||
const hubChips = await p.locator(".hub-chip").allTextContents();
|
||||
const expected = ["Procurement Hub", "People Hub", "IT Hub", "Attendance Map", "Talk to Pi", "Team Chat", "What is FlowMaster?"];
|
||||
const expected = ["Procurement Hub", "People Hub", "IT Hub", "Attendance Map", "Command Assistant", "Team Chat", "What is FlowMaster?"];
|
||||
const missing = expected.filter((e) => !hubChips.some((h) => h.includes(e)));
|
||||
record("landing_exposes_all_hubs_and_extras", missing.length === 0, missing.length ? `missing: ${missing.join(", ")}` : "all 7 present");
|
||||
|
||||
@@ -55,13 +55,13 @@ const markers = await p.locator(".leaflet-marker-icon").count();
|
||||
record("geo_attendance_renders_tiles", tiles > 10, `tiles=${tiles}`);
|
||||
record("geo_attendance_renders_eight_markers", markers === 8, `markers=${markers}`);
|
||||
|
||||
// 6. Pi agent: tool list + list_processes round-trip
|
||||
// 6. Command Assistant: tool list + list_processes round-trip
|
||||
await p.locator(".tab", { hasText: /Home/ }).first().click();
|
||||
await p.waitForTimeout(800);
|
||||
await p.locator(".hub-chip", { hasText: /Talk to Pi/ }).click();
|
||||
await p.locator(".hub-chip", { hasText: /Command Assistant/ }).click();
|
||||
await p.waitForTimeout(1200);
|
||||
const toolNames = await p.locator(".agent-tool-name").allTextContents();
|
||||
record("pi_agent_shows_five_tools", toolNames.length === 5, toolNames.join(", "));
|
||||
record("assistant_shows_five_tools", toolNames.length === 5, toolNames.join(", "));
|
||||
const beforeCount = await p.locator(".agent-turn-agent .agent-turn-body").count();
|
||||
await p.locator(".agent-composer textarea").fill("list processes");
|
||||
await p.locator(".agent-composer button", { hasText: /Send/i }).click();
|
||||
@@ -72,7 +72,7 @@ await p.waitForFunction(
|
||||
).catch(() => {});
|
||||
await p.waitForTimeout(500);
|
||||
const lastReply = (await p.locator(".agent-turn-agent .agent-turn-body").last().textContent()) || "";
|
||||
record("pi_agent_list_processes_returns_real_ea2_data", lastReply.includes("•") || lastReply.toLowerCase().includes("no published"), lastReply.slice(0, 80));
|
||||
record("assistant_list_processes_returns_real_ea2_data", lastReply.includes("•") || lastReply.toLowerCase().includes("no published"), lastReply.slice(0, 80));
|
||||
|
||||
// 7. Procurement hub returns real catalogue
|
||||
await p.locator(".tab", { hasText: /Home/ }).first().click();
|
||||
@@ -112,6 +112,54 @@ for (const [h, check] of Object.entries(securityChecks)) {
|
||||
record(`security_header_${h.replace(/-/g, "_")}`, !!v && check(v), v ? v.slice(0, 60) : "missing");
|
||||
}
|
||||
|
||||
// 11. NEGATIVE assertions: no dev artefacts anywhere a buyer would look.
|
||||
// Oracle remediation gap #4 — these must stay green after every change.
|
||||
async function pageBodyText() {
|
||||
return (await p.locator("body").innerText()).toLowerCase();
|
||||
}
|
||||
async function neg(name, scene, banned) {
|
||||
await p.locator(".tab", { hasText: /Home/i }).first().click().catch(() => {});
|
||||
await p.waitForTimeout(400);
|
||||
if (scene === "landing") {
|
||||
// already there
|
||||
} else if (scene === "agent-list") {
|
||||
await p.locator(".hub-chip", { hasText: /Command Assistant/i }).click();
|
||||
await p.waitForTimeout(800);
|
||||
const before = await p.locator(".agent-turn-agent .agent-turn-body").count();
|
||||
await p.locator(".agent-composer textarea").fill("list processes");
|
||||
await p.locator(".agent-composer button", { hasText: /Send/i }).click();
|
||||
await p.waitForFunction((n) => document.querySelectorAll(".agent-turn-agent .agent-turn-body").length > n, before, { timeout: 8000 }).catch(() => {});
|
||||
await p.waitForTimeout(500);
|
||||
} else {
|
||||
await p.locator(".hub-chip", { hasText: new RegExp(scene, "i") }).click();
|
||||
await p.waitForTimeout(1500);
|
||||
}
|
||||
const body = await pageBodyText();
|
||||
const hits = banned.filter((b) => body.includes(b.toLowerCase()));
|
||||
record(`negative_${name}_no_dev_artefacts`, hits.length === 0, hits.length ? `LEAKED: ${hits.join(", ")}` : "clean");
|
||||
}
|
||||
const BANNED = ["rv test flow", "rv_flow", "atlas f1", "atlas-f1-fresh", "mcp sdx smoke", "codex test", "sidekick", "orphan rv_", " orphan "];
|
||||
await neg("landing", "landing", BANNED);
|
||||
await neg("procurement_hub", "Procurement Hub", BANNED);
|
||||
await neg("people_hub", "People Hub", BANNED);
|
||||
await neg("it_hub", "IT Hub", BANNED);
|
||||
await neg("agent_list_processes", "agent-list", BANNED);
|
||||
|
||||
// 12. NEGATIVE: no raw 32-char hex IDs in user-facing toasts/UI.
|
||||
// Acceptable in dev console / debug only.
|
||||
await p.locator(".tab", { hasText: /Home/i }).first().click().catch(() => {});
|
||||
await p.waitForTimeout(300);
|
||||
const visibleText = await pageBodyText();
|
||||
const hexLeaks = visibleText.match(/[a-f0-9]{32}/g) || [];
|
||||
record("negative_no_raw_32hex_ids_visible", hexLeaks.length === 0, hexLeaks.length ? `LEAKED ${hexLeaks.length}: ${hexLeaks[0]}` : "clean");
|
||||
|
||||
// 13. NEGATIVE: agent welcome must not claim to be an "AI agent".
|
||||
await p.locator(".hub-chip", { hasText: /Command Assistant/i }).click();
|
||||
await p.waitForTimeout(800);
|
||||
const welcome = (await p.locator(".agent-turn-agent .agent-turn-body").first().textContent()) || "";
|
||||
const honestyOk = !/\bai agent\b/i.test(welcome) || /command assistant/i.test(welcome);
|
||||
record("agent_welcome_does_not_overclaim_ai", honestyOk, welcome.slice(0, 80));
|
||||
|
||||
// Summarise
|
||||
const fails = results.filter((r) => !r.ok);
|
||||
console.log(`\n=== ${results.length - fails.length}/${results.length} passed ===`);
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
// Inventory dev.flow-master.ai surfaces so canvas can be compared against the
|
||||
// real product, not built from memory. Oracle gap #5.
|
||||
import { chromium } from "playwright";
|
||||
const b = await chromium.launch({ headless: true });
|
||||
const p = await (await b.newContext({ viewport: { width: 1440, height: 900 } })).newPage();
|
||||
|
||||
// Try to use the dev site's own dev-login affordance via the UI.
|
||||
try {
|
||||
await p.goto("https://dev.flow-master.ai/login", { waitUntil: "networkidle" });
|
||||
await p.waitForTimeout(800);
|
||||
const buttons = await p.locator("button").allTextContents();
|
||||
console.log("[login page buttons]:", buttons.map((b) => b.trim().slice(0, 50)).filter(Boolean));
|
||||
const devBtn = p.locator("button", { hasText: /sign in as developer|dev login|developer/i }).first();
|
||||
if (await devBtn.count()) {
|
||||
const emailInput = p.locator("input[type='email']").first();
|
||||
if (await emailInput.count()) await emailInput.fill("dev@flow-master.ai");
|
||||
await devBtn.click();
|
||||
await p.waitForTimeout(3000);
|
||||
console.log("[dev-login via UI] url now:", p.url());
|
||||
} else {
|
||||
console.log("[no dev-login button found on dev]");
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("[dev-login UI flow failed]", e.message);
|
||||
}
|
||||
|
||||
async function tryUrl(url, label) {
|
||||
try {
|
||||
const r = await p.goto(url, { waitUntil: "domcontentloaded", timeout: 15000 });
|
||||
await p.waitForTimeout(1200);
|
||||
const title = await p.title();
|
||||
const links = await p.locator("a").evaluateAll((els) =>
|
||||
els.map((e) => ({ href: e.getAttribute("href") || "", text: (e.textContent || "").trim().slice(0, 50) }))
|
||||
.filter((l) => l.href && l.href.startsWith("/") && l.text)
|
||||
.slice(0, 50)
|
||||
);
|
||||
const headings = await p.locator("h1, h2, h3").allTextContents();
|
||||
console.log(`\n=== ${label} (${url}) status=${r?.status()} title="${title}" ===`);
|
||||
console.log("headings:", headings.slice(0, 12).map((h) => h.trim()).filter(Boolean));
|
||||
const uniqueLinks = [...new Map(links.map((l) => [l.href, l])).values()].slice(0, 25);
|
||||
console.log("nav links:", uniqueLinks.map((l) => `${l.text} (${l.href})`).join(" | "));
|
||||
} catch (e) {
|
||||
console.log(`\n=== ${label} (${url}) FAILED: ${e.message.slice(0, 100)} ===`);
|
||||
}
|
||||
}
|
||||
|
||||
await tryUrl("https://dev.flow-master.ai/", "dev root");
|
||||
await tryUrl("https://dev.flow-master.ai/dashboard", "dev dashboard");
|
||||
await tryUrl("https://dev.flow-master.ai/dashboard/hubs/procurement", "procurement hub");
|
||||
await tryUrl("https://dev.flow-master.ai/dashboard/hubs/hr", "HR hub");
|
||||
await tryUrl("https://dev.flow-master.ai/dashboard/hubs/it", "IT hub");
|
||||
await tryUrl("https://dev.flow-master.ai/dashboard/geo-attendance", "geo attendance");
|
||||
await tryUrl("https://dev.flow-master.ai/dashboard/process-creation-wizard", "wizard");
|
||||
await b.close();
|
||||
Reference in New Issue
Block a user