port aisuite#380: slack installer pre-add, mcp oauth quarantine, automation toast, e2e fixes

Slack installer joins the workspace allow-list on managed connect; MCP interactive oauth only from explicit connects.
Run-started toast over a new app-wide /ws/events socket; Automations e2e locators scoped to the account menu.
This commit is contained in:
Rohit C Prasad
2026-07-21 12:56:36 -07:00
parent d7af8af9c6
commit 2451486609
17 changed files with 467 additions and 19 deletions
+44
View File
@@ -0,0 +1,44 @@
// UX-026: the automation-start toast — top-right, 5s, schedule-fired runs only.
// The server pushes automation_run_started over the app-wide /ws/events stream;
// the toast names the automation, offers one View-run action (opens the run's
// session), an ✕, and auto-dismisses via the drain bar.
import { expect } from "@playwright/test";
import { sendAppEvent, test } from "./fixtures";
const RUN_STARTED = {
type: "automation_run_started",
data: {
task_id: "task-1",
task_title: "Daily AI News",
session_id: "run-live-1",
workspace: "/tmp/aw",
agent: "cowork",
trigger: "schedule",
},
};
test("a schedule-fired run pops the toast; View run opens its session", async ({ page }) => {
await page.goto("/");
await sendAppEvent(page, RUN_STARTED);
const toast = page.getByTestId("automation-toast");
await expect(toast).toContainText("Automation started");
await expect(toast).toContainText("Daily AI News");
await toast.getByTestId("toast-view-run").click();
await expect(page.getByTestId("automation-toast")).toHaveCount(0);
// the run's session is now the active conversation (composer visible = session surface)
await expect(page.getByPlaceholder(/Ask the coworker/)).toBeVisible();
});
test("the toast dismisses on ✕ and by itself after ~5s", async ({ page }) => {
await page.goto("/");
await sendAppEvent(page, RUN_STARTED);
await expect(page.getByTestId("automation-toast")).toBeVisible();
await page.getByTestId("toast-dismiss").click();
await expect(page.getByTestId("automation-toast")).toHaveCount(0);
await sendAppEvent(page, { ...RUN_STARTED, data: { ...RUN_STARTED.data, task_title: "Weekly CRM digest" } });
await expect(page.getByTestId("automation-toast")).toContainText("Weekly CRM digest");
// auto-dismiss: gone within the 5s drain (+ slack for CI)
await expect(page.getByTestId("automation-toast")).toHaveCount(0, { timeout: 7000 });
});
+1 -1
View File
@@ -7,7 +7,7 @@ import { test } from "./fixtures";
async function openAutomations(page) {
await page.goto("/");
await page.getByTestId("account-row").click();
await page.getByRole("button", { name: "Automations", exact: true }).click();
await page.getByTestId("account-menu").getByRole("button", { name: "Automations", exact: true }).click();
await expect(page.getByText("Recurring tasks OpenWorker runs on a schedule.")).toBeVisible();
}
@@ -8,7 +8,7 @@ import { test } from "./fixtures";
async function openAutomations(page) {
await page.goto("/");
await page.getByTestId("account-row").click();
await page.getByRole("button", { name: "Automations", exact: true }).click();
await page.getByTestId("account-menu").getByRole("button", { name: "Automations", exact: true }).click();
await expect(page.getByText("Recurring tasks OpenWorker runs on a schedule.")).toBeVisible();
}
+1 -1
View File
@@ -8,7 +8,7 @@ test("scheduled run session shows the run banner; Back returns to the task detai
}) => {
await page.goto("/");
await page.getByTestId("account-row").click();
await page.getByRole("button", { name: "Automations", exact: true }).click();
await page.getByTestId("account-menu").getByRole("button", { name: "Automations", exact: true }).click();
// Task list → detail (runs list).
await page.getByText("Daily AI News").first().click();
+19 -1
View File
@@ -1,4 +1,17 @@
import { test as base, expect } from "@playwright/test";
import { test as base, expect, type Page } from "@playwright/test";
// The app-wide /ws/events socket each page opened (UX-026 toast et al.) — specs
// push server events through it via sendAppEvent below.
const eventSockets = new WeakMap<Page, { send: (data: string) => void }>();
/** Push an app-wide event exactly as the server would over /ws/events. Waits for
* the GUI to have connected its socket first. */
export async function sendAppEvent(page: Page, obj: unknown): Promise<void> {
for (let i = 0; i < 50 && !eventSockets.get(page); i++) await page.waitForTimeout(100);
const ws = eventSockets.get(page);
if (!ws) throw new Error("the app never opened /ws/events");
ws.send(JSON.stringify(obj));
}
// Hermetic API mock. Every /v1 request the GUI makes is fulfilled from the fixtures below (shapes
// mirrored from the real backend), and the event WebSocket is a SCRIPTED FAKE AGENT (ready on
@@ -542,6 +555,11 @@ export async function mockApi(page: import("@playwright/test").Page) {
// assistant_deltas → assistant_message "Echo: <text>" → turn_done
// · a message containing "run a tool": tool_proposed + permission_required, then the turn
// SUSPENDS until the client's approval decision arrives (deny → skipped; else → ran)
// App-wide event stream: register the socket so sendAppEvent can push into it.
await page.routeWebSocket(/\/ws\/events$/, (ws) => {
eventSockets.set(page, ws);
});
await page.routeWebSocket(/\/ws\/session\//, (ws) => {
const send = (type: string, data: Record<string, unknown> = {}) =>
ws.send(JSON.stringify({ type, data }));
+1 -1
View File
@@ -8,7 +8,7 @@ import { test, expect } from "./fixtures";
async function openTaskDetail(page: import("@playwright/test").Page) {
await page.goto("/");
await page.getByTestId("account-row").click();
await page.getByRole("button", { name: "Automations", exact: true }).click();
await page.getByTestId("account-menu").getByRole("button", { name: "Automations", exact: true }).click();
await page.getByText("Daily AI News").first().click();
await expect(page.getByRole("button", { name: /Run now/ })).toBeVisible();
}