import { test, expect, type Page } from '@playwright/test'; import { createRequire } from 'node:module'; import { fileURLToPath } from 'node:url'; import { dirname, resolve, join } from 'node:path'; import { tmpdir } from 'node:os'; // ── M0: WORKSPACE FILE → AGENT INPUT E2E ───────────────────────────────────── // // Covers the user-observable parts of M0 (spec: // docs/superpowers/specs/2026-06-24-workspace-consolidation-m0-file-input-consistency-design.md): // // 1. Creating a case workspace through the UI scaffolds files/input + files/output // (scaffoldCaseSpace) — they show up as folders in the Files tab right away. // 2. The new-chat dialog warns (data-testid="ephemeral-warning") when the user // switches the workspace mode to 一時的 (ephemeral), and hides it for the // default 永続 (persistent). // // "The agent actually sees pre-placed files" is asserted by the unit test // (src/engine/agent-loop.test.ts → buildSystemPrompt existing workspace files), // since asserting LLM behaviour end-to-end is non-deterministic. // // Runs with LOCAL AUTH on (ui/playwright.auth.config.ts → config.e2e-auth.yaml) // against the real built server. Seeds one regular user via the built Repository // into the same deterministic DB the webServer opens. const __dirname = dirname(fileURLToPath(import.meta.url)); const require = createRequire(import.meta.url); const repoRoot = resolve(__dirname, '..', '..'); const e2eTmp = join(tmpdir(), 'maestro-e2e-auth'); const DB_PATH = join(e2eTmp, 'e2e-auth.db'); const USER = { email: 'wsfile@m0.local', password: 'WsFile123!', name: 'WsFileUser' }; async function login(page: Page, email: string, password: string) { await page.goto('/auth/login'); await page.fill('input[name="email"]', email); await page.fill('input[name="password"]', password); await Promise.all([ page.waitForURL(/\/ui(\/|$)/, { timeout: 15_000 }), page.click('button[type="submit"]'), ]); expect(page.url(), 'login should not bounce back to /auth/login').not.toContain('/auth/login'); } /** Create a fresh case workspace via the UI (this is what triggers scaffoldCaseSpace). */ async function createAndOpenCaseSpace(page: Page, label: string): Promise { await page.goto('/ui'); await page.getByTestId('nav-spaces').click(); const rail = page.getByTestId('space-rail'); await expect(rail).toBeVisible(); await page.getByTestId('create-space-btn').click(); const titleInput = page.getByTestId('space-title-input'); await expect(titleInput).toBeVisible(); const title = `${label}-${Date.now()}`; await titleInput.fill(title); await page.getByTestId('space-form-submit').click(); await expect(titleInput).toHaveCount(0); const caseRow = rail .locator('[data-testid="space-row"][data-space-kind="case"]') .filter({ hasText: title }); await expect(caseRow).toBeVisible(); await caseRow.click(); await expect(page.getByTestId('space-detail')).toBeVisible(); } test.beforeAll(async () => { const { Repository } = require(resolve(repoRoot, 'dist/db/repository.js')) as { Repository: new (dbPath: string) => { getUserByEmail: (email: string) => { id: string } | null; createLocalUser: (p: { email: string; password: string; name?: string; role: string; status: string }) => { id: string }; close?: () => void; }; }; const repo = new Repository(DB_PATH); try { if (!repo.getUserByEmail(USER.email)) { repo.createLocalUser({ email: USER.email, password: USER.password, name: USER.name, role: 'user', status: 'active' }); } } finally { repo.close?.(); } }); test('new case workspace shows input/ and output/ folders in the Files tab', async ({ page }) => { await login(page, USER.email, USER.password); await createAndOpenCaseSpace(page, 'm0-files'); await page.getByTestId('space-tab-files').click(); await expect(page.getByTestId('space-files')).toBeVisible(); const inputTile = page.locator('[data-testid="space-file-tile"][data-kind="directory"][data-name="input"]'); const outputTile = page.locator('[data-testid="space-file-tile"][data-kind="directory"][data-name="output"]'); await expect(inputTile).toBeVisible({ timeout: 15_000 }); await expect(outputTile).toBeVisible(); }); test('fresh workspace chat list shows next-action guidance, and +新規 opens the create dialog (step 3 entry)', async ({ page }) => { await login(page, USER.email, USER.password); await createAndOpenCaseSpace(page, 'm0-step3'); // The chat tab is the default. A brand-new workspace has no chats, so the // empty state should guide the user to send a request and explain where files // and outputs live (step 3 / step 4 next-action copy). await expect(page.getByTestId('space-tab-chat')).toBeVisible(); await expect(page.getByText('「+ 新規」からエージェントに依頼を送れます')).toBeVisible(); await expect(page.getByText(/成果物は ファイルタブの output\/ に保存されます/)).toBeVisible(); // Step 3 entry: the +新規 button opens the create dialog (no LLM run needed — // we only assert the entry point reaches the dialog). await page.getByTestId('space-new-chat-btn').click(); await expect(page.getByTestId('ephemeral-warning')).toHaveCount(0); // dialog open, persistent default await expect(page.getByRole('button', { name: /永続/ })).toBeVisible(); }); test('opening a fresh chat surfaces step 4 guidance: artifacts live in the output/ folder', async ({ page }) => { await login(page, USER.email, USER.password); await createAndOpenCaseSpace(page, 'm0-step4'); // Step 3 → create a chat through the dialog. No LLM runs in this env // (e2e-noop worker), so the job never completes — we only verify the // create → open → "where do artifacts go" path, which is step 4's next-action. await page.getByTestId('space-new-chat-btn').click(); await page.getByTestId('create-task-body').fill('step4 の成果物の在りかを確認するためのチャット'); await page.getByTestId('create-task-submit').click(); // The created chat opens inline (handleSpaceCreate → onSelectSpaceTask). await expect(page.getByTestId('space-conversation')).toBeVisible({ timeout: 15_000 }); // Open the Files tab of the opened chat, then switch to the output/ section. // Brand-new chat → output/ is empty → the step 4 hint explains where the // agent's artifacts will land. (The FileBrowser section switcher buttons are // plain-text 'workspace'/'input'/'output'/'logs'.) await page.getByTestId('space-chat-tab-files').click(); await page.getByRole('button', { name: 'output', exact: true }).click(); await expect(page.getByTestId('output-empty-hint')).toBeVisible({ timeout: 15_000 }); await expect(page.getByTestId('output-empty-hint')).toContainText('output/'); }); test('new-chat dialog warns when ephemeral mode is selected, not for persistent', async ({ page }) => { await login(page, USER.email, USER.password); await createAndOpenCaseSpace(page, 'm0-warn'); await page.getByTestId('space-new-chat-btn').click(); // Default is persistent → no warning. await expect(page.getByTestId('ephemeral-warning')).toHaveCount(0); // Switch to 一時的 → warning appears. await page.getByRole('button', { name: '一時的' }).click(); await expect(page.getByTestId('ephemeral-warning')).toBeVisible(); // Switch back to 永続 → warning gone. await page.getByRole('button', { name: /永続/ }).click(); await expect(page.getByTestId('ephemeral-warning')).toHaveCount(0); });