Files
Mohamed BoudraandGitHub d2de309fc3 Make relay opt-in when pairing devices (#2706)
* feat: make relay opt-in for device pairing

* fix(relay): keep pairing state authoritative

* fix(relay): refresh pairing state across transports

* fix(onboard): honor explicit relay opt-out

* fix(relay): surface transport startup failures

* fix(pairing): gate offers for older daemons

* test(cli): rely on real pairing transport coverage

* fix(cli): report live relay without a pid file

* fix(relay): synchronize config during daemon startup

* fix(relay): preserve mutability for ignored overrides

* fix(cli): use reachable daemon for pairing

* fix(app): redirect invalid pair device routes

* fix(pairing): support remote host device pairing

Keep Pair Device host-relative through the existing settings and daemon RPC flow. Route CLI output through an injected adapter so workflow tests do not mutate process globals.

* fix(pairing): validate pairing destinations

Expose the host-relative Pair Device flow on every supported app platform. Reject a reachable daemon whose identity does not match the requested Paseo home before reading or mutating its pairing state.

* fix(pairing): render QR codes across platforms

Generate SVG text without DOM or canvas and render it through the existing cross-platform SVG stack.

* fix(cli): pair from persisted relay state

Allow explicit relay pairing while the daemon is stopped when relay is already enabled in persisted configuration.
2026-08-01 04:35:06 +08:00

105 lines
4.1 KiB
TypeScript

#!/usr/bin/env npx tsx
import assert from "node:assert";
import { readFile, mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { $ } from "zx";
import { getAvailablePort } from "./helpers/network.ts";
$.verbose = false;
console.log("=== Onboarding Command ===\n");
const paseoHome = await mkdtemp(join(tmpdir(), "paseo-onboard-home-"));
const port = await getAvailablePort();
try {
console.log("Test 1: `paseo` runs blocking onboarding without implicit relay pairing");
const onboard =
await $`PASEO_HOME=${paseoHome} PASEO_LISTEN=127.0.0.1:${port} PASEO_PAIRING_QR=0 npx paseo`.nothrow();
assert.strictEqual(
onboard.exitCode,
0,
`onboard should succeed:\nstdout:\n${onboard.stdout}\nstderr:\n${onboard.stderr}`,
);
assert(!onboard.stdout.includes("Scan to pair"), "onboard output should not include scan header");
assert(!onboard.stdout.includes("#offer="), "onboard output should not include a pairing offer");
assert(
onboard.stdout.includes("Daemon is running with relay off"),
"onboard output should explain the direct connection path",
);
assert(
onboard.stdout.includes("CLI quick reference"),
"onboard output should include CLI quick reference",
);
assert(onboard.stdout.includes("paseo --help"), "onboard output should include --help shortcut");
assert(onboard.stdout.includes("paseo ls"), "onboard output should include ls shortcut");
assert(
onboard.stdout.includes('paseo run "your prompt"'),
"onboard output should include run shortcut",
);
assert(onboard.stdout.includes("paseo status"), "onboard output should include status shortcut");
assert(
onboard.stdout.includes(join(paseoHome, "daemon.log")),
"onboard output should include daemon log path",
);
const status =
await $`PASEO_HOME=${paseoHome} npx paseo daemon status --home ${paseoHome}`.nothrow();
assert.strictEqual(status.exitCode, 0, `daemon status should succeed: ${status.stderr}`);
assert(status.stdout.includes("running"), "daemon should be running when onboarding exits");
console.log("✓ onboarding keeps relay disabled and waits for daemon readiness\n");
console.log("Test 2: --no-relay suppresses pairing for an already-running daemon");
const enableRelay =
await $`PASEO_HOME=${paseoHome} npx paseo daemon pair --home ${paseoHome} --relay`.nothrow();
assert.strictEqual(enableRelay.exitCode, 0, `relay enable should succeed: ${enableRelay.stderr}`);
assert(enableRelay.stdout.includes("#offer="), "relay enable should produce a pairing offer");
const noRelayOnboard =
await $`PASEO_HOME=${paseoHome} PASEO_LISTEN=127.0.0.1:${port} npx paseo --no-relay`.nothrow();
assert.strictEqual(
noRelayOnboard.exitCode,
0,
`--no-relay onboarding should succeed: ${noRelayOnboard.stderr}`,
);
assert(
!noRelayOnboard.stdout.includes("#offer="),
"--no-relay onboarding should not include a pairing offer",
);
console.log("✓ --no-relay suppresses pairing for an already-running daemon\n");
console.log("Test 3: non-interactive onboarding persists voice disabled config");
const configRaw = await readFile(join(paseoHome, "config.json"), "utf-8");
const config = JSON.parse(configRaw) as {
features?: {
dictation?: { enabled?: boolean };
voiceMode?: { enabled?: boolean };
};
};
assert.strictEqual(
config.features?.dictation?.enabled,
false,
"dictation.enabled should be false",
);
assert.strictEqual(
config.features?.voiceMode?.enabled,
false,
"voiceMode.enabled should be false",
);
const daemonLog = await readFile(join(paseoHome, "daemon.log"), "utf-8");
assert(
!daemonLog.includes("Ensuring local speech models"),
"daemon should not attempt local speech model setup when voice is disabled",
);
console.log("✓ non-interactive run persisted voice disabled choices\n");
} finally {
await $`PASEO_HOME=${paseoHome} npx paseo daemon stop --home ${paseoHome} --force`.nothrow();
await rm(paseoHome, { recursive: true, force: true });
}
console.log("=== Onboarding tests passed ===");