import { test, expect, type Page } from '@playwright/test'; // ── M2: Tasks tab removed, workspace one-true-home ──────────────────────────── // // The top-level "タスク" tab was removed; normal use is now entirely through // Workspaces (spaces). On startup the personal workspace opens automatically so // the user never lands on an empty rail. Legacy `?page=tasks` deep links are // normalized onto the workspace model (Option A redirect) at load time. // // With auth disabled the /api/auth/me probe 404s on purpose and the UI falls // back to the synthetic local user; the personal workspace is auto-created on // the first /api/local/spaces call. Personal-workspace file listing may 404 // when the tree is empty — that is benign here. const TASKS_BENIGN_PATHS = new Set([ '/api/auth/me', '/api/users/me/orgs', '/api/mcp/connections', '/api/mcp/servers', '/api/mcp/user-servers', '/api/ssh/connections', ]); const TASKS_BENIGN_ASSET = /\.(ico|png|svg|map|webmanifest|json)$/i; // Personal-workspace files endpoints may legitimately 404 with an empty tree // (the SpaceFiles source-library group probes for source/index.jsonl content). const TASKS_BENIGN_PATH_RE = /\/api\/local\/spaces\/[^/]+\/files(\/content)?($|\?|\/)/; function trackFatalErrors(page: Page): string[] { const fatalErrors: string[] = []; page.on('pageerror', (err) => fatalErrors.push(`pageerror: ${err.message}`)); page.on('response', (res) => { if (res.status() >= 400) { const { pathname } = new URL(res.url()); if ( !TASKS_BENIGN_ASSET.test(pathname) && !TASKS_BENIGN_PATHS.has(pathname) && !TASKS_BENIGN_PATH_RE.test(pathname) ) { fatalErrors.push(`HTTP ${res.status()} ${res.url()}`); } } }); return fatalErrors; } test('nav: the Tasks tab no longer exists', async ({ page }) => { const fatalErrors = trackFatalErrors(page); await page.goto('/ui'); // The removed top-level Tasks tab must not render in either layout. await expect(page.getByTestId('nav-tasks')).toHaveCount(0); // Workspaces is present (the new home). await expect(page.getByTestId('nav-spaces')).toBeVisible(); // Removed sub-tab strip must be gone. await expect(page.getByTestId('tasks-subtabs')).toHaveCount(0); expect(fatalErrors).toEqual([]); }); test('startup opens the workspaces page (personal workspace auto-selected)', async ({ page }) => { const fatalErrors = trackFatalErrors(page); await page.goto('/ui'); // Spaces is the default page; the rail + a selected workspace detail render // (the personal workspace is auto-opened so the rail is never empty). await expect(page.getByTestId('space-detail')).toBeVisible(); // We must NOT be on the legacy tasks page. expect(page.url()).not.toContain('page=tasks'); expect(fatalErrors).toEqual([]); }); test('legacy ?page=tasks deep link redirects onto the workspace model', async ({ page }) => { const fatalErrors = trackFatalErrors(page); await page.goto('/ui?page=tasks'); // The legacy page=tasks is normalized away (Option A) and the workspace // detail surfaces instead. await expect(page.getByTestId('space-detail')).toBeVisible(); await expect.poll(() => page.url()).not.toContain('page=tasks'); // The personal workspace is targeted via the space param. await expect.poll(() => page.url()).toMatch(/[?&]space=/); expect(fatalErrors).toEqual([]); });