chore: consolidate new foundation and archive v1 (#1495)

This commit is contained in:
2026-09-07 12:32:57 -05:00
3511 changed files with 727899 additions and 10 deletions
+64
View File
@@ -0,0 +1,64 @@
import { test, expect } from '@playwright/test';
import { loginAs, ADMIN_USER, REQUIRE_SEEDED_AUTH, TEST_USER } from './helpers/auth.js';
test.describe('Admin page — admin user', () => {
test.beforeEach(async ({ page }) => {
await loginAs(page, ADMIN_USER.email, ADMIN_USER.password);
const url = page.url();
test.skip(
!REQUIRE_SEEDED_AUTH && !url.includes('/chat'),
'No seeded admin user — skipping admin tests',
);
});
test('admin page loads with the Admin Panel heading', async ({ page }) => {
await page.goto('/admin');
await expect(page.getByRole('heading', { name: /admin panel/i })).toBeVisible({
timeout: 10_000,
});
});
test('shows User Management and System Health tabs', async ({ page }) => {
await page.goto('/admin');
await expect(page.getByRole('button', { name: /user management/i })).toBeVisible();
await expect(page.getByRole('button', { name: /system health/i })).toBeVisible();
});
test('User Management tab is active by default', async ({ page }) => {
await page.goto('/admin');
// The users tab shows a "+ New User" button
await expect(page.getByRole('button', { name: /new user/i })).toBeVisible({ timeout: 10_000 });
});
test('clicking System Health tab switches to health view', async ({ page }) => {
await page.goto('/admin');
await page.getByRole('button', { name: /system health/i }).click();
// Health cards or loading indicator should appear
const loadingOrCard = page
.getByText(/loading health/i)
.or(page.getByText(/database/i))
.first();
await expect(loadingOrCard).toBeVisible({ timeout: 10_000 });
});
});
test.describe('Admin page — non-admin user', () => {
test.beforeEach(async ({ page }) => {
await loginAs(page, TEST_USER.email, TEST_USER.password);
const url = page.url();
test.skip(
!REQUIRE_SEEDED_AUTH && !url.includes('/chat'),
'No seeded test user — skipping non-admin tests',
);
});
test('non-admin visiting /admin never sees the admin panel', async ({ page }) => {
await page.goto('/admin');
// Wait for the app shell to render (redirect and access-denied views both
// keep the sidebar), then assert the panel itself is absent. globalSetup
// seeds TEST_USER with role 'member', so this is a real authorization
// assertion, not environment-dependent.
await expect(page.getByRole('img', { name: /mosaic logo/i })).toBeVisible({ timeout: 10_000 });
await expect(page.getByRole('heading', { name: /admin panel/i })).not.toBeVisible();
});
});
+115
View File
@@ -0,0 +1,115 @@
import { test, expect } from '@playwright/test';
import { REQUIRE_SEEDED_AUTH, TEST_USER } from './helpers/auth.js';
// ── Login page ────────────────────────────────────────────────────────────────
test.describe('Login page', () => {
test('loads and shows the sign-in heading', async ({ page }) => {
await page.goto('/login');
await expect(page).toHaveTitle(/mosaic/i);
await expect(page.getByRole('heading', { name: /sign in/i })).toBeVisible();
});
test('shows email and password fields', async ({ page }) => {
await page.goto('/login');
await expect(page.getByLabel('Email')).toBeVisible();
await expect(page.getByLabel('Password')).toBeVisible();
});
test('shows submit button', async ({ page }) => {
await page.goto('/login');
await expect(page.getByRole('button', { name: /sign in/i })).toBeVisible();
});
test('shows link to registration page', async ({ page }) => {
await page.goto('/login');
const signUpLink = page.getByRole('link', { name: /sign up/i });
await expect(signUpLink).toBeVisible();
await signUpLink.click();
await expect(page).toHaveURL(/\/register/);
});
test('shows an error alert for invalid credentials', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('Email').fill('[email protected]');
await page.getByLabel('Password').fill('wrongpassword');
await page.getByRole('button', { name: /sign in/i }).click();
// The error banner should appear; it has role="alert"
await expect(page.getByRole('alert')).toBeVisible({ timeout: 10_000 });
});
test('email field requires valid format (HTML5 validation)', async ({ page }) => {
await page.goto('/login');
// Fill a non-email value — browser prevents submission
await page.getByLabel('Email').fill('notanemail');
await page.getByLabel('Password').fill('somepass');
await page.getByRole('button', { name: /sign in/i }).click();
// Still on the login page
await expect(page).toHaveURL(/\/login/);
});
test('redirects to /chat after successful login', async ({ page }) => {
// Only meaningful with known-good credentials; against a live environment
// this would just probe someone else's user table.
test.skip(!REQUIRE_SEEDED_AUTH, 'needs seeded credentials (E2E_REQUIRE_SEEDED_AUTH=1)');
await page.goto('/login');
await page.getByLabel('Email').fill(TEST_USER.email);
await page.getByLabel('Password').fill(TEST_USER.password);
await page.getByRole('button', { name: /sign in/i }).click();
await expect(page).toHaveURL(/\/chat/, { timeout: 10_000 });
});
});
// ── Registration page ─────────────────────────────────────────────────────────
test.describe('Registration page', () => {
test('loads and shows the create account heading', async ({ page }) => {
await page.goto('/register');
await expect(page.getByRole('heading', { name: /create account/i })).toBeVisible();
});
test('shows name, email and password fields', async ({ page }) => {
await page.goto('/register');
await expect(page.getByLabel('Name')).toBeVisible();
await expect(page.getByLabel('Email')).toBeVisible();
await expect(page.getByLabel('Password')).toBeVisible();
});
test('shows submit button', async ({ page }) => {
await page.goto('/register');
await expect(page.getByRole('button', { name: /create account/i })).toBeVisible();
});
test('shows link to login page', async ({ page }) => {
await page.goto('/register');
const signInLink = page.getByRole('link', { name: /sign in/i });
await expect(signInLink).toBeVisible();
await signInLink.click();
await expect(page).toHaveURL(/\/login/);
});
test('name field is required — empty form stays on page', async ({ page }) => {
await page.goto('/register');
// Submit with nothing filled in — browser required validation blocks it
await page.getByRole('button', { name: /create account/i }).click();
await expect(page).toHaveURL(/\/register/);
});
test('all required fields must be filled (HTML5 validation)', async ({ page }) => {
await page.goto('/register');
await page.getByLabel('Name').fill('Test User');
// Do NOT fill email or password — still on page
await page.getByRole('button', { name: /create account/i }).click();
await expect(page).toHaveURL(/\/register/);
});
});
// ── Root redirect ─────────────────────────────────────────────────────────────
test.describe('Root route', () => {
test('visiting / redirects to /login or /chat', async ({ page }) => {
await page.goto('/');
// Unauthenticated users should land on /login; authenticated on /chat
await expect(page).toHaveURL(/\/(login|chat)/, { timeout: 10_000 });
});
});
+43
View File
@@ -0,0 +1,43 @@
import { test, expect } from '@playwright/test';
import { loginAs, REQUIRE_SEEDED_AUTH, TEST_USER } from './helpers/auth.js';
test.describe('Chat page', () => {
test.beforeEach(async ({ page }) => {
await loginAs(page, TEST_USER.email, TEST_USER.password);
// If login failed (no seeded user in env) we may be on /login — skip
const url = page.url();
test.skip(
!REQUIRE_SEEDED_AUTH && !url.includes('/chat'),
'No seeded test user — skipping authenticated tests',
);
});
test('chat page loads and shows the conversation area', async ({ page }) => {
await page.goto('/chat');
await expect(page.getByRole('heading', { level: 1, name: /chat/i })).toBeVisible({
timeout: 10_000,
});
await expect(page.getByRole('log', { name: /conversation/i })).toBeVisible();
});
test('message composer input is visible', async ({ page }) => {
await page.goto('/chat');
await expect(page.getByLabel('Message')).toBeVisible({ timeout: 10_000 });
});
test('command panel lists /new and exposes the run controls', async ({ page }) => {
await page.goto('/chat');
// Conversations are command-driven: /new starts one via the commands panel.
const commandList = page.getByRole('list', { name: /available commands/i });
await expect(commandList).toBeVisible({ timeout: 10_000 });
await expect(commandList.getByText('/new', { exact: true })).toBeVisible();
await expect(page.getByLabel('Command name')).toBeVisible();
await expect(page.getByRole('button', { name: /run command/i })).toBeVisible();
});
test('sidebar navigation is present on chat page', async ({ page }) => {
await page.goto('/chat');
// The app-shell sidebar should be visible
await expect(page.getByRole('link', { name: /chat/i }).first()).toBeVisible();
});
});
+95
View File
@@ -0,0 +1,95 @@
import type { FullConfig } from '@playwright/test';
import { ADMIN_USER, REQUIRE_SEEDED_AUTH, TEST_USER } from './helpers/auth.js';
/**
* Seed the E2E users through the gateway's real APIs (#1445, P6).
*
* On a fresh database (CI boots the gateway on the embedded PGlite path):
* 1. POST /api/bootstrap/setup creates ADMIN_USER as the first admin.
* 2. The admin signs in and creates TEST_USER via the better-auth admin API.
*
* Against an environment that already has users (needsSetup=false), seeding is
* skipped entirely: the specs keep their own skip-when-login-fails guards, so
* a live environment stays usable as a test target without mutation. Under
* E2E_REQUIRE_SEEDED_AUTH=1 (CI) that state is instead a hard failure and the
* guards are disabled — see helpers/auth.ts.
*
* On a fresh database, any seeding failure throws and fails the whole run: an
* E2E gate whose authenticated suites silently skip would pass while proving
* nothing.
*/
export default async function globalSetup(config: FullConfig): Promise<void> {
const baseURL = config.projects[0]?.use?.baseURL ?? 'http://localhost:14242';
const statusRes = await fetch(`${baseURL}/api/bootstrap/status`);
if (!statusRes.ok) {
throw new Error(`GET /api/bootstrap/status returned ${statusRes.status} — is the gateway up?`);
}
const status = (await statusRes.json()) as { needsSetup: boolean };
if (!status.needsSetup) {
if (REQUIRE_SEEDED_AUTH) {
// CI boots the gateway on a fresh HOME-isolated database, so an
// already-populated one means the isolation regressed — refuse to run
// against unknown data rather than skip-and-pass.
throw new Error(
'E2E_REQUIRE_SEEDED_AUTH=1 but the database already has users — gateway HOME isolation regressed?',
);
}
console.info('[e2e setup] users already exist; skipping seed');
return;
}
const setupRes = await fetch(`${baseURL}/api/bootstrap/setup`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
name: ADMIN_USER.name,
email: ADMIN_USER.email,
password: ADMIN_USER.password,
}),
});
if (!setupRes.ok) {
throw new Error(
`POST /api/bootstrap/setup failed (${setupRes.status}): ${await setupRes.text()}`,
);
}
console.info(`[e2e setup] bootstrap admin created: ${ADMIN_USER.email}`);
// better-auth's CSRF protection rejects requests without an Origin header
// (403 MISSING_OR_NULL_ORIGIN), so the server-side fetches here send the
// gateway's own origin — the same value a browser tab on the SPA would send.
const authHeaders = { 'content-type': 'application/json', origin: baseURL };
const signInRes = await fetch(`${baseURL}/api/auth/sign-in/email`, {
method: 'POST',
headers: authHeaders,
body: JSON.stringify({ email: ADMIN_USER.email, password: ADMIN_USER.password }),
});
if (!signInRes.ok) {
throw new Error(`admin sign-in failed (${signInRes.status}): ${await signInRes.text()}`);
}
const cookies = signInRes.headers
.getSetCookie()
.map((cookie) => cookie.split(';', 1)[0])
.join('; ');
if (!cookies) {
throw new Error('admin sign-in returned no session cookie');
}
const createRes = await fetch(`${baseURL}/api/auth/admin/create-user`, {
method: 'POST',
headers: { ...authHeaders, cookie: cookies },
body: JSON.stringify({
name: TEST_USER.name,
email: TEST_USER.email,
password: TEST_USER.password,
role: 'member',
}),
});
if (!createRes.ok) {
throw new Error(
`POST /api/auth/admin/create-user failed (${createRes.status}): ${await createRes.text()}`,
);
}
console.info(`[e2e setup] test user created: ${TEST_USER.email}`);
}
+40
View File
@@ -0,0 +1,40 @@
import type { Page } from '@playwright/test';
export const TEST_USER = {
email: process.env['E2E_USER_EMAIL'] ?? '[email protected]',
password: process.env['E2E_USER_PASSWORD'] ?? 'password123',
name: 'E2E Test User',
};
export const ADMIN_USER = {
email: process.env['E2E_ADMIN_EMAIL'] ?? '[email protected]',
password: process.env['E2E_ADMIN_PASSWORD'] ?? 'adminpass123',
name: 'E2E Admin User',
};
/**
* Set when the database was seeded by global-setup (CI sets it in the
* publish.yml e2e step). Seeded credentials MUST work, so login failures are
* hard failures and the skip-when-login-fails guards are disabled — otherwise
* a login regression would skip every authenticated suite and the gate would
* pass while proving nothing. Unset (a live environment used as a test
* target), the guards stay on and unseeded credentials skip their suites.
*/
export const REQUIRE_SEEDED_AUTH = process.env['E2E_REQUIRE_SEEDED_AUTH'] === '1';
/**
* Fill the login form and submit, then wait for the post-login redirect to
* /chat. Under REQUIRE_SEEDED_AUTH a missed redirect throws (failing the
* test). Otherwise the timeout is swallowed: the page stays on /login and the
* callers' `test.skip(...)` guards see that. Without this wait, every guard
* read page.url() before the redirect happened and skipped its suite even
* when login succeeded (#1445).
*/
export async function loginAs(page: Page, email: string, password: string): Promise<void> {
await page.goto('/login');
await page.getByLabel('Email').fill(email);
await page.getByLabel('Password').fill(password);
await page.getByRole('button', { name: /sign in/i }).click();
const redirect = page.waitForURL(/\/chat/, { timeout: 10_000 });
await (REQUIRE_SEEDED_AUTH ? redirect : redirect.catch(() => {}));
}
+98
View File
@@ -0,0 +1,98 @@
import { test, expect } from '@playwright/test';
import { loginAs, REQUIRE_SEEDED_AUTH, TEST_USER } from './helpers/auth.js';
test.describe('Sidebar navigation', () => {
test.beforeEach(async ({ page }) => {
await loginAs(page, TEST_USER.email, TEST_USER.password);
const url = page.url();
test.skip(
!REQUIRE_SEEDED_AUTH && !url.includes('/chat'),
'No seeded test user — skipping authenticated tests',
);
});
test('sidebar shows the Mosaic brand', async ({ page }) => {
await page.goto('/chat');
// The brand block is a logo image plus "Mosaic / Mission Control" text,
// not a link.
await expect(page.getByRole('img', { name: /mosaic logo/i })).toBeVisible();
await expect(page.getByText('Mission Control')).toBeVisible();
});
test('Chat nav link navigates to /chat', async ({ page }) => {
await page.goto('/settings');
await page
.getByRole('link', { name: /^chat$/i })
.first()
.click();
await expect(page).toHaveURL(/\/chat/);
});
test('Projects nav link navigates to /projects', async ({ page }) => {
await page.goto('/chat');
await page
.getByRole('link', { name: /projects/i })
.first()
.click();
await expect(page).toHaveURL(/\/projects/);
});
test('Settings nav link navigates to /settings', async ({ page }) => {
await page.goto('/chat');
await page
.getByRole('link', { name: /settings/i })
.first()
.click();
await expect(page).toHaveURL(/\/settings/);
});
test('Tasks nav link navigates to /tasks', async ({ page }) => {
await page.goto('/chat');
await page.getByRole('link', { name: /tasks/i }).first().click();
await expect(page).toHaveURL(/\/tasks/);
});
test('active link is visually highlighted', async ({ page }) => {
await page.goto('/chat');
// The sidebar marks the active item with `font-medium` (plus an inline
// primary-color style); inactive items get the hover class instead.
const chatLink = page.getByRole('link', { name: /^chat$/i }).first();
const projectsLink = page.getByRole('link', { name: /^projects$/i }).first();
await expect(chatLink).toHaveClass(/font-medium/);
await expect(projectsLink).not.toHaveClass(/font-medium/);
});
});
test.describe('Route transitions', () => {
test.beforeEach(async ({ page }) => {
await loginAs(page, TEST_USER.email, TEST_USER.password);
const url = page.url();
test.skip(
!REQUIRE_SEEDED_AUTH && !url.includes('/chat'),
'No seeded test user — skipping authenticated tests',
);
});
test('navigating chat → projects → settings → chat works without errors', async ({ page }) => {
await page.goto('/chat');
await expect(page).toHaveURL(/\/chat/);
// level: 1 — empty-state h2s ("No projects yet") also match the loose
// patterns, and a two-element match is a strict-mode violation.
await page.goto('/projects');
await expect(page.getByRole('heading', { level: 1, name: /projects/i })).toBeVisible();
await page.goto('/settings');
await expect(page.getByRole('heading', { level: 1, name: /settings/i })).toBeVisible();
await page.goto('/chat');
await expect(page).toHaveURL(/\/chat/);
});
test('back-button navigation works between pages', async ({ page }) => {
await page.goto('/chat');
await page.goto('/projects');
await page.goBack();
await expect(page).toHaveURL(/\/chat/);
});
});
+39
View File
@@ -0,0 +1,39 @@
import { test, expect } from '@playwright/test';
import { loginAs, REQUIRE_SEEDED_AUTH, TEST_USER } from './helpers/auth.js';
test.describe('Projects page', () => {
test.beforeEach(async ({ page }) => {
await loginAs(page, TEST_USER.email, TEST_USER.password);
const url = page.url();
test.skip(
!REQUIRE_SEEDED_AUTH && !url.includes('/chat'),
'No seeded test user — skipping authenticated tests',
);
});
test('projects page loads with heading', async ({ page }) => {
await page.goto('/projects');
// level: 1 — the "No projects yet" empty-state h2 also matches /projects/i
// and a two-element match is a strict-mode violation.
await expect(page.getByRole('heading', { level: 1, name: /projects/i })).toBeVisible({
timeout: 10_000,
});
});
test('shows empty state or project cards when loaded', async ({ page }) => {
await page.goto('/projects');
// Wait for loading state to clear
await expect(page.getByText(/loading projects/i)).not.toBeVisible({ timeout: 10_000 });
const cardsOrEmpty = page
.locator('[class*="grid"]')
.or(page.getByText(/no projects yet/i))
.first();
await expect(cardsOrEmpty).toBeVisible({ timeout: 10_000 });
});
test('sidebar navigation is present', async ({ page }) => {
await page.goto('/projects');
await expect(page.getByRole('link', { name: /projects/i }).first()).toBeVisible();
});
});
+59
View File
@@ -0,0 +1,59 @@
import { test, expect } from '@playwright/test';
import { loginAs, REQUIRE_SEEDED_AUTH, TEST_USER } from './helpers/auth.js';
test.describe('Settings page', () => {
test.beforeEach(async ({ page }) => {
await loginAs(page, TEST_USER.email, TEST_USER.password);
const url = page.url();
test.skip(
!REQUIRE_SEEDED_AUTH && !url.includes('/chat'),
'No seeded test user — skipping authenticated tests',
);
});
test('settings page loads with heading', async ({ page }) => {
await page.goto('/settings');
await expect(page.getByRole('heading', { name: /^settings$/i })).toBeVisible({
timeout: 10_000,
});
});
test('shows the four settings tabs', async ({ page }) => {
await page.goto('/settings');
await expect(page.getByRole('button', { name: /profile/i })).toBeVisible();
await expect(page.getByRole('button', { name: /appearance/i })).toBeVisible();
await expect(page.getByRole('button', { name: /notifications/i })).toBeVisible();
await expect(page.getByRole('button', { name: /providers/i })).toBeVisible();
});
test('profile tab is active by default', async ({ page }) => {
await page.goto('/settings');
await expect(page.getByRole('heading', { name: /^profile$/i })).toBeVisible({
timeout: 10_000,
});
});
test('clicking Appearance tab switches content', async ({ page }) => {
await page.goto('/settings');
await page.getByRole('button', { name: /appearance/i }).click();
await expect(page.getByRole('heading', { name: /appearance/i })).toBeVisible({
timeout: 5_000,
});
});
test('clicking Notifications tab switches content', async ({ page }) => {
await page.goto('/settings');
await page.getByRole('button', { name: /notifications/i }).click();
await expect(page.getByRole('heading', { name: /notifications/i })).toBeVisible({
timeout: 5_000,
});
});
test('clicking Providers tab switches content', async ({ page }) => {
await page.goto('/settings');
await page.getByRole('button', { name: /providers/i }).click();
await expect(page.getByRole('heading', { name: /llm providers/i })).toBeVisible({
timeout: 5_000,
});
});
});