From 380c9927d704d00df9bae714f52cf74af5e37d78 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Tue, 14 Apr 2026 19:12:13 +0700 Subject: [PATCH] fix: stop logging pairing offer during daemon startup --- packages/app/e2e/global-setup.ts | 88 +++++++++++-------- .../server/src/server/bootstrap.smoke.test.ts | 27 +++--- packages/server/src/server/bootstrap.ts | 3 +- .../daemon-e2e/connection-offer.e2e.test.ts | 66 +++++++++----- .../daemon-e2e/relay-transport.e2e.test.ts | 47 +++++++--- 5 files changed, 140 insertions(+), 91 deletions(-) diff --git a/packages/app/e2e/global-setup.ts b/packages/app/e2e/global-setup.ts index 3ac61ee2e..9e319e851 100644 --- a/packages/app/e2e/global-setup.ts +++ b/packages/app/e2e/global-setup.ts @@ -1,4 +1,4 @@ -import { spawn, type ChildProcess, execSync } from "node:child_process"; +import { spawn, type ChildProcess, execFileSync, execSync } from "node:child_process"; import { existsSync } from "node:fs"; import { mkdtemp, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; @@ -224,6 +224,51 @@ function decodeOfferFromFragmentUrl(url: string): OfferPayload { return offer as OfferPayload; } +function loadPairingOfferFromCli(repoRoot: string, paseoHomePath: string): OfferPayload { + const stdout = execFileSync( + process.execPath, + ["--import", "tsx", "packages/cli/src/index.ts", "daemon", "pair", "--json"], + { + cwd: repoRoot, + env: { + ...process.env, + PASEO_HOME: paseoHomePath, + }, + encoding: "utf8", + }, + ); + const payload = JSON.parse(stdout) as { relayEnabled?: boolean; url?: string | null }; + if (payload.relayEnabled !== true || typeof payload.url !== "string") { + throw new Error(`Unexpected daemon pair response: ${stdout}`); + } + return decodeOfferFromFragmentUrl(payload.url); +} + +async function waitForPairingOfferFromCli(args: { + repoRoot: string; + paseoHome: string; + timeoutMs?: number; +}): Promise { + const timeoutMs = args.timeoutMs ?? 15000; + const start = Date.now(); + let lastError: unknown = null; + + while (Date.now() - start < timeoutMs) { + try { + return loadPairingOfferFromCli(args.repoRoot, args.paseoHome); + } catch (error) { + lastError = error; + await sleep(100); + } + } + + throw new Error( + `Timed out waiting for \`paseo daemon pair --json\` to produce a pairing offer: ${ + lastError instanceof Error ? lastError.message : String(lastError) + }`, + ); +} + export default async function globalSetup() { const repoRoot = path.resolve(__dirname, "../../.."); ensureRelayBuildArtifact(repoRoot); @@ -433,12 +478,6 @@ export default async function globalSetup() { const serverDir = path.resolve(__dirname, "../../..", "packages/server"); const tsxBin = execSync("which tsx").toString().trim(); - let offerPayload: OfferPayload | null = null; - let offerResolve: (() => void) | null = null; - const offerPromise = new Promise((resolve) => { - offerResolve = resolve; - }); - daemonProcess = spawn(tsxBin, ["src/server/index.ts"], { cwd: serverDir, env: { @@ -473,26 +512,6 @@ export default async function globalSetup() { const trimmed = line.trim(); if (!trimmed) continue; daemonLineBuffer.add(`[stdout] ${trimmed}`); - if (!offerPayload) { - const clean = stripAnsi(trimmed); - try { - const obj = JSON.parse(clean) as { msg?: string; url?: string }; - if (obj.msg === "pairing_offer" && typeof obj.url === "string") { - offerPayload = decodeOfferFromFragmentUrl(obj.url); - offerResolve?.(); - } - } catch { - const match = clean.match(/https?:\/\/[^\s"]+#offer=[A-Za-z0-9_-]+/); - if (match && clean.includes("pairing_offer")) { - try { - offerPayload = decodeOfferFromFragmentUrl(match[0]); - offerResolve?.(); - } catch { - // ignore parsing failures - } - } - } - } console.log(`[daemon] ${trimmed}`); } }); @@ -523,17 +542,10 @@ export default async function globalSetup() { }), ]); - // Wait for daemon to emit a pairing offer (includes relay session ID). - await Promise.race([ - offerPromise, - new Promise((_, reject) => - setTimeout(() => reject(new Error("Timed out waiting for pairing_offer log")), 15000), - ), - ]); - if (!offerPayload) { - throw new Error("pairing_offer was not parsed from daemon logs"); - } - const offer = offerPayload as OfferPayload; + const offer = await waitForPairingOfferFromCli({ + repoRoot, + paseoHome, + }); process.env.E2E_DAEMON_PORT = String(port); process.env.E2E_RELAY_PORT = String(relayPort); diff --git a/packages/server/src/server/bootstrap.smoke.test.ts b/packages/server/src/server/bootstrap.smoke.test.ts index ee2b4e520..ccc42eccf 100644 --- a/packages/server/src/server/bootstrap.smoke.test.ts +++ b/packages/server/src/server/bootstrap.smoke.test.ts @@ -1,11 +1,11 @@ import os from "node:os"; import path from "node:path"; import { mkdir, mkdtemp, rm } from "node:fs/promises"; -import { Writable } from "node:stream"; import pino from "pino"; import { afterEach, describe, expect, test, vi } from "vitest"; import { createPaseoDaemon, parseListenString, type PaseoDaemonConfig } from "./bootstrap.js"; +import { generateLocalPairingOffer } from "./pairing-offer.js"; import { createTestPaseoDaemon } from "./test-utils/paseo-daemon.js"; import { createTestAgentClients } from "./test-utils/fake-agent-client.js"; @@ -150,24 +150,14 @@ describe("paseo daemon bootstrap", () => { }); }); - test("emits a relay pairing offer for unix socket listeners", async () => { + test("generates a relay pairing offer for unix socket listeners", async () => { const paseoHomeRoot = await mkdtemp(path.join(os.tmpdir(), "paseo-socket-relay-")); const paseoHome = path.join(paseoHomeRoot, ".paseo"); const staticDir = await mkdtemp(path.join(os.tmpdir(), "paseo-static-")); const socketPath = path.join(paseoHomeRoot, "run", "paseo.sock"); await mkdir(path.dirname(socketPath), { recursive: true }); await mkdir(paseoHome, { recursive: true }); - - const lines: string[] = []; - const logger = pino( - { level: "info" }, - new Writable({ - write(chunk, _encoding, callback) { - lines.push(chunk.toString("utf8")); - callback(); - }, - }), - ); + const logger = pino({ level: "silent" }); const config: PaseoDaemonConfig = { listen: socketPath, @@ -191,7 +181,16 @@ describe("paseo daemon bootstrap", () => { try { await daemon.start(); - expect(lines.some((line) => line.includes('"msg":"pairing_offer"'))).toBe(true); + const pairing = await generateLocalPairingOffer({ + paseoHome, + relayEnabled: true, + relayEndpoint: "127.0.0.1:9", + relayPublicEndpoint: "127.0.0.1:9", + appBaseUrl: "https://app.paseo.sh", + includeQr: false, + }); + expect(pairing.relayEnabled).toBe(true); + expect(pairing.url?.startsWith("https://app.paseo.sh/#offer=")).toBe(true); } finally { await daemon.stop().catch(() => undefined); await daemon.agentManager.flush().catch(() => undefined); diff --git a/packages/server/src/server/bootstrap.ts b/packages/server/src/server/bootstrap.ts index 6a5e9b476..b224a674c 100644 --- a/packages/server/src/server/bootstrap.ts +++ b/packages/server/src/server/bootstrap.ts @@ -646,8 +646,7 @@ export async function createPaseoDaemon( relay: { endpoint: relayPublicEndpoint }, }); - const url = encodeOfferToFragmentUrl({ offer, appBaseUrl }); - logger.info({ url }, "pairing_offer"); + encodeOfferToFragmentUrl({ offer, appBaseUrl }); relayTransport?.stop().catch(() => undefined); relayTransport = startRelayTransport({ diff --git a/packages/server/src/server/daemon-e2e/connection-offer.e2e.test.ts b/packages/server/src/server/daemon-e2e/connection-offer.e2e.test.ts index 80b464d3f..2c065c93c 100644 --- a/packages/server/src/server/daemon-e2e/connection-offer.e2e.test.ts +++ b/packages/server/src/server/daemon-e2e/connection-offer.e2e.test.ts @@ -8,6 +8,7 @@ import { Writable } from "node:stream"; import { spawn } from "node:child_process"; import { fileURLToPath } from "node:url"; +import { generateLocalPairingOffer } from "../pairing-offer.js"; import { createTestPaseoDaemon } from "../test-utils/paseo-daemon.js"; function createCapturingLogger() { @@ -22,18 +23,25 @@ function createCapturingLogger() { return { logger, lines }; } -function parseOfferUrlFromLogs(lines: string[]): string { - for (const line of lines) { - try { - const obj = JSON.parse(line) as { msg?: string; url?: string }; - if (obj.msg === "pairing_offer" && typeof obj.url === "string") { - return obj.url; - } - } catch { - // ignore non-JSON lines - } +async function getPairingOfferUrl(args: { + paseoHome: string; + relayEnabled?: boolean; + relayEndpoint?: string; + relayPublicEndpoint?: string; + appBaseUrl?: string; +}): Promise { + const pairing = await generateLocalPairingOffer({ + paseoHome: args.paseoHome, + relayEnabled: args.relayEnabled, + relayEndpoint: args.relayEndpoint, + relayPublicEndpoint: args.relayPublicEndpoint, + appBaseUrl: args.appBaseUrl, + includeQr: false, + }); + if (!pairing.url) { + throw new Error("Expected relay pairing URL to be available"); } - throw new Error(`pairing_offer log not found. saw ${lines.length} lines`); + return pairing.url; } function decodeOfferFromFragmentUrl(url: string): unknown { @@ -72,7 +80,7 @@ describe("ConnectionOfferV2 (daemon E2E)", () => { test("emits relay-only offer URL with stable serverId", async () => { process.env.PASEO_PRIMARY_LAN_IP = "192.168.1.12"; - const { logger, lines } = createCapturingLogger(); + const { logger } = createCapturingLogger(); const daemon = await createTestPaseoDaemon({ listen: "0.0.0.0", @@ -81,7 +89,13 @@ describe("ConnectionOfferV2 (daemon E2E)", () => { }); try { - const offerUrl = parseOfferUrlFromLogs(lines); + const offerUrl = await getPairingOfferUrl({ + paseoHome: daemon.paseoHome, + relayEnabled: daemon.config.relayEnabled, + relayEndpoint: daemon.config.relayEndpoint, + relayPublicEndpoint: daemon.config.relayPublicEndpoint, + appBaseUrl: daemon.config.appBaseUrl, + }); expect(offerUrl.startsWith("https://app.paseo.sh/#offer=")).toBe(true); const offer = decodeOfferFromFragmentUrl(offerUrl) as { @@ -111,7 +125,7 @@ describe("ConnectionOfferV2 (daemon E2E)", () => { const tempHomeRoot = await mkdtemp(path.join(os.tmpdir(), "paseo-offer-home-")); - const { logger: logger1, lines: lines1 } = createCapturingLogger(); + const { logger: logger1 } = createCapturingLogger(); const daemon1 = await createTestPaseoDaemon({ listen: "0.0.0.0", logger: logger1, @@ -124,7 +138,13 @@ describe("ConnectionOfferV2 (daemon E2E)", () => { let staticDir2: string | null = null; try { - const offerUrl1 = parseOfferUrlFromLogs(lines1); + const offerUrl1 = await getPairingOfferUrl({ + paseoHome: daemon1.paseoHome, + relayEnabled: daemon1.config.relayEnabled, + relayEndpoint: daemon1.config.relayEndpoint, + relayPublicEndpoint: daemon1.config.relayPublicEndpoint, + appBaseUrl: daemon1.config.appBaseUrl, + }); const offer1 = decodeOfferFromFragmentUrl(offerUrl1) as { serverId: string; daemonPublicKeyB64: string; @@ -133,7 +153,7 @@ describe("ConnectionOfferV2 (daemon E2E)", () => { await daemon1.close(); - const { logger: logger2, lines: lines2 } = createCapturingLogger(); + const { logger: logger2 } = createCapturingLogger(); const daemon2 = await createTestPaseoDaemon({ listen: "0.0.0.0", logger: logger2, @@ -144,7 +164,13 @@ describe("ConnectionOfferV2 (daemon E2E)", () => { staticDir2 = daemon2.staticDir; try { - const offerUrl2 = parseOfferUrlFromLogs(lines2); + const offerUrl2 = await getPairingOfferUrl({ + paseoHome: daemon2.paseoHome, + relayEnabled: daemon2.config.relayEnabled, + relayEndpoint: daemon2.config.relayEndpoint, + relayPublicEndpoint: daemon2.config.relayPublicEndpoint, + appBaseUrl: daemon2.config.appBaseUrl, + }); const offer2 = decodeOfferFromFragmentUrl(offerUrl2) as { serverId: string; daemonPublicKeyB64: string; @@ -207,12 +233,6 @@ describe("ConnectionOfferV2 (daemon E2E)", () => { stdoutLines.push(text); for (const line of text.split("\n")) { if (!line.trim()) continue; - if (line.includes("pairing_offer")) { - clearTimeout(timeout); - reject(new Error("unexpected pairing_offer log when --no-relay is set")); - return; - } - try { const parsed = JSON.parse(line) as { msg?: string }; if (parsed.msg !== `Server listening on http://0.0.0.0:${port}`) continue; diff --git a/packages/server/src/server/daemon-e2e/relay-transport.e2e.test.ts b/packages/server/src/server/daemon-e2e/relay-transport.e2e.test.ts index ecc474c65..6241d7578 100644 --- a/packages/server/src/server/daemon-e2e/relay-transport.e2e.test.ts +++ b/packages/server/src/server/daemon-e2e/relay-transport.e2e.test.ts @@ -7,6 +7,7 @@ import path from "node:path"; import { spawn, type ChildProcess } from "node:child_process"; import { Buffer } from "node:buffer"; +import { generateLocalPairingOffer } from "../pairing-offer.js"; import { createTestPaseoDaemon } from "../test-utils/paseo-daemon.js"; import { createClientChannel, type Transport } from "@getpaseo/relay/e2ee"; import { buildRelayWebSocketUrl } from "../../shared/daemon-endpoints.js"; @@ -26,19 +27,25 @@ function createCapturingLogger() { return { logger, lines }; } -function parseOfferUrlFromLogs(lines: string[]): string { - for (const line of lines) { - if (!line.includes("pairing_offer")) continue; - try { - const obj = JSON.parse(line) as { msg?: string; url?: string }; - if (obj.msg === "pairing_offer" && typeof obj.url === "string") { - return obj.url; - } - } catch { - // ignore - } +async function getPairingOfferUrl(args: { + paseoHome: string; + relayEnabled?: boolean; + relayEndpoint?: string; + relayPublicEndpoint?: string; + appBaseUrl?: string; +}): Promise { + const pairing = await generateLocalPairingOffer({ + paseoHome: args.paseoHome, + relayEnabled: args.relayEnabled, + relayEndpoint: args.relayEndpoint, + relayPublicEndpoint: args.relayPublicEndpoint, + appBaseUrl: args.appBaseUrl, + includeQr: false, + }); + if (!pairing.url) { + throw new Error("Expected relay pairing URL to be available"); } - throw new Error(`pairing_offer log not found. saw ${lines.length} lines`); + return pairing.url; } function decodeOfferFromFragmentUrl(url: string): { @@ -199,7 +206,13 @@ async function waitForRelayWebSocketReady(port: number, timeout = 60000): Promis }); try { - const offerUrl = parseOfferUrlFromLogs(lines); + const offerUrl = await getPairingOfferUrl({ + paseoHome: daemon.paseoHome, + relayEnabled: daemon.config.relayEnabled, + relayEndpoint: daemon.config.relayEndpoint, + relayPublicEndpoint: daemon.config.relayPublicEndpoint, + appBaseUrl: daemon.config.appBaseUrl, + }); const { serverId, daemonPublicKeyB64 } = decodeOfferFromFragmentUrl(offerUrl); const stableClientId = `cid_test_${Date.now().toString(36)}_${Math.random().toString(36).slice(2)}`; @@ -314,7 +327,13 @@ async function waitForRelayWebSocketReady(port: number, timeout = 60000): Promis }); try { - const offerUrl = parseOfferUrlFromLogs(lines); + const offerUrl = await getPairingOfferUrl({ + paseoHome: daemon.paseoHome, + relayEnabled: daemon.config.relayEnabled, + relayEndpoint: daemon.config.relayEndpoint, + relayPublicEndpoint: daemon.config.relayPublicEndpoint, + appBaseUrl: daemon.config.appBaseUrl, + }); const { serverId, daemonPublicKeyB64 } = decodeOfferFromFragmentUrl(offerUrl); // Previously, the daemon would time out waiting for `hello` and reconnect every ~10s.