fix: studio createDraft split, llm fallback offers tools, topbar a11y pass
Three Oracle-flagged gaps:
1. Studio createDraft 500 (was masked by retries). Empirical curl
reproducer proved POST /api/ea2/flow returns 201 with the minimal
shape but intermittently 500s when config.wizard.* is in the
initial body. Split: POST creates the draft minimally, then a
follow-up PUT attaches the wizard config best-effort. Draft
creation now lands reliably.
2. LLM 'temporarily unavailable' was a dead end for the user. Now
the assistant returns ok:true with a concrete menu of tools the
user CAN use right now ('list processes', 'start <name>',
'open <hub>', 'tell <name> that ...', 'remember ...', 'recall ...').
The reply renders as a normal assistant message, not a red error.
3. Topbar UI/UX Pro Max pass:
- Added aria-label to the ⌘K and avatar buttons (icon-only a11y)
- Removed the duplicate topbar-mid chips on Mission (Mission scene
already shows family/defName/version inline). Cleaner topbar.
- Added focus-visible outlines (2px amber, 2px offset) on all
topbar action buttons + user-menu items
- Hover/active scale on the avatar (1.04 / 0.97) with 160ms ease
- Tabular numerals for the ⌘K kbd and notification badge
- prefers-reduced-motion respected
30/30 vitest pass. tsc + vite build green.
This commit is contained in:
@@ -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();
|
||||
+23
-16
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user