Relay pairing offer v2 (E2EE) + host connections
This commit is contained in:
@@ -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: {
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 }
|
||||
);
|
||||
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
@@ -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 }
|
||||
);
|
||||
});
|
||||
@@ -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 });
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user