From 333667366dfa9830057efa13de55bb1cad842b54 Mon Sep 17 00:00:00 2001 From: shad Date: Sun, 14 Jun 2026 12:37:33 +0400 Subject: [PATCH] fix(chat): read messages via /api/ea2/edges/defines instead of /graph process-definitions/{key}/graph filters to published process definitions and returns empty for chat-thread flows, so the second persona saw no message bodies. The raw defines edge endpoint returns the link correctly. --- qa/audit_chat.mjs | 91 ++++++++++++++++++++++++++++++++++++++++++++++ src/lib/chatApi.ts | 8 ++-- 2 files changed, 95 insertions(+), 4 deletions(-) create mode 100644 qa/audit_chat.mjs diff --git a/qa/audit_chat.mjs b/qa/audit_chat.mjs new file mode 100644 index 0000000..f8e4477 --- /dev/null +++ b/qa/audit_chat.mjs @@ -0,0 +1,91 @@ +// 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 + const chatTab = p.locator(".tab", { hasText: /Chat/i }).first(); + await chatTab.click(); + await p.waitForTimeout(1500); + 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); + } + // Send a message + 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.locator(".chat-composer button", { hasText: /Send/i }).click(); + await p.waitForTimeout(2500); + } + 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) => { + const chatTab = p.locator(".tab", { hasText: /Chat/i }).first(); + await chatTab.click(); + await p.waitForTimeout(2000); + 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(); diff --git a/src/lib/chatApi.ts b/src/lib/chatApi.ts index e3b46a3..031480a 100644 --- a/src/lib/chatApi.ts +++ b/src/lib/chatApi.ts @@ -88,13 +88,13 @@ export const chatApi = { return body._key; }, - /** Read every message under a thread via the graph endpoint. */ + /** Read every message under a thread via the edges endpoint. */ async listMessages(threadKey: string, signal?: AbortSignal): Promise { - const res = await jsonFetch(`/api/ea2/process-definitions/${threadKey}/graph`, { signal }); - const edges = (res?.edges || []) as any[]; + const res = await jsonFetch(`/api/ea2/edges/defines?from=flow/${threadKey}&limit=200`, { signal }); + const edges = (res?.items || []) as any[]; const childKeys = edges .filter((e) => e?.role === "next") - .map((e) => (e.to_id || e._to || "").split("/").pop()) + .map((e) => (e._to || "").split("/").pop()) .filter(Boolean) as string[]; const messages = await Promise.all(