import { _electron as electron, expect, test } from "@playwright/test"; import { mkdirSync } from "node:fs"; import { tmpdir } from "node:os"; import { join, resolve } from "node:path"; import { randomUUID } from "node:crypto"; import { seedBuiltinSkillsFixture } from "./builtin-skills-fixture"; /** * The founder's direct line to the CEO: onboarding leaves a pinned CEO * conversation at the top of Messages, the page opens on it, a founder * message lands there and queues a CEO wake run, and no amount of channel * recency displaces the pin. */ test("the pinned CEO conversation is the Messages front door", async () => { const profileDir = join(tmpdir(), `agentcompany-new-e2e-ceo-${randomUUID()}`); mkdirSync(profileDir, { recursive: true }); const builtinDir = seedBuiltinSkillsFixture(join(tmpdir(), `agentcompany-new-e2e-ceo-builtin-${randomUUID()}`)); const app = await electron.launch({ args: [resolve("out/main/index.js")], env: { ...process.env, AGENTCOMPANY_NEW_PROFILE_DIR: profileDir, AGENTCOMPANY_NEW_AUTORUN: "0", AGENTCOMPANY_NEW_BUILTIN_SKILLS_DIR: builtinDir, }, }); try { const page = await app.firstWindow(); await expect(page.getByRole("heading", { name: "Welcome" })).toBeVisible(); await page.getByRole("button", { name: "Get started" }).click(); await page.getByRole("button", { name: /^OpenAI API key/ }).click(); await page.getByLabel("API key").fill("e2e-secret"); await page.getByLabel("Model").fill("gpt-5.4"); await page.getByRole("button", { name: "Continue" }).click(); await page.getByLabel("Company name").fill("FlowPilot"); await page.getByLabel("Your name").fill("Yang"); await page.getByLabel("Description").fill("A CSM analytics product for SMBs."); await page.getByRole("button", { name: "Continue" }).click(); await page.getByRole("button", { name: "Continue" }).click(); await page.getByLabel("Goal").fill("Ship the FlowPilot MVP."); await page.getByLabel("Acceptance criteria").fill("A working product, not documents about one."); await page.getByRole("button", { name: "Launch" }).click(); await expect(page.getByRole("heading", { name: "Connectors" })).toBeVisible(); await page.getByRole("button", { name: "Finish" }).click(); // The CEO conversation is the first row, badged, and already selected — // opening Messages IS opening the CEO chat. await page.getByRole("button", { name: "Messages" }).click(); const firstRow = page.locator(".convo-item").first(); await expect(firstRow).toContainText("CEO"); await expect(firstRow.locator(".convo-ceo-badge")).toBeVisible(); await expect(firstRow).toHaveClass(/active/); await expect(firstRow).toContainText("Ask about progress, goals, or direction"); // A founder message lands in the conversation and queues the CEO's wake // run — the reply pipeline is armed even with autonomous execution off. await page.getByRole("textbox", { name: "Message" }).fill("How are we tracking against the goal?"); await page.getByRole("button", { name: "Send" }).click(); const chatLog = page.getByRole("log", { name: "Conversation messages" }); await expect(chatLog.getByText("How are we tracking against the goal?")).toBeVisible(); const wake = await page.evaluate(async () => { const api = (window as unknown as { agentCompany: { loadSnapshot: () => Promise<{ ok: true; data: { agents: Array<{ id: string; roleLevel: string }>; taskRuns: Array<{ agentId: string | null; trigger: string; state: string }>; } }> } }).agentCompany; const snapshot = await api.loadSnapshot(); const ceo = snapshot.data.agents.find((agent) => agent.roleLevel === "c_suite")!; return snapshot.data.taskRuns.filter((run) => run.agentId === ceo.id && run.trigger === "chat_mention").map((run) => run.state); }); expect(wake).toEqual(["queued"]); // The message itself says where the reply stands. await expect(chatLog.locator(".chat-message", { hasText: "How are we tracking against the goal?" }).first()).toContainText("CEO will reply; queued."); // From a task, its goal, run and back are one click each. await page.getByRole("button", { name: "Work" }).click(); await page.getByRole("button", { name: /Operationalize goal: Ship the FlowPilot MVP\./ }).first().click(); const taskDialog = page.getByRole("dialog", { name: "Operationalize goal: Ship the FlowPilot MVP." }); await taskDialog.getByRole("button", { name: "Open goal Ship the FlowPilot MVP." }).click(); await expect(page.getByText("Goal: Ship the FlowPilot MVP.", { exact: true })).toBeVisible(); await page.getByRole("button", { name: /Operationalize goal: Ship the FlowPilot MVP\./ }).first().click(); await taskDialog.getByRole("button", { name: /^Open run/ }).first().click(); await expect(page.getByRole("button", { name: "Open task Operationalize goal: Ship the FlowPilot MVP." })).toBeVisible(); await page.getByRole("button", { name: "Open task Operationalize goal: Ship the FlowPilot MVP." }).click(); await expect(taskDialog).toBeVisible(); // A wrong link is corrected in the same task's editor, as at creation. await taskDialog.getByRole("button", { name: "Edit" }).click(); await taskDialog.getByLabel("Writing").selectOption("technical"); await taskDialog.getByLabel("Requires workspace access").check(); await taskDialog.getByRole("button", { name: "Save" }).click(); await expect(taskDialog.getByRole("heading", { name: "Edit task" })).toHaveCount(0); const edited = await page.evaluate(async () => { const api = (window as unknown as { agentCompany: { loadSnapshot: () => Promise<{ ok: true; data: { tasks: Array<{ title: string; writingMode: string; requiresFilesystem: boolean }> } }> } }).agentCompany; const task = (await api.loadSnapshot()).data.tasks.find((item) => item.title === "Operationalize goal: Ship the FlowPilot MVP.")!; return [task.writingMode, task.requiresFilesystem]; }); expect(edited).toEqual(["technical", true]); await taskDialog.getByRole("button", { name: "Close" }).click(); await page.getByRole("button", { name: "Messages" }).click(); // Recency pressure: newer traffic in a channel must not displace the pin. await page.evaluate(async () => { const api = (window as unknown as { agentCompany: { loadSnapshot: () => Promise<{ ok: true; data: { activeOrgId: string; employees: Array<{ id: string; orgId: string }>; chatSpaces: Array<{ id: string; orgId: string; name: string }>; } }>; sendMessage: (input: unknown) => Promise; } }).agentCompany; const snapshot = await api.loadSnapshot(); const orgId = snapshot.data.activeOrgId; const employee = snapshot.data.employees.find((item) => item.orgId === orgId)!; const general = snapshot.data.chatSpaces.find((item) => item.orgId === orgId && item.name === "general")!; await api.sendMessage({ orgId, spaceId: general.id, threadId: null, senderType: "employee", senderId: employee.id, bodyMd: "Channel chatter arriving after the founder's CEO message.", attachmentFileIds: [], }); }); await expect(page.locator(".convo-item").first().locator(".convo-ceo-badge")).toBeVisible(); await expect(page.locator(".convo-item").first()).toContainText("How are we tracking against the goal?"); // A send the backend refuses keeps the draft and says why, instead of clearing it. const composer = page.getByRole("textbox", { name: "Message" }); await composer.fill("/nosuch keep this draft"); await page.getByRole("button", { name: "Send" }).click(); await expect(page.getByRole("alert")).toContainText("Not sent: Slash command /nosuch is unavailable."); await expect(composer).toHaveValue("/nosuch keep this draft"); await composer.fill("Second thought: what is blocked?"); await page.getByRole("button", { name: "Send" }).click(); await expect(chatLog.getByText("Second thought: what is blocked?")).toBeVisible(); await expect(composer).toHaveValue(""); await expect(page.getByRole("alert")).toHaveCount(0); // An edit the backend refuses keeps the editor open with the text and says why. const own = chatLog.locator(".chat-message", { hasText: "Second thought: what is blocked?" }); await own.getByRole("button", { name: "Edit message" }).click(); const editor = own.getByRole("textbox", { name: "Edit message" }); await editor.fill("/status everything"); await own.getByRole("button", { name: "Save" }).click(); await expect(own.getByRole("alert")).toContainText("Not saved: Slash command messages are immutable."); await expect(editor).toHaveValue("/status everything"); await editor.fill("Second thought: what is blocked right now?"); await own.getByRole("button", { name: "Save" }).click(); await expect(chatLog.getByText("Second thought: what is blocked right now?")).toBeVisible(); await expect(chatLog.getByRole("textbox", { name: "Edit message" })).toHaveCount(0); // Each conversation keeps its own unsent draft. await composer.fill("Draft for the CEO only"); await page.locator(".convo-item", { hasText: "general" }).click(); await expect(composer).toHaveValue(""); await composer.fill("Draft for general"); await page.locator(".convo-item").first().click(); await expect(composer).toHaveValue("Draft for the CEO only"); await page.locator(".convo-item", { hasText: "general" }).click(); await expect(composer).toHaveValue("Draft for general"); // A reply goes into the thread of the message it answers, and the timeline says what it answers. await page.locator(".convo-item").first().click(); const root = chatLog.locator(".chat-message", { hasText: "How are we tracking against the goal?" }).first(); await root.getByRole("button", { name: "Reply in thread" }).click(); const thread = page.getByRole("complementary", { name: "Thread" }); await expect(thread).toContainText("How are we tracking against the goal?"); await thread.getByRole("textbox", { name: "Reply" }).fill("Also: which task is late?"); await thread.getByRole("button", { name: "Send reply" }).click(); await expect(thread.locator(".chat-message", { hasText: "Also: which task is late?" })).toBeVisible(); const reply = chatLog.locator(".chat-message", { hasText: "Also: which task is late?" }); await expect(reply.getByRole("button", { name: /^Replying to Yang/ })).toContainText("How are we tracking against the goal?"); await expect(root).toContainText("1 reply"); await thread.getByRole("button", { name: "Close thread" }).click(); await expect(thread).toHaveCount(0); // A message that arrives while the reader is up in history stays unread until they scroll down to it. await page.evaluate(async () => { const api = (window as unknown as { agentCompany: { loadSnapshot: () => Promise<{ ok: true; data: { activeOrgId: string; employees: Array<{ id: string; orgId: string }>; chatSpaces: Array<{ id: string; orgId: string; spaceType: string }> } }>; sendMessage: (input: unknown) => Promise; } }).agentCompany; const snapshot = (await api.loadSnapshot()).data; const employee = snapshot.employees.find((item) => item.orgId === snapshot.activeOrgId)!; const ceoChat = snapshot.chatSpaces.find((item) => item.orgId === snapshot.activeOrgId && item.spaceType === "direct")!; for (let index = 0; index < 40; index += 1) { await api.sendMessage({ orgId: snapshot.activeOrgId, spaceId: ceoChat.id, threadId: null, senderType: "employee", senderId: employee.id, bodyMd: `History line ${index}`, attachmentFileIds: [] }); } }); await expect(chatLog.getByText("History line 39")).toBeVisible(); await chatLog.evaluate((element) => { element.scrollTop = 0; element.dispatchEvent(new Event("scroll")); }); await page.evaluate(async () => { const api = (window as unknown as { agentCompany: { loadSnapshot: () => Promise<{ ok: true; data: { activeOrgId: string; agents: Array<{ id: string; orgId: string; roleLevel: string }>; chatSpaces: Array<{ id: string; orgId: string; spaceType: string }> } }>; sendMessage: (input: unknown) => Promise; } }).agentCompany; const snapshot = (await api.loadSnapshot()).data; const ceo = snapshot.agents.find((agent) => agent.orgId === snapshot.activeOrgId && agent.roleLevel === "c_suite")!; const ceoChat = snapshot.chatSpaces.find((item) => item.orgId === snapshot.activeOrgId && item.spaceType === "direct")!; await api.sendMessage({ orgId: snapshot.activeOrgId, spaceId: ceoChat.id, threadId: null, senderType: "agent", senderId: ceo.id, bodyMd: "An update you have not seen yet.", attachmentFileIds: [] }); }); const ceoRow = page.locator(".convo-item").first(); await expect(ceoRow.locator(".nav-badge")).toHaveText("1"); await page.getByRole("button", { name: "Jump to latest" }).click(); await expect(ceoRow.locator(".nav-badge")).toHaveCount(0); // History older than the loaded window comes from the database, back to the very first message. await page.evaluate(async () => { const api = (window as unknown as { agentCompany: { loadSnapshot: () => Promise<{ ok: true; data: { activeOrgId: string; employees: Array<{ id: string; orgId: string }>; chatSpaces: Array<{ id: string; orgId: string; name: string }> } }>; sendMessage: (input: unknown) => Promise; } }).agentCompany; const snapshot = (await api.loadSnapshot()).data; const employee = snapshot.employees.find((item) => item.orgId === snapshot.activeOrgId)!; const general = snapshot.chatSpaces.find((item) => item.orgId === snapshot.activeOrgId && item.name === "general")!; for (let index = 0; index < 520; index += 1) { await api.sendMessage({ orgId: snapshot.activeOrgId, spaceId: general.id, threadId: null, senderType: "employee", senderId: employee.id, bodyMd: `General line ${index}`, attachmentFileIds: [] }); } }); await page.locator(".convo-item", { hasText: "general" }).click(); await expect(chatLog.getByText("General line 519")).toBeVisible(); const start = chatLog.getByText("This is the start of the conversation."); for (let click = 0; click < 10 && !(await start.isVisible()); click += 1) { await chatLog.getByRole("button", { name: "Show earlier messages" }).click(); await page.waitForTimeout(300); } await expect(start).toBeVisible(); await expect(chatLog.getByText("General line 0", { exact: true })).toBeVisible(); // The header lists the real members, how each agent replies, and who can read. await page.getByRole("button", { name: /^Members \(/ }).click(); const roster = page.getByRole("menu"); await expect(roster).toContainText("Everyone in the company can read this channel."); await expect(roster.locator(".members-roster")).toHaveCSS("overflow-y", "auto"); await expect(roster).toContainText("Replies when @mentioned, in threads it joined, and to messages that name no one"); await page.keyboard.press("Escape"); await page.locator(".convo-item").first().click(); await page.getByRole("button", { name: /^Members \(/ }).click(); await expect(page.getByRole("menu")).toContainText("Replies to every message"); } finally { await app.close(); } });