feat: migrate from wsUrl to endpoints + add relay transport and pairing

This commit is contained in:
Mohamed Boudra
2026-01-21 14:55:23 +07:00
parent ae9a98223f
commit ddbcc272f1
29 changed files with 1729 additions and 280 deletions
+195 -13
View File
@@ -1,7 +1,142 @@
import { expect, type Page } from '@playwright/test';
function escapeRegex(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
function getE2EDaemonPort(): string {
const port = process.env.E2E_DAEMON_PORT;
if (!port) {
throw new Error('E2E_DAEMON_PORT is not set (expected from Playwright globalSetup).');
}
if (port === '6767') {
throw new Error('E2E_DAEMON_PORT is 6767. Refusing to run e2e against the default local daemon.');
}
return port;
}
async function ensureE2EStorageSeeded(page: Page): Promise<void> {
const port = getE2EDaemonPort();
const expectedEndpoint = `127.0.0.1:${port}`;
const needsReset = await page.evaluate(({ expectedEndpoint }) => {
const raw = localStorage.getItem('@paseo:daemon-registry');
if (!raw) return true;
try {
const parsed = JSON.parse(raw);
if (!Array.isArray(parsed) || parsed.length !== 1) return true;
const entry = parsed[0] as any;
const endpoints = entry?.endpoints;
if (!Array.isArray(endpoints)) return true;
if (endpoints.some((e: unknown) => typeof e === 'string' && /:6767\b/.test(e))) return true;
return !endpoints.some((e: unknown) => e === expectedEndpoint);
} catch {
return true;
}
}, { expectedEndpoint });
if (!needsReset) {
return;
}
const nowIso = new Date().toISOString();
await page.evaluate(
({ expectedEndpoint, nowIso }) => {
localStorage.setItem('@paseo:e2e', '1');
localStorage.setItem(
'@paseo:daemon-registry',
JSON.stringify([
{
id: 'e2e-test-daemon',
label: 'localhost',
endpoints: [expectedEndpoint],
relay: null,
metadata: null,
createdAt: nowIso,
updatedAt: nowIso,
},
])
);
localStorage.setItem(
'@paseo:create-agent-preferences',
JSON.stringify({
serverId: 'e2e-test-daemon',
provider: 'claude',
providerPreferences: {
claude: { model: 'haiku' },
codex: { model: 'gpt-5.1-codex-mini' },
},
})
);
localStorage.removeItem('@paseo:settings');
},
{ expectedEndpoint, nowIso }
);
await page.reload();
}
async function assertE2EUsesSeededTestDaemon(page: Page): Promise<void> {
const port = getE2EDaemonPort();
const expectedEndpoint = `127.0.0.1:${port}`;
const snapshot = await page.evaluate(() => {
const registryRaw = localStorage.getItem('@paseo:daemon-registry');
const prefsRaw = localStorage.getItem('@paseo:create-agent-preferences');
return { registryRaw, prefsRaw };
});
if (!snapshot.registryRaw) {
throw new Error('E2E expected @paseo:daemon-registry to be set before app load.');
}
let registry: any;
try {
registry = JSON.parse(snapshot.registryRaw);
} catch {
throw new Error('E2E expected @paseo:daemon-registry to be valid JSON.');
}
if (!Array.isArray(registry) || registry.length !== 1) {
throw new Error(
`E2E expected @paseo:daemon-registry to contain exactly 1 daemon (got ${Array.isArray(registry) ? registry.length : 'non-array'}).`
);
}
const daemon = registry[0];
if (typeof daemon?.id !== 'string' || daemon.id.length === 0) {
throw new Error(`E2E expected seeded daemon to have a string id (got ${String(daemon?.id)}).`);
}
const endpoints: unknown = daemon?.endpoints;
if (!Array.isArray(endpoints) || !endpoints.some((e) => e === expectedEndpoint)) {
throw new Error(
`E2E expected seeded daemon endpoints to include ${expectedEndpoint} (got ${JSON.stringify(endpoints)}).`
);
}
if (Array.isArray(endpoints) && endpoints.some((e) => typeof e === 'string' && /:6767\b/.test(e))) {
throw new Error(`E2E detected a daemon endpoint pointing at :6767 (${JSON.stringify(endpoints)}).`);
}
if (!snapshot.prefsRaw) {
throw new Error('E2E expected @paseo:create-agent-preferences to be set before app load.');
}
try {
const prefs = JSON.parse(snapshot.prefsRaw) as any;
if (prefs?.serverId !== daemon.id) {
throw new Error(
`E2E expected create-agent-preferences.serverId to match seeded daemon id (${daemon.id}) (got ${String(prefs?.serverId)}).`
);
}
} catch (error) {
if (error instanceof Error) throw error;
throw new Error('E2E expected @paseo:create-agent-preferences to be valid JSON.');
}
}
export const gotoHome = async (page: Page) => {
await page.goto('/');
await ensureE2EStorageSeeded(page);
await expect(page.getByText('New Agent', { exact: true }).first()).toBeVisible();
await expect(page.getByRole('textbox', { name: 'Message agent...' })).toBeVisible();
};
@@ -30,6 +165,43 @@ export const setWorkingDirectory = async (page: Page, directory: string) => {
};
export const ensureHostSelected = async (page: Page) => {
await ensureE2EStorageSeeded(page);
// Absolute verification that we're using the per-run e2e daemon (never :6767).
// Also self-heal a rare case where app code rewrites daemon IDs after boot, by
// realigning create-agent-preferences.serverId to the sole seeded daemon.
try {
await assertE2EUsesSeededTestDaemon(page);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
if (!/create-agent-preferences\.serverId/i.test(message)) {
throw error;
}
const fix = await page.evaluate(() => {
const registryRaw = localStorage.getItem('@paseo:daemon-registry');
const prefsRaw = localStorage.getItem('@paseo:create-agent-preferences');
if (!registryRaw || !prefsRaw) return { ok: false, reason: 'missing storage' } as const;
const registry = JSON.parse(registryRaw) as any[];
const prefs = JSON.parse(prefsRaw) as any;
if (!Array.isArray(registry) || registry.length !== 1) return { ok: false, reason: 'registry shape' } as const;
const daemonId = registry[0]?.id;
if (typeof daemonId !== 'string' || daemonId.length === 0) return { ok: false, reason: 'missing daemon id' } as const;
prefs.serverId = daemonId;
localStorage.setItem('@paseo:create-agent-preferences', JSON.stringify(prefs));
// Prevent the fixture's init-script from overwriting the corrected prefs on reload.
localStorage.setItem('@paseo:e2e-disable-default-seed-once', '1');
return { ok: true } as const;
});
if (!fix.ok) {
throw error;
}
await page.reload();
await assertE2EUsesSeededTestDaemon(page);
}
const input = page.getByRole('textbox', { name: 'Message agent...' });
await expect(input).toBeVisible();
@@ -37,14 +209,20 @@ export const ensureHostSelected = async (page: Page) => {
return;
}
const selectHost = page.getByText('Select host', { exact: true });
if (await selectHost.isVisible()) {
await selectHost.click();
const selectHostLabel = page.getByText('Select host', { exact: true });
if (await selectHostLabel.isVisible()) {
await selectHostLabel.click();
// Wait for the host option to appear and click it
const hostOption = page.getByText('localhost', { exact: true }).first();
await expect(hostOption).toBeVisible();
await hostOption.click();
// E2E safety: we enforce a single seeded daemon, so the option should be unambiguous.
const localhostOption = page.getByText('localhost', { exact: true }).first();
const daemonIdOption = page.getByText('e2e-test-daemon', { exact: true }).first();
if (await localhostOption.isVisible()) {
await localhostOption.click();
} else {
await expect(daemonIdOption).toBeVisible();
await daemonIdOption.click();
}
}
await expect(input).toBeEditable();
@@ -90,10 +268,12 @@ export const selectModel = async (page: Page, model: string) => {
// Type to search/filter models
await searchInput.fill(model);
// Wait for a matching option to appear (partial match via regex)
const option = page.getByText(new RegExp(model, 'i')).first();
const dialog = page.getByRole('dialog');
const option = dialog
.getByText(new RegExp(`^${escapeRegex(model)}$`, 'i'))
.first();
await expect(option).toBeVisible({ timeout: 30000 });
await option.click();
await option.click({ force: true });
// Wait for dropdown to close
await expect(searchInput).not.toBeVisible({ timeout: 5000 });
@@ -111,10 +291,12 @@ export const selectMode = async (page: Page, mode: string) => {
// Type to filter modes
await searchInput.fill(mode);
// Click the matching option (use last() since the field label also contains the mode text)
const option = page.getByText(mode, { exact: true }).last();
const dialog = page.getByRole('dialog');
const option = dialog
.getByText(new RegExp(`^${escapeRegex(mode)}$`, 'i'))
.first();
await expect(option).toBeVisible();
await option.click();
await option.click({ force: true });
// Wait for dropdown to close
await expect(searchInput).not.toBeVisible({ timeout: 5000 });