moving
Some checks failed
Installer Smoke / installer-smoke (push) Has been cancelled

This commit is contained in:
Oleg Maslov
2026-09-02 10:10:29 +02:00
commit 0c3e2ead3b
3841 changed files with 970576 additions and 0 deletions

View File

@@ -0,0 +1,387 @@
/**
* Full Product Audit — comprehensive E2E covering every app and flow.
*
* Tests every surface a user can reach from the dock, verifies API health,
* and walks through critical user journeys.
*
* Run: npx playwright test tests/e2e/full-product-audit.spec.ts --reporter=list
*/
import { test, expect, type Page } from '@playwright/test';
const BASE = process.env.WAGGLE_E2E_BASE_URL ?? 'http://127.0.0.1:3333';
// ── Helpers ───────────────────────────────────────────────────────────
async function dismissOverlay(page: Page) {
for (let attempt = 0; attempt < 3; attempt++) {
const overlay = page.locator('.fixed.backdrop-blur-sm');
if (!await overlay.isVisible({ timeout: 1000 }).catch(() => false)) break;
const startBtn = page.locator('button:has-text("Start Working")');
if (await startBtn.isVisible({ timeout: 500 }).catch(() => false)) {
await startBtn.click({ force: true });
await page.waitForTimeout(500);
continue;
}
await page.mouse.click(5, 5);
await page.waitForTimeout(500);
}
}
async function gotoDesktop(page: Page) {
await page.goto(`${BASE}/home?skipOnboarding=true&skipBoot=true&tier=power`, { waitUntil: 'domcontentloaded' });
await page.waitForSelector('[role="navigation"], main', { timeout: 15_000 });
await page.waitForTimeout(600);
await dismissOverlay(page);
}
function routeWithSkip(route: string): string {
const separator = route.includes('?') ? '&' : '?';
return `${BASE}${route}${separator}skipOnboarding=true&skipBoot=true&tier=power`;
}
async function openCurrentApp(page: Page, label: string) {
const nav = page.locator('[role="navigation"]');
const navAliases: Record<string, string[]> = {
Chat: ['Chat'],
Memory: ['Memory'],
Agents: ['Agents'],
Connectors: ['Connectors'],
Home: ['Home'],
Settings: ['Account and settings'],
};
for (const alias of navAliases[label] ?? [label]) {
const btn = nav.locator('button', { hasText: alias }).first();
if (await btn.isVisible({ timeout: 700 }).catch(() => false)) {
await btn.click();
await page.waitForTimeout(700);
return;
}
}
const routes: Record<string, string> = {
Home: '/home',
Room: '/room',
Agents: '/agents',
Files: '/files',
Approvals: '/approvals',
'Mission Control': '/settings/mission-control',
Timeline: '/settings/timeline',
'Usage & Cost': '/settings/usage',
'Events & Logs': '/settings/events',
'Team Governance': '/team',
'Skills Hub': '/skills',
'Connector Hub': '/connectors',
'MCP Hub': '/mcps',
Marketplace: '/marketplace',
Settings: '/settings',
Vault: '/settings/vault',
};
const route = routes[label];
if (!route) throw new Error(`No current navigation target configured for "${label}"`);
await page.goto(routeWithSkip(route), { waitUntil: 'domcontentloaded' });
await page.waitForSelector('[role="navigation"], main', { timeout: 15_000 });
await page.waitForTimeout(700);
await dismissOverlay(page);
}
async function openAppViaDock(page: Page, label: string) {
// Direct dock button (has aria-label)
const directBtn = page.locator(`button[aria-label="${label}"]`);
if (await directBtn.isVisible({ timeout: 1000 }).catch(() => false)) {
await directBtn.click();
await page.waitForTimeout(800);
return;
}
// Try zone parents (Ops, Extend) — click to open tray, then click child by text
for (const zone of ['Ops', 'Extend']) {
const zoneBtn = page.locator(`button[aria-label="${zone}"]`);
if (await zoneBtn.isVisible({ timeout: 500 }).catch(() => false)) {
await zoneBtn.click();
await page.waitForTimeout(400);
// The tray renders as a fixed portal with [data-dock-tray]
const tray = page.locator('[data-dock-tray]');
if (await tray.isVisible({ timeout: 1000 }).catch(() => false)) {
const childBtn = tray.locator('button', { hasText: label });
if (await childBtn.isVisible({ timeout: 500 }).catch(() => false)) {
await childBtn.click();
await page.waitForTimeout(800);
return;
}
}
// Close the tray if we didn't find the child
await page.mouse.click(5, 5);
await page.waitForTimeout(200);
}
}
}
async function getVisibleText(page: Page): Promise<string> {
return page.locator('body').innerText();
}
// ── 1. API Health ─────────────────────────────────────────────────────
test.describe('1. API Health', () => {
test('health endpoint', async ({ request }) => {
const res = await request.get(`${BASE}/health`);
expect(res.ok()).toBeTruthy();
const data = await res.json();
expect(['ok', 'degraded']).toContain(data.status);
expect(data.database.healthy).toBe(true);
});
test('workspaces API', async ({ request }) => {
const res = await request.get(`${BASE}/api/workspaces`);
expect(res.ok()).toBeTruthy();
const data = await res.json();
expect(Array.isArray(data)).toBeTruthy();
});
test('personas API', async ({ request }) => {
const res = await request.get(`${BASE}/api/personas`);
expect([200, 404]).toContain(res.status());
if (res.ok()) {
const data = await res.json();
const list = Array.isArray(data) ? data : data.personas ?? [];
expect(list.length).toBeGreaterThan(0);
}
});
test('events API', async ({ request }) => {
const res = await request.get(`${BASE}/api/events?limit=5`);
expect(res.ok()).toBeTruthy();
});
test('vault API', async ({ request }) => {
const res = await request.get(`${BASE}/api/vault`);
expect(res.ok()).toBeTruthy();
});
test('settings API', async ({ request }) => {
const res = await request.get(`${BASE}/api/settings`);
expect(res.ok()).toBeTruthy();
});
test('memory search API', async ({ request }) => {
const res = await request.get(`${BASE}/api/memory/search?q=test&limit=3`);
expect(res.ok()).toBeTruthy();
});
test('marketplace API', async ({ request }) => {
const res = await request.get(`${BASE}/api/marketplace/packs`);
expect([200, 503]).toContain(res.status());
});
test('compliance API', async ({ request }) => {
const res = await request.get(`${BASE}/api/compliance/status`);
expect(res.ok()).toBeTruthy();
});
test('workspace templates API', async ({ request }) => {
const res = await request.get(`${BASE}/api/workspace-templates`);
expect(res.ok()).toBeTruthy();
const data = await res.json();
const list = Array.isArray(data) ? data : data.templates ?? [];
expect(list.length).toBeGreaterThanOrEqual(7);
});
test('cost API responds', async ({ request }) => {
const res = await request.get(`${BASE}/api/cost/by-workspace`);
// Cost endpoint may return various codes depending on workspace state
expect(res.status()).toBeLessThan(600);
});
test('offline status API', async ({ request }) => {
const res = await request.get(`${BASE}/api/offline/status`);
expect(res.ok()).toBeTruthy();
});
test('cron API', async ({ request }) => {
const res = await request.get(`${BASE}/api/cron`);
expect(res.ok()).toBeTruthy();
});
test('skills API', async ({ request }) => {
const res = await request.get(`${BASE}/api/skills`);
expect(res.ok()).toBeTruthy();
});
test('connectors API', async ({ request }) => {
const res = await request.get(`${BASE}/api/connectors`);
expect(res.ok()).toBeTruthy();
});
test('backup metadata API', async ({ request }) => {
const res = await request.get(`${BASE}/api/backup/metadata`);
// May be 200 or 404 depending on backup existence
expect([200, 404]).toContain(res.status());
});
});
// ── 2. Desktop Shell ──────────────────────────────────────────────────
test.describe('2. Desktop Shell', () => {
test('status bar renders with clock', async ({ page }) => {
await gotoDesktop(page);
const text = await getVisibleText(page);
expect(text).toContain('Waggle AI');
});
test('sidebar renders in power tier', async ({ page }) => {
await gotoDesktop(page);
for (const label of ['Chat', 'Memory', 'Agents', 'Library']) {
const btn = page.locator('[role="navigation"]').locator('button', { hasText: label });
await expect(btn).toBeVisible({ timeout: 5000 });
}
});
test('Ctrl+K opens global search', async ({ page }) => {
await gotoDesktop(page);
await page.evaluate(() => {
window.dispatchEvent(new KeyboardEvent('keydown', {
key: 'k', code: 'KeyK', ctrlKey: true, bubbles: true,
}));
});
await page.waitForTimeout(500);
const searchInput = page.locator('input[placeholder*="Search"]');
await expect(searchInput).toBeVisible({ timeout: 3000 });
});
});
// ── 3. Every App Opens ───────────────────────────────────────────────
const DIRECT_APPS = [
{ label: 'Chat', expect: /persona|message|waggle/i },
{ label: 'Room', expect: /room|agent|specialist|no.*running|empty/i },
{ label: 'Agents', expect: /agent|task|persona|group/i },
{ label: 'Files', expect: /file|folder|workspace|document/i },
{ label: 'Approvals', expect: /approval|pending|no.*pending|history|upgrade|team/i },
];
// Phase 4B sweep: zone names/membership match dock-tiers.ts (System zone, not
// "Ops"; Skills Hub lives under Intelligence; Governance under Team; the
// Extend zone now carries Connector Hub + MCP Hub + Marketplace; Backup left
// the dock — it lives in Settings → Backup, P23).
const ZONE_APPS = [
{ label: 'Mission Control', zone: 'System', expect: /cockpit|health|cost|command/i },
{ label: 'Timeline', zone: 'System', expect: /timeline|activity|no.*activity|last/i },
{ label: 'Usage & Cost', zone: 'System', expect: /usage|telemetry|token|cost/i },
{ label: 'Events & Logs', zone: 'System', expect: /event|log|step|filter/i },
{ label: 'Team Governance', zone: 'Team', expect: /governance|role|team|permission/i },
{ label: 'Skills Hub', zone: 'Intelligence', expect: /skill|installed|marketplace|starter|build/i },
{ label: 'Connector Hub', zone: 'Extend', expect: /connector|connect|service|integration/i },
{ label: 'MCP Hub', zone: 'Extend', expect: /mcp|installed|catalog|server/i },
{ label: 'Marketplace', zone: 'Extend', expect: /marketplace|browse|extension|install/i },
];
test.describe('3. Direct Dock Apps', () => {
for (const app of DIRECT_APPS) {
test(`${app.label} opens and renders content`, async ({ page }) => {
await gotoDesktop(page);
await openCurrentApp(page, app.label);
const text = await getVisibleText(page);
expect(text).toMatch(app.expect);
});
}
});
test.describe('4. Zone Apps (Ops + Extend)', () => {
for (const app of ZONE_APPS) {
test(`${app.label} opens from ${app.zone} zone`, async ({ page }) => {
await gotoDesktop(page);
await openCurrentApp(page, app.label);
const text = await getVisibleText(page);
expect(text).toMatch(app.expect);
});
}
});
// ── 5. Standalone Apps ────────────────────────────────────────────────
test.describe('5. Standalone Apps', () => {
test('Settings opens', async ({ page }) => {
await gotoDesktop(page);
await openCurrentApp(page, 'Settings');
const text = await getVisibleText(page);
expect(text).toMatch(/setting|general|model|billing/i);
});
test('Vault opens', async ({ page }) => {
await gotoDesktop(page);
await openCurrentApp(page, 'Vault');
const text = await getVisibleText(page);
expect(text).toMatch(/vault|key|api|secret|provider/i);
});
test('Home (Dashboard) opens', async ({ page }) => {
await gotoDesktop(page);
await openCurrentApp(page, 'Home');
const text = await getVisibleText(page);
expect(text).toMatch(/workspace|welcome|dashboard|create/i);
});
});
// ── 6. User Journey: Workspace → Chat → Memory ───────────────────────
test.describe('6. User Journey', () => {
test('can open chat and see persona + model in header', async ({ page }) => {
await gotoDesktop(page);
await openCurrentApp(page, 'Chat');
await page.waitForTimeout(1500);
const text = await getVisibleText(page);
// Should see persona selector and model name
expect(text).toMatch(/persona|sonnet|claude|ollama|model|message waggle/i);
});
test('can open memory and see frames or empty state', async ({ page }) => {
await gotoDesktop(page);
// Memory might be in a zone or direct — try both
const memBtn = page.locator('button[aria-label="Memory"]');
if (await memBtn.isVisible({ timeout: 1000 }).catch(() => false)) {
await memBtn.click();
} else {
// Open via Ctrl+Shift+5 (shortcut)
await page.evaluate(() => {
window.dispatchEvent(new KeyboardEvent('keydown', {
key: '5', code: 'Digit5', ctrlKey: true, shiftKey: true, bubbles: true,
}));
});
}
await page.waitForTimeout(1000);
const text = await getVisibleText(page);
expect(text).toMatch(/memory|frame|knowledge|harvest|search/i);
});
});
// ── 7. No Console Errors ──────────────────────────────────────────────
test.describe('7. Stability', () => {
test('no critical console errors on load', async ({ page }) => {
const errors: string[] = [];
page.on('console', msg => {
if (msg.type() === 'error') errors.push(msg.text());
});
await gotoDesktop(page);
await page.waitForTimeout(3000);
const critical = errors.filter(e =>
!e.includes('Failed to fetch') && !e.includes('net::ERR') &&
!e.includes('favicon') && !e.includes('401') && !e.includes('404') &&
!e.includes('sync') && !e.includes('WebSocket') && !e.includes('fetch') &&
!e.includes('model') && !e.includes('chunk')
);
expect(critical).toHaveLength(0);
expect(errors.filter(e => /clerk|content security policy|csp/i.test(e))).toHaveLength(0);
});
test('no uncaught exceptions after opening 3 apps', async ({ page }) => {
const errors: string[] = [];
page.on('pageerror', err => errors.push(err.message));
await gotoDesktop(page);
await openCurrentApp(page, 'Chat');
await openCurrentApp(page, 'Room');
await openCurrentApp(page, 'Files');
await page.waitForTimeout(1000);
expect(errors).toHaveLength(0);
});
});