Files
paseo/packages/cli/src/commands/daemon/pair.test.ts
T
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

140 lines
4.0 KiB
TypeScript

import { describe, expect, test, vi } from "vitest";
import { runPairCommand, type PairCommandOutput, type PairingOffer } from "./pair.js";
const disabledOffer: PairingOffer = { relayEnabled: false, url: null, qr: null };
const enabledOffer: PairingOffer = {
relayEnabled: true,
url: "https://app.paseo.sh/#offer=test",
qr: null,
};
interface RecordedPairCommandOutput extends PairCommandOutput {
stdout: string[];
stderr: string[];
successes: string[];
exitCode: number | undefined;
}
function createRecordedOutput(): RecordedPairCommandOutput {
return {
columns: 80,
stdout: [],
stderr: [],
successes: [],
exitCode: undefined,
writeStdout(message) {
this.stdout.push(message);
},
writeStderr(message) {
this.stderr.push(message);
},
setExitCode(code) {
this.exitCode = code;
},
success(message) {
this.successes.push(message);
},
};
}
describe("daemon pair workflow", () => {
test("interactive decline prints direct guidance and creates no pairing output", async () => {
const resolveOffer = vi.fn(async () => disabledOffer);
const confirmRelay = vi.fn(async () => false);
const printDirectGuidance = vi.fn();
const output = createRecordedOutput();
await runPairCommand(
{},
{ resolveOffer, confirmRelay, printDirectGuidance, isInteractive: () => true, output },
);
expect(confirmRelay).toHaveBeenCalledOnce();
expect(printDirectGuidance).toHaveBeenCalledOnce();
expect(output.stderr.join("")).toContain("No pairing QR was created");
expect(output.exitCode).toBe(1);
});
test("interactive consent enables relay and prints the refreshed offer", async () => {
const resolveOffer = vi
.fn<(options: { paseoHome: string; enableRelay?: boolean }) => Promise<PairingOffer>>()
.mockResolvedValueOnce(disabledOffer)
.mockResolvedValueOnce(enabledOffer);
const output = createRecordedOutput();
await runPairCommand(
{},
{
resolveOffer,
confirmRelay: async () => true,
printDirectGuidance: vi.fn(),
isInteractive: () => true,
output,
},
);
expect(resolveOffer).toHaveBeenNthCalledWith(2, expect.objectContaining({ enableRelay: true }));
expect(output.stdout.join("")).toContain(enabledOffer.url ?? "");
expect(output.successes).toEqual(["Relay enabled"]);
});
test("JSON mode never prompts and returns a structured relay-disabled error", async () => {
const confirmRelay = vi.fn(async () => true);
const output = createRecordedOutput();
await runPairCommand(
{ json: true },
{
resolveOffer: async () => disabledOffer,
confirmRelay,
printDirectGuidance: vi.fn(),
isInteractive: () => true,
output,
},
);
expect(confirmRelay).not.toHaveBeenCalled();
expect(output.stderr.join("")).toContain('"code":"RELAY_DISABLED"');
expect(output.exitCode).toBe(1);
});
test("explicit relay opts in without prompting", async () => {
const resolveOffer = vi.fn(async () => enabledOffer);
const confirmRelay = vi.fn(async () => false);
const output = createRecordedOutput();
await runPairCommand(
{ relay: true, json: true },
{
resolveOffer,
confirmRelay,
printDirectGuidance: vi.fn(),
isInteractive: () => false,
output,
},
);
expect(resolveOffer).toHaveBeenCalledWith(expect.objectContaining({ enableRelay: true }));
expect(confirmRelay).not.toHaveBeenCalled();
expect(output.exitCode).toBeUndefined();
});
test("surfaces launch-override rejection", async () => {
await expect(
runPairCommand(
{ relay: true },
{
resolveOffer: async () => {
throw new Error("Relay is controlled by a daemon launch override");
},
confirmRelay: vi.fn(),
printDirectGuidance: vi.fn(),
isInteractive: () => false,
output: createRecordedOutput(),
},
),
).rejects.toThrow("launch override");
});
});