Files
canvas-frontend/qa/audit_chat.mjs
T
shad b5d1ea54b0 feat(geo): EA2-backed attendance sites + versioned seed scripts
Closes Oracle deferred caveats:
- raw-SQL persona promotion (now qa/seeds/001_personas.mjs, idempotent)
- no scripted multi-persona interaction (now qa/seeds/002_persona_
  interactions.mjs - HR onboarding tx, CEO budget tx, IT provisioning
  tx + 3 cross-persona chat threads, verified end-to-end)
- geo-attendance mock data (now src/lib/attendanceApi.ts pulls 8 sites
  via flow.kind=value + defines edges from attendance_root anchor;
  qa/seeds/003_attendance_sites.mjs seeds them idempotently)

Each site is a flow.kind=value doc with config.attendance = { lat,
lng, kind, status, who, city, label }. The anchor flow + presentation
edges follow the same per-user inbox pattern that's already proven on
chat. No hardcoded JS array left in src/scenes/GeoAttendance.tsx.
2026-06-14 14:07:57 +04:00

99 lines
4.1 KiB
JavaScript

// Full chat dog-food: HR persona opens a thread with CEO, sends a message,
// then we re-load as CEO and verify the message is visible.
import { chromium } from "playwright";
const URL = "https://canvas.flow-master.ai/";
const args = ["--host-resolver-rules=MAP canvas.flow-master.ai 65.21.71.186", "--ignore-certificate-errors"];
async function asPersona(b, persona, work) {
const ctx = await b.newContext({ viewport: { width: 1440, height: 900 } });
const p = await ctx.newPage();
const writes = [];
p.on("request", (req) => {
const u = req.url();
if ((req.method() === "POST" || req.method() === "PUT") && /\/api\/(ea2|v1)\//.test(u)) {
writes.push({ method: req.method(), path: u.replace("https://canvas.flow-master.ai", "") });
}
});
p.on("response", async (res) => {
if (/\/api\/(ea2|v1)\//.test(res.url()) && res.status() >= 400) {
const t = await res.text().catch(() => "");
console.log(`[${persona.label} resp ${res.status()}] ${res.url().replace("https://canvas.flow-master.ai","")}${t.slice(0,180)}`);
}
});
await p.goto(URL, { waitUntil: "networkidle" });
await p.waitForTimeout(800);
await p.locator(".persona-chip", { hasText: persona.chipText }).click();
await p.locator(".dev-login-btn").click();
await p.waitForTimeout(2500);
console.log(`[${persona.label}] logged in`);
const enter = p.locator("button", { hasText: /Enter Mission Control/i }).first();
if (await enter.count()) {
await enter.click();
await p.waitForTimeout(1200);
}
await work(p, writes);
await ctx.close();
return writes;
}
const b = await chromium.launch({ headless: true, args });
// HR opens a chat with CEO and sends a message
const hrWrites = await asPersona(b, { label: "HR", chipText: /Aisha/ }, async (p, writes) => {
// Go to chat (Chat tab is a sibling of Assistant tab; match the exact label)
await p.waitForTimeout(2000);
const chatTab = p.locator(".tab").filter({ hasText: /^\s*Chat\s*$/ }).first();
await chatTab.click({ timeout: 10000 });
await p.waitForTimeout(2000);
console.log("[HR] visible after Chat click:");
for (const b of await p.locator("button, .chat-thread-name").all()) {
const t = (await b.textContent())?.trim();
if (t) console.log(" -", t.slice(0, 70));
}
// Pick CEO from select
const sel = p.locator(".chat-new-row select").first();
if (await sel.count()) {
await sel.selectOption("ceo-head@flow-master.ai");
await p.locator(".chat-new-row button").click();
await p.waitForTimeout(2000);
}
await p.waitForTimeout(1500);
const ta = p.locator(".chat-composer textarea").first();
if (await ta.count()) {
await ta.fill("Hey Mariana, store manager is requesting a new laptop. What's the approval cap this quarter?");
await p.waitForTimeout(400);
const sendBtn = p.locator(".chat-composer button", { hasText: /Send/i }).first();
console.log(`[HR] send disabled? ${await sendBtn.isDisabled()}`);
await sendBtn.click();
await p.waitForTimeout(3500);
} else {
console.log("[HR] no composer textarea found");
}
console.log(`[HR] writes so far: ${writes.length}`);
});
// CEO logs in and verifies the thread + message is visible
await asPersona(b, { label: "CEO", chipText: /Mariana/ }, async (p) => {
await p.waitForTimeout(2000);
const chatTab = p.locator(".tab").filter({ hasText: /^\s*Chat\s*$/ }).first();
await chatTab.click({ timeout: 10000 });
await p.waitForTimeout(2500);
const threadRows = await p.locator(".chat-thread-name").allTextContents();
console.log("[CEO] visible threads:", threadRows);
// Click the first thread that mentions Aisha
const aishaThread = p.locator(".chat-thread-row", { hasText: /Aisha/ }).first();
if (await aishaThread.count()) {
await aishaThread.click();
await p.waitForTimeout(1500);
const bodies = await p.locator(".chat-bubble-body").allTextContents();
console.log("[CEO] visible message bodies:", bodies);
} else {
console.log("[CEO] no Aisha thread found");
}
});
console.log(`\n=== HR side EA2 writes: ${hrWrites.length} ===`);
hrWrites.forEach((w) => console.log(` ${w.method} ${w.path}`));
await b.close();