Relay pairing offer v2 (E2EE) + host connections

This commit is contained in:
Mohamed Boudra
2026-02-04 22:53:00 +07:00
parent 9ce4ad9758
commit 0218729316
59 changed files with 2022 additions and 1547 deletions
+14 -5
View File
@@ -54,19 +54,28 @@ test.beforeEach(async ({ page }) => {
const nowIso = new Date().toISOString();
const seedNonce = Math.random().toString(36).slice(2);
const serverId = process.env.E2E_SERVER_ID;
if (!serverId) {
throw new Error('E2E_SERVER_ID is not set - expected from Playwright globalSetup.');
}
const testDaemon = {
id: 'e2e-test-daemon',
serverId,
label: 'localhost',
endpoints: [`127.0.0.1:${daemonPort}`],
relay: null,
metadata: null,
connections: [
{
id: `direct:127.0.0.1:${daemonPort}`,
type: 'direct',
endpoint: `127.0.0.1:${daemonPort}`,
},
],
preferredConnectionId: `direct:127.0.0.1:${daemonPort}`,
createdAt: nowIso,
updatedAt: nowIso,
};
const createAgentPreferences = {
// Ensure create flow never uses a remembered host from the developer's real app.
serverId: testDaemon.id,
serverId: testDaemon.serverId,
// Keep e2e fast/cheap by default.
provider: 'claude',
providerPreferences: {
+8 -8
View File
@@ -46,9 +46,10 @@ let paseoHome: string | null = null;
let relayServer: RelayServer | null = null;
type OfferPayload = {
sessionId: string;
v: 2;
serverId: string;
daemonPublicKeyB64: string;
endpoints: string[];
relay: { endpoint: string };
};
function stripAnsi(input: string): string {
@@ -64,11 +65,10 @@ function decodeOfferFromFragmentUrl(url: string): OfferPayload {
const encoded = url.slice(idx + marker.length);
const json = Buffer.from(encoded, 'base64url').toString('utf8');
const offer = JSON.parse(json) as Partial<OfferPayload>;
if (!offer.sessionId) throw new Error('offer.sessionId missing');
if (offer.v !== 2) throw new Error('offer.v missing/invalid');
if (!offer.serverId) throw new Error('offer.serverId missing');
if (!offer.daemonPublicKeyB64) throw new Error('offer.daemonPublicKeyB64 missing');
if (!Array.isArray(offer.endpoints) || offer.endpoints.length === 0) {
throw new Error('offer.endpoints missing');
}
if (!offer.relay?.endpoint) throw new Error('offer.relay.endpoint missing');
return offer as OfferPayload;
}
@@ -118,7 +118,7 @@ export default async function globalSetup() {
env: {
...process.env,
PASEO_HOME: paseoHome,
PASEO_SERVER_ID: 'e2e-test-daemon',
PASEO_SERVER_ID: 'srv_e2e_test_daemon',
PASEO_LISTEN: `0.0.0.0:${port}`,
PASEO_RELAY_ENDPOINT: `127.0.0.1:${relayPort}`,
PASEO_CORS_ORIGINS: `http://localhost:${metroPort}`,
@@ -184,7 +184,7 @@ export default async function globalSetup() {
process.env.E2E_DAEMON_PORT = String(port);
process.env.E2E_RELAY_PORT = String(relayPort);
process.env.E2E_RELAY_SESSION_ID = offer.sessionId;
process.env.E2E_SERVER_ID = offer.serverId;
process.env.E2E_RELAY_DAEMON_PUBLIC_KEY = offer.daemonPublicKeyB64;
process.env.E2E_METRO_PORT = String(metroPort);
console.log(`[e2e] Test daemon started on port ${port}, Metro on port ${metroPort}, home: ${paseoHome}`);
+46 -26
View File
@@ -18,22 +18,27 @@ function getE2EDaemonPort(): string {
async function ensureE2EStorageSeeded(page: Page): Promise<void> {
const port = getE2EDaemonPort();
const expectedEndpoint = `127.0.0.1:${port}`;
const expectedServerId = process.env.E2E_SERVER_ID;
if (!expectedServerId) {
throw new Error('E2E_SERVER_ID is not set (expected from Playwright globalSetup).');
}
const needsReset = await page.evaluate(({ expectedEndpoint }) => {
const needsReset = await page.evaluate(({ expectedEndpoint, expectedServerId }) => {
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);
if (entry?.serverId !== expectedServerId) return true;
const connections = entry?.connections;
if (!Array.isArray(connections)) return true;
if (connections.some((c: any) => c?.type === 'direct' && typeof c?.endpoint === 'string' && /:6767\b/.test(c.endpoint))) return true;
return !connections.some((c: any) => c?.type === 'direct' && c?.endpoint === expectedEndpoint);
} catch {
return true;
}
}, { expectedEndpoint });
}, { expectedEndpoint, expectedServerId });
if (!needsReset) {
return;
@@ -41,17 +46,22 @@ async function ensureE2EStorageSeeded(page: Page): Promise<void> {
const nowIso = new Date().toISOString();
await page.evaluate(
({ expectedEndpoint, nowIso }) => {
({ expectedEndpoint, nowIso, expectedServerId }) => {
localStorage.setItem('@paseo:e2e', '1');
localStorage.setItem(
'@paseo:daemon-registry',
JSON.stringify([
{
id: 'e2e-test-daemon',
serverId: expectedServerId,
label: 'localhost',
endpoints: [expectedEndpoint],
relay: null,
metadata: null,
connections: [
{
id: `direct:${expectedEndpoint}`,
type: 'direct',
endpoint: expectedEndpoint,
},
],
preferredConnectionId: `direct:${expectedEndpoint}`,
createdAt: nowIso,
updatedAt: nowIso,
},
@@ -60,7 +70,7 @@ async function ensureE2EStorageSeeded(page: Page): Promise<void> {
localStorage.setItem(
'@paseo:create-agent-preferences',
JSON.stringify({
serverId: 'e2e-test-daemon',
serverId: expectedServerId,
provider: 'claude',
providerPreferences: {
claude: { model: 'haiku' },
@@ -70,7 +80,7 @@ async function ensureE2EStorageSeeded(page: Page): Promise<void> {
);
localStorage.removeItem('@paseo:settings');
},
{ expectedEndpoint, nowIso }
{ expectedEndpoint, nowIso, expectedServerId }
);
await page.reload();
@@ -79,6 +89,10 @@ async function ensureE2EStorageSeeded(page: Page): Promise<void> {
async function assertE2EUsesSeededTestDaemon(page: Page): Promise<void> {
const port = getE2EDaemonPort();
const expectedEndpoint = `127.0.0.1:${port}`;
const expectedServerId = process.env.E2E_SERVER_ID;
if (!expectedServerId) {
throw new Error('E2E_SERVER_ID is not set (expected from Playwright globalSetup).');
}
const snapshot = await page.evaluate(() => {
const registryRaw = localStorage.getItem('@paseo:daemon-registry');
@@ -104,18 +118,24 @@ async function assertE2EUsesSeededTestDaemon(page: Page): Promise<void> {
}
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)}).`);
if (typeof daemon?.serverId !== 'string' || daemon.serverId.length === 0) {
throw new Error(`E2E expected seeded daemon to have a string serverId (got ${String(daemon?.serverId)}).`);
}
if (daemon.serverId !== expectedServerId) {
throw new Error(`E2E expected seeded daemon serverId to be ${expectedServerId} (got ${daemon.serverId}).`);
}
const endpoints: unknown = daemon?.endpoints;
if (!Array.isArray(endpoints) || !endpoints.some((e) => e === expectedEndpoint)) {
const connections: unknown = daemon?.connections;
if (
!Array.isArray(connections) ||
!connections.some((c: any) => c?.type === 'direct' && c?.endpoint === expectedEndpoint)
) {
throw new Error(
`E2E expected seeded daemon endpoints to include ${expectedEndpoint} (got ${JSON.stringify(endpoints)}).`
`E2E expected seeded daemon connections to include direct ${expectedEndpoint} (got ${JSON.stringify(connections)}).`
);
}
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 (Array.isArray(connections) && connections.some((c: any) => c?.type === 'direct' && typeof c?.endpoint === 'string' && /:6767\b/.test(c.endpoint))) {
throw new Error(`E2E detected a daemon endpoint pointing at :6767 (${JSON.stringify(connections)}).`);
}
if (!snapshot.prefsRaw) {
@@ -123,9 +143,9 @@ async function assertE2EUsesSeededTestDaemon(page: Page): Promise<void> {
}
try {
const prefs = JSON.parse(snapshot.prefsRaw) as any;
if (prefs?.serverId !== daemon.id) {
if (prefs?.serverId !== daemon.serverId) {
throw new Error(
`E2E expected create-agent-preferences.serverId to match seeded daemon id (${daemon.id}) (got ${String(prefs?.serverId)}).`
`E2E expected create-agent-preferences.serverId to match seeded daemon serverId (${daemon.serverId}) (got ${String(prefs?.serverId)}).`
);
}
} catch (error) {
@@ -261,9 +281,9 @@ export const ensureHostSelected = async (page: Page) => {
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;
const serverId = registry[0]?.serverId;
if (typeof serverId !== 'string' || serverId.length === 0) return { ok: false, reason: 'missing serverId' } as const;
prefs.serverId = serverId;
localStorage.setItem('@paseo:create-agent-preferences', JSON.stringify(prefs));
// Prevent the fixture's init-script from overwriting the corrected prefs on reload.
const nonce = localStorage.getItem('@paseo:e2e-seed-nonce') ?? '1';
@@ -292,7 +312,7 @@ export const ensureHostSelected = async (page: Page) => {
// 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();
const daemonIdOption = page.getByText(process.env.E2E_SERVER_ID ?? 'srv_e2e_test_daemon', { exact: true }).first();
if (await localhostOption.isVisible()) {
await localhostOption.click();
+19 -13
View File
@@ -2,30 +2,36 @@ import { test, expect } from './fixtures';
test('host removal removes the host from UI and persists after reload', async ({ page }) => {
const daemonPort = process.env.E2E_DAEMON_PORT;
const seededServerId = process.env.E2E_SERVER_ID;
if (!daemonPort) {
throw new Error('E2E_DAEMON_PORT is not set (expected from globalSetup).');
}
if (!seededServerId) {
throw new Error('E2E_SERVER_ID is not set (expected from globalSetup).');
}
const extraPort = Number(daemonPort) + 1;
const extraEndpoint = `127.0.0.1:${extraPort}`;
const nowIso = new Date().toISOString();
const extraDaemon = {
id: 'e2e-extra-daemon',
serverId: 'srv_e2e_extra_daemon',
label: 'extra',
endpoints: [extraEndpoint],
relay: null,
metadata: null,
connections: [
{ id: `direct:${extraEndpoint}`, type: 'direct', endpoint: extraEndpoint },
],
preferredConnectionId: `direct:${extraEndpoint}`,
createdAt: nowIso,
updatedAt: nowIso,
};
const seededTestDaemon = {
id: 'e2e-test-daemon',
serverId: seededServerId,
label: 'localhost',
endpoints: [`127.0.0.1:${daemonPort}`],
relay: null,
metadata: null,
connections: [
{ id: `direct:127.0.0.1:${daemonPort}`, type: 'direct', endpoint: `127.0.0.1:${daemonPort}` },
],
preferredConnectionId: `direct:127.0.0.1:${daemonPort}`,
createdAt: nowIso,
updatedAt: nowIso,
};
@@ -52,12 +58,12 @@ test('host removal removes the host from UI and persists after reload', async ({
localStorage.setItem(seedOnceKey, '1');
const next = [...list];
const hasSeeded = next.some((entry: any) => entry && entry.id === seededTestDaemon.id);
const hasSeeded = next.some((entry: any) => entry && entry.serverId === seededTestDaemon.serverId);
if (!hasSeeded) {
next.push(seededTestDaemon);
}
const alreadyPresent = next.some((entry: any) => entry && entry.id === daemon.id);
const alreadyPresent = next.some((entry: any) => entry && entry.serverId === daemon.serverId);
if (!alreadyPresent) {
next.push(daemon);
}
@@ -80,17 +86,17 @@ test('host removal removes the host from UI and persists after reload', async ({
await expect(page.getByText(extraEndpoint, { exact: true })).toHaveCount(0);
await page.waitForFunction(
(daemonId) => {
(serverId) => {
const raw = localStorage.getItem('@paseo:daemon-registry');
if (!raw) return false;
try {
const parsed = JSON.parse(raw);
return Array.isArray(parsed) && !parsed.some((entry: any) => entry && entry.id === daemonId);
return Array.isArray(parsed) && !parsed.some((entry: any) => entry && entry.serverId === serverId);
} catch {
return false;
}
},
extraDaemon.id,
extraDaemon.serverId,
{ timeout: 10000 }
);
+28 -99
View File
@@ -15,23 +15,32 @@ test('new agent auto-selects the previous host', async ({ page }) => {
test('new agent respects serverId in the URL', async ({ page }) => {
const daemonPort = process.env.E2E_DAEMON_PORT;
const serverId = process.env.E2E_SERVER_ID;
if (!daemonPort) {
throw new Error('E2E_DAEMON_PORT is not set (expected from globalSetup).');
}
if (!serverId) {
throw new Error('E2E_SERVER_ID is not set (expected from globalSetup).');
}
// Ensure this test's storage is deterministic even under parallel load.
const nowIso = new Date().toISOString();
const testDaemon = {
id: 'e2e-test-daemon',
serverId,
label: 'localhost',
endpoints: [`127.0.0.1:${daemonPort}`],
relay: null,
metadata: null,
connections: [
{
id: `direct:127.0.0.1:${daemonPort}`,
type: 'direct',
endpoint: `127.0.0.1:${daemonPort}`,
},
],
preferredConnectionId: `direct:127.0.0.1:${daemonPort}`,
createdAt: nowIso,
updatedAt: nowIso,
};
const createAgentPreferences = {
serverId: testDaemon.id,
serverId: testDaemon.serverId,
provider: 'claude',
providerPreferences: {
claude: { model: 'haiku' },
@@ -53,19 +62,7 @@ test('new agent respects serverId in the URL', async ({ page }) => {
await page.reload();
await expect(page.getByText('Online', { exact: true }).first()).toBeVisible({ timeout: 20000 });
const seededDaemonId = await page.evaluate(() => {
const raw = localStorage.getItem('@paseo:daemon-registry');
if (!raw) return null;
const parsed = JSON.parse(raw) as any[];
return Array.isArray(parsed) && parsed.length > 0 && typeof parsed[0]?.id === 'string'
? (parsed[0].id as string)
: null;
});
if (!seededDaemonId) {
throw new Error('Expected daemon registry to contain a seeded daemon id.');
}
await page.goto(`/?serverId=${encodeURIComponent(seededDaemonId)}`);
await page.goto(`/?serverId=${encodeURIComponent(serverId)}`);
await expect(page.getByText('New agent', { exact: true }).first()).toBeVisible();
const input = page.getByRole('textbox', { name: 'Message agent...' });
@@ -74,17 +71,26 @@ test('new agent respects serverId in the URL', async ({ page }) => {
test('new agent auto-selects first online host when no preference is stored', async ({ page }) => {
const daemonPort = process.env.E2E_DAEMON_PORT;
const serverId = process.env.E2E_SERVER_ID;
if (!daemonPort) {
throw new Error('E2E_DAEMON_PORT is not set (expected from globalSetup).');
}
if (!serverId) {
throw new Error('E2E_SERVER_ID is not set (expected from globalSetup).');
}
const nowIso = new Date().toISOString();
const testDaemon = {
id: 'e2e-test-daemon',
serverId,
label: 'localhost',
endpoints: [`127.0.0.1:${daemonPort}`],
relay: null,
metadata: null,
connections: [
{
id: `direct:127.0.0.1:${daemonPort}`,
type: 'direct',
endpoint: `127.0.0.1:${daemonPort}`,
},
],
preferredConnectionId: `direct:127.0.0.1:${daemonPort}`,
createdAt: nowIso,
updatedAt: nowIso,
};
@@ -110,80 +116,3 @@ test('new agent auto-selects first online host when no preference is stored', as
const input = page.getByRole('textbox', { name: 'Message agent...' });
await expect(input).toBeEditable({ timeout: 30000 });
});
test('adopts daemon-provided serverId for legacy host ids', async ({ page }) => {
const daemonPort = process.env.E2E_DAEMON_PORT;
if (!daemonPort) {
throw new Error('E2E_DAEMON_PORT is not set (expected from globalSetup).');
}
const nowIso = new Date().toISOString();
const legacyId = 'legacy-daemon-id';
const daemon = {
id: legacyId,
label: 'localhost',
endpoints: [`127.0.0.1:${daemonPort}`],
relay: null,
metadata: null,
createdAt: nowIso,
updatedAt: nowIso,
};
await page.goto('/settings');
await page.evaluate(
({ daemon, legacyId }) => {
const nonce = localStorage.getItem('@paseo:e2e-seed-nonce') ?? '1';
localStorage.setItem('@paseo:e2e-disable-default-seed-once', nonce);
localStorage.setItem('@paseo:daemon-registry', JSON.stringify([daemon]));
localStorage.setItem(
'@paseo:create-agent-preferences',
JSON.stringify({
serverId: legacyId,
provider: 'claude',
providerPreferences: {
claude: { model: 'haiku' },
codex: { model: 'gpt-5.1-codex-mini' },
},
})
);
localStorage.removeItem('@paseo:settings');
},
{ daemon, legacyId }
);
await page.reload();
await expect(page.getByText('Online', { exact: true }).first()).toBeVisible({ timeout: 20000 });
// Wait for server_info handshake to rekey registry + preferences.
await page.waitForFunction(
() => {
const raw = localStorage.getItem('@paseo:daemon-registry');
const prefsRaw = localStorage.getItem('@paseo:create-agent-preferences');
if (!raw || !prefsRaw) return false;
const registry = JSON.parse(raw);
const prefs = JSON.parse(prefsRaw);
if (!Array.isArray(registry) || registry.length !== 1) return false;
if (registry[0]?.id !== 'e2e-test-daemon') return false;
if (prefs?.serverId !== 'e2e-test-daemon') return false;
const legacyIds = registry[0]?.metadata?.legacyIds;
return Array.isArray(legacyIds) && legacyIds.includes('legacy-daemon-id');
},
{ timeout: 20000 }
);
const snapshot = await page.evaluate(() => {
const raw = localStorage.getItem('@paseo:daemon-registry');
const prefsRaw = localStorage.getItem('@paseo:create-agent-preferences');
return { raw, prefsRaw };
});
if (!snapshot.raw || !snapshot.prefsRaw) {
throw new Error('Expected registry + preferences to be present after rekey.');
}
const registry = JSON.parse(snapshot.raw) as any[];
expect(registry[0].id).toBe('e2e-test-daemon');
expect(registry[0].metadata.legacyIds).toContain('legacy-daemon-id');
const prefs = JSON.parse(snapshot.prefsRaw) as any;
expect(prefs.serverId).toBe('e2e-test-daemon');
});
+12 -5
View File
@@ -1,10 +1,14 @@
import { test, expect } from './fixtures';
test('manual host add accepts host:port only and persists endpoints', async ({ page }) => {
test('manual host add accepts host:port only and persists a direct connection', async ({ page }) => {
const daemonPort = process.env.E2E_DAEMON_PORT;
const serverId = process.env.E2E_SERVER_ID;
if (!daemonPort) {
throw new Error('E2E_DAEMON_PORT is not set (expected from globalSetup).');
}
if (!serverId) {
throw new Error('E2E_SERVER_ID is not set (expected from globalSetup).');
}
// Override the default fixture seeding for this navigation (must run before app boot).
await page.addInitScript(() => {
@@ -26,7 +30,7 @@ test('manual host add accepts host:port only and persists endpoints', async ({ p
await expect(page.getByText('Online', { exact: true })).toBeVisible({ timeout: 15000 });
await page.waitForFunction(
(port) => {
({ port, serverId }) => {
const raw = localStorage.getItem('@paseo:daemon-registry');
if (!raw) return false;
try {
@@ -34,14 +38,17 @@ test('manual host add accepts host:port only and persists endpoints', async ({ p
if (!Array.isArray(parsed) || parsed.length !== 1) return false;
const entry = parsed[0];
return (
Array.isArray(entry?.endpoints) &&
entry.endpoints.some((endpoint: unknown) => endpoint === `127.0.0.1:${port}`)
entry?.serverId === serverId &&
Array.isArray(entry?.connections) &&
entry.connections.some(
(conn: any) => conn?.type === 'direct' && conn?.endpoint === `127.0.0.1:${port}`
)
);
} catch {
return false;
}
},
daemonPort,
{ port: daemonPort, serverId },
{ timeout: 10000 }
);
});
@@ -1,47 +0,0 @@
import { test, expect } from './fixtures';
test('legacy wsUrl registry entries migrate to host:port endpoints', async ({ page }) => {
const daemonPort = process.env.E2E_DAEMON_PORT;
if (!daemonPort) {
throw new Error('E2E_DAEMON_PORT is not set (expected from globalSetup).');
}
const nowIso = new Date().toISOString();
const legacy = [
{
id: 'legacy-daemon',
label: 'localhost',
wsUrl: `ws://127.0.0.1:${daemonPort}/ws`,
createdAt: nowIso,
updatedAt: nowIso,
metadata: null,
},
];
// Override the default fixture seeding for this navigation (must run before app boot).
await page.addInitScript((legacyRegistry) => {
localStorage.setItem('@paseo:daemon-registry', JSON.stringify(legacyRegistry));
localStorage.removeItem('@paseo:settings');
}, legacy);
await page.goto('/settings');
await expect(page.getByText(`127.0.0.1:${daemonPort}`, { exact: true })).toBeVisible();
await expect(page.getByText('Online', { exact: true }).first()).toBeVisible({ timeout: 15000 });
await page.waitForFunction(
(port) => {
const raw = localStorage.getItem('@paseo:daemon-registry');
if (!raw) return false;
try {
const parsed = JSON.parse(raw);
if (!Array.isArray(parsed) || parsed.length !== 1) return false;
const entry = parsed[0];
const hasWsUrl = typeof entry?.wsUrl === 'string';
return !hasWsUrl && Array.isArray(entry?.endpoints) && entry.endpoints[0] === `127.0.0.1:${port}`;
} catch {
return false;
}
},
daemonPort,
{ timeout: 10000 }
);
});
+12 -14
View File
@@ -9,12 +9,7 @@ function encodeBase64Url(input: string): string {
.replace(/=+$/g, '');
}
test('pairing flow accepts #offer=ConnectionOfferV1 and stores sessionId + endpoints', async ({ page }) => {
const daemonPort = process.env.E2E_DAEMON_PORT;
if (!daemonPort) {
throw new Error('E2E_DAEMON_PORT is not set (expected from globalSetup).');
}
test('pairing flow accepts #offer=ConnectionOfferV2 and stores relay-only host', async ({ page }) => {
// Override the default fixture seeding for this test.
await page.goto('/settings');
await page.evaluate(() => {
@@ -26,10 +21,10 @@ test('pairing flow accepts #offer=ConnectionOfferV1 and stores sessionId + endpo
await page.reload();
const offer = {
v: 1 as const,
sessionId: 'e2e-session-123',
endpoints: [`127.0.0.1:${daemonPort}`, 'relay.local:443'],
v: 2 as const,
serverId: 'e2e-server-123',
daemonPublicKeyB64: Buffer.from('e2e-public-key', 'utf8').toString('base64'),
relay: { endpoint: 'relay.local:443' },
};
const offerUrl = `https://app.paseo.sh/#offer=${encodeBase64Url(JSON.stringify(offer))}`;
@@ -53,12 +48,15 @@ test('pairing flow accepts #offer=ConnectionOfferV1 and stores sessionId + endpo
const parsed = JSON.parse(raw);
if (!Array.isArray(parsed) || parsed.length !== 1) return false;
const entry = parsed[0];
const relayId = `relay:${expected.relay.endpoint}`;
return (
entry?.daemonPublicKeyB64 === expected.daemonPublicKeyB64 &&
entry?.relay?.sessionId === expected.sessionId &&
Array.isArray(entry?.endpoints) &&
entry.endpoints[0] === expected.endpoints[0] &&
entry.endpoints[1] === expected.endpoints[1]
entry?.serverId === expected.serverId &&
Array.isArray(entry?.connections) &&
entry.connections.length === 1 &&
entry.connections[0]?.id === relayId &&
entry.connections[0]?.type === 'relay' &&
entry.connections[0]?.relayEndpoint === expected.relay.endpoint &&
entry.connections[0]?.daemonPublicKeyB64 === expected.daemonPublicKeyB64
);
} catch {
return false;
@@ -2,11 +2,11 @@ import { test, expect } from './fixtures';
test('connects via relay when direct endpoints fail', async ({ page }) => {
const relayPort = process.env.E2E_RELAY_PORT;
const sessionId = process.env.E2E_RELAY_SESSION_ID;
const serverId = process.env.E2E_SERVER_ID;
const daemonPublicKeyB64 = process.env.E2E_RELAY_DAEMON_PUBLIC_KEY;
if (!relayPort || !sessionId || !daemonPublicKeyB64) {
if (!relayPort || !serverId || !daemonPublicKeyB64) {
throw new Error(
'E2E_RELAY_PORT, E2E_RELAY_SESSION_ID, or E2E_RELAY_DAEMON_PUBLIC_KEY is not set (expected from globalSetup).'
'E2E_RELAY_PORT, E2E_SERVER_ID, or E2E_RELAY_DAEMON_PUBLIC_KEY is not set (expected from globalSetup).'
);
}
@@ -14,12 +14,13 @@ test('connects via relay when direct endpoints fail', async ({ page }) => {
const relayEndpoint = `127.0.0.1:${relayPort}`;
const host = {
id: 'relay-only-daemon',
serverId,
label: 'relay-daemon',
endpoints: [relayEndpoint],
daemonPublicKeyB64,
relay: { endpoint: relayEndpoint, sessionId },
metadata: null,
connections: [
{ id: 'direct:127.0.0.1:9', type: 'direct', endpoint: '127.0.0.1:9' },
{ id: `relay:${relayEndpoint}`, type: 'relay', relayEndpoint, daemonPublicKeyB64 },
],
preferredConnectionId: 'direct:127.0.0.1:9',
createdAt: nowIso,
updatedAt: nowIso,
};
@@ -35,6 +36,6 @@ test('connects via relay when direct endpoints fail', async ({ page }) => {
await page.reload();
// Should eventually connect through the relay candidate URL.
await expect(page.getByText(relayEndpoint, { exact: true })).toBeVisible();
await expect(page.getByText('Connected via relay', { exact: true }).first()).toBeVisible({ timeout: 20000 });
await expect(page.getByText('Online', { exact: true }).first()).toBeVisible({ timeout: 20000 });
});