157 lines
7.5 KiB
TypeScript
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);
|
|
});
|