evOo-local/apps/web/e2e/bot-crud.spec.ts
cynarAI de95cf1b91
Fix red main: typecheck, lint, i18n, unit and web E2E (#222)
* Fix red main: typecheck, lint, i18n and stale unit tests

Direct pushes to main left typecheck, lint and 16 unit tests red.

- Typecheck: AskCard test uses the current ask block shape, the sandbox
  fault test calls the two-argument desktop writeFile, removing a queued
  message notifies with the thread's latest event seq, and the replay
  fixture only reads ref from steps that carry one.
- Lint: format and import order.
- i18n: transcript status labels were German source strings; they are
  English now with German translations, catalogs re-extracted, and the
  mobile "Failed to remove message" string is translated.
- Tests follow intended behavior changes: the time instruction now
  closes the system prompt, short chat turns start at low reasoning,
  owner-owned read-only files (git objects) pass the home check, and the
  desktop computer may read and write host files outside its home. The
  old desktop write-containment suite is replaced by a host file test.
- The composer task pill test mocks the Lingui macros like other tests.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TnT7eVHuaCzZKY5CnYzpr1

* Bump Expo patch releases so expo install --check passes

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TnT7eVHuaCzZKY5CnYzpr1

* Bring web E2E back in line with the new side panel

The side panel lost its computer tab, which left the full computer window
(take control, teach, release) unreachable from the web app. The browser
view now offers an "Open in full window" button, and opening the panel no
longer boots a stopped computer.

Also: the slash picker scrolls instead of growing under the header, user
skills come first in it, the agent overview hides an empty routines block,
the hover time hides again with collapsed details, and the light destructive
token meets contrast on the sidebar. E2E specs follow the new panel layout,
and the cursor and hover tests no longer race the UI.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TnT7eVHuaCzZKY5CnYzpr1

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-22 21:58:24 +02:00

245 lines
12 KiB
TypeScript

import { expect, test } from "@playwright/test";
import {
captureScreenshot,
completeOnboarding,
createBotFromPicker,
openNewBot,
signup,
} from "./helpers";
test("bot creation, editing, and deletion persist", async ({ page }, testInfo) => {
const stamp = Date.now();
await signup(page, `bot-crud-${stamp}@rakazo.test`, "password12", "Bot CRUD");
await completeOnboarding(page);
await page.goto("/app");
await page.waitForURL(/\/app\/[^/]+$/);
const botList = page.locator("aside").first();
await expect(botList.getByRole("button", { name: /^Chief/ })).toBeVisible();
let createFailed = false;
let resolveCreateAborted!: () => void;
const createAborted = new Promise<void>((resolve) => {
resolveCreateAborted = resolve;
});
await page.route("**/rpc/bots/create", async (route) => {
createFailed = true;
await route.abort("failed");
resolveCreateAborted();
});
await openNewBot(page);
const form = page.getByTestId("create-bot-form");
await form.locator("label:has-text('Name') input").fill("New Bot");
await form.getByRole("button", { name: "Create", exact: true }).click();
await createAborted;
// Failed create keeps the form open on the current bot chat.
await expect(page.getByRole("combobox", { name: "Message" })).toBeVisible();
await expect(page.getByTestId("side-panel")).toHaveAttribute("data-panel", "create");
await expect(page.getByTestId("create-bot-error")).toBeVisible();
expect(createFailed).toBe(true);
await page.unroute("**/rpc/bots/create");
await page.getByRole("button", { name: "Cancel new bot" }).click();
await expect(page.getByTestId("side-panel")).toHaveAttribute("data-panel", "closed");
let failedPostCreateRefresh = false;
await page.route("**/rpc/spaces/list", async (route) => {
if (failedPostCreateRefresh) {
await route.fallback();
return;
}
failedPostCreateRefresh = true;
await route.abort("failed");
});
await createBotFromPicker(page);
await expect(page.getByRole("combobox", { name: "Message" })).toBeVisible();
await expect.poll(() => failedPostCreateRefresh).toBe(true);
await page.unroute("**/rpc/spaces/list");
await expect(botList.getByRole("button", { name: /^New Bot/ })).toBeVisible();
await page.waitForURL(/\/app\/[^/]+$/);
const deletedBotPath = new URL(page.url()).pathname;
await page.evaluate(() => window.dispatchEvent(new Event("focus")));
await page.waitForTimeout(500);
expect(new URL(page.url()).pathname).toBe(deletedBotPath);
await captureScreenshot(page, testInfo, "27-created-bot");
await page.locator("main").getByRole("button", { name: "New Bot", exact: true }).click();
await expect(page.getByText("Settings", { exact: true })).toBeVisible();
const nameInput = page.locator("label:has-text('Name') input");
const titleInput = page.locator("label:has-text('Title') input");
const descriptionInput = page.locator("label:has-text('Description') textarea");
const longTitle = `Market researcher ${"and source verifier ".repeat(9)}`;
const normalizedLongTitle = longTitle.trim();
expect(longTitle.length).toBeGreaterThan(160);
await nameInput.fill("Researcher");
await titleInput.fill(longTitle);
await descriptionInput.fill("Finds reliable sources and turns them into concise briefs.");
await page.getByTestId("oo-atelier-trigger").click();
const studio = page.getByTestId("oo-atelier");
await expect(studio).toBeVisible();
await expect(studio.getByTestId("oo-atelier-body")).toBeVisible();
await studio.getByRole("button", { name: "Color #EAB308" }).click();
await studio.getByRole("button", { name: "Done", exact: true }).click();
await expect(studio).toBeHidden();
await page.getByRole("button", { name: "Save", exact: true }).click();
await expect(botList.getByRole("button", { name: /^Researcher/ })).toBeVisible();
await expect(page.getByRole("combobox", { name: "Message" })).toBeVisible();
await page.locator("main").getByRole("button", { name: "Researcher", exact: true }).click();
await expect(nameInput).toHaveValue("Researcher");
await expect(titleInput).toHaveValue(normalizedLongTitle);
await expect(descriptionInput).toHaveValue(
"Finds reliable sources and turns them into concise briefs.",
);
const settings = page.getByTestId("bot-settings");
await settings.getByTestId("oo-atelier-trigger").click();
await expect(studio).toBeVisible();
await expect(studio.getByRole("button", { name: "Color #EAB308" })).toHaveClass(/ring-2/);
await studio.getByRole("button", { name: "Done", exact: true }).click();
const modelSelect = settings.locator("label:has-text('Model') select");
const teamComputer = settings.getByRole("button", { name: "Team" });
const openWork = settings.getByTestId("bot-scratchpad");
await expect(teamComputer).toBeHidden();
await expect(modelSelect).toBeHidden();
await expect(openWork).toBeHidden();
await expect(settings.getByRole("button", { name: "Save", exact: true })).toBeVisible();
await expect(settings.getByRole("button", { name: "Recover computer" })).toHaveCount(0);
await expect(settings.getByRole("button", { name: "Reset computer" })).toHaveCount(0);
await expect(settings.getByRole("button", { name: "Update computer" })).toHaveCount(0);
await captureScreenshot(page, testInfo, "27a-settings-panel");
await settings.getByText("Advanced", { exact: true }).click();
await expect(teamComputer).toBeVisible();
await expect(openWork).toBeVisible();
await expect(modelSelect).toBeVisible();
await expect(modelSelect).toContainText("Space default");
await captureScreenshot(page, testInfo, "27a-bot-settings-model");
await page.getByRole("button", { name: "Show computer" }).click();
const sidePanel = page.getByTestId("side-panel");
await expect(sidePanel).toHaveAttribute("data-panel", /^(computer|browser)$/);
await expect(page.getByRole("button", { name: "Show settings" })).toBeVisible();
// Overlay may flash during boot or never appear (already ready/asleep/stopped). Assert panel
// chrome, then wait until any overlay has cleared — avoid Locator.or() strict-mode multi-hits.
const bootOverlay = page.getByText(/Booting up .* computer/);
await expect(sidePanel.getByTestId("oo-computer-browser-viewport")).toBeVisible();
await expect(bootOverlay).toBeHidden();
await expect(sidePanel.getByText("Teach a task")).toHaveCount(0);
await expect(sidePanel.getByTestId("teach-start-button")).toHaveCount(0);
await expect(sidePanel.getByRole("button", { name: "Recover computer" })).toHaveCount(0);
await expect(sidePanel.getByRole("button", { name: "Reset computer" })).toHaveCount(0);
await expect(sidePanel.getByRole("button", { name: "Update computer" })).toHaveCount(0);
await expect(sidePanel.getByRole("button", { name: "Take control" })).toHaveCount(0);
await expect(sidePanel.getByTestId("computer-more-button")).toHaveCount(0);
await captureScreenshot(page, testInfo, "27b-computer-panel");
await page.getByRole("button", { name: "Show settings" }).click();
await nameInput.fill("Atlas");
await titleInput.fill("Research lead");
await descriptionInput.fill("Builds durable, source-backed research briefs.");
await page.getByRole("button", { name: "Save", exact: true }).click();
await expect(botList.getByRole("button", { name: /^Atlas/ })).toBeVisible();
await expect(page.getByRole("combobox", { name: "Message" })).toBeVisible();
await captureScreenshot(page, testInfo, "28-edited-bot-profile");
await page.reload();
await expect(botList.getByRole("button", { name: /^Atlas/ })).toBeVisible();
await expect(page.getByRole("combobox", { name: "Message" })).toBeVisible();
await page.locator("main").getByRole("button", { name: "Atlas", exact: true }).click();
await expect(nameInput).toHaveValue("Atlas");
await expect(titleInput).toHaveValue("Research lead");
await expect(descriptionInput).toHaveValue("Builds durable, source-backed research briefs.");
await captureScreenshot(page, testInfo, "29-reloaded-bot-profile");
// Wrap in the narrow dialog without pushing Delete below the Playwright viewport.
const longUnbrokenName = "A".repeat(48);
expect(longUnbrokenName.length).toBe(48);
await nameInput.fill(longUnbrokenName);
await page.getByRole("button", { name: "Save", exact: true }).click();
const longNameBot = botList.getByRole("button", {
name: new RegExp(`^${longUnbrokenName}`),
});
await expect(longNameBot).toBeVisible();
await expect(page.getByRole("combobox", { name: "Message" })).toBeVisible();
await longNameBot.click({ button: "right" });
await page.getByRole("menuitem", { name: "Delete" }).click();
const deleteDialog = page.getByRole("alertdialog", {
name: `Delete ${longUnbrokenName}?`,
});
await expect(deleteDialog).toBeVisible();
await captureScreenshot(page, testInfo, "30-delete-bot-confirmation");
await deleteDialog.getByRole("button", { name: "Delete", exact: true }).click();
await expect(botList.getByText(longUnbrokenName, { exact: true })).toHaveCount(0);
await expect(botList.getByRole("button", { name: /^Chief/ })).toBeVisible();
await page.waitForURL((url) => url.pathname !== deletedBotPath);
await page.goto(deletedBotPath);
await page.waitForURL((url) => url.pathname !== deletedBotPath);
await expect(botList.getByText(longUnbrokenName, { exact: true })).toHaveCount(0);
await expect(botList.getByRole("button", { name: /^Chief/ })).toBeVisible();
await expect(page.getByRole("combobox", { name: "Message" })).toBeVisible();
await captureScreenshot(page, testInfo, "31-deleted-bot-fallback");
});
test("a bot export restores as a new bot", async ({ page }, testInfo) => {
const stamp = Date.now();
await signup(page, `bot-import-${stamp}@rakazo.test`, "password12", "Bot Import");
await completeOnboarding(page);
await page.goto("/app");
await page.waitForURL(/\/app\/[^/]+$/);
await openNewBot(page);
const form = page.getByTestId("create-bot-form");
await expect(form.getByRole("button", { name: "Import bot" })).toBeVisible();
for (const colorScheme of ["light", "dark"] as const) {
await page.emulateMedia({ colorScheme });
await expect(page.locator("html")).toHaveAttribute("data-theme", colorScheme);
await captureScreenshot(page, testInfo, `27d-create-bot-import-${colorScheme}`);
}
const fileInput = form.getByTestId("import-bot-file");
await fileInput.setInputFiles({
name: "broken.json",
mimeType: "application/json",
buffer: Buffer.from("{"),
});
await expect(page.getByTestId("create-bot-error")).toHaveText("Could not import this file");
await fileInput.setInputFiles({
name: "archivist-export.json",
mimeType: "application/json",
buffer: Buffer.from(
JSON.stringify({
version: 1,
exportedAt: new Date().toISOString(),
bot: {
name: "Archivist",
title: "Keeps the records",
description: "Files and finds documents.",
instructions: "Name every file by date.",
},
memory: [{ path: "MEMORY.md", content: "# Archive\n\nPrefers ISO dates." }],
routines: [
{
name: "Weekly sweep",
prompt: "Tidy the archive",
crons: ["0 8 * * 1"],
timezone: "UTC",
},
],
files: [{ path: "notes/index.md", content: "imported" }],
history: [],
}),
),
});
await expect(page.getByTestId("side-panel")).toHaveAttribute("data-panel", "closed");
const botList = page.locator("aside").first();
await expect(botList.getByRole("button", { name: /^Archivist/ })).toBeVisible();
await page.locator("main").getByRole("button", { name: "Archivist", exact: true }).click();
await expect(page.locator("label:has-text('Title') input")).toHaveValue("Keeps the records");
for (const colorScheme of ["light", "dark"] as const) {
await page.emulateMedia({ colorScheme });
await expect(page.locator("html")).toHaveAttribute("data-theme", colorScheme);
await captureScreenshot(page, testInfo, `27e-imported-bot-${colorScheme}`);
}
});