maestro/ui/e2e-auth/workspace-file-input.auth.spec.ts
oss-sync b857c33ef6
Some checks failed
CI / build-and-test (push) Has been cancelled
sync: update from private repo (f6d625db)
2026-06-26 03:35:45 +00:00

157 lines
7.5 KiB
TypeScript

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<void> {
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);
});