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,632 @@
/**
* Full E2E Wiring Audit — tests every major UI flow against the live backend.
*
* FIXED: Updated from old apps/web desktop OS (port 8080, dock/windows paradigm)
* to current Tauri app shell (port 3333, sidebar navigation paradigm).
*
* Run: node node_modules\playwright\cli.js test tests/e2e/full-wiring-audit.spec.ts --reporter=list
*/
import { test, expect, type Page } from '@playwright/test';
const API = process.env.WAGGLE_E2E_BASE_URL ?? 'http://127.0.0.1:3333';
// App is served by the same server; Playwright can override the URL for isolated runs.
// ── Helpers ──────────────────────────────────────────────────────────────────
async function skipOnboarding(page: Page) {
await page.evaluate(() => {
localStorage.setItem('waggle:onboarding', JSON.stringify({ completed: true, step: 7 }));
localStorage.setItem('waggle:first-run', 'done');
localStorage.setItem('waggle-booted', 'true');
});
}
async function waitForApp(page: Page) {
await page.waitForSelector(
'.waggle-app-shell, .waggle-sidebar, [role="navigation"], [class*="onboarding"]',
{ timeout: 15_000 },
).catch(() => {});
await page.waitForTimeout(600);
}
async function navigateSidebar(page: Page, label: string) {
const nav = page.locator('[role="navigation"]');
const sidebarSelectors: Record<string, string[]> = {
Chat: ['[data-testid="nav-chat"]', 'button[aria-label="Chat"]'],
Memory: ['[data-testid="nav-memory"]', 'button[aria-label="Memory"]'],
Settings: ['[data-testid="sidebar-user"]', 'button[aria-label="Account and settings"]'],
};
for (const selector of sidebarSelectors[label] ?? []) {
const btn = nav.locator(selector).first();
if (await btn.isVisible().catch(() => false)) {
await btn.click();
await page.waitForTimeout(500);
return true;
}
}
const textButton = nav.locator('button', { hasText: label }).first();
if (await textButton.isVisible().catch(() => false)) {
await textButton.click();
await page.waitForTimeout(500);
return true;
}
const routeByLabel: Record<string, string> = {
'Skills Hub': '/marketplace',
Events: '/settings/events',
Cockpit: '/settings/mission-control',
'Mission Control': '/settings/mission-control',
Settings: '/settings',
};
const route = routeByLabel[label];
if (route) {
await page.goto(`${route}?skipOnboarding=true&skipBoot=true`);
await waitForApp(page);
return true;
}
throw new Error(`No current navigation target configured for "${label}"`);
}
function collectErrors(page: Page): string[] {
const errors: string[] = [];
page.on('console', msg => {
if (msg.type() === 'error') errors.push(msg.text());
});
page.on('pageerror', err => errors.push(err.message));
return errors;
}
// ══════════════════════════════════════════════════════════════════════════════
// SECTION 1 — Backend API Health (all formerly passing — keep identical)
// ══════════════════════════════════════════════════════════════════════════════
test.describe('Backend API Endpoints', () => {
test('GET /health returns ok', async ({ request }) => {
const res = await request.get(`${API}/health`);
expect(res.ok()).toBeTruthy();
const data = await res.json();
expect(data.status).toBeDefined();
expect(data.llm).toBeDefined();
});
test('GET /api/workspaces returns array', async ({ request }) => {
const res = await request.get(`${API}/api/workspaces`);
expect(res.ok()).toBeTruthy();
expect(Array.isArray(await res.json())).toBeTruthy();
});
test('GET /api/events returns object with events array', async ({ request }) => {
const res = await request.get(`${API}/api/events`);
expect(res.ok()).toBeTruthy();
const data = await res.json();
expect(data.events).toBeDefined();
expect(Array.isArray(data.events)).toBeTruthy();
});
test('GET /api/skills returns object with skills array', async ({ request }) => {
const res = await request.get(`${API}/api/skills`);
expect(res.ok()).toBeTruthy();
const data = await res.json();
expect(Array.isArray(data.skills)).toBeTruthy();
});
test('GET /api/memory/frames returns object with results array', async ({ request }) => {
const res = await request.get(`${API}/api/memory/frames?limit=5&workspace=default`);
expect(res.ok()).toBeTruthy();
const data = await res.json();
expect(Array.isArray(data.results)).toBeTruthy();
});
test('GET /api/connectors returns connectors', async ({ request }) => {
const res = await request.get(`${API}/api/connectors`);
expect(res.ok()).toBeTruthy();
const data = await res.json();
expect(data.connectors).toBeDefined();
});
test('GET /api/personas returns personas', async ({ request }) => {
const res = await request.get(`${API}/api/personas`);
expect(res.ok()).toBeTruthy();
const data = await res.json();
expect(data.personas).toBeDefined();
});
test('GET /api/fleet returns sessions', async ({ request }) => {
const res = await request.get(`${API}/api/fleet`);
expect(res.ok()).toBeTruthy();
const data = await res.json();
expect(data.sessions).toBeDefined();
});
test('GET /api/cron returns schedules', async ({ request }) => {
const res = await request.get(`${API}/api/cron`);
expect(res.ok()).toBeTruthy();
const data = await res.json();
expect(data.schedules).toBeDefined();
});
test('GET /api/marketplace/packs returns packs', async ({ request }) => {
const res = await request.get(`${API}/api/marketplace/packs`);
expect(res.ok()).toBeTruthy();
const data = await res.json();
expect(data.packs).toBeDefined();
});
test('GET /api/settings returns settings object', async ({ request }) => {
const res = await request.get(`${API}/api/settings`);
expect([200, 404]).toContain(res.status()); // May or may not exist
});
test('POST /api/workspaces creates workspace', async ({ request }) => {
const name = `E2E-Audit-${Date.now()}`;
const res = await request.post(`${API}/api/workspaces`, {
data: { name, group: 'Workspaces', description: 'Wiring audit test' },
});
expect([200, 201, 403, 409]).toContain(res.status());
if (res.ok()) {
const ws = await res.json();
const wsData = ws.workspace ?? ws.data ?? ws;
const id = wsData.id ?? wsData.name ?? wsData;
expect(id).toBeDefined();
}
});
test('GET /api/vault returns vault data', async ({ request }) => {
const res = await request.get(`${API}/api/vault`);
expect([200, 404]).toContain(res.status());
});
test('GET /api/costs returns cost data or 403', async ({ request }) => {
const res = await request.get(`${API}/api/costs`);
expect([200, 403]).toContain(res.status());
});
test('GET /api/tier returns tier info', async ({ request }) => {
const res = await request.get(`${API}/api/tier`);
expect(res.ok()).toBeTruthy();
const data = await res.json();
expect(['TRIAL', 'FREE', 'TEAMS', 'ENTERPRISE']).toContain(data.tier);
});
test('GET /api/hooks returns rules array', async ({ request }) => {
const res = await request.get(`${API}/api/hooks`);
expect(res.ok()).toBeTruthy();
const data = await res.json();
expect(Array.isArray(data.rules)).toBeTruthy();
});
test('GET /api/cloud-sync returns sync status', async ({ request }) => {
const res = await request.get(`${API}/api/cloud-sync`);
expect(res.ok()).toBeTruthy();
const data = await res.json();
expect(typeof data.available).toBe('boolean');
});
test('GET /api/marketplace/search returns packages', async ({ request }) => {
const res = await request.get(`${API}/api/marketplace/search?query=pdf&limit=3`);
expect(res.ok()).toBeTruthy();
const data = await res.json();
expect(Array.isArray(data.packages)).toBeTruthy();
});
});
// ══════════════════════════════════════════════════════════════════════════════
// SECTION 2 — App Shell Load (FIXED: port 3333, Tauri app shell selectors)
// ══════════════════════════════════════════════════════════════════════════════
test.describe('Frontend App Load', () => {
test('app loads at port 3333 without crash', async ({ page }) => {
const errors = collectErrors(page);
await page.goto('/');
await skipOnboarding(page);
await page.reload();
await waitForApp(page);
const fatalErrors = errors.filter(e =>
!e.includes('favicon') && !e.includes('net::ERR') &&
!e.includes('ResizeObserver') && !e.includes('404') &&
(e.includes('is not a function') || e.includes('Cannot read') || e.includes('Uncaught'))
);
expect(fatalErrors).toHaveLength(0);
});
test('onboarding or main shell renders — no blank screen', async ({ page }) => {
await page.goto('/');
await page.waitForTimeout(1200);
const body = await page.textContent('body') ?? '';
expect(body.trim().length).toBeGreaterThan(20);
});
test('app shell renders after onboarding skip', async ({ page }) => {
await page.goto('/');
await skipOnboarding(page);
await page.reload();
await waitForApp(page);
const html = await page.content();
expect(html.length).toBeGreaterThan(1000);
});
test('no 404 errors on static assets', async ({ page }) => {
const failed: string[] = [];
page.on('response', res => {
if (res.status() === 404 && !res.url().includes('/api/')) failed.push(res.url());
});
await page.goto('/');
await waitForApp(page);
expect(failed).toHaveLength(0);
});
});
// ══════════════════════════════════════════════════════════════════════════════
// SECTION 3 — Sidebar Navigation (FIXED: sidebar paradigm, not dock/windows)
// ══════════════════════════════════════════════════════════════════════════════
test.describe('Sidebar Navigation', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
await skipOnboarding(page);
await page.reload();
await waitForApp(page);
});
test('sidebar navigation is visible with buttons', async ({ page }) => {
const nav = page.locator('[role="navigation"]');
const visible = await nav.isVisible().catch(() => false);
if (visible) {
const buttons = nav.locator('button');
expect(await buttons.count()).toBeGreaterThanOrEqual(5);
} else {
// App may be in onboarding — just verify it loaded
const body = await page.textContent('body') ?? '';
expect(body.length).toBeGreaterThan(50);
}
});
test('clicking Settings navigates to settings panel', async ({ page }) => {
const navigated = await navigateSidebar(page, 'Settings');
if (navigated) {
await page.waitForTimeout(500);
const body = await page.textContent('body') ?? '';
expect(body.includes('General') || body.includes('Models') || body.includes('Settings')).toBe(true);
}
});
test('clicking Capabilities navigates to marketplace', async ({ page }) => {
const navigated = await navigateSidebar(page, 'Skills Hub');
if (navigated) {
await page.waitForTimeout(500);
const body = await page.textContent('body') ?? '';
expect(body.length).toBeGreaterThan(50);
}
});
test('clicking Memory navigates to memory view', async ({ page }) => {
const navigated = await navigateSidebar(page, 'Memory');
if (navigated) {
await page.waitForTimeout(500);
const body = await page.textContent('body') ?? '';
expect(body.length).toBeGreaterThan(50);
}
});
test('clicking Chat navigates to chat view', async ({ page }) => {
const navigated = await navigateSidebar(page, 'Chat');
if (navigated) {
await page.waitForTimeout(500);
const body = await page.textContent('body') ?? '';
expect(body.length).toBeGreaterThan(50);
}
});
test('clicking Events navigates to events view', async ({ page }) => {
const navigated = await navigateSidebar(page, 'Events');
if (navigated) {
await page.waitForTimeout(500);
const body = await page.textContent('body') ?? '';
expect(body.length).toBeGreaterThan(50);
}
});
test('clicking Cockpit navigates to cockpit view', async ({ page }) => {
const navigated = await navigateSidebar(page, 'Cockpit');
if (navigated) {
await page.waitForTimeout(500);
const body = await page.textContent('body') ?? '';
expect(body.length).toBeGreaterThan(50);
}
});
});
// ══════════════════════════════════════════════════════════════════════════════
// SECTION 4 — Workspace Wiring
// ══════════════════════════════════════════════════════════════════════════════
test.describe('Workspace Wiring', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
await skipOnboarding(page);
await page.reload();
await waitForApp(page);
});
test('workspaces load from API', async ({ page }) => {
const res = await page.request.get(`${API}/api/workspaces`);
expect(res.ok()).toBeTruthy();
const data = await res.json();
expect(Array.isArray(data)).toBeTruthy();
expect(data.length).toBeGreaterThanOrEqual(1);
});
test('workspace list is not empty', async ({ page }) => {
const res = await page.request.get(`${API}/api/workspaces`);
expect(res.ok()).toBeTruthy();
const workspaces = await res.json();
expect(Array.isArray(workspaces)).toBeTruthy();
expect(workspaces.length).toBeGreaterThanOrEqual(1);
});
});
// ══════════════════════════════════════════════════════════════════════════════
// SECTION 5 — Chat Wiring
// ══════════════════════════════════════════════════════════════════════════════
test.describe('Chat Wiring', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
await skipOnboarding(page);
await page.reload();
await waitForApp(page);
});
test('chat view opens without JS crash', async ({ page }) => {
const errors = collectErrors(page);
await navigateSidebar(page, 'Chat');
await page.waitForTimeout(800);
const fatal = errors.filter(e =>
e.includes('is not a function') || e.includes('Cannot read properties of') || e.includes('is not defined')
);
expect(fatal).toEqual([]);
});
test('chat view renders message input or workspace selector', async ({ page }) => {
await navigateSidebar(page, 'Chat');
await page.waitForTimeout(600);
const hasTextarea = await page.locator('textarea').isVisible().catch(() => false);
const hasInput = await page.locator('input[type="text"]').isVisible().catch(() => false);
const body = await page.textContent('body') ?? '';
const hasContent = body.includes('Ask') || body.includes('message') || body.includes('workspace') || body.includes('Chat');
expect(hasTextarea || hasInput || hasContent).toBe(true);
});
test('chat textarea accepts text input', async ({ page }) => {
await navigateSidebar(page, 'Chat');
await page.waitForTimeout(600);
const textarea = page.locator('textarea').first();
if (await textarea.isVisible().catch(() => false)) {
await textarea.fill('Hello E2E test');
expect(await textarea.inputValue()).toContain('Hello');
}
});
});
// ══════════════════════════════════════════════════════════════════════════════
// SECTION 6 — Memory Wiring
// ══════════════════════════════════════════════════════════════════════════════
test.describe('Memory Wiring', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
await skipOnboarding(page);
await page.reload();
await waitForApp(page);
});
test('memory view opens without JS crash', async ({ page }) => {
const errors = collectErrors(page);
await navigateSidebar(page, 'Memory');
await page.waitForTimeout(800);
const fatal = errors.filter(e =>
e.includes('is not a function') || e.includes('Cannot read properties of')
);
expect(fatal).toEqual([]);
});
test('memory API responds when memory view is open', async ({ page }) => {
await navigateSidebar(page, 'Memory');
const res = await page.request.get(`${API}/api/memory/frames?workspace=default&limit=5`);
expect(res.ok()).toBeTruthy();
const data = await res.json();
expect(Array.isArray(data.results)).toBeTruthy();
});
});
// ══════════════════════════════════════════════════════════════════════════════
// SECTION 7 — Settings Wiring
// ══════════════════════════════════════════════════════════════════════════════
test.describe('Settings Wiring', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
await skipOnboarding(page);
await page.reload();
await waitForApp(page);
});
test('settings view opens without JS crash', async ({ page }) => {
const errors = collectErrors(page);
await navigateSidebar(page, 'Settings');
await page.waitForTimeout(800);
const fatal = errors.filter(e =>
e.includes('is not a function') || e.includes('Cannot read properties of')
);
expect(fatal).toEqual([]);
});
test('settings tabs are visible after navigation', async ({ page }) => {
await navigateSidebar(page, 'Settings');
await page.waitForTimeout(500);
const body = await page.textContent('body') ?? '';
// At least one settings tab label must appear
const hasTab = body.includes('General') || body.includes('Models') || body.includes('Keys') || body.includes('Advanced');
expect(hasTab).toBe(true);
});
});
// ══════════════════════════════════════════════════════════════════════════════
// SECTION 8 — Capabilities / Marketplace Wiring
// ══════════════════════════════════════════════════════════════════════════════
test.describe('Capabilities Wiring', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
await skipOnboarding(page);
await page.reload();
await waitForApp(page);
});
test('capabilities view opens without JS crash', async ({ page }) => {
const errors = collectErrors(page);
await navigateSidebar(page, 'Skills Hub');
await page.waitForTimeout(800);
const fatal = errors.filter(e =>
e.includes('is not a function') || e.includes('Cannot read properties of')
);
expect(fatal).toEqual([]);
});
test('marketplace search API is accessible from capabilities view', async ({ page }) => {
await navigateSidebar(page, 'Skills Hub');
const res = await page.request.get(`${API}/api/marketplace/search?query=&limit=5`);
expect(res.ok()).toBeTruthy();
const data = await res.json();
expect(Array.isArray(data.packages)).toBeTruthy();
expect(data.packages.length).toBeGreaterThan(0);
});
});
// ══════════════════════════════════════════════════════════════════════════════
// SECTION 9 — Events + Cockpit + Mission Control Wiring
// ══════════════════════════════════════════════════════════════════════════════
test.describe('Events / Cockpit / Mission Control Wiring', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/');
await skipOnboarding(page);
await page.reload();
await waitForApp(page);
});
test('events view opens without crash', async ({ page }) => {
const errors = collectErrors(page);
await navigateSidebar(page, 'Events');
await page.waitForTimeout(600);
const fatal = errors.filter(e => e.includes('is not a function') || e.includes('Cannot read'));
expect(fatal).toEqual([]);
});
test('cockpit view opens without crash', async ({ page }) => {
const errors = collectErrors(page);
await navigateSidebar(page, 'Cockpit');
await page.waitForTimeout(600);
const fatal = errors.filter(e => e.includes('is not a function') || e.includes('Cannot read'));
expect(fatal).toEqual([]);
});
test('mission control view opens without crash', async ({ page }) => {
const errors = collectErrors(page);
await navigateSidebar(page, 'Mission Control');
await page.waitForTimeout(600);
const fatal = errors.filter(e => e.includes('is not a function') || e.includes('Cannot read'));
expect(fatal).toEqual([]);
});
});
// ══════════════════════════════════════════════════════════════════════════════
// SECTION 10 — Console Error Audit (traverse all views, collect errors)
// ══════════════════════════════════════════════════════════════════════════════
test.describe('Full Console Error Audit', () => {
test('traverse all sidebar views — zero critical JS errors', async ({ page }) => {
const criticalErrors: { view: string; error: string }[] = [];
page.on('console', msg => {
if (msg.type() === 'error') {
const text = msg.text();
if (
text.includes('is not a function') ||
text.includes('Cannot read properties of') ||
text.includes('is not defined') ||
text.includes('Uncaught')
) {
criticalErrors.push({ view: 'unknown', error: text });
}
}
});
page.on('pageerror', err => {
criticalErrors.push({ view: 'pageerror', error: err.message });
});
await page.goto('/');
await skipOnboarding(page);
await page.reload();
await waitForApp(page);
const views = ['Chat', 'Memory', 'Events', 'Skills Hub', 'Cockpit', 'Mission Control', 'Settings'];
for (const view of views) {
const nav = page.locator('[role="navigation"]');
const btn = nav.locator('button', { hasText: view });
if (await btn.isVisible().catch(() => false)) {
const before = criticalErrors.length;
await btn.click();
await page.waitForTimeout(600);
// Tag any new errors with the view name
if (criticalErrors.length > before) {
criticalErrors.slice(before).forEach(e => e.view = view);
}
}
}
if (criticalErrors.length > 0) {
const report = criticalErrors.map(e => `[${e.view}] ${e.error}`).join('\n');
expect(criticalErrors.length, `Critical JS errors found:\n${report}`).toBe(0);
}
});
test('no 404 on API calls made during app lifecycle', async ({ page }) => {
const apiErrors: string[] = [];
page.on('response', res => {
if (res.url().includes('/api/') && res.status() === 404) {
apiErrors.push(`404: ${res.url()}`);
}
});
await page.goto('/');
await skipOnboarding(page);
await page.reload();
await waitForApp(page);
// Navigate through key views to trigger their API calls
for (const view of ['Chat', 'Settings', 'Skills Hub']) {
const btn = page.locator('[role="navigation"] button', { hasText: view });
if (await btn.isVisible().catch(() => false)) {
await btn.click();
await page.waitForTimeout(500);
}
}
// Filter out expected 404s (endpoints that are legitimately optional)
const unexpectedErrors = apiErrors.filter(url =>
!url.includes('favicon') && !url.includes('notifications/history')
);
if (unexpectedErrors.length > 0) {
console.log('API 404s detected:', unexpectedErrors);
}
// Warn but don't fail — some 404s may be expected for unimplemented optional endpoints
expect(unexpectedErrors.length).toBeLessThan(5);
});
});