diff --git a/qa/_assistant_test.mjs b/qa/_assistant_test.mjs new file mode 100644 index 0000000..a93d7b8 --- /dev/null +++ b/qa/_assistant_test.mjs @@ -0,0 +1,40 @@ +// Tight assistant test +import { chromium } from "playwright"; + +const b = await chromium.launch({ headless: true }); +const ctx = await b.newContext({ viewport: { width: 1440, height: 900 } }); +const p = await ctx.newPage(); +await ctx.addInitScript(() => localStorage.setItem("fm.canvas.tour.seen", String(Date.now()))); + +await p.goto("https://canvas.flow-master.ai/", { waitUntil: "domcontentloaded" }); +await p.waitForTimeout(2500); +await p.locator(".persona-chip").filter({ hasText: /Mariana/ }).click(); +await p.waitForSelector(".topbar", { timeout: 30000 }); +await p.waitForTimeout(2000); + +console.log("on mission, navigating to Assistant"); +await p.locator(".tab").filter({ hasText: /Assistant/i }).first().click(); +await p.waitForTimeout(3500); + +const textarea = p.locator(".agent-composer textarea").first(); +const textareaPresent = await textarea.count() > 0; +console.log("textarea present:", textareaPresent); + +if (textareaPresent) { + await textarea.fill("list processes"); + await textarea.press("Enter"); + await p.waitForTimeout(8000); + + // Try different selectors for the reply + const turns = await p.evaluate(() => { + const t = Array.from(document.querySelectorAll(".agent-turn, .agent-msg, [class*='turn'], [class*='msg']")); + return t.map(el => ({ class: el.className.slice(0, 50), text: el.textContent?.slice(0, 200) })); + }); + console.log("turns:", JSON.stringify(turns, null, 2)); + + const sceneText = await p.evaluate(() => document.querySelector(".scene")?.innerText?.slice(-800)); + console.log("scene tail:", sceneText); +} + +await p.screenshot({ path: "/tmp/canvas-evidence/assistant-test.png" }); +await b.close(); diff --git a/qa/_oracle_e2e.mjs b/qa/_oracle_e2e.mjs index b7e61ca..abcbc12 100644 --- a/qa/_oracle_e2e.mjs +++ b/qa/_oracle_e2e.mjs @@ -84,12 +84,17 @@ if (await ta.count()) { // 5. Assistant list processes await p.locator(".tab").filter({ hasText: /Assistant/i }).first().click(); -await p.waitForTimeout(2500); -const agentInput = p.locator(".agent-input, input[placeholder*='Ask']").first(); -await agentInput.fill("list processes"); -await agentInput.press("Enter"); -await p.waitForTimeout(6000); -const replies = await p.locator(".agent-msg-text, .agent-msg-body").allTextContents(); +await p.waitForTimeout(3500); +await p.waitForSelector(".agent-composer textarea", { timeout: 10000 }).catch(() => {}); +const agentInput = p.locator(".agent-composer textarea").first(); +if (await agentInput.count() === 0) { + fail("assistant_input_visible", "no textarea"); +} else { + await agentInput.fill("list processes"); + await agentInput.press("Enter"); + await p.waitForTimeout(8000); +} +const replies = await p.locator(".agent-turn-body").allTextContents(); const lastReply = replies.slice(-1)[0] || ""; const mentionsProcess = /purchase|laptop|procurement|requisition|process/i.test(lastReply); if (mentionsProcess) pass("assistant_lists_processes", lastReply.slice(0, 100)); @@ -98,12 +103,11 @@ await p.screenshot({ path: `${OUT}/04-assistant.png` }); // 6. Chat opens await p.locator(".tab").filter({ hasText: /Chat/i }).first().click(); -await p.waitForTimeout(4000); +await p.waitForTimeout(5000); const chatErr = await p.locator(".chat-err").count(); -const chatEmpty = await p.locator(".chat-empty").count(); -const chatShell = await p.locator(".chat-shell, .chat-scene, .scene").first().innerText().catch(() => ""); +const chatShell = await p.locator(".scene").first().innerText().catch(() => ""); if (chatErr > 0) fail("chat_opens_without_error", "chat-err visible"); -else if (chatShell.includes("Conversations") || chatShell.includes("Talk to your team") || chatShell.includes("No conversations")) pass("chat_opens_without_error", "chat surface visible"); +else if (chatShell.toLowerCase().includes("conversation") || chatShell.toLowerCase().includes("talk to") || chatShell.toLowerCase().includes("no conversations") || chatShell.toLowerCase().includes("inbox")) pass("chat_opens_without_error", "chat surface visible"); else fail("chat_opens_without_error", chatShell.slice(0, 200)); await p.screenshot({ path: `${OUT}/05-chat.png` }); @@ -114,12 +118,15 @@ else fail("no_snapshot_text_anywhere", "found 'snapshot' in body"); // 8. LLM gateway: ask the assistant something non-tool await p.locator(".tab").filter({ hasText: /Assistant/i }).first().click(); -await p.waitForTimeout(1500); -const llmInput = p.locator(".agent-input, input[placeholder*='Ask']").first(); -await llmInput.fill("What is the difference between a flow and a view in EA2?"); -await llmInput.press("Enter"); -await p.waitForTimeout(8000); -const llmReplies = await p.locator(".agent-msg-text, .agent-msg-body").allTextContents(); +await p.waitForTimeout(3000); +const llmInput = p.locator(".agent-composer textarea").first(); +if (await llmInput.count() === 0) { fail("llm_input_visible", "no textarea"); } +else { + await llmInput.fill("What is the difference between a flow and a view in EA2?"); + await llmInput.press("Enter"); + await p.waitForTimeout(10000); +} +const llmReplies = await p.locator(".agent-turn-body").allTextContents(); const llmLast = llmReplies.slice(-1)[0] || ""; const llmCallsList = calls.filter(c => c.url.includes("/api/v1/llm/generate")); if (llmCallsList.length > 0) { diff --git a/src/App.tsx b/src/App.tsx index 698b556..9411139 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,6 +1,6 @@ // App shell — scene switcher + top bar + command palette + console + toaster. import { useEffect, useRef, useState } from "react"; -import { useApp, scenarioById } from "./state/store"; +import { useApp } from "./state/store"; import Landing from "./scenes/Landing"; import MissionControl from "./scenes/MissionControl"; import RunHistory from "./scenes/RunHistory"; @@ -27,8 +27,7 @@ export default function App() { const scene = useApp((s) => s.scene); const setScene = useApp((s) => s.setScene); const setCmdOpen = useApp((s) => s.setCmdOpen); - const scenarioId = useApp((s) => s.scenarioId); - const sc = scenarioById(scenarioId); + const mode = useApp((s) => s.mode); const refreshLive = useApp((s) => s.refreshLive); @@ -165,24 +164,7 @@ export default function App() { -