molecule-core/canvas/e2e/chat-separation.spec.ts
Hongming Wang 24fec62d7f initial commit — Molecule AI platform
Forked clean from public hackathon repo (Starfire-AgentTeam, BSL 1.1)
with full rebrand to Molecule AI under github.com/Molecule-AI/molecule-monorepo.

Brand: Starfire → Molecule AI.
Slug: starfire / agent-molecule → molecule.
Env vars: STARFIRE_* → MOLECULE_*.
Go module: github.com/agent-molecule/platform → github.com/Molecule-AI/molecule-monorepo/platform.
Python packages: starfire_plugin → molecule_plugin, starfire_agent → molecule_agent.
DB: agentmolecule → molecule.

History truncated; see public repo for prior commits and contributor
attribution. Verified green: go test -race ./... (platform), pytest
(workspace-template 1129 + sdk 132), vitest (canvas 352), build (mcp).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 11:55:37 -07:00

337 lines
13 KiB
TypeScript

import { test, expect } from "@playwright/test";
const API = process.env.E2E_API_URL ?? "http://localhost:8080";
test.describe("Chat Sub-Tabs", () => {
test("chat tab shows My Chat and Agent Comms sub-tabs", async ({ page }) => {
const res = await page.request.get(`${API}/workspaces`);
const workspaces = await res.json();
test.skip(workspaces.length === 0, "No workspaces to test");
await page.goto("/");
await page.waitForTimeout(3000);
// Click first workspace node
await page.locator(".react-flow__node").first().click();
await page.waitForTimeout(500);
// Click Chat tab
const chatTab = page.getByRole("button", { name: /Chat/ }).first();
await chatTab.click();
await page.waitForTimeout(500);
// Sub-tabs should be visible
await expect(page.locator("text=My Chat")).toBeVisible({ timeout: 3000 });
await expect(page.locator("text=Agent Comms")).toBeVisible({ timeout: 3000 });
});
test("My Chat is selected by default", async ({ page }) => {
const res = await page.request.get(`${API}/workspaces`);
const workspaces = await res.json();
test.skip(workspaces.length === 0, "No workspaces");
await page.goto("/");
await page.waitForTimeout(3000);
await page.locator(".react-flow__node").first().click();
await page.waitForTimeout(500);
await page.getByRole("button", { name: /Chat/ }).first().click();
await page.waitForTimeout(500);
// My Chat sub-tab should have active styling (border-blue-500)
const myChatBtn = page.locator("button", { hasText: "My Chat" });
await expect(myChatBtn).toHaveClass(/border-blue-500/);
});
test("switching to Agent Comms shows different content", async ({ page }) => {
const res = await page.request.get(`${API}/workspaces`);
const workspaces = await res.json();
test.skip(workspaces.length === 0, "No workspaces");
await page.goto("/");
await page.waitForTimeout(3000);
await page.locator(".react-flow__node").first().click();
await page.waitForTimeout(500);
await page.getByRole("button", { name: /Chat/ }).first().click();
await page.waitForTimeout(500);
// Click Agent Comms
await page.locator("button", { hasText: "Agent Comms" }).click();
await page.waitForTimeout(500);
// Should show empty state or agent comms messages
const hasEmpty = await page.locator("text=No agent-to-agent communications").isVisible().catch(() => false);
const hasMessages = await page.locator("[class*=cyan]").count() > 0;
expect(hasEmpty || hasMessages).toBeTruthy();
});
test("My Chat has input box, Agent Comms does not", async ({ page }) => {
const res = await page.request.get(`${API}/workspaces`);
const workspaces = await res.json();
test.skip(workspaces.length === 0, "No workspaces");
await page.goto("/");
await page.waitForTimeout(3000);
await page.locator(".react-flow__node").first().click();
await page.waitForTimeout(500);
await page.getByRole("button", { name: /Chat/ }).first().click();
await page.waitForTimeout(500);
// My Chat should have textarea
await expect(page.locator("textarea")).toBeVisible();
// Switch to Agent Comms
await page.locator("button", { hasText: "Agent Comms" }).click();
await page.waitForTimeout(500);
// Agent Comms should NOT have textarea
await expect(page.locator("textarea")).not.toBeVisible();
});
test("switching back to My Chat preserves messages", async ({ page }) => {
const res = await page.request.get(`${API}/workspaces`);
const workspaces = await res.json();
test.skip(workspaces.length === 0, "No workspaces");
await page.goto("/");
await page.waitForTimeout(3000);
await page.locator(".react-flow__node").first().click();
await page.waitForTimeout(500);
await page.getByRole("button", { name: /Chat/ }).first().click();
await page.waitForTimeout(1000);
// Check if there are messages or empty state in My Chat
const hasContent = await page.locator("text=No messages yet").isVisible().catch(() => false) ||
await page.locator("[class*=blue-600]").count() > 0;
// Switch to Agent Comms and back
await page.locator("button", { hasText: "Agent Comms" }).click();
await page.waitForTimeout(300);
await page.locator("button", { hasText: "My Chat" }).click();
await page.waitForTimeout(300);
// Same content should be there
const hasContentAfter = await page.locator("text=No messages yet").isVisible().catch(() => false) ||
await page.locator("[class*=blue-600]").count() > 0;
// Both should be truthy (content exists before and after switch)
expect(hasContent || hasContentAfter).toBeTruthy();
});
});
test.describe("Activity API Source Filter", () => {
test("source=canvas returns only canvas-initiated entries", async ({ request }) => {
const wsRes = await request.get(`${API}/workspaces`);
const workspaces = await wsRes.json();
if (workspaces.length === 0) return;
const wsId = workspaces[0].id;
const res = await request.get(`${API}/workspaces/${wsId}/activity?source=canvas`);
expect(res.ok()).toBeTruthy();
const entries = await res.json();
expect(Array.isArray(entries)).toBeTruthy();
// All entries should have source_id null
for (const e of entries) {
expect(e.source_id).toBeNull();
}
});
test("source=agent returns only agent-initiated entries", async ({ request }) => {
const wsRes = await request.get(`${API}/workspaces`);
const workspaces = await wsRes.json();
if (workspaces.length === 0) return;
const wsId = workspaces[0].id;
const res = await request.get(`${API}/workspaces/${wsId}/activity?source=agent`);
expect(res.ok()).toBeTruthy();
const entries = await res.json();
expect(Array.isArray(entries)).toBeTruthy();
// All entries should have non-null source_id
for (const e of entries) {
if (e.source_id !== undefined) {
expect(e.source_id).not.toBeNull();
}
}
});
test("source=invalid returns 400", async ({ request }) => {
const wsRes = await request.get(`${API}/workspaces`);
const workspaces = await wsRes.json();
if (workspaces.length === 0) return;
const wsId = workspaces[0].id;
const res = await request.get(`${API}/workspaces/${wsId}/activity?source=bogus`);
expect(res.status()).toBe(400);
});
test("source+type filters combine correctly", async ({ request }) => {
const wsRes = await request.get(`${API}/workspaces`);
const workspaces = await wsRes.json();
if (workspaces.length === 0) return;
const wsId = workspaces[0].id;
const res = await request.get(`${API}/workspaces/${wsId}/activity?type=a2a_receive&source=canvas`);
expect(res.ok()).toBeTruthy();
const entries = await res.json();
expect(Array.isArray(entries)).toBeTruthy();
for (const e of entries) {
expect(e.activity_type).toBe("a2a_receive");
expect(e.source_id).toBeNull();
}
});
});
test.describe("Data Flow — Initial Prompt in Chat", () => {
test("initial prompt appears as user message in My Chat", async ({ page }) => {
// Find a workspace that has activity with source=canvas (initial prompt via proxy)
const wsRes = await page.request.get(`${API}/workspaces`);
const workspaces = await wsRes.json();
test.skip(workspaces.length === 0, "No workspaces");
// Find a workspace with canvas-initiated activity
let targetWs: { id: string; name: string } | null = null;
for (const ws of workspaces) {
const actRes = await page.request.get(`${API}/workspaces/${ws.id}/activity?source=canvas&type=a2a_receive&limit=1`);
const entries = await actRes.json();
if (entries.length > 0) {
targetWs = ws;
break;
}
}
test.skip(!targetWs, "No workspace has canvas-initiated activity (initial prompt may not have run)");
await page.goto("/");
await page.waitForTimeout(3000);
// Click the workspace node
const node = page.locator(`.react-flow__node`).filter({ hasText: targetWs!.name });
await node.first().click();
await page.waitForTimeout(500);
// Open Chat tab
await page.getByRole("button", { name: /Chat/ }).first().click();
await page.waitForTimeout(500);
// Ensure we're on My Chat
await page.locator("button", { hasText: "My Chat" }).click();
await page.waitForTimeout(2000);
// The chat should NOT show "No messages yet" — it should have the initial prompt
const emptyState = page.locator("text=No messages yet");
await expect(emptyState).not.toBeVisible({ timeout: 5000 });
// There should be at least one user message bubble (blue) and one agent message bubble
const userBubbles = page.locator('[class*="bg-blue-600"]');
const agentBubbles = page.locator('[class*="bg-zinc-800"]');
expect(await userBubbles.count()).toBeGreaterThan(0);
expect(await agentBubbles.count()).toBeGreaterThan(0);
});
test("initial prompt text matches config content", async ({ page }) => {
const wsRes = await page.request.get(`${API}/workspaces`);
const workspaces = await wsRes.json();
test.skip(workspaces.length === 0, "No workspaces");
// Find workspace with activity
let targetWs: { id: string; name: string } | null = null;
let promptText = "";
for (const ws of workspaces) {
const actRes = await page.request.get(`${API}/workspaces/${ws.id}/activity?source=canvas&type=a2a_receive&limit=1`);
const entries = await actRes.json();
if (entries.length > 0) {
const reqBody = entries[0].request_body;
const parts = reqBody?.params?.message?.parts;
if (parts?.[0]?.text) {
targetWs = ws;
promptText = parts[0].text;
break;
}
}
}
test.skip(!targetWs, "No workspace has canvas-initiated activity with text");
await page.goto("/");
await page.waitForTimeout(3000);
const node = page.locator(`.react-flow__node`).filter({ hasText: targetWs!.name });
await node.first().click();
await page.waitForTimeout(500);
await page.getByRole("button", { name: /Chat/ }).first().click();
await page.waitForTimeout(500);
await page.locator("button", { hasText: "My Chat" }).click();
await page.waitForTimeout(2000);
// The first few words of the initial prompt should be visible in the chat
const firstWords = promptText.split(/\s+/).slice(0, 4).join(" ");
await expect(page.locator(`text=${firstWords}`).first()).toBeVisible({ timeout: 5000 });
});
test("agent response to initial prompt is visible", async ({ page }) => {
const wsRes = await page.request.get(`${API}/workspaces`);
const workspaces = await wsRes.json();
test.skip(workspaces.length === 0, "No workspaces");
let targetWs: { id: string } | null = null;
let responseText = "";
for (const ws of workspaces) {
const actRes = await page.request.get(`${API}/workspaces/${ws.id}/activity?source=canvas&type=a2a_receive&limit=1`);
const entries = await actRes.json();
if (entries.length > 0 && entries[0].response_body) {
const result = entries[0].response_body.result;
const parts = result?.parts;
if (parts?.[0]?.text) {
targetWs = ws;
responseText = parts[0].text;
break;
}
}
}
test.skip(!targetWs, "No workspace has response in activity");
await page.goto("/");
await page.waitForTimeout(3000);
await page.locator(".react-flow__node").first().click();
await page.waitForTimeout(500);
await page.getByRole("button", { name: /Chat/ }).first().click();
await page.waitForTimeout(2000);
// Agent response should be visible — check for agent message bubble existence
// (response text varies per agent, so check for non-empty agent bubble instead of exact text)
await page.locator("button", { hasText: "My Chat" }).click();
await page.waitForTimeout(2000);
const agentBubbles = page.locator('[class*="bg-zinc-800"]');
expect(await agentBubbles.count()).toBeGreaterThan(0);
});
});
test.describe("No JS Errors", () => {
test("page loads without errors with chat sub-tabs", async ({ page }) => {
const errors: string[] = [];
page.on("pageerror", (err) => errors.push(err.message));
await page.goto("/");
await page.waitForTimeout(3000);
const nodes = page.locator(".react-flow__node");
if (await nodes.count() > 0) {
await nodes.first().click();
await page.waitForTimeout(500);
await page.getByRole("button", { name: /Chat/ }).first().click();
await page.waitForTimeout(1000);
// Switch between tabs
await page.locator("button", { hasText: "Agent Comms" }).click();
await page.waitForTimeout(500);
await page.locator("button", { hasText: "My Chat" }).click();
await page.waitForTimeout(500);
}
const critical = errors.filter(
(e) => !e.includes("WebSocket") && !e.includes("favicon") && !e.includes("hydration")
);
expect(critical).toEqual([]);
});
});