Refactor daemon connection probe tests (#886)

This commit is contained in:
Mohamed Boudra
2026-05-10 08:20:16 +00:00
committed by GitHub
parent 36e54a097e
commit ca11fc667b
2 changed files with 196 additions and 143 deletions
@@ -1,154 +1,142 @@
import { describe, expect, it, vi, beforeEach } from "vitest";
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { DaemonClientConfig } from "@server/client/daemon-client";
import type { DaemonConnectionDependencies, DaemonProbeClient } from "./test-daemon-connection";
const daemonClientMock = vi.hoisted(() => {
const createdConfigs: Array<{ clientId?: string; url?: string; password?: string }> = [];
let nextConnectError: Error | null = null;
let nextLastError: string | null = null;
class FakeDaemonClient implements DaemonProbeClient {
readonly lastError: string | null;
class MockDaemonClient {
public lastError: string | null = nextLastError;
private lastServerInfo = {
status: "server_info" as const,
serverId: "srv_probe_test",
hostname: "probe-host" as string | null,
version: "0.0.0",
};
constructor(
private readonly probe: FakeDaemonProbe,
readonly config: DaemonClientConfig,
) {
this.lastError = probe.nextLastError;
}
constructor(config: { clientId?: string; url?: string; password?: string }) {
createdConfigs.push(config);
}
subscribeConnectionStatus(): () => void {
return () => undefined;
}
on(): () => void {
return () => undefined;
}
async connect(): Promise<void> {
if (nextConnectError) {
throw nextConnectError;
}
return;
}
getLastServerInfoMessage() {
return this.lastServerInfo;
}
async ping(): Promise<{ rttMs: number }> {
return { rttMs: 42 };
}
async close(): Promise<void> {
return;
async connect(): Promise<void> {
if (this.probe.nextConnectError) {
throw this.probe.nextConnectError;
}
}
return {
MockDaemonClient,
createdConfigs,
setNextConnectFailure: (error: Error, lastError: string | null) => {
nextConnectError = error;
nextLastError = lastError;
getLastServerInfoMessage() {
return {
serverId: "srv_probe_test",
hostname: "probe-host",
};
}
async close(): Promise<void> {
this.probe.closedClients.push(this);
}
}
class FakeDaemonProbe {
createdClients: FakeDaemonClient[] = [];
closedClients: FakeDaemonClient[] = [];
clientIdsRequested = 0;
nextConnectError: Error | null = null;
nextLastError: string | null = null;
readonly deps: DaemonConnectionDependencies<FakeDaemonClient> = {
getClientId: async () => {
this.clientIdsRequested += 1;
return "cid_shared_probe_test";
},
reset: () => {
createdConfigs.length = 0;
nextConnectError = null;
nextLastError = null;
resolveAppVersion: () => null,
createLocalTransportFactory: () => null,
buildLocalTransportUrl: ({ transportType, transportPath }) =>
`paseo+local://${transportType}?path=${encodeURIComponent(transportPath)}`,
createClient: (config) => {
const client = new FakeDaemonClient(this, config);
this.createdClients.push(client);
return client;
},
};
});
const clientIdMock = vi.hoisted(() => ({
getOrCreateClientId: vi.fn(async () => "cid_shared_probe_test"),
}));
failNextConnection(error: Error, lastError: string | null): void {
this.nextConnectError = error;
this.nextLastError = lastError;
}
vi.mock("@server/client/daemon-client", () => ({
DaemonClient: daemonClientMock.MockDaemonClient,
}));
vi.mock("./client-id", () => ({
getOrCreateClientId: clientIdMock.getOrCreateClientId,
}));
vi.mock("@/desktop/daemon/desktop-daemon-transport", () => ({
createDesktopLocalDaemonTransportFactory: vi.fn(() => null),
buildLocalDaemonTransportUrl: vi.fn(
({
transportType,
transportPath,
}: {
transportType: "socket" | "pipe";
transportPath: string;
}) => `paseo+local://${transportType}?path=${encodeURIComponent(transportPath)}`,
),
}));
createdConfigs(): DaemonClientConfig[] {
return this.createdClients.map((client) => client.config);
}
}
describe("test-daemon-connection connectToDaemon", () => {
let probe: FakeDaemonProbe;
beforeEach(() => {
daemonClientMock.reset();
clientIdMock.getOrCreateClientId.mockClear();
vi.stubGlobal("__DEV__", false);
probe = new FakeDaemonProbe();
});
it("reuses the app clientId for direct connections", async () => {
const mod = await import("./test-daemon-connection");
const first = await mod.connectToDaemon({
id: "direct:lan:6767",
type: "directTcp",
endpoint: "lan:6767",
});
const { connectToDaemon } = await import("./test-daemon-connection");
const first = await connectToDaemon(
{
id: "direct:lan:6767",
type: "directTcp",
endpoint: "lan:6767",
},
undefined,
probe.deps,
);
await first.client.close();
const second = await mod.connectToDaemon({
id: "direct:lan:6767",
type: "directTcp",
endpoint: "lan:6767",
});
const second = await connectToDaemon(
{
id: "direct:lan:6767",
type: "directTcp",
endpoint: "lan:6767",
},
undefined,
probe.deps,
);
await second.client.close();
const [firstConfig, secondConfig] = daemonClientMock.createdConfigs;
const [firstConfig, secondConfig] = probe.createdConfigs();
expect(firstConfig?.clientId).toBe("cid_shared_probe_test");
expect(secondConfig?.clientId).toBe("cid_shared_probe_test");
expect(clientIdMock.getOrCreateClientId).toHaveBeenCalledTimes(2);
expect(probe.clientIdsRequested).toBe(2);
});
it("encodes the local socket target into the client config", async () => {
const mod = await import("./test-daemon-connection");
const result = await mod.connectToDaemon({
id: "socket:/tmp/paseo.sock",
type: "directSocket",
path: "/tmp/paseo.sock",
});
const { connectToDaemon } = await import("./test-daemon-connection");
const result = await connectToDaemon(
{
id: "socket:/tmp/paseo.sock",
type: "directSocket",
path: "/tmp/paseo.sock",
},
undefined,
probe.deps,
);
await result.client.close();
expect(daemonClientMock.createdConfigs[0]?.url).toBe(
"paseo+local://socket?path=%2Ftmp%2Fpaseo.sock",
);
expect(probe.createdConfigs()[0]?.url).toBe("paseo+local://socket?path=%2Ftmp%2Fpaseo.sock");
});
it("passes direct TCP connection passwords into the client config", async () => {
const mod = await import("./test-daemon-connection");
const result = await mod.connectToDaemon({
id: "direct:lan:6767",
type: "directTcp",
endpoint: "lan:6767",
password: "shared-secret",
});
const { connectToDaemon } = await import("./test-daemon-connection");
const result = await connectToDaemon(
{
id: "direct:lan:6767",
type: "directTcp",
endpoint: "lan:6767",
password: "shared-secret",
},
undefined,
probe.deps,
);
await result.client.close();
expect(daemonClientMock.createdConfigs[0]?.password).toBe("shared-secret");
expect(probe.createdConfigs()[0]?.password).toBe("shared-secret");
});
it("uses relay TLS from the stored connection", async () => {
const mod = await import("./test-daemon-connection");
const tlsResult = await mod.connectToDaemon(
const { connectToDaemon } = await import("./test-daemon-connection");
const tlsResult = await connectToDaemon(
{
id: "relay:wss:[::1]:443",
type: "relay",
@@ -157,10 +145,11 @@ describe("test-daemon-connection connectToDaemon", () => {
daemonPublicKeyB64: "pubkey",
},
{ serverId: "srv_probe_test" },
probe.deps,
);
await tlsResult.client.close();
const plainResult = await mod.connectToDaemon(
const plainResult = await connectToDaemon(
{
id: "relay:relay.paseo.sh:443",
type: "relay",
@@ -169,43 +158,52 @@ describe("test-daemon-connection connectToDaemon", () => {
daemonPublicKeyB64: "pubkey",
},
{ serverId: "srv_probe_test" },
probe.deps,
);
await plainResult.client.close();
expect(daemonClientMock.createdConfigs[0]?.url).toMatch(/^wss:\/\/\[::1\]\/ws\?/);
expect(daemonClientMock.createdConfigs[1]?.url).toMatch(/^ws:\/\/relay\.paseo\.sh:443\/ws\?/);
expect(probe.createdConfigs()[0]?.url).toMatch(/^wss:\/\/\[::1\]\/ws\?/);
expect(probe.createdConfigs()[1]?.url).toMatch(/^ws:\/\/relay\.paseo\.sh:443\/ws\?/);
});
it("surfaces auth rejection as an incorrect password", async () => {
const mod = await import("./test-daemon-connection");
daemonClientMock.setNextConnectFailure(
const { connectToDaemon } = await import("./test-daemon-connection");
probe.failNextConnection(
new Error("Transport closed (code 4001)"),
"Transport closed (code 4001)",
);
await expect(
mod.connectToDaemon({
id: "direct:lan:6767",
type: "directTcp",
endpoint: "lan:6767",
password: "wrong-secret",
}),
connectToDaemon(
{
id: "direct:lan:6767",
type: "directTcp",
endpoint: "lan:6767",
password: "wrong-secret",
},
undefined,
probe.deps,
),
).rejects.toMatchObject({
message: "Incorrect password",
});
});
it("keeps generic transport failures generic when a password was supplied", async () => {
const mod = await import("./test-daemon-connection");
daemonClientMock.setNextConnectFailure(new Error("Transport error"), "Transport error");
const { connectToDaemon } = await import("./test-daemon-connection");
probe.failNextConnection(new Error("Transport error"), "Transport error");
await expect(
mod.connectToDaemon({
id: "direct:lan:6767",
type: "directTcp",
endpoint: "lan:6767",
password: "shared-secret",
}),
connectToDaemon(
{
id: "direct:lan:6767",
type: "directTcp",
endpoint: "lan:6767",
password: "shared-secret",
},
undefined,
probe.deps,
),
).rejects.toMatchObject({
message: "Transport error",
});
@@ -13,6 +13,34 @@ import {
createDesktopLocalDaemonTransportFactory,
} from "@/desktop/daemon/desktop-daemon-transport";
export interface DaemonProbeClient {
readonly lastError: string | null;
connect(): Promise<void>;
close(): Promise<void>;
getLastServerInfoMessage(): { serverId: string; hostname: string | null } | null;
}
interface LocalTransportUrlInput {
transportType: "socket" | "pipe";
transportPath: string;
}
export interface DaemonConnectionDependencies<TClient extends DaemonProbeClient> {
getClientId(): Promise<string>;
resolveAppVersion(): string | null;
createLocalTransportFactory(): DaemonClientConfig["transportFactory"] | null;
buildLocalTransportUrl(input: LocalTransportUrlInput): string;
createClient(config: DaemonClientConfig): TClient;
}
const defaultDaemonConnectionDependencies: DaemonConnectionDependencies<DaemonClient> = {
getClientId: getOrCreateClientId,
resolveAppVersion,
createLocalTransportFactory: createDesktopLocalDaemonTransportFactory,
buildLocalTransportUrl: buildLocalDaemonTransportUrl,
createClient: (config) => new DaemonClient(config),
};
function normalizeNonEmptyString(value: unknown): string | null {
if (typeof value !== "string") return null;
const trimmed = value.trim();
@@ -69,13 +97,17 @@ export class DaemonConnectionTestError extends Error {
export async function buildClientConfig(
connection: HostConnection,
serverId?: string,
deps: Pick<
DaemonConnectionDependencies<DaemonProbeClient>,
"getClientId" | "resolveAppVersion" | "createLocalTransportFactory" | "buildLocalTransportUrl"
> = defaultDaemonConnectionDependencies,
): Promise<DaemonClientConfig> {
const clientId = await getOrCreateClientId();
const localTransportFactory = createDesktopLocalDaemonTransportFactory();
const clientId = await deps.getClientId();
const localTransportFactory = deps.createLocalTransportFactory();
const base = {
clientId,
clientType: "mobile" as const,
appVersion: resolveAppVersion() ?? undefined,
appVersion: deps.resolveAppVersion() ?? undefined,
suppressSendErrors: true,
reconnect: { enabled: false },
...((connection.type === "directSocket" || connection.type === "directPipe") &&
@@ -87,7 +119,7 @@ export async function buildClientConfig(
if (connection.type === "directSocket" || connection.type === "directPipe") {
return {
...base,
url: buildLocalDaemonTransportUrl({
url: deps.buildLocalTransportUrl({
transportType: connection.type === "directSocket" ? "socket" : "pipe",
transportPath: connection.path,
}),
@@ -120,10 +152,23 @@ export async function buildClientConfig(
export function connectAndProbe(
config: DaemonClientConfig,
timeoutMs: number,
): Promise<{ client: DaemonClient; serverId: string; hostname: string | null }> {
const client = new DaemonClient(config);
): Promise<{ client: DaemonClient; serverId: string; hostname: string | null }>;
export function connectAndProbe<TClient extends DaemonProbeClient>(
config: DaemonClientConfig,
timeoutMs: number,
deps: Pick<DaemonConnectionDependencies<TClient>, "createClient">,
): Promise<{ client: TClient; serverId: string; hostname: string | null }>;
export function connectAndProbe(
config: DaemonClientConfig,
timeoutMs: number,
deps: Pick<
DaemonConnectionDependencies<DaemonProbeClient>,
"createClient"
> = defaultDaemonConnectionDependencies,
): Promise<{ client: DaemonProbeClient; serverId: string; hostname: string | null }> {
const client = deps.createClient(config);
return new Promise<{ client: DaemonClient; serverId: string; hostname: string | null }>(
return new Promise<{ client: DaemonProbeClient; serverId: string; hostname: string | null }>(
(resolve, reject) => {
const timer = setTimeout(() => {
void client.close().catch(() => undefined);
@@ -183,10 +228,20 @@ function resolveTimeout(connection: HostConnection, options?: ProbeOptions): num
return connection.type === "relay" ? 10_000 : 6_000;
}
export function connectToDaemon(
connection: HostConnection,
options?: ProbeOptions,
): Promise<{ client: DaemonClient; serverId: string; hostname: string | null }>;
export function connectToDaemon<TClient extends DaemonProbeClient>(
connection: HostConnection,
options: ProbeOptions | undefined,
deps: DaemonConnectionDependencies<TClient>,
): Promise<{ client: TClient; serverId: string; hostname: string | null }>;
export async function connectToDaemon(
connection: HostConnection,
options?: ProbeOptions,
): Promise<{ client: DaemonClient; serverId: string; hostname: string | null }> {
const config = await buildClientConfig(connection, options?.serverId);
return connectAndProbe(config, resolveTimeout(connection, options));
deps: DaemonConnectionDependencies<DaemonProbeClient> = defaultDaemonConnectionDependencies,
): Promise<{ client: DaemonProbeClient; serverId: string; hostname: string | null }> {
const config = await buildClientConfig(connection, options?.serverId, deps);
return connectAndProbe(config, resolveTimeout(connection, options), deps);
}