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,190 @@
/**
* P6 regression — Room canvas visualises multiple parallel sub-agents
* correctly, without cross-contamination.
*
* Mocks the adapter's /health handshake and replaces window.EventSource
* with a stub that emits one `subagent_status` event carrying two
* simultaneously-running agents. Then asserts the Room renders two
* distinct tiles with the right role badges + running status.
*
* Pairs with `apps/web/src/lib/room-state-reducer.test.ts` (12 unit
* tests covering the reducer directly). This E2E adds the render-path
* guarantee on top.
*
* Run: npx playwright test tests/e2e/room-parallel-agents.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';
// Canonical test payload — two agents in one status event, different roles.
const AGENT_ALPHA_ID = 'agent-alpha-p6';
const AGENT_BETA_ID = 'agent-beta-p6';
const MOCK_ROSTER = {
type: 'subagent_status',
workspaceId: 'default',
agents: [
{
id: AGENT_ALPHA_ID,
name: 'Alpha Researcher',
role: 'researcher',
status: 'running',
task: 'Scout the competitive landscape for bee pollinator tech',
toolsUsed: ['web_search'],
startedAt: Date.now() - 30_000,
},
{
id: AGENT_BETA_ID,
name: 'Beta Coder',
role: 'coder',
status: 'running',
task: 'Draft the TypeScript API surface for the honey-ledger module',
toolsUsed: ['read_file'],
startedAt: Date.now() - 15_000,
},
],
timestamp: new Date().toISOString(),
};
async function installSseMock(page: Page) {
// Adapter gates subscribe() on `_connected`, which flips to true only
// after a successful /health call. Mock it.
await page.route('**/health', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ wsToken: 'p6-ws-token', authToken: 'p6-auth-token' }),
});
});
// Replace EventSource with a stub that dispatches our mock roster on
// the `subagent_status` named event type. Installed via addInitScript
// so it lands before the adapter ever calls `new EventSource(...)`.
await page.addInitScript((roster) => {
type Listener = (e: MessageEvent) => void;
interface MockWindow {
__p6MockSources?: unknown[];
EventSource: unknown;
localStorage: Storage;
}
const win = window as unknown as MockWindow;
class MockEventSource {
url: string;
readyState = 1; // OPEN
onerror: ((e: Event) => void) | null = null;
onmessage: ((e: MessageEvent) => void) | null = null;
onopen: ((e: Event) => void) | null = null;
private listeners: Record<string, Listener[]> = {};
constructor(url: string) {
this.url = url;
win.__p6MockSources = (win.__p6MockSources || []);
win.__p6MockSources.push(this);
// Give React a moment to mount and the adapter to call addEventListener.
setTimeout(() => {
const ls = this.listeners['subagent_status'] ?? [];
const evt = new MessageEvent('subagent_status', {
data: JSON.stringify(roster),
});
for (const l of ls) l(evt);
}, 300);
}
addEventListener(type: string, listener: Listener) {
(this.listeners[type] ??= []).push(listener);
}
removeEventListener(type: string, listener: Listener) {
this.listeners[type] = (this.listeners[type] ?? []).filter((l) => l !== listener);
}
close() { this.readyState = 2; }
// Test helper — re-emit the same roster, useful for cross-contamination check.
__reemit() {
const ls = this.listeners['subagent_status'] ?? [];
const evt = new MessageEvent('subagent_status', { data: JSON.stringify(roster) });
for (const l of ls) l(evt);
}
}
win.EventSource = MockEventSource;
// Skip boot screen so we get to Desktop immediately.
try {
window.localStorage.setItem('waggle-booted', 'true');
} catch { /* storage might be unavailable */ }
}, MOCK_ROSTER);
}
async function dismissOverlay(page: Page) {
for (let i = 0; i < 3; i++) {
const overlay = page.locator('.fixed.backdrop-blur-sm').first();
if (!(await overlay.isVisible({ timeout: 500 }).catch(() => false))) break;
const startBtn = page.locator('button:has-text("Start Working")').first();
if (await startBtn.isVisible({ timeout: 400 }).catch(() => false)) {
await startBtn.click({ force: true });
} else {
await page.mouse.click(5, 5);
}
await page.waitForTimeout(400);
}
}
function routeWithSkip(route: string) {
const sep = route.includes('?') ? '&' : '?';
return `${BASE}${route}${sep}skipOnboarding=true&skipBoot=true&tier=power&skipBriefing=true`;
}
async function openRoom(page: Page) {
await page.goto(routeWithSkip('/room'), { waitUntil: 'domcontentloaded' });
await expect(page.locator('[data-testid="room-root"]')).toBeVisible({ timeout: 10_000 });
}
test.describe('Room — parallel agent visualization (P6)', () => {
test('renders two simultaneous agents with distinct tiles', async ({ page }) => {
await installSseMock(page);
await openRoom(page);
await dismissOverlay(page);
// Both tiles present, keyed by agent id.
const alpha = page.locator(`[data-testid="room-agent-tile"][data-agent-id="${AGENT_ALPHA_ID}"]`);
const beta = page.locator(`[data-testid="room-agent-tile"][data-agent-id="${AGENT_BETA_ID}"]`);
await expect(alpha).toBeVisible({ timeout: 5000 });
await expect(beta).toBeVisible({ timeout: 5000 });
// Live count chip reflects both.
await expect(page.locator('[data-testid="room-live-count"]')).toContainText('2 live');
// Total tile count = exactly 2 (no duplicates, no phantom tiles).
const allTiles = page.locator('[data-testid="room-agent-tile"]');
await expect(allTiles).toHaveCount(2);
});
test('role badges do not cross-contaminate between simultaneous agents', async ({ page }) => {
await installSseMock(page);
await openRoom(page);
await dismissOverlay(page);
const alpha = page.locator(`[data-testid="room-agent-tile"][data-agent-id="${AGENT_ALPHA_ID}"]`);
const beta = page.locator(`[data-testid="room-agent-tile"][data-agent-id="${AGENT_BETA_ID}"]`);
await expect(alpha).toBeVisible({ timeout: 5000 });
await expect(beta).toBeVisible();
// Each tile carries its own role attribute — verify independence.
await expect(alpha).toHaveAttribute('data-agent-role', 'researcher');
await expect(beta).toHaveAttribute('data-agent-role', 'coder');
// Both are running concurrently.
await expect(alpha).toHaveAttribute('data-agent-status', 'running');
await expect(beta).toHaveAttribute('data-agent-status', 'running');
// Task bodies are distinct — one tile's task text must not leak into the other.
await expect(alpha).toContainText('Scout the competitive landscape');
await expect(beta).toContainText('honey-ledger module');
await expect(alpha).not.toContainText('honey-ledger');
await expect(beta).not.toContainText('Scout the competitive');
});
});