import { afterEach, describe, expect, it, vi } from "vitest"; import type { AppStateStatus } from "react-native"; import { bindHostRuntimeAppState, startHostRuntimeBootstrap, } from "@/navigation/host-runtime-bootstrap"; import type { DaemonClient, ConnectionState, FetchAgentsEntry, FetchAgentsOptions, } from "@getpaseo/client/internal/daemon-client"; import { DaemonAuthenticationError } from "@getpaseo/client/internal/daemon-client"; import type { ConnectionOffer } from "@getpaseo/protocol/connection-offer"; import type { SessionOutboundMessage } from "@getpaseo/protocol/messages"; import type { AgentPermissionRequest } from "@getpaseo/protocol/agent-types"; import type { HostConnection, HostProfile } from "@/types/host-connection"; import { defaultHostAppearance } from "@/hosts/appearance"; import { useSessionStore, type Agent } from "@/stores/session-store"; import { normalizeAgentSnapshot } from "@/utils/agent-snapshots"; import { isAgentArchiving, setAgentArchiving } from "@/hooks/use-archive-agent"; import { queryClient } from "@/data/query-client"; import { HostRuntimeController, HostRuntimeStore, readInitialDaemonConnectionHint, type HostRuntimeControllerDeps, type HostRuntimeStorage, } from "./host-runtime"; import type { ReplicaRow, ReplicaRowStore } from "./replica-cache/row-store"; import { subscriptionFixture } from "./subscription-fixture"; import { readDesktopManagedLocalCredential } from "@/desktop/daemon/local-credential"; import type { HostConfirmationRequest } from "./host-confirmation"; it("requests the managed connection credential through desktop main without a web hint", async () => { const requests: string[] = []; const connection: HostConnection = { id: "direct:localhost:6767", type: "directTcp", endpoint: "localhost:6767", }; let token = "local-token"; const invoke = async (listen: string) => { requests.push(listen); return token; }; expect(await readDesktopManagedLocalCredential(connection, invoke)).toBe("local-token"); token = "rotated-token"; expect(await readDesktopManagedLocalCredential(connection, invoke)).toBe("rotated-token"); expect(requests).toEqual(["localhost:6767", "localhost:6767"]); }); class FakeDaemonClient { supportsBackgroundWorkspaces(): boolean { return false; } private state: ConnectionState = { status: "idle" }; private listeners = new Set<(status: ConnectionState) => void>(); private error: string | null = null; authFailureReason: "password_required" | "incorrect_password" | null = null; private heartbeatRttMs: number | null = null; private latencyMeasurementFailure: Error | null = null; private latencyMeasurementsRequested: Array<{ timeoutMs?: number }> = []; public connectCalls = 0; public ensureConnectedCalls = 0; public connectionVerifications = 0; public reconnectEnabledChanges: boolean[] = []; public fetchAgentsCalls: FetchAgentsOptions[] = []; public fetchAgentsResponses: Array< Awaited> | ReturnType > = []; public sentAgentMessages: Array> = []; public sendAgentMessageFailures: Error[] = []; public sendAgentMessageResponses: Promise[] = []; private agentUpdateListeners = new Set< (message: Extract) => void >(); private fetchWaiters = new Set<() => void>(); private agentListenerWaiters = new Set<() => void>(); private sentMessageWaiters = new Set<() => void>(); on( type: "agent_update", listener: (message: Extract) => void, ): () => void { if (type === "agent_update") this.agentUpdateListeners.add(listener); for (const waiter of this.agentListenerWaiters) waiter(); return () => this.agentUpdateListeners.delete(listener); } observeAgents(options: Parameters[0]) { return subscriptionFixture(this.fetchAgents({ ...options, subscribe: {} }), (receive) => this.on("agent_update", receive), ); } observeEvents() { return subscriptionFixture(Promise.resolve({}), () => () => {}); } async waitForAgentUpdates(): Promise { if (this.agentUpdateListeners.size > 0) return; await new Promise((resolve) => { const waiter = () => { if (this.agentUpdateListeners.size === 0) return; this.agentListenerWaiters.delete(waiter); resolve(); }; this.agentListenerWaiters.add(waiter); }); } agentUpdate(payload: Extract["payload"]): void { for (const listener of this.agentUpdateListeners) { listener({ type: "agent_update", payload }); } } async connect(): Promise { this.connectCalls += 1; this.setConnectionState({ status: "connected" }); } async close(): Promise { this.setConnectionState({ status: "disconnected", reason: "client_closed" }); } async sendAgentMessage( ...args: Parameters ): ReturnType { this.sentAgentMessages.push(args); for (const waiter of this.sentMessageWaiters) waiter(); const response = this.sendAgentMessageResponses.shift(); if (response) await response; const failure = this.sendAgentMessageFailures.shift(); if (failure) throw failure; } async waitForSentMessages(count: number): Promise { if (this.sentAgentMessages.length >= count) return; await new Promise((resolve) => { const waiter = () => { if (this.sentAgentMessages.length < count) return; this.sentMessageWaiters.delete(waiter); resolve(); }; this.sentMessageWaiters.add(waiter); }); } ensureConnected(options?: { verify?: boolean }): void { this.ensureConnectedCalls += 1; if (options?.verify) this.connectionVerifications += 1; if (this.state.status !== "connected") { this.setConnectionState({ status: "connected" }); } } getConnectionState(): ConnectionState { return this.state; } public ownedSubscriptions = true; private daemonVersion: string | null = "0.8.0"; getLastServerInfoMessage(): ReturnType { if (this.daemonVersion === null) return null; return { status: "server_info", serverId: "srv_test", hostname: "test", version: this.daemonVersion, features: { ownedSubscriptions: this.ownedSubscriptions }, }; } // Like the real client, the server info is cleared while disconnected and replaced by the // restarted daemon's handshake before the client reports connected again. daemonRestartsAs(version: string): void { this.daemonVersion = null; this.setConnectionState({ status: "disconnected", reason: "Connection lost" }); this.daemonVersion = version; this.setConnectionState({ status: "connected" }); } subscribeConnectionStatus(listener: (status: ConnectionState) => void): () => void { this.listeners.add(listener); listener(this.state); return () => { this.listeners.delete(listener); }; } get lastError(): string | null { return this.error; } async fetchAgents( options?: FetchAgentsOptions, ): Promise>> { this.fetchAgentsCalls.push(options ?? {}); for (const waiter of this.fetchWaiters) waiter(); const queued = this.fetchAgentsResponses.shift(); if (queued) { return await queued; } return makeFetchAgentsPayload({ entries: [], subscriptionId: options?.subscribe?.subscriptionId ?? undefined, }); } async waitForFetches(count: number): Promise { if (this.fetchAgentsCalls.length >= count) return; await new Promise((resolve) => { const waiter = () => { if (this.fetchAgentsCalls.length < count) return; this.fetchWaiters.delete(waiter); resolve(); }; this.fetchWaiters.add(waiter); }); } async ping(): Promise<{ rttMs: number }> { return { rttMs: 0 }; } async measureLatency(params?: { timeoutMs?: number }): Promise { this.latencyMeasurementsRequested.push(params ?? {}); if (this.latencyMeasurementFailure) { throw this.latencyMeasurementFailure; } const result = await this.ping(); return result.rttMs; } setReconnectEnabled(enabled: boolean): void { this.reconnectEnabledChanges.push(enabled); } getLastLivenessRttMs(): number | null { return this.heartbeatRttMs; } heartbeatReportsRtt(rttMs: number | null): void { this.heartbeatRttMs = rttMs; } latencyMeasurementsFailWith(message: string): void { this.latencyMeasurementFailure = new Error(message); } latencyMeasurements(): Array<{ timeoutMs?: number }> { return this.latencyMeasurementsRequested; } clearLatencyMeasurements(): void { this.latencyMeasurementsRequested = []; } isDisposed(): boolean { return this.state.status === "disconnected" && this.state.reason === "client_closed"; } setConnectionState(next: ConnectionState): void { this.state = next; if (next.status === "disconnected") { this.error = next.reason ?? this.error; } for (const listener of this.listeners) { listener(next); } } } afterEach(() => { vi.useRealTimers(); delete (globalThis as Record).__PASEO_INITIAL_DAEMON_CONNECTION__; delete (globalThis as { window?: unknown }).window; }); function useHostRuntimeClock(): void { vi.useFakeTimers({ toFake: ["Date", "setTimeout", "clearTimeout", "setInterval", "clearInterval", "performance"], }); } function makeFetchAgentsPayload(input: { entries: FetchAgentsEntry[]; hasMore?: boolean; nextCursor?: string | null; subscriptionId?: string; }): Awaited> { return { entries: input.entries, pageInfo: { nextCursor: input.nextCursor ?? null, prevCursor: null, hasMore: input.hasMore ?? false, } as Awaited>["pageInfo"], ...(input.subscriptionId ? { subscriptionId: input.subscriptionId } : {}), requestId: "req_test", }; } class Deferred { readonly promise: Promise; private resolvePromise!: (value: T) => void; private rejectPromise!: (error: Error) => void; constructor() { this.promise = new Promise((resolve, reject) => { this.resolvePromise = resolve; this.rejectPromise = reject; }); } resolve(value: T): void { this.resolvePromise(value); } reject(error: Error): void { this.rejectPromise(error); } } async function waitForHostOnline(store: HostRuntimeStore, serverId: string): Promise { if (store.getSnapshot(serverId)?.connectionStatus === "online") return; await new Promise((resolve) => { const unsubscribe = store.subscribe(serverId, () => { if (store.getSnapshot(serverId)?.connectionStatus !== "online") return; unsubscribe(); resolve(); }); }); } function makeFetchAgentsEntry(input: { id: string; cwd: string; updatedAt: string; title?: string | null; requiresAttention?: boolean; attentionReason?: "permission" | "error" | null; archivedAt?: string | null; }): FetchAgentsEntry { return { agent: { id: input.id, provider: "codex", status: "idle", createdAt: input.updatedAt, updatedAt: input.updatedAt, lastUserMessageAt: null, lastError: undefined, runtimeInfo: { provider: "codex", sessionId: null, }, capabilities: { supportsStreaming: true, supportsSessionPersistence: true, supportsDynamicModes: true, supportsMcpServers: true, supportsReasoningStream: true, supportsToolInvocations: true, }, currentModeId: null, availableModes: [], pendingPermissions: [], persistence: null, title: input.title ?? null, cwd: input.cwd, model: null, thinkingOptionId: null, requiresAttention: input.requiresAttention ?? false, attentionReason: input.attentionReason ?? null, attentionTimestamp: input.requiresAttention && input.attentionReason ? input.updatedAt : null, archivedAt: input.archivedAt ?? null, labels: {}, }, project: { projectKey: input.cwd, projectName: "workspace", checkout: { cwd: input.cwd, isGit: false, currentBranch: null, remoteUrl: null, worktreeRoot: null, isPaseoOwnedWorktree: false, mainRepoRoot: null, }, }, }; } function replicaAgent(snapshot: FetchAgentsEntry["agent"], serverId: string): Agent { return { ...normalizeAgentSnapshot(snapshot, serverId), projectPlacement: null }; } function agentPermission(id: string): AgentPermissionRequest { return { id, provider: "codex", name: id, kind: "tool", title: id }; } function makeHost(input?: Partial): HostProfile { const direct: HostConnection = { id: "direct:lan:6767", type: "directTcp", endpoint: "lan:6767", }; const relay: HostConnection = { id: "relay:relay.paseo.sh:443", type: "relay", relayEndpoint: "relay.paseo.sh:443", daemonPublicKeyB64: "pk_test", }; return { serverId: input?.serverId ?? "srv_test", ...(input?.password ? { password: input.password } : {}), label: input?.label ?? "test host", appearance: input?.appearance ?? defaultHostAppearance(), lifecycle: input?.lifecycle ?? {}, connections: input?.connections ?? [direct, relay], preferredConnectionId: input?.preferredConnectionId ?? direct.id, createdAt: input?.createdAt ?? new Date(0).toISOString(), updatedAt: input?.updatedAt ?? new Date(0).toISOString(), }; } function makeOffer(input?: Partial): ConnectionOffer { return { v: 2, serverId: input?.serverId ?? "srv_offer", daemonPublicKeyB64: input?.daemonPublicKeyB64 ?? "pk_test_offer", relay: { endpoint: input?.relay?.endpoint ?? "relay.paseo.sh:443", useTls: input?.relay?.useTls ?? false, }, }; } function encodeOfferUrl(payload: unknown): string { const encoded = Buffer.from(JSON.stringify(payload), "utf8") .toString("base64") .replace(/\+/g, "-") .replace(/\//g, "_") .replace(/=+$/g, ""); return `https://app.paseo.sh/#offer=${encoded}`; } function makeDeps( latencyByConnectionId: Record, createdClients: FakeDaemonClient[], ): HostRuntimeControllerDeps { return { createClient: () => { const client = new FakeDaemonClient(); createdClients.push(client); return client as unknown as DaemonClient; }, connectToDaemon: async ({ host, connection }) => { const readLatency = (): number => { const value = latencyByConnectionId[connection.id]; if (value instanceof Error) { throw value; } if (typeof value !== "number") { throw new Error(`missing latency for ${connection.id}`); } return value; }; readLatency(); const client = new FakeDaemonClient(); client.connectCalls = 1; client.setConnectionState({ status: "connected" }); client.ping = async () => ({ rttMs: readLatency() }); createdClients.push(client); return { client: client as unknown as DaemonClient, serverId: host.serverId, hostname: host.label ?? null, }; }, getClientId: async () => "cid_test_runtime", }; } function createDeferred() { let resolve: ((value: T | PromiseLike) => void) | null = null; let reject: ((reason?: unknown) => void) | null = null; const promise = new Promise((res, rej) => { resolve = res; reject = rej; }); return { promise, resolve: (value: T | PromiseLike) => resolve?.(value), reject: (reason?: unknown) => reject?.(reason), }; } function makeConnectedProbeClient(latencyMs: number): FakeDaemonClient { const client = new FakeDaemonClient(); client.connectCalls = 1; client.setConnectionState({ status: "connected" }); client.ping = async () => ({ rttMs: latencyMs }); return client; } /** * Answers every host confirmation the store asks, the way the sheet does, and * records each request. */ function answerHostConfirmations( store: HostRuntimeStore, answer: boolean | ((request: HostConfirmationRequest, index: number) => boolean), ): HostConfirmationRequest[] { const requests: HostConfirmationRequest[] = []; store.subscribeHostConfirmation(() => { const pending = store.getPendingHostConfirmation(); if (!pending) return; requests.push(pending); store.answerHostConfirmation( pending.id, typeof answer === "function" ? answer(pending, requests.length - 1) : answer, ); }); return requests; } function requirePendingConfirmation(store: HostRuntimeStore): HostConfirmationRequest { const pending = store.getPendingHostConfirmation(); if (!pending) throw new Error("Expected a pending host confirmation"); return pending; } function createPairingStore( input: { hostname?: string; password?: string; connectAttempts?: string[]; serverIdForHost?: (host: HostProfile) => string; storage?: HostRuntimeStorage; } = {}, ): HostRuntimeStore { return new HostRuntimeStore({ storage: input.storage ?? createMemoryHostRuntimeStorage(), deps: { createClient: () => new FakeDaemonClient() as unknown as DaemonClient, connectToDaemon: async ({ host, connection }) => { input.connectAttempts?.push(connection.id); if (input.password && host.password !== input.password) { throw new DaemonAuthenticationError("password_required"); } return { client: makeConnectedProbeClient(5) as unknown as DaemonClient, serverId: input.serverIdForHost?.(host) ?? host.serverId, hostname: input.hostname ?? "paired", }; }, getClientId: async () => "cid_pairing", readInitialConnectionHint: () => null, }, }); } function createMemoryHostRuntimeStorage(entries: Record = {}): HostRuntimeStorage { const values = new Map(Object.entries(entries)); return { getItem: async (key) => values.get(key) ?? null, setItem: async (key, value) => { values.set(key, value); }, removeItem: async (key) => { values.delete(key); }, }; } function createMemoryReplicaRowStore(): ReplicaRowStore { const rows = new Map(); const keyOf = (row: Pick) => `${row.serverId}:${row.kind}:${row.id}`; return { open: async () => undefined, read: async (serverId, kinds, ids) => { const acceptedKinds = new Set(kinds); const acceptedIds = ids ? new Set(ids) : null; return [...rows.values()].filter( (row) => row.serverId === serverId && acceptedKinds.has(row.kind) && (!acceptedIds || acceptedIds.has(row.id)), ); }, readAll: async () => { const hosts = new Map(); for (const row of rows.values()) { const hostRows = hosts.get(row.serverId) ?? []; hostRows.push(row); hosts.set(row.serverId, hostRows); } return [...hosts].map(([serverId, hostRows]) => ({ serverId, rows: hostRows })); }, apply: async (changes) => { for (const key of changes.deletes) rows.delete(keyOf(key)); for (const row of changes.upserts) rows.set(keyOf(row), row); }, deleteHost: async (serverId) => { for (const [key, row] of rows) if (row.serverId === serverId) rows.delete(key); }, renameHost: async (oldServerId, newServerId) => { for (const [key, row] of rows) { if (row.serverId !== oldServerId) continue; rows.delete(key); const renamed = { ...row, serverId: newServerId }; rows.set(keyOf(renamed), renamed); } }, clear: async () => rows.clear(), }; } function createAppearanceStore(storage: HostRuntimeStorage): HostRuntimeStore { return new HostRuntimeStore({ storage, deps: { createClient: () => { throw new Error("createClient should not be called"); }, connectToDaemon: async () => { throw new Error("connectToDaemon should not be called"); }, getClientId: async () => "cid_test_appearance", }, }); } function onceHostListMatches(store: HostRuntimeStore, predicate: () => boolean): Promise { if (predicate()) { return Promise.resolve(); } return new Promise((resolve) => { let unsubscribe = (): void => {}; unsubscribe = store.subscribeHostList(() => { if (!predicate()) { return; } unsubscribe(); resolve(); }); }); } function onceHostSnapshotMatches( store: HostRuntimeStore, serverId: string, predicate: (snapshot: ReturnType) => boolean, ): Promise { if (predicate(store.getSnapshot(serverId))) return Promise.resolve(); return new Promise((resolve) => { const unsubscribe = store.subscribe(serverId, () => { if (!predicate(store.getSnapshot(serverId))) return; unsubscribe(); resolve(); }); }); } class BrowserClientLifecycle { public active: Array<{ serverId: string; connectionId: string }> = []; mount(input: { host: HostProfile; connection: HostConnection }): () => void { const entry = { serverId: input.host.serverId, connectionId: input.connection.id }; this.active.push(entry); return () => { this.active = this.active.filter((current) => current !== entry); }; } } describe("HostRuntimeController", () => { it("publishes an old host and mounts observations through the client interface", async () => { const host = makeHost(); const client = new FakeDaemonClient(); client.ownedSubscriptions = false; let mounts = 0; const controller = new HostRuntimeController({ host, deps: { createClient: () => client as unknown as DaemonClient, connectToDaemon: async () => ({ client: client as unknown as DaemonClient, serverId: host.serverId, hostname: null, }), getClientId: async () => "app-version-gate", mountClientHandlers: () => { mounts++; return () => {}; }, }, }); const publishedClients: Array = []; const stop = controller.subscribe(() => publishedClients.push(controller.getSnapshot().client)); await controller.activateConnection({ connectionId: host.connections[0]!.id }); expect(controller.getSnapshot()).toMatchObject({ connectionStatus: "online", client, lastError: null, }); expect(mounts).toBe(1); expect(publishedClients).toContain(client); expect(client.fetchAgentsCalls).toEqual([]); stop(); await controller.stop(); }); it("replaces the active relay client when re-pairing changes the daemon public key", async () => { const oldRelay: HostConnection = { id: "relay:wss:relay.paseo.sh:443", type: "relay", relayEndpoint: "relay.paseo.sh:443", useTls: true, daemonPublicKeyB64: "pk_old", }; const newRelay: HostConnection = { ...oldRelay, daemonPublicKeyB64: "pk_new", }; const createdClients: Array<{ client: FakeDaemonClient; connection: HostConnection }> = []; const controller = new HostRuntimeController({ host: makeHost({ connections: [oldRelay], preferredConnectionId: oldRelay.id, }), deps: { createClient: ({ connection }) => { const client = new FakeDaemonClient(); createdClients.push({ client, connection }); return client as unknown as DaemonClient; }, connectToDaemon: async ({ host, connection }) => ({ client: makeConnectedProbeClient(5) as unknown as DaemonClient, serverId: host.serverId, hostname: connection.id, }), getClientId: async () => "cid_test_runtime", }, }); await controller.activateConnection({ connectionId: oldRelay.id }); expect(controller.getSnapshot().client).toBe(createdClients[0]?.client); await controller.updateHost( makeHost({ connections: [newRelay], preferredConnectionId: newRelay.id, }), ); expect(createdClients.map((entry) => entry.connection)).toEqual([oldRelay, newRelay]); expect(createdClients[0]?.client.isDisposed()).toBe(true); expect(controller.getSnapshot().client).toBe(createdClients[1]?.client); }); it("keeps known hosts in connecting when a created client reports idle during connect", async () => { const host = makeHost({ connections: [ { id: "direct:lan:6767", type: "directTcp", endpoint: "lan:6767", }, ], }); const idleClient = new FakeDaemonClient(); const deps: HostRuntimeControllerDeps = { createClient: () => idleClient as unknown as DaemonClient, connectToDaemon: async () => { throw new Error("probe unavailable"); }, getClientId: async () => "cid_test_runtime", }; const controller = new HostRuntimeController({ host, deps, }); idleClient.connect = async () => { idleClient.connectCalls += 1; // Intentionally do not emit a connected state; stay in idle. }; await controller.activateConnection({ connectionId: "direct:lan:6767" }); expect(controller.getSnapshot().activeConnectionId).toBe("direct:lan:6767"); expect(controller.getSnapshot().connectionStatus).toBe("connecting"); expect(controller.getSnapshot().agentDirectoryStatus).toBe("initial_loading"); }); it("passes resolved client id into created active clients", async () => { const host = makeHost({ connections: [ { id: "direct:lan:6767", type: "directTcp", endpoint: "lan:6767", }, ], }); const seenClientIds: string[] = []; const fakeClient = new FakeDaemonClient(); const controller = new HostRuntimeController({ host, deps: { createClient: ({ clientId }) => { seenClientIds.push(clientId); return fakeClient as unknown as DaemonClient; }, connectToDaemon: async () => { throw new Error("probe unavailable"); }, getClientId: async () => "cid_runtime_stable", }, }); await controller.activateConnection({ connectionId: "direct:lan:6767" }); expect(seenClientIds).toEqual(["cid_runtime_stable"]); expect(controller.getSnapshot().connectionStatus).toBe("online"); }); it("keeps browser client lifecycle tied to the active host runtime client", async () => { const host = makeHost({ preferredConnectionId: "direct:lan:6767" }); const fakeClient = makeConnectedProbeClient(12); const lifecycle = new BrowserClientLifecycle(); const controller = new HostRuntimeController({ host, deps: { createClient: () => new FakeDaemonClient() as unknown as DaemonClient, connectToDaemon: async ({ host: hostProfile, connection }) => ({ client: makeConnectedProbeClient(10) as unknown as DaemonClient, serverId: hostProfile.serverId, hostname: connection.id, }), getClientId: async () => "cid_runtime_stable", mountClientHandlers: (input) => lifecycle.mount(input), }, }); await controller.start({ autoProbe: false, initialConnection: { connectionId: "direct:lan:6767", existingClient: fakeClient as unknown as DaemonClient, }, }); expect(lifecycle.active).toEqual([{ serverId: "srv_test", connectionId: "direct:lan:6767" }]); await controller.stop(); expect(lifecycle.active).toEqual([]); }); it("adopts the first successful probe on startup", async () => { const host = makeHost({ preferredConnectionId: "direct:lan:6767" }); const clients: FakeDaemonClient[] = []; const latencies: Record = { "direct:lan:6767": 82, "relay:relay.paseo.sh:443": 18, }; const controller = new HostRuntimeController({ host, deps: makeDeps(latencies, clients), }); await controller.start({ autoProbe: false }); const snapshot = controller.getSnapshot(); expect(snapshot.activeConnectionId).toBe("direct:lan:6767"); expect(snapshot.connectionStatus).toBe("online"); expect(clients).toHaveLength(2); expect(snapshot.client).toBe(clients[0] as unknown as DaemonClient); expect(clients[0]?.connectCalls).toBe(1); expect(clients[1]?.isDisposed()).toBe(true); }); it("activates the first successful probe without waiting for slower probes", async () => { const host = makeHost({ preferredConnectionId: "direct:lan:6767" }); const slowPing = createDeferred(); const clients: FakeDaemonClient[] = []; const controller = new HostRuntimeController({ host, deps: { createClient: () => { throw new Error("should adopt the probe client"); }, connectToDaemon: async ({ host: hostProfile, connection }) => { const client = makeConnectedProbeClient(connection.id === "direct:lan:6767" ? 12 : 30); if (connection.id === "relay:relay.paseo.sh:443") { client.ping = async () => ({ rttMs: await slowPing.promise }); } clients.push(client); return { client: client as unknown as DaemonClient, serverId: hostProfile.serverId, hostname: hostProfile.label ?? null, }; }, getClientId: async () => "cid_test_runtime", }, }); const probeCycle = controller.runProbeCycleNow(); const timeoutAt = Date.now() + 200; while (Date.now() < timeoutAt) { const snapshot = controller.getSnapshot(); if ( snapshot.activeConnectionId === "direct:lan:6767" && snapshot.connectionStatus === "online" ) { break; } await new Promise((resolve) => setTimeout(resolve, 0)); } expect(controller.getSnapshot().activeConnectionId).toBe("direct:lan:6767"); expect(controller.getSnapshot().connectionStatus).toBe("online"); slowPing.resolve(30); await probeCycle; }); it("ranks the live connection by its heartbeat RTT without pinging it again", async () => { useHostRuntimeClock(); const host = makeHost({ preferredConnectionId: "direct:lan:6767" }); const probeAttempts: string[] = []; const latencies: Record = { "direct:lan:6767": 12, "relay:relay.paseo.sh:443": 65, }; const controller = new HostRuntimeController({ host, deps: { createClient: () => { throw new Error("should adopt probe clients"); }, connectToDaemon: async ({ host: hostProfile, connection }) => { probeAttempts.push(connection.id); const value = latencies[connection.id]; if (value instanceof Error) { throw value; } if (typeof value !== "number") { throw new Error(`missing latency for ${connection.id}`); } return { client: makeConnectedProbeClient(value) as unknown as DaemonClient, serverId: hostProfile.serverId, hostname: hostProfile.label ?? null, }; }, getClientId: async () => "cid_test_runtime", }, }); await controller.start({ autoProbe: false }); expect(controller.getSnapshot().activeConnectionId).toBe("direct:lan:6767"); probeAttempts.length = 0; const activeClient = controller.getSnapshot().client as unknown as FakeDaemonClient; activeClient.heartbeatReportsRtt(42); activeClient.clearLatencyMeasurements(); activeClient.ping = async () => ({ rttMs: 9 }); await vi.advanceTimersByTimeAsync(10_000); await controller.runProbeCycleNow(); expect(probeAttempts).toEqual([]); expect(controller.getSnapshot().activeConnectionId).toBe("direct:lan:6767"); expect(controller.getSnapshot().connectionStatus).toBe("online"); expect(controller.getSnapshot().probeByConnectionId.get("direct:lan:6767")).toEqual({ status: "available", latencyMs: 42, }); expect(activeClient.latencyMeasurements()).toEqual([]); }); it("does not create a probe client for the selected connection while it reconnects", async () => { useHostRuntimeClock(); const relay: HostConnection = { id: "relay:relay.paseo.sh:443", type: "relay", relayEndpoint: "relay.paseo.sh:443", daemonPublicKeyB64: "pk_test", }; const host = makeHost({ connections: [relay], preferredConnectionId: relay.id }); const activeClient = new FakeDaemonClient(); activeClient.setConnectionState({ status: "connected" }); const probeAttempts: string[] = []; const controller = new HostRuntimeController({ host, deps: { createClient: () => { throw new Error("the existing client owns the selected connection"); }, connectToDaemon: async ({ connection }) => { probeAttempts.push(connection.id); return { client: makeConnectedProbeClient(10) as unknown as DaemonClient, serverId: host.serverId, hostname: host.label ?? null, }; }, getClientId: async () => "cid_test_runtime", }, }); await controller.start({ autoProbe: false, initialConnection: { connectionId: relay.id, existingClient: activeClient as unknown as DaemonClient, }, }); activeClient.setConnectionState({ status: "disconnected", reason: "network lost" }); await vi.advanceTimersByTimeAsync(10_000); await controller.runProbeCycleNow(); expect(probeAttempts).toEqual([]); expect(controller.getSnapshot().client).toBe(activeClient as unknown as DaemonClient); }); it("rejects probes that resolve to a different server id", async () => { const host = makeHost({ serverId: "srv_old", connections: [ { id: "direct:localhost:6767", type: "directTcp", endpoint: "localhost:6767", }, ], }); const mismatchedClient = makeConnectedProbeClient(8); const controller = new HostRuntimeController({ host, deps: { createClient: () => { throw new Error("should not create active client"); }, connectToDaemon: async () => ({ client: mismatchedClient as unknown as DaemonClient, serverId: "srv_current", hostname: "current host", }), getClientId: async () => "cid_test_runtime", }, }); await controller.start({ autoProbe: false }); expect(controller.getSnapshot().connectionStatus).toBe("connecting"); expect(controller.getSnapshot().activeConnectionId).toBeNull(); expect(controller.getSnapshot().probeByConnectionId.get("direct:localhost:6767")).toEqual({ status: "unavailable", latencyMs: null, }); expect(mismatchedClient.isDisposed()).toBe(true); }); it("keeps the live connection when one probe cycle looks slow", async () => { useHostRuntimeClock(); const host = makeHost({ preferredConnectionId: "direct:lan:6767" }); const clients: FakeDaemonClient[] = []; const latencies: Record = { "direct:lan:6767": 15, "relay:relay.paseo.sh:443": 55, }; const controller = new HostRuntimeController({ host, deps: makeDeps(latencies, clients), }); await controller.start({ autoProbe: false }); expect(controller.getSnapshot().activeConnectionId).toBe("direct:lan:6767"); const initialClient = controller.getSnapshot().client; expect(initialClient).toBeTruthy(); const activeClient = initialClient as unknown as FakeDaemonClient; activeClient.heartbeatReportsRtt(200); activeClient.latencyMeasurementsFailWith("active measurement failed"); latencies["relay:relay.paseo.sh:443"] = 42; await vi.advanceTimersByTimeAsync(120_000); await controller.runProbeCycleNow(); const snapshot = controller.getSnapshot(); expect(snapshot.activeConnectionId).toBe("direct:lan:6767"); expect(snapshot.connectionStatus).toBe("online"); expect(snapshot.client).toBe(initialClient); expect(activeClient.isDisposed()).toBe(false); }); it("does not mark the live connection unavailable before its first heartbeat resolves", async () => { useHostRuntimeClock(); const direct: HostConnection = { id: "direct:lan:6767", type: "directTcp", endpoint: "lan:6767", }; const host = makeHost({ connections: [direct], preferredConnectionId: direct.id, }); const activeClient = new FakeDaemonClient(); activeClient.setConnectionState({ status: "connected" }); activeClient.latencyMeasurementsFailWith("heartbeat has not resolved"); const controller = new HostRuntimeController({ host, deps: makeDeps({ [direct.id]: 12 }, []), }); await controller.start({ autoProbe: false, initialConnection: { connectionId: direct.id, existingClient: activeClient as unknown as DaemonClient, }, }); const snapshot = controller.getSnapshot(); expect(snapshot.activeConnectionId).toBe(direct.id); expect(snapshot.connectionStatus).toBe("online"); expect(snapshot.probeByConnectionId.get(direct.id)).toEqual({ status: "pending", latencyMs: null, }); }); it("backs off inactive connection probes while a host is online", async () => { useHostRuntimeClock(); const host = makeHost({ preferredConnectionId: "direct:lan:6767" }); const clients: FakeDaemonClient[] = []; const latencies: Record = { "direct:lan:6767": 10, "relay:relay.paseo.sh:443": 50, }; const controller = new HostRuntimeController({ host, deps: makeDeps(latencies, clients), }); await controller.start({ autoProbe: false }); expect(controller.getSnapshot().activeConnectionId).toBe("direct:lan:6767"); const activeClient = controller.getSnapshot().client as unknown as FakeDaemonClient; const initialClientCount = clients.length; const initialRelayProbe = controller .getSnapshot() .probeByConnectionId.get("relay:relay.paseo.sh:443"); latencies["direct:lan:6767"] = 12; latencies["relay:relay.paseo.sh:443"] = 25; activeClient.heartbeatReportsRtt(12); await vi.advanceTimersByTimeAsync(60_000); await controller.runProbeCycleNow(); const snapshot = controller.getSnapshot(); expect(clients.length).toBe(initialClientCount); expect(snapshot.probeByConnectionId.get("direct:lan:6767")).toEqual({ status: "available", latencyMs: 12, }); expect(snapshot.probeByConnectionId.get("relay:relay.paseo.sh:443")).toEqual(initialRelayProbe); }); it("switches only after the faster alternative wins consecutive probes", async () => { useHostRuntimeClock(); const host = makeHost({ preferredConnectionId: "direct:lan:6767" }); const clients: FakeDaemonClient[] = []; const latencies: Record = { "direct:lan:6767": 15, "relay:relay.paseo.sh:443": 60, }; const controller = new HostRuntimeController({ host, deps: makeDeps(latencies, clients), }); await controller.start({ autoProbe: false }); expect(controller.getSnapshot().activeConnectionId).toBe("direct:lan:6767"); const activeClient = controller.getSnapshot().client as unknown as FakeDaemonClient; latencies["direct:lan:6767"] = 95; latencies["relay:relay.paseo.sh:443"] = 30; activeClient.heartbeatReportsRtt(95); await vi.advanceTimersByTimeAsync(120_000); await controller.runProbeCycleNow(); expect(controller.getSnapshot().activeConnectionId).toBe("direct:lan:6767"); await vi.advanceTimersByTimeAsync(120_000); await controller.runProbeCycleNow(); expect(controller.getSnapshot().activeConnectionId).toBe("direct:lan:6767"); let switched = controller.getSnapshot().activeConnectionId === "relay:relay.paseo.sh:443"; for (let index = 0; index < 6 && !switched; index += 1) { await vi.advanceTimersByTimeAsync(120_000); await controller.runProbeCycleNow(); switched = controller.getSnapshot().activeConnectionId === "relay:relay.paseo.sh:443"; } expect(switched).toBe(true); expect(controller.getSnapshot().client).not.toBeNull(); }); it("does not switch on a transient latency spike", async () => { useHostRuntimeClock(); const host = makeHost({ preferredConnectionId: "direct:lan:6767" }); const clients: FakeDaemonClient[] = []; const latencies: Record = { "direct:lan:6767": 15, "relay:relay.paseo.sh:443": 80, }; const controller = new HostRuntimeController({ host, deps: makeDeps(latencies, clients), }); await controller.start({ autoProbe: false }); expect(controller.getSnapshot().activeConnectionId).toBe("direct:lan:6767"); const activeClient = controller.getSnapshot().client as unknown as FakeDaemonClient; latencies["direct:lan:6767"] = 100; latencies["relay:relay.paseo.sh:443"] = 20; activeClient.heartbeatReportsRtt(100); await vi.advanceTimersByTimeAsync(120_000); await controller.runProbeCycleNow(); expect(controller.getSnapshot().activeConnectionId).toBe("direct:lan:6767"); latencies["direct:lan:6767"] = 20; latencies["relay:relay.paseo.sh:443"] = 90; activeClient.heartbeatReportsRtt(20); await vi.advanceTimersByTimeAsync(120_000); await controller.runProbeCycleNow(); expect(controller.getSnapshot().activeConnectionId).toBe("direct:lan:6767"); latencies["direct:lan:6767"] = 100; latencies["relay:relay.paseo.sh:443"] = 20; activeClient.heartbeatReportsRtt(100); await vi.advanceTimersByTimeAsync(120_000); await controller.runProbeCycleNow(); expect(controller.getSnapshot().activeConnectionId).toBe("direct:lan:6767"); await vi.advanceTimersByTimeAsync(120_000); await controller.runProbeCycleNow(); expect(controller.getSnapshot().activeConnectionId).toBe("direct:lan:6767"); let switched = controller.getSnapshot().activeConnectionId === "relay:relay.paseo.sh:443"; for (let index = 0; index < 6 && !switched; index += 1) { await vi.advanceTimersByTimeAsync(120_000); await controller.runProbeCycleNow(); switched = controller.getSnapshot().activeConnectionId === "relay:relay.paseo.sh:443"; } expect(switched).toBe(true); }); it("exposes one snapshot with active connection and status from same source", async () => { const host = makeHost(); const clients: FakeDaemonClient[] = []; const latencies: Record = { "direct:lan:6767": 12, "relay:relay.paseo.sh:443": 65, }; const controller = new HostRuntimeController({ host, deps: makeDeps(latencies, clients), }); const observed = new Array>(); const unsubscribe = controller.subscribe(() => { observed.push(controller.getSnapshot()); }); await controller.start({ autoProbe: false }); clients[0]?.setConnectionState({ status: "disconnected", reason: "transport closed", }); const latest = observed[observed.length - 1]; expect(latest?.activeConnectionId).toBe("direct:lan:6767"); expect(latest?.connectionStatus).toBe("error"); expect(latest?.lastError).toBe("transport closed"); unsubscribe(); }); it("preserves transport disconnect reasons on the runtime snapshot", async () => { const host = makeHost({ connections: [ { id: "direct:lan:6767", type: "directTcp", endpoint: "lan:6767", }, ], }); const clients: FakeDaemonClient[] = []; const controller = new HostRuntimeController({ host, deps: makeDeps( { "direct:lan:6767": 12, }, clients, ), }); await controller.start({ autoProbe: false }); clients[0]?.setConnectionState({ status: "disconnected", reason: "transport closed", }); expect(controller.getSnapshot()).toMatchObject({ connectionStatus: "error", lastError: "transport closed", }); }); it("does not emit legacy typed reason-code transition logs", async () => { const infoSpy = vi.spyOn(console, "info").mockImplementation(() => undefined); try { const host = makeHost({ connections: [ { id: "direct:lan:6767", type: "directTcp", endpoint: "lan:6767", }, ], }); const clients: FakeDaemonClient[] = []; const controller = new HostRuntimeController({ host, deps: makeDeps( { "direct:lan:6767": 12, }, clients, ), }); await controller.start({ autoProbe: false }); clients[0]?.setConnectionState({ status: "disconnected", reason: "transport closed", }); const transitionPayloads = infoSpy.mock.calls .filter((call) => call[0] === "[HostRuntimeTransition]") .map((call) => call[1] as { reasonCode?: string | null }); const lastTransition = transitionPayloads[transitionPayloads.length - 1] ?? null; expect(lastTransition?.reasonCode).toBeUndefined(); } finally { infoSpy.mockRestore(); } }); it("marks directory loading on first connection before any directory sync succeeds", async () => { const host = makeHost(); const clients: FakeDaemonClient[] = []; const latencies: Record = { "direct:lan:6767": 12, "relay:relay.paseo.sh:443": 65, }; const controller = new HostRuntimeController({ host, deps: makeDeps(latencies, clients), }); await controller.start({ autoProbe: false }); const snapshot = controller.getSnapshot(); expect(snapshot.connectionStatus).toBe("online"); expect(snapshot.hasEverLoadedAgentDirectory).toBe(false); expect(snapshot.agentDirectoryStatus).toBe("initial_loading"); }); it("keeps directory ready through reconnects after the first successful directory load", async () => { const host = makeHost(); const clients: FakeDaemonClient[] = []; const latencies: Record = { "direct:lan:6767": 12, "relay:relay.paseo.sh:443": 65, }; const controller = new HostRuntimeController({ host, deps: makeDeps(latencies, clients), }); await controller.start({ autoProbe: false }); const firstEpoch = controller.getSnapshot().connectionEpoch; controller.markAgentDirectorySyncReady(); expect(controller.getSnapshot().agentDirectoryStatus).toBe("ready"); expect(controller.getSnapshot().hasEverLoadedAgentDirectory).toBe(true); clients[0]?.setConnectionState({ status: "disconnected", reason: "client_closed", }); expect(controller.getSnapshot().connectionStatus).toBe("offline"); expect(controller.getSnapshot().agentDirectoryStatus).toBe("ready"); clients[0]?.setConnectionState({ status: "connected" }); expect(controller.getSnapshot().connectionStatus).toBe("online"); expect(controller.getSnapshot().agentDirectoryStatus).toBe("ready"); expect(controller.getSnapshot().connectionEpoch).toBe(firstEpoch + 1); }); it("stores directory sync errors as non-blocking after a successful directory load", async () => { const host = makeHost(); const clients: FakeDaemonClient[] = []; const latencies: Record = { "direct:lan:6767": 12, "relay:relay.paseo.sh:443": 65, }; const controller = new HostRuntimeController({ host, deps: makeDeps(latencies, clients), }); await controller.start({ autoProbe: false }); controller.markAgentDirectorySyncReady(); controller.markAgentDirectorySyncError("bootstrap failed"); const snapshot = controller.getSnapshot(); expect(snapshot.agentDirectoryStatus).toBe("error_after_ready"); expect(snapshot.agentDirectoryError).toBe("bootstrap failed"); expect(snapshot.hasEverLoadedAgentDirectory).toBe(true); }); it("keeps online snapshots coupled to a live client reference", async () => { const host = makeHost(); const clients: FakeDaemonClient[] = []; const latencies: Record = { "direct:lan:6767": 12, "relay:relay.paseo.sh:443": 65, }; const controller = new HostRuntimeController({ host, deps: makeDeps(latencies, clients), }); const observed = new Array>(); const unsubscribe = controller.subscribe(() => { observed.push(controller.getSnapshot()); }); await controller.start({ autoProbe: false }); for (const snapshot of observed) { if (snapshot.connectionStatus === "online") { expect(snapshot.client).toBeTruthy(); } } expect(controller.getSnapshot().connectionStatus).toBe("online"); expect(controller.getSnapshot().client).toBeTruthy(); unsubscribe(); }); it("ignores stale switch failures after a newer connection is already online", async () => { const host = makeHost({ connections: [ { id: "direct:lan:6767", type: "directTcp", endpoint: "lan:6767", }, { id: "relay:relay.paseo.sh:443", type: "relay", relayEndpoint: "relay.paseo.sh:443", daemonPublicKeyB64: "pk_test", }, ], }); const firstConnectGate = createDeferred(); const createdClients: FakeDaemonClient[] = []; const deps: HostRuntimeControllerDeps = { createClient: ({ connection }) => { const client = new FakeDaemonClient(); if (connection.id === "direct:lan:6767") { client.connect = async () => { client.connectCalls += 1; await firstConnectGate.promise; throw new Error("stale direct connect failed"); }; } createdClients.push(client); return client as unknown as DaemonClient; }, connectToDaemon: async ({ host: hostProfile }) => ({ client: makeConnectedProbeClient(10) as unknown as DaemonClient, serverId: hostProfile.serverId, hostname: hostProfile.label ?? null, }), getClientId: async () => "cid_test_runtime", }; const controller = new HostRuntimeController({ host, deps, }); const waitUntil = async (predicate: () => boolean, timeoutMs = 200): Promise => { const timeoutAt = Date.now() + timeoutMs; while (!predicate()) { if (Date.now() >= timeoutAt) { throw new Error("timed out waiting for predicate"); } await new Promise((resolve) => setTimeout(resolve, 0)); } }; const switchDirect = controller.activateConnection({ connectionId: "direct:lan:6767" }); await waitUntil(() => { const snapshot = controller.getSnapshot(); return ( createdClients.length === 1 && snapshot.activeConnectionId === "direct:lan:6767" && snapshot.connectionStatus === "connecting" ); }); const switchRelay = controller.activateConnection({ connectionId: "relay:relay.paseo.sh:443", }); await waitUntil(() => { const snapshot = controller.getSnapshot(); return ( snapshot.activeConnectionId === "relay:relay.paseo.sh:443" && snapshot.connectionStatus === "online" ); }); firstConnectGate.resolve(); await Promise.allSettled([switchDirect, switchRelay]); const snapshot = controller.getSnapshot(); expect(snapshot.activeConnectionId).toBe("relay:relay.paseo.sh:443"); expect(snapshot.connectionStatus).toBe("online"); expect(snapshot.lastError).toBeNull(); expect(createdClients).toHaveLength(2); expect(createdClients[0]?.isDisposed()).toBe(true); }); it("coalesces overlapping probe cycles instead of invalidating the in-flight result", async () => { const host = makeHost({ connections: [ { id: "direct:lan:6767", type: "directTcp", endpoint: "lan:6767", }, ], }); const slowProbe = createDeferred(); let probeCalls = 0; const controller = new HostRuntimeController({ host, deps: { createClient: () => new FakeDaemonClient() as unknown as DaemonClient, connectToDaemon: async ({ host: hostProfile }) => { probeCalls += 1; const client = new FakeDaemonClient(); client.connectCalls = 1; client.setConnectionState({ status: "connected" }); client.ping = async () => { if (probeCalls === 1) { return { rttMs: await slowProbe.promise }; } throw new Error("unexpected probe call"); }; return { client: client as unknown as DaemonClient, serverId: hostProfile.serverId, hostname: hostProfile.label ?? null, }; }, getClientId: async () => "cid_test_runtime", }, }); const first = controller.runProbeCycleNow(); const second = controller.runProbeCycleNow(); expect(probeCalls).toBe(1); slowProbe.resolve(900); await Promise.all([first, second]); const probeAfterCycle = controller.getSnapshot().probeByConnectionId.get("direct:lan:6767"); expect(probeAfterCycle).toEqual({ status: "available", latencyMs: 900, }); }); it("keeps active client generation stable during background probe cycles", async () => { useHostRuntimeClock(); const host = makeHost({ connections: [ { id: "direct:lan:6767", type: "directTcp", endpoint: "lan:6767", }, ], }); const createdClients: FakeDaemonClient[] = []; const controller = new HostRuntimeController({ host, deps: { createClient: () => { const client = new FakeDaemonClient(); createdClients.push(client); return client as unknown as DaemonClient; }, connectToDaemon: async ({ host: hostProfile }) => { const client = makeConnectedProbeClient(10); return { client: client as unknown as DaemonClient, serverId: hostProfile.serverId, hostname: hostProfile.label ?? null, }; }, getClientId: async () => "cid_test_runtime", }, }); await controller.start({ autoProbe: false }); const activeClientBeforeProbes = controller.getSnapshot().client; const generationBeforeProbes = controller.getSnapshot().clientGeneration; await vi.advanceTimersByTimeAsync(10_000); await controller.runProbeCycleNow(); expect(controller.getSnapshot().client).toBe(activeClientBeforeProbes); expect(controller.getSnapshot().clientGeneration).toBe(generationBeforeProbes); expect(createdClients).toHaveLength(0); }); }); describe("HostRuntimeStore", () => { it.each(["active", "inactive", "background"] as const)( "keeps reconnect enabled through inactive/background and resumes immediately (mounted %s)", async (currentState) => { const relay = (suffix: string): HostConnection => ({ id: `relay:relay-${suffix}.paseo.sh:443`, type: "relay", relayEndpoint: `relay-${suffix}.paseo.sh:443`, daemonPublicKeyB64: `pk_${suffix}`, }); const hostAConnection = relay("a"); const hostBConnection = relay("b"); const hostA = makeHost({ serverId: "srv_a", connections: [hostAConnection], preferredConnectionId: hostAConnection.id, }); const hostB = makeHost({ serverId: "srv_b", connections: [hostBConnection], preferredConnectionId: hostBConnection.id, }); const clientA = new FakeDaemonClient(); const clientB = new FakeDaemonClient(); clientA.setConnectionState({ status: "connected" }); clientB.setConnectionState({ status: "connected" }); const store = new HostRuntimeStore({ storage: createMemoryHostRuntimeStorage(), deps: { createClient: () => { throw new Error("initial clients are supplied"); }, connectToDaemon: async () => { throw new Error("single-connection hosts reuse their active clients"); }, getClientId: async () => "cid_test_runtime", }, }); let changeAppState: (state: AppStateStatus) => void = () => { throw new Error("AppState listener not registered"); }; const unbind = bindHostRuntimeAppState(store, { currentState, addEventListener: (_event, listener) => { changeAppState = listener; return { remove: () => {} }; }, }); store.syncHosts([hostA, hostB], { initialConnectionByServerId: new Map([ [ hostA.serverId, { connectionId: hostAConnection.id, existingClient: clientA as unknown as DaemonClient, }, ], [ hostB.serverId, { connectionId: hostBConnection.id, existingClient: clientB as unknown as DaemonClient, }, ], ]), }); await waitForHostOnline(store, hostA.serverId); await waitForHostOnline(store, hostB.serverId); expect(clientA.reconnectEnabledChanges.at(-1)).toBe(true); expect(clientB.reconnectEnabledChanges.at(-1)).toBe(true); changeAppState("inactive"); expect(clientA.reconnectEnabledChanges.at(-1)).toBe(true); expect(clientB.reconnectEnabledChanges.at(-1)).toBe(true); changeAppState("background"); expect(clientA.reconnectEnabledChanges.at(-1)).toBe(true); expect(clientB.reconnectEnabledChanges.at(-1)).toBe(true); expect(clientA.getConnectionState()).toEqual({ status: "connected" }); expect(clientB.getConnectionState()).toEqual({ status: "connected" }); expect(clientA.ensureConnectedCalls).toBe(0); expect(clientB.ensureConnectedCalls).toBe(0); clientA.setConnectionState({ status: "disconnected", reason: "backgrounded" }); clientB.setConnectionState({ status: "disconnected", reason: "backgrounded" }); changeAppState("active"); expect(clientA.reconnectEnabledChanges.at(-1)).toBe(true); expect(clientB.reconnectEnabledChanges.at(-1)).toBe(true); expect(clientA.ensureConnectedCalls).toBe(1); expect(clientA.connectionVerifications).toBe(1); expect(clientB.connectionVerifications).toBe(1); expect(clientB.ensureConnectedCalls).toBe(1); expect(store.getSnapshot(hostA.serverId)?.connectionStatus).toBe("online"); expect(store.getSnapshot(hostB.serverId)?.connectionStatus).toBe("online"); unbind(); store.syncHosts([]); }, ); it("revokes push notifications before removing a host", async () => { const host = makeHost({ connections: [makeHost().connections[0]!] }); const revocation = createDeferred(); const storage = createMemoryHostRuntimeStorage({ "@paseo:daemon-registry": JSON.stringify([host]), "@paseo:e2e": "1", }); const store = new HostRuntimeStore({ storage, deps: makeDeps({}, []), revokePushNotifications: async ({ serverId }) => { expect(serverId).toBe(host.serverId); expect(store.getHosts().map((candidate) => candidate.serverId)).toEqual([host.serverId]); await revocation.promise; }, }); await store.boot(); const removal = store.removeHost(host.serverId); expect(store.getHosts().map((candidate) => candidate.serverId)).toEqual([host.serverId]); revocation.resolve(); await removal; expect(store.getHosts()).toEqual([]); }); it("revokes push notifications when removing a host's final connection", async () => { const host = makeHost({ connections: [makeHost().connections[0]!] }); const revokedServerIds: string[] = []; const storage = createMemoryHostRuntimeStorage({ "@paseo:daemon-registry": JSON.stringify([host]), "@paseo:e2e": "1", }); const store = new HostRuntimeStore({ storage, deps: makeDeps({}, []), revokePushNotifications: async ({ serverId }) => { revokedServerIds.push(serverId); }, }); await store.boot(); await store.removeConnection(host.serverId, host.connections[0]!.id); expect(revokedServerIds).toEqual([host.serverId]); expect(store.getHosts()).toEqual([]); }); it("loads the host registry and restores its cached directory", async () => { const host = makeHost(); const storage = createMemoryHostRuntimeStorage(); let fullScans = 0; const backingStore = createMemoryReplicaRowStore(); const replicaRowStore: ReplicaRowStore = { ...backingStore, read: async (...args) => { fullScans += 1; return backingStore.read(...args); }, }; await storage.setItem("@paseo:daemon-registry", JSON.stringify([host])); await storage.setItem("@paseo:e2e", "1"); const session = useSessionStore.getState(); const store = new HostRuntimeStore({ storage, replicaRowStore, deps: { createClient: () => { throw new Error("createClient should not be called"); }, connectToDaemon: async () => { throw new Error("connectToDaemon should not be called"); }, getClientId: async () => "cid_test_runtime", }, }); const registryLoaded = onceHostListMatches(store, () => store.isHostRegistryLoaded()); store.boot(); await registryLoaded; await vi.waitFor(() => expect(fullScans).toBe(1)); expect(useSessionStore.getState().sessions[host.serverId]).toMatchObject({ client: null, hasHydratedAgents: false, hasHydratedWorkspaces: false, }); expect(useSessionStore.getState().sessions[host.serverId]?.agents.size).toBe(0); expect(useSessionStore.getState().sessions[host.serverId]?.workspaces.size).toBe(0); store.syncHosts([]); session.clearSession(host.serverId); }); it("marks the host registry loaded after boot reads storage", async () => { const previousOverride = process.env.EXPO_PUBLIC_LOCAL_DAEMON; process.env.EXPO_PUBLIC_LOCAL_DAEMON = "not-an-endpoint"; const store = new HostRuntimeStore({ deps: { createClient: () => { throw new Error("createClient should not be called"); }, connectToDaemon: async () => { throw new Error("connectToDaemon should not be called"); }, getClientId: async () => "cid_test_runtime", }, }); try { let hostListNotifications = 0; let unsubscribeHostList = () => {}; const registryLoaded = new Promise((resolve) => { unsubscribeHostList = store.subscribeHostList(() => { hostListNotifications += 1; if (store.isHostRegistryLoaded()) { unsubscribeHostList(); resolve(); } }); }); store.boot(); await registryLoaded; expect(store.isHostRegistryLoaded()).toBe(true); expect(hostListNotifications).toBe(2); } finally { if (previousOverride === undefined) { delete process.env.EXPO_PUBLIC_LOCAL_DAEMON; } else { process.env.EXPO_PUBLIC_LOCAL_DAEMON = previousOverride; } } }); it("exposes the default appearance for a host stored before the field existed", async () => { const storage = createMemoryHostRuntimeStorage(); await storage.setItem( "@paseo:daemon-registry", JSON.stringify([ { serverId: "srv_legacy", label: "Legacy", connections: [ { id: "socket:/tmp/legacy.sock", type: "directSocket", path: "/tmp/legacy.sock" }, ], preferredConnectionId: "socket:/tmp/legacy.sock", }, ]), ); await storage.setItem("@paseo:e2e", "1"); const store = createAppearanceStore(storage); const registryLoaded = onceHostListMatches(store, () => store.isHostRegistryLoaded()); store.boot(); await registryLoaded; expect(store.getHosts()[0]?.appearance).toEqual({ color: "none", badgeDisplay: null }); store.syncHosts([]); }); it("records a chosen host color and writes it through to storage", async () => { const host = makeHost({ serverId: "srv_appearance", updatedAt: new Date(0).toISOString() }); const storage = createMemoryHostRuntimeStorage(); await storage.setItem("@paseo:daemon-registry", JSON.stringify([host])); await storage.setItem("@paseo:e2e", "1"); const store = createAppearanceStore(storage); const registryLoaded = onceHostListMatches(store, () => store.isHostRegistryLoaded()); store.boot(); await registryLoaded; const hostListChanged = onceHostListMatches( store, () => store.getHosts()[0]?.appearance.color === "teal", ); await store.setHostColor("srv_appearance", "teal"); await hostListChanged; const updated = store.getHosts()[0]; expect(updated?.appearance).toEqual({ color: "teal", badgeDisplay: null }); expect(updated?.updatedAt).not.toBe(host.updatedAt); const persisted = await storage.getItem("@paseo:daemon-registry"); expect(JSON.parse(persisted ?? "[]")[0].appearance).toEqual({ color: "teal", badgeDisplay: null, }); store.syncHosts([]); }); it("records a chosen badge display without disturbing the color", async () => { const host = makeHost({ serverId: "srv_appearance", appearance: { color: "amber", badgeDisplay: null }, }); const storage = createMemoryHostRuntimeStorage(); await storage.setItem("@paseo:daemon-registry", JSON.stringify([host])); await storage.setItem("@paseo:e2e", "1"); const store = createAppearanceStore(storage); const registryLoaded = onceHostListMatches(store, () => store.isHostRegistryLoaded()); store.boot(); await registryLoaded; const hostListChanged = onceHostListMatches( store, () => store.getHosts()[0]?.appearance.badgeDisplay === "icon", ); await store.setHostBadgeDisplay("srv_appearance", "icon"); await hostListChanged; expect(store.getHosts()[0]?.appearance).toEqual({ color: "amber", badgeDisplay: "icon" }); const persisted = await storage.getItem("@paseo:daemon-registry"); expect(JSON.parse(persisted ?? "[]")[0].appearance).toEqual({ color: "amber", badgeDisplay: "icon", }); store.syncHosts([]); }); it("keeps host appearance unchanged when persistence fails", async () => { const host = makeHost({ serverId: "srv_appearance" }); const storage = createMemoryHostRuntimeStorage(); await storage.setItem("@paseo:daemon-registry", JSON.stringify([host])); await storage.setItem("@paseo:e2e", "1"); const store = createAppearanceStore(storage); const registryLoaded = onceHostListMatches(store, () => store.isHostRegistryLoaded()); store.boot(); await registryLoaded; storage.setItem = async () => { throw new Error("disk full"); }; await expect(store.setHostColor("srv_appearance", "teal")).rejects.toThrow("disk full"); expect(store.getHosts()[0]?.appearance).toEqual(defaultHostAppearance()); store.syncHosts([]); }); it("serializes overlapping host appearance writes", async () => { const host = makeHost({ serverId: "srv_appearance" }); const storage = createMemoryHostRuntimeStorage(); await storage.setItem("@paseo:daemon-registry", JSON.stringify([host])); await storage.setItem("@paseo:e2e", "1"); const store = createAppearanceStore(storage); const registryLoaded = onceHostListMatches(store, () => store.isHostRegistryLoaded()); store.boot(); await registryLoaded; const firstWrite = createDeferred(); let writeCount = 0; const setItem = storage.setItem.bind(storage); storage.setItem = async (key, value) => { writeCount += 1; if (writeCount === 1) await firstWrite.promise; await setItem(key, value); }; const color = store.setHostColor("srv_appearance", "teal"); const display = store.setHostBadgeDisplay("srv_appearance", "icon"); await Promise.resolve(); expect(writeCount).toBe(1); firstWrite.resolve(); await Promise.all([color, display]); expect(store.getHosts()[0]?.appearance).toEqual({ color: "teal", badgeDisplay: "icon" }); const persistedHosts = JSON.parse((await storage.getItem("@paseo:daemon-registry")) ?? "[]"); expect(persistedHosts[0]?.appearance).toEqual({ color: "teal", badgeDisplay: "icon" }); store.syncHosts([]); }); it("tracks connection status transitions independently of agent panels", async () => { useHostRuntimeClock(); const host = makeHost({ connections: [ { id: "direct:lan:6767", type: "directTcp", endpoint: "lan:6767", }, ], }); const fakeClient = new FakeDaemonClient(); fakeClient.setConnectionState({ status: "connected" }); const store = new HostRuntimeStore({ deps: { createClient: () => fakeClient as unknown as DaemonClient, connectToDaemon: async ({ host: hostProfile }) => ({ client: fakeClient as unknown as DaemonClient, serverId: hostProfile.serverId, hostname: hostProfile.label ?? null, }), getClientId: async () => "cid_test_runtime", }, }); store.syncHosts([host]); await waitForHostOnline(store, host.serverId); await vi.advanceTimersByTimeAsync(1_500); const outageStartedAt = Date.now(); fakeClient.setConnectionState({ status: "disconnected", reason: "transport closed" }); expect(store.getConnectionStatusSince(host.serverId)).toBe(outageStartedAt); await vi.advanceTimersByTimeAsync(500); expect(store.getConnectionStatusSince(host.serverId)).toBe(outageStartedAt); const reconnectStartedAt = Date.now(); fakeClient.setConnectionState({ status: "connected" }); expect(store.getConnectionStatusSince(host.serverId)).toBe(reconnectStartedAt); store.syncHosts([]); }); it("loads an agent directory only after a consumer requests it", async () => { const host = makeHost({ connections: [ { id: "direct:lan:6767", type: "directTcp", endpoint: "lan:6767", }, ], }); const fakeClient = new FakeDaemonClient(); fakeClient.setConnectionState({ status: "connected" }); const store = new HostRuntimeStore({ deps: { createClient: () => fakeClient as unknown as DaemonClient, connectToDaemon: async ({ host: hostProfile }) => ({ client: fakeClient as unknown as DaemonClient, serverId: hostProfile.serverId, hostname: hostProfile.label ?? null, }), getClientId: async () => "cid_test_runtime", }, }); useSessionStore .getState() .initializeSession(host.serverId, fakeClient as unknown as DaemonClient, 1); useSessionStore.getState().updateSessionServerInfo(host.serverId, { serverId: host.serverId, hostname: null, version: "test", features: { workspaceMultiplicity: false }, }); store.syncHosts([host]); await waitForHostOnline(store, host.serverId); expect(fakeClient.fetchAgentsCalls).toHaveLength(0); const releaseDemand = store.acquireDirectoryDemand(host.serverId); await fakeClient.waitForFetches(1); await vi.waitFor(() => { expect(store.getSnapshot(host.serverId)?.agentDirectoryStatus).toBe("ready"); }); expect(fakeClient.fetchAgentsCalls).toHaveLength(1); expect(fakeClient.fetchAgentsCalls[0]).toEqual({ scope: "active", sort: [{ key: "updated_at", direction: "desc" }], subscribe: {}, page: { limit: 200 }, }); const snapshot = store.getSnapshot(host.serverId); expect(snapshot?.agentDirectoryStatus).toBe("ready"); expect(snapshot?.hasEverLoadedAgentDirectory).toBe(true); fakeClient.setConnectionState({ status: "disconnected", reason: "transport closed" }); fakeClient.setConnectionState({ status: "connected" }); await fakeClient.waitForFetches(2); expect(fakeClient.fetchAgentsCalls[1]).toMatchObject({ subscribe: {} }); await store.refreshAgentDirectory({ serverId: host.serverId }); expect(fakeClient.fetchAgentsCalls[2]).toEqual({ scope: "active", sort: [{ key: "updated_at", direction: "desc" }], page: { limit: 200 }, }); releaseDemand(); store.syncHosts([]); useSessionStore.getState().clearSession(host.serverId); }); it("owns the session replica for the registered host lifecycle", async () => { const host = makeHost({ serverId: "srv_no_session", connections: [ { id: "direct:lan:6767", type: "directTcp", endpoint: "lan:6767", }, ], }); const fakeClient = new FakeDaemonClient(); fakeClient.setConnectionState({ status: "connected" }); const store = new HostRuntimeStore({ deps: { createClient: () => fakeClient as unknown as DaemonClient, connectToDaemon: async ({ host: hostProfile }) => ({ client: fakeClient as unknown as DaemonClient, serverId: hostProfile.serverId, hostname: hostProfile.label ?? null, }), getClientId: async () => "cid_test_runtime", }, }); store.syncHosts([host]); await waitForHostOnline(store, host.serverId); const load = store.refreshAgentDirectory({ serverId: host.serverId, subscribe: {}, page: { limit: 200 }, }); await fakeClient.waitForFetches(1); await load; const session = useSessionStore.getState().sessions[host.serverId]; expect(session?.client).toBe(fakeClient); expect(session?.clientGeneration).toBe(1); expect(fakeClient.fetchAgentsCalls).toHaveLength(1); expect(fakeClient.fetchAgentsCalls[0]).toEqual({ scope: "active", sort: [{ key: "updated_at", direction: "desc" }], subscribe: {}, page: { limit: 200 }, }); store.syncHosts([]); expect(useSessionStore.getState().sessions[host.serverId]).toBeUndefined(); }); it("shows the restarted daemon's version after the same client reconnects", async () => { const host = makeHost({ serverId: "srv_restarted", connections: [{ id: "direct:lan:6767", type: "directTcp", endpoint: "lan:6767" }], }); const fakeClient = new FakeDaemonClient(); fakeClient.setConnectionState({ status: "connected" }); const store = new HostRuntimeStore({ deps: { createClient: () => fakeClient as unknown as DaemonClient, connectToDaemon: async ({ host: hostProfile }) => ({ client: fakeClient as unknown as DaemonClient, serverId: hostProfile.serverId, hostname: hostProfile.label ?? null, }), getClientId: async () => "cid_test_runtime", }, }); store.syncHosts([host]); await waitForHostOnline(store, host.serverId); expect(useSessionStore.getState().sessions[host.serverId]?.serverInfo?.version).toBe("0.8.0"); fakeClient.daemonRestartsAs("0.9.1"); expect(store.getSnapshot(host.serverId)?.client).toBe(fakeClient); expect(useSessionStore.getState().sessions[host.serverId]?.serverInfo?.version).toBe("0.9.1"); store.syncHosts([]); }); it("drains snapshot and buffered running transitions exactly once", async () => { const host = makeHost({ serverId: "srv_legacy_transitions", connections: [{ id: "direct:lan:6767", type: "directTcp", endpoint: "lan:6767" }], }); const fakeClient = new FakeDaemonClient(); fakeClient.setConnectionState({ status: "connected" }); const pageTwo = new Deferred>>(); const snapshotAgent = makeFetchAgentsEntry({ id: "legacy-snapshot", cwd: "/legacy/repo", updatedAt: "2026-07-12T10:00:00.000Z", }); const bufferedAgent = makeFetchAgentsEntry({ id: "legacy-buffered", cwd: "/legacy/repo", updatedAt: "2026-07-12T10:00:00.000Z", }); fakeClient.fetchAgentsResponses.push( makeFetchAgentsPayload({ entries: [ { ...snapshotAgent, agent: { ...snapshotAgent.agent, status: "idle" } }, { ...bufferedAgent, agent: { ...bufferedAgent.agent, status: "running" } }, ], hasMore: true, nextCursor: "legacy-page-two", }), pageTwo.promise, ); const store = new HostRuntimeStore({ deps: { createClient: () => fakeClient as unknown as DaemonClient, connectToDaemon: async () => ({ client: fakeClient as unknown as DaemonClient, serverId: host.serverId, hostname: null, }), getClientId: async () => "cid_legacy_transitions", }, }); const sessionStore = useSessionStore.getState(); sessionStore.initializeSession(host.serverId, fakeClient as unknown as DaemonClient, 1); sessionStore.updateSessionServerInfo(host.serverId, { serverId: host.serverId, hostname: null, version: "0.1.96", }); sessionStore.setAgents( host.serverId, new Map([ [ "legacy-snapshot", { ...replicaAgent(snapshotAgent.agent, host.serverId), turn: { phase: "open", turnId: null, startedAt: null, cancellationRequestId: null, }, }, ], [ "legacy-buffered", { ...replicaAgent(bufferedAgent.agent, host.serverId), turn: { phase: "open", turnId: null, startedAt: null, cancellationRequestId: null, }, }, ], ]), ); sessionStore.setQueuedMessages( host.serverId, new Map([ ["legacy-snapshot", [{ id: "legacy-snapshot-message", text: "snapshot", attachments: [] }]], ["legacy-buffered", [{ id: "legacy-buffered-message", text: "buffered", attachments: [] }]], ]), ); store.syncHosts([host]); await waitForHostOnline(store, host.serverId); const load = store.refreshAgentDirectory({ serverId: host.serverId, subscribe: {}, page: { limit: 200 }, }); await fakeClient.waitForFetches(2); fakeClient.agentUpdate({ kind: "upsert", agent: { ...bufferedAgent.agent, status: "idle" }, project: bufferedAgent.project, }); pageTwo.resolve(makeFetchAgentsPayload({ entries: [] })); await load; await fakeClient.waitForSentMessages(2); expect(fakeClient.sentAgentMessages.map(([agentId, text]) => [agentId, text])).toEqual([ ["legacy-snapshot", "snapshot"], ["legacy-buffered", "buffered"], ]); expect( Array.from(useSessionStore.getState().sessions[host.serverId]?.agents.values() ?? []).map( ({ id, status, workspaceId }) => [id, status, workspaceId], ), ).toEqual([ ["legacy-snapshot", "idle", snapshotAgent.agent.workspaceId], ["legacy-buffered", "idle", bufferedAgent.agent.workspaceId], ]); store.syncHosts([]); useSessionStore.getState().clearSession(host.serverId); }); it("fetches all pages during bootstrap within the active agent scope", async () => { const host = makeHost({ serverId: "srv_paged", connections: [ { id: "direct:lan:6767", type: "directTcp", endpoint: "lan:6767", }, ], }); const fakeClient = new FakeDaemonClient(); fakeClient.setConnectionState({ status: "connected" }); fakeClient.fetchAgentsResponses.push( makeFetchAgentsPayload({ entries: [ makeFetchAgentsEntry({ id: "agent-recent", cwd: "/workspaces/paseo", updatedAt: "2026-03-04T12:00:00.000Z", title: "Recent agent", }), ], hasMore: true, nextCursor: "cursor-page-2", subscriptionId: "app:srv_paged", }), makeFetchAgentsPayload({ entries: [ makeFetchAgentsEntry({ id: "agent-stale-attention", cwd: "/workspaces/paseo-pr67-review", updatedAt: "2026-02-20T08:00:00.000Z", title: "Needs triage", requiresAttention: true, attentionReason: "error", }), ], hasMore: false, }), ); const store = new HostRuntimeStore({ deps: { createClient: () => fakeClient as unknown as DaemonClient, connectToDaemon: async ({ host: hostProfile }) => ({ client: fakeClient as unknown as DaemonClient, serverId: hostProfile.serverId, hostname: hostProfile.label ?? null, }), getClientId: async () => "cid_test_runtime", }, }); useSessionStore .getState() .initializeSession(host.serverId, fakeClient as unknown as DaemonClient, 1); store.syncHosts([host]); await waitForHostOnline(store, host.serverId); const load = store.refreshAgentDirectory({ serverId: host.serverId, subscribe: {}, page: { limit: 200 }, }); await fakeClient.waitForFetches(2); await load; expect(fakeClient.fetchAgentsCalls).toHaveLength(2); expect(fakeClient.fetchAgentsCalls[0]).toEqual({ scope: "active", sort: [{ key: "updated_at", direction: "desc" }], subscribe: {}, page: { limit: 200 }, }); expect(fakeClient.fetchAgentsCalls[1]).toEqual({ scope: "active", sort: [{ key: "updated_at", direction: "desc" }], page: { limit: 200, cursor: "cursor-page-2" }, }); const staleAgent = useSessionStore.getState().sessions[host.serverId]?.agents?.get("agent-stale-attention") ?? null; expect(staleAgent?.requiresAttention).toBe(true); expect(staleAgent?.attentionReason).toBe("error"); const snapshot = store.getSnapshot(host.serverId); expect(snapshot?.agentDirectoryStatus).toBe("ready"); expect(snapshot?.hasEverLoadedAgentDirectory).toBe(true); store.syncHosts([]); useSessionStore.getState().clearSession(host.serverId); }); it("replays agent updates received while a later bootstrap page is loading", async () => { const host = makeHost({ serverId: "srv_paged_delta", connections: [{ id: "direct:lan:6767", type: "directTcp", endpoint: "lan:6767" }], }); const fakeClient = new FakeDaemonClient(); fakeClient.setConnectionState({ status: "connected" }); let finishPageTwo!: (payload: Awaited>) => void; const pageTwo = new Promise>>((resolve) => { finishPageTwo = resolve; }); const pageOneAgent = makeFetchAgentsEntry({ id: "agent-a", cwd: "/repo", updatedAt: "2026-07-12T10:00:00.000Z", title: "snapshot", }); const recoveredAgent = makeFetchAgentsEntry({ id: "agent-recovered", cwd: "/repo", updatedAt: "2026-07-12T10:00:00.000Z", title: "before refresh", }); fakeClient.fetchAgentsResponses.push( makeFetchAgentsPayload({ entries: [pageOneAgent], hasMore: true, nextCursor: "page-two", subscriptionId: "app:srv_paged_delta", }), pageTwo, ); const store = new HostRuntimeStore({ deps: { createClient: () => fakeClient as unknown as DaemonClient, connectToDaemon: async () => ({ client: fakeClient as unknown as DaemonClient, serverId: host.serverId, hostname: null, }), getClientId: async () => "cid_paged_delta", }, }); const sessionStore = useSessionStore.getState(); sessionStore.initializeSession(host.serverId, fakeClient as unknown as DaemonClient, 1); sessionStore.setAgents( host.serverId, new Map([[recoveredAgent.agent.id, replicaAgent(recoveredAgent.agent, host.serverId)]]), ); sessionStore.setAgentTimelineCursor( host.serverId, new Map([[recoveredAgent.agent.id, { epoch: "epoch", startSeq: 10, endSeq: 20 }]]), ); store.syncHosts([host]); await waitForHostOnline(store, host.serverId); const load = store.refreshAgentDirectory({ serverId: host.serverId, subscribe: {}, page: { limit: 200 }, }); await fakeClient.waitForFetches(2); fakeClient.agentUpdate({ kind: "upsert", agent: { ...pageOneAgent.agent, title: "live" }, project: pageOneAgent.project, }); fakeClient.agentUpdate({ kind: "upsert", agent: { ...recoveredAgent.agent, title: "recovered by delta" }, project: recoveredAgent.project, }); finishPageTwo( makeFetchAgentsPayload({ entries: [ makeFetchAgentsEntry({ id: "agent-b", cwd: "/repo", updatedAt: "2026-07-12T09:00:00.000Z", }), ], }), ); await load; expect( Array.from(useSessionStore.getState().sessions[host.serverId]?.agents.values() ?? []).map( (agent) => [agent.id, agent.title], ), ).toEqual([ ["agent-a", "live"], ["agent-b", null], ["agent-recovered", "recovered by delta"], ]); expect( useSessionStore .getState() .sessions[host.serverId]?.agentTimelineCursor.get(recoveredAgent.agent.id), ).toEqual({ epoch: "epoch", startSeq: 10, endSeq: 20 }); const agentB = makeFetchAgentsEntry({ id: "agent-b", cwd: "/repo", updatedAt: "2026-07-12T11:00:00.000Z", title: "immediate", }); fakeClient.agentUpdate({ kind: "upsert", agent: agentB.agent, project: agentB.project }); expect(useSessionStore.getState().sessions[host.serverId]?.agents.get("agent-b")?.title).toBe( "immediate", ); fakeClient.agentUpdate({ kind: "remove", agentId: "agent-b" }); expect(useSessionStore.getState().sessions[host.serverId]?.agents.has("agent-b")).toBe(false); store.syncHosts([]); useSessionStore.getState().clearSession(host.serverId); }); it("applies agent updates received during initial directory bootstrap", async () => { const host = makeHost({ serverId: "srv_pre_session", connections: [{ id: "direct:lan:6767", type: "directTcp", endpoint: "lan:6767" }], }); const fakeClient = new FakeDaemonClient(); fakeClient.setConnectionState({ status: "connected" }); const snapshotEntry = makeFetchAgentsEntry({ id: "agent-pre-session", cwd: "/repo", updatedAt: "2026-07-12T10:00:00.000Z", title: "snapshot", }); fakeClient.fetchAgentsResponses.push(makeFetchAgentsPayload({ entries: [snapshotEntry] })); const store = new HostRuntimeStore({ deps: { createClient: () => fakeClient as unknown as DaemonClient, connectToDaemon: async () => ({ client: fakeClient as unknown as DaemonClient, serverId: host.serverId, hostname: null, }), getClientId: async () => "cid_pre_session", }, }); store.syncHosts([host]); await waitForHostOnline(store, host.serverId); const load = store.refreshAgentDirectory({ serverId: host.serverId, subscribe: {}, page: { limit: 200 }, }); await fakeClient.waitForAgentUpdates(); fakeClient.agentUpdate({ kind: "upsert", agent: { ...snapshotEntry.agent, title: "before-session" }, project: snapshotEntry.project, }); useSessionStore.getState().updateSessionServerInfo(host.serverId, { serverId: host.serverId, hostname: null, version: "test", }); await fakeClient.waitForFetches(1); await load; expect( useSessionStore.getState().sessions[host.serverId]?.agents.get("agent-pre-session")?.title, ).toBe("before-session"); store.syncHosts([]); useSessionStore.getState().clearSession(host.serverId); }); it("rejects a superseded refresh without overwriting the newer replica", async () => { const host = makeHost({ serverId: "srv_overlap", connections: [{ id: "direct:lan:6767", type: "directTcp", endpoint: "lan:6767" }], }); const fakeClient = new FakeDaemonClient(); fakeClient.setConnectionState({ status: "connected" }); fakeClient.fetchAgentsResponses.push(makeFetchAgentsPayload({ entries: [] })); const store = new HostRuntimeStore({ deps: { createClient: () => fakeClient as unknown as DaemonClient, connectToDaemon: async () => ({ client: fakeClient as unknown as DaemonClient, serverId: host.serverId, hostname: null, }), getClientId: async () => "cid_overlap", }, }); useSessionStore .getState() .initializeSession(host.serverId, fakeClient as unknown as DaemonClient, 1); store.syncHosts([host]); await waitForHostOnline(store, host.serverId); await store.refreshAgentDirectory({ serverId: host.serverId, subscribe: {} }); const olderPage = new Deferred>>(); fakeClient.fetchAgentsResponses.push(olderPage.promise); const olderRefresh = store.refreshAgentDirectory({ serverId: host.serverId }); await fakeClient.waitForFetches(2); const newerEntry = makeFetchAgentsEntry({ id: "newer", cwd: "/repo", updatedAt: "2026-07-12T11:00:00.000Z", }); fakeClient.fetchAgentsResponses.push(makeFetchAgentsPayload({ entries: [newerEntry] })); const newerRefresh = store.refreshAgentDirectory({ serverId: host.serverId }); await fakeClient.waitForFetches(3); await newerRefresh; olderPage.resolve( makeFetchAgentsPayload({ entries: [ makeFetchAgentsEntry({ id: "older", cwd: "/repo", updatedAt: "2026-07-12T09:00:00.000Z", }), ], }), ); await expect(olderRefresh).rejects.toThrow(); fakeClient.agentUpdate({ kind: "upsert", agent: { ...newerEntry.agent, title: "after cleanup" }, project: newerEntry.project, }); expect( Array.from(useSessionStore.getState().sessions[host.serverId]?.agents.values() ?? []).map( ({ id, title }) => [id, title], ), ).toEqual([["newer", "after cleanup"]]); store.syncHosts([]); useSessionStore.getState().clearSession(host.serverId); }); it("replays inherited deltas when a superseding refresh fails", async () => { const host = makeHost({ serverId: "srv_overlap_failure", connections: [{ id: "direct:lan:6767", type: "directTcp", endpoint: "lan:6767" }], }); const fakeClient = new FakeDaemonClient(); fakeClient.setConnectionState({ status: "connected" }); fakeClient.fetchAgentsResponses.push(makeFetchAgentsPayload({ entries: [] })); const store = new HostRuntimeStore({ deps: { createClient: () => fakeClient as unknown as DaemonClient, connectToDaemon: async () => ({ client: fakeClient as unknown as DaemonClient, serverId: host.serverId, hostname: null, }), getClientId: async () => "cid_overlap_failure", }, }); useSessionStore .getState() .initializeSession(host.serverId, fakeClient as unknown as DaemonClient, 1); store.syncHosts([host]); await waitForHostOnline(store, host.serverId); await store.refreshAgentDirectory({ serverId: host.serverId, subscribe: {} }); const olderPage = new Deferred>>(); fakeClient.fetchAgentsResponses.push(olderPage.promise); const olderRefresh = store.refreshAgentDirectory({ serverId: host.serverId }); await fakeClient.waitForFetches(2); const liveEntry = makeFetchAgentsEntry({ id: "live-delta", cwd: "/repo", updatedAt: "2026-07-17T10:00:00.000Z", title: "preserved", }); fakeClient.agentUpdate({ kind: "upsert", agent: liveEntry.agent, project: liveEntry.project }); const newerPage = new Deferred>>(); fakeClient.fetchAgentsResponses.push(newerPage.promise); const newerRefresh = store.refreshAgentDirectory({ serverId: host.serverId }); await fakeClient.waitForFetches(3); newerPage.reject(new Error("newer refresh failed")); await expect(newerRefresh).rejects.toThrow("newer refresh failed"); expect( useSessionStore.getState().sessions[host.serverId]?.agents.get("live-delta")?.title, ).toBe("preserved"); olderPage.resolve(makeFetchAgentsPayload({ entries: [] })); await expect(olderRefresh).rejects.toThrow(); store.syncHosts([]); useSessionStore.getState().clearSession(host.serverId); }); it("rejects a refresh when the session generation changes before commit", async () => { const host = makeHost({ serverId: "srv_stale_generation", connections: [{ id: "direct:lan:6767", type: "directTcp", endpoint: "lan:6767" }], }); const fakeClient = new FakeDaemonClient(); fakeClient.setConnectionState({ status: "connected" }); const existingEntry = makeFetchAgentsEntry({ id: "existing", cwd: "/repo", updatedAt: "2026-07-12T10:00:00.000Z", }); fakeClient.fetchAgentsResponses.push(makeFetchAgentsPayload({ entries: [existingEntry] })); const store = new HostRuntimeStore({ deps: { createClient: () => fakeClient as unknown as DaemonClient, connectToDaemon: async () => ({ client: fakeClient as unknown as DaemonClient, serverId: host.serverId, hostname: null, }), getClientId: async () => "cid_stale_generation", }, }); const sessionStore = useSessionStore.getState(); sessionStore.initializeSession(host.serverId, fakeClient as unknown as DaemonClient, 1); store.syncHosts([host]); await waitForHostOnline(store, host.serverId); await store.refreshAgentDirectory({ serverId: host.serverId }); const stalePage = new Deferred>>(); fakeClient.fetchAgentsResponses.push(stalePage.promise); const refresh = store.refreshAgentDirectory({ serverId: host.serverId }); await fakeClient.waitForFetches(2); sessionStore.updateSessionClient(host.serverId, fakeClient as unknown as DaemonClient, 2); stalePage.resolve( makeFetchAgentsPayload({ entries: [ makeFetchAgentsEntry({ id: "stale", cwd: "/repo", updatedAt: "2026-07-12T11:00:00.000Z", }), ], }), ); await expect(refresh).rejects.toThrow(); expect( Array.from(useSessionStore.getState().sessions[host.serverId]?.agents.keys() ?? []), ).toEqual(["existing"]); store.syncHosts([]); useSessionStore.getState().clearSession(host.serverId); }); it("drains queued messages once for snapshot and buffered running transitions", async () => { const host = makeHost({ serverId: "srv_queued_transitions", connections: [{ id: "direct:lan:6767", type: "directTcp", endpoint: "lan:6767" }], }); const fakeClient = new FakeDaemonClient(); fakeClient.setConnectionState({ status: "connected" }); const pageTwo = new Deferred>>(); const snapshotAgent = makeFetchAgentsEntry({ id: "snapshot-transition", cwd: "/repo", updatedAt: "2026-07-12T10:00:00.000Z", }); const bufferedAgent = makeFetchAgentsEntry({ id: "buffered-transition", cwd: "/repo", updatedAt: "2026-07-12T10:00:00.000Z", }); fakeClient.fetchAgentsResponses.push( makeFetchAgentsPayload({ entries: [ { ...snapshotAgent, agent: { ...snapshotAgent.agent, status: "idle" } }, { ...bufferedAgent, agent: { ...bufferedAgent.agent, status: "running" } }, ], hasMore: true, nextCursor: "page-two", }), pageTwo.promise, ); const store = new HostRuntimeStore({ deps: { createClient: () => fakeClient as unknown as DaemonClient, connectToDaemon: async () => ({ client: fakeClient as unknown as DaemonClient, serverId: host.serverId, hostname: null, }), getClientId: async () => "cid_queued_transitions", }, }); const sessionStore = useSessionStore.getState(); sessionStore.initializeSession(host.serverId, fakeClient as unknown as DaemonClient, 1); sessionStore.setAgents( host.serverId, new Map([ [ "snapshot-transition", { ...replicaAgent(snapshotAgent.agent, host.serverId), turn: { phase: "open", turnId: null, startedAt: null, cancellationRequestId: null, }, }, ], [ "buffered-transition", { ...replicaAgent(bufferedAgent.agent, host.serverId), turn: { phase: "open", turnId: null, startedAt: null, cancellationRequestId: null, }, }, ], ]), ); sessionStore.setQueuedMessages( host.serverId, new Map([ [ "snapshot-transition", [{ id: "message-snapshot", text: "snapshot queued", attachments: [] }], ], [ "buffered-transition", [{ id: "message-buffered", text: "buffered queued", attachments: [] }], ], ]), ); store.syncHosts([host]); await waitForHostOnline(store, host.serverId); const load = store.refreshAgentDirectory({ serverId: host.serverId, subscribe: {} }); await fakeClient.waitForFetches(2); fakeClient.agentUpdate({ kind: "upsert", agent: { ...bufferedAgent.agent, status: "idle" }, project: bufferedAgent.project, }); pageTwo.resolve(makeFetchAgentsPayload({ entries: [] })); await load; await fakeClient.waitForSentMessages(2); expect(fakeClient.sentAgentMessages.map(([agentId, text]) => [agentId, text])).toEqual([ ["snapshot-transition", "snapshot queued"], ["buffered-transition", "buffered queued"], ]); expect( Array.from(useSessionStore.getState().sessions[host.serverId]?.queuedMessages.values() ?? []), ).toEqual([[], []]); store.syncHosts([]); useSessionStore.getState().clearSession(host.serverId); }); it("submits an automatically drained message through the submission producer", async () => { const host = makeHost({ serverId: "srv_drain_submission" }); const fakeClient = new FakeDaemonClient(); const send = new Deferred(); fakeClient.sendAgentMessageResponses.push(send.promise); const store = new HostRuntimeStore({ deps: { createClient: () => fakeClient as unknown as DaemonClient, connectToDaemon: async () => ({ client: fakeClient as unknown as DaemonClient, serverId: host.serverId, hostname: null, }), getClientId: async () => "cid_drain_submission", }, }); const sessionStore = useSessionStore.getState(); sessionStore.initializeSession(host.serverId, fakeClient as unknown as DaemonClient, 1); sessionStore.updateSessionServerInfo(host.serverId, { serverId: host.serverId, hostname: null, version: null, features: { canonicalSubmittedPrompts: true }, }); sessionStore.setQueuedMessages( host.serverId, new Map([ [ "agent", [ { id: "queued-with-attachment", text: "read this file", attachments: [ { kind: "workspace_file" as const, path: "src/main.ts", selection: { kind: "whole_file" as const }, }, ], }, ], ], ]), ); store.drainQueuedAgentMessage(host.serverId, "agent"); await fakeClient.waitForSentMessages(1); // The row and the pending submission must exist while the RPC is still in flight — // the user sees their message and the working footer immediately, exactly as when // they press send. const session = useSessionStore.getState().sessions[host.serverId]; const tail = session?.agentStreamTail.get("agent") ?? []; expect(tail).toHaveLength(1); expect(tail[0]).toMatchObject({ kind: "user_message", text: "read this file", attachments: [{ type: "text", title: "main.ts", text: "Workspace file: src/main.ts" }], }); expect(session?.messageSubmissions.get("agent")).toBeDefined(); send.resolve(); useSessionStore.getState().clearSession(host.serverId); }); it("restores an automatically drained message when sending fails", async () => { const host = makeHost({ serverId: "srv_failed_queue_drain" }); const fakeClient = new FakeDaemonClient(); fakeClient.sendAgentMessageFailures.push(new Error("connection lost")); const store = new HostRuntimeStore({ deps: { createClient: () => fakeClient as unknown as DaemonClient, connectToDaemon: async () => ({ client: fakeClient as unknown as DaemonClient, serverId: host.serverId, hostname: null, }), getClientId: async () => "cid_failed_queue_drain", }, }); const sessionStore = useSessionStore.getState(); sessionStore.initializeSession(host.serverId, fakeClient as unknown as DaemonClient, 1); sessionStore.setQueuedMessages( host.serverId, new Map([ [ "agent", [ { id: "first", text: "retry me", attachments: [] }, { id: "second", text: "keep me behind", attachments: [] }, ], ], ]), ); store.drainQueuedAgentMessage(host.serverId, "agent"); await vi.waitFor(() => { expect(fakeClient.sentAgentMessages).toHaveLength(1); expect( useSessionStore.getState().sessions[host.serverId]?.queuedMessages.get("agent"), ).toEqual([ { id: "first", text: "retry me", attachments: [] }, { id: "second", text: "keep me behind", attachments: [] }, ]); }); useSessionStore.getState().clearSession(host.serverId); }); it("serializes queued-message drains for the same agent", async () => { const host = makeHost({ serverId: "srv_serialized_queue_drain" }); const fakeClient = new FakeDaemonClient(); const send = new Deferred(); fakeClient.sendAgentMessageResponses.push(send.promise); const store = new HostRuntimeStore({ deps: { createClient: () => fakeClient as unknown as DaemonClient, connectToDaemon: async () => ({ client: fakeClient as unknown as DaemonClient, serverId: host.serverId, hostname: null, }), getClientId: async () => "cid_serialized_queue_drain", }, }); const sessionStore = useSessionStore.getState(); sessionStore.initializeSession(host.serverId, fakeClient as unknown as DaemonClient, 1); sessionStore.setQueuedMessages( host.serverId, new Map([["agent", [{ id: "first", text: "send once", attachments: [] }]]]), ); store.drainQueuedAgentMessage(host.serverId, "agent"); store.drainQueuedAgentMessage(host.serverId, "agent"); await fakeClient.waitForSentMessages(1); expect(fakeClient.sentAgentMessages).toHaveLength(1); send.resolve(); await vi.waitFor(() => { expect( useSessionStore.getState().sessions[host.serverId]?.queuedMessages.get("agent"), ).toEqual([]); }); useSessionStore.getState().clearSession(host.serverId); }); it("drains a queued message as a steer so a turn the agent just started is not interrupted", async () => { const host = makeHost({ serverId: "srv_steer_queue_drain" }); const fakeClient = new FakeDaemonClient(); const store = new HostRuntimeStore({ deps: { createClient: () => fakeClient as unknown as DaemonClient, connectToDaemon: async () => ({ client: fakeClient as unknown as DaemonClient, serverId: host.serverId, hostname: null, }), getClientId: async () => "cid_steer_queue_drain", }, }); const sessionStore = useSessionStore.getState(); sessionStore.initializeSession(host.serverId, fakeClient as unknown as DaemonClient, 1); sessionStore.setQueuedMessages( host.serverId, new Map([["agent", [{ id: "queued", text: "next task", attachments: [] }]]]), ); store.drainQueuedAgentMessage(host.serverId, "agent"); await fakeClient.waitForSentMessages(1); expect(fakeClient.sentAgentMessages[0]?.[2]?.activeTurnBehavior).toBe("steer"); sessionStore.clearSession(host.serverId); }); it("uses legacy GitHub attachments when draining a queue for an old daemon", async () => { const host = makeHost({ serverId: "srv_legacy_queue_attachment" }); const fakeClient = new FakeDaemonClient(); const store = new HostRuntimeStore({ deps: { createClient: () => fakeClient as unknown as DaemonClient, connectToDaemon: async () => ({ client: fakeClient as unknown as DaemonClient, serverId: host.serverId, hostname: null, }), getClientId: async () => "cid_legacy_queue_attachment", }, }); const sessionStore = useSessionStore.getState(); sessionStore.initializeSession(host.serverId, fakeClient as unknown as DaemonClient, 1); sessionStore.updateSessionServerInfo(host.serverId, { serverId: host.serverId, hostname: null, version: "0.1.105", features: { forgeSearch: false }, }); sessionStore.setQueuedMessages( host.serverId, new Map([ [ "agent", [ { id: "queued-legacy-attachment", text: "review this", attachments: [ { kind: "github_pr" as const, item: { kind: "change_request" as const, number: 42, title: "Compatibility fix", url: "https://github.com/acme/repo/pull/42", state: "open" as const, body: "Details", labels: [], baseRefName: "main", headRefName: "fix", }, }, ], }, ], ], ]), ); store.drainQueuedAgentMessage(host.serverId, "agent"); await fakeClient.waitForSentMessages(1); expect(fakeClient.sentAgentMessages[0]?.[2]?.attachments).toEqual([ { type: "github_pr", mimeType: "application/github-pr", number: 42, title: "Compatibility fix", url: "https://github.com/acme/repo/pull/42", body: "Details", baseRefName: "main", headRefName: "fix", }, ]); sessionStore.clearSession(host.serverId); }); it("applies buffered stale side effects from the accepted page agent", async () => { const host = makeHost({ serverId: "srv_buffered_stale_side_effects", connections: [{ id: "direct:lan:6767", type: "directTcp", endpoint: "lan:6767" }], }); const fakeClient = new FakeDaemonClient(); fakeClient.setConnectionState({ status: "connected" }); const pageTwo = new Deferred>>(); const base = makeFetchAgentsEntry({ id: "stale-side-effects", cwd: "/repo", updatedAt: "2026-07-12T12:00:00.000Z", title: "newer page", }); const pageAgent = { ...base.agent, status: "running" as const, lastUsage: { inputTokens: 10, outputTokens: 5 }, pendingPermissions: [agentPermission("current-permission")], }; fakeClient.fetchAgentsResponses.push( makeFetchAgentsPayload({ entries: [{ ...base, agent: pageAgent }], hasMore: true, nextCursor: "page-two", }), pageTwo.promise, ); const store = new HostRuntimeStore({ deps: { createClient: () => fakeClient as unknown as DaemonClient, connectToDaemon: async () => ({ client: fakeClient as unknown as DaemonClient, serverId: host.serverId, hostname: null, }), getClientId: async () => "cid_buffered_stale_side_effects", }, }); const sessionStore = useSessionStore.getState(); sessionStore.initializeSession(host.serverId, fakeClient as unknown as DaemonClient, 1); sessionStore.setAgents( host.serverId, new Map([ [ pageAgent.id, replicaAgent({ ...pageAgent, updatedAt: "2026-07-12T10:00:00.000Z" }, host.serverId), ], ]), ); sessionStore.setAgentLastActivity(pageAgent.id, new Date("2026-07-12T12:00:00.000Z")); sessionStore.flushAgentLastActivity(); setAgentArchiving({ queryClient, serverId: host.serverId, agentId: pageAgent.id, isArchiving: true, }); store.syncHosts([host]); await waitForHostOnline(store, host.serverId); const load = store.refreshAgentDirectory({ serverId: host.serverId, subscribe: {} }); await fakeClient.waitForFetches(2); fakeClient.agentUpdate({ kind: "upsert", agent: { ...pageAgent, status: "idle", title: "stale live", updatedAt: "2026-07-12T11:00:00.000Z", lastUsage: { inputTokens: 20, outputTokens: 8 }, pendingPermissions: [agentPermission("stale-permission")], archivedAt: "2026-07-12T11:00:00.000Z", }, project: base.project, }); pageTwo.resolve(makeFetchAgentsPayload({ entries: [] })); await load; sessionStore.flushAgentLastActivity(); const state = useSessionStore.getState(); const agent = state.sessions[host.serverId]?.agents.get(pageAgent.id); expect({ title: agent?.title, status: agent?.status, usage: agent?.lastUsage, permissions: Array.from(state.sessions[host.serverId]?.pendingPermissions.values() ?? []).map( ({ request }) => request.id, ), archivePending: isAgentArchiving({ queryClient, serverId: host.serverId, agentId: pageAgent.id, }), activity: state.agentLastActivity.get(pageAgent.id)?.toISOString(), sentMessages: fakeClient.sentAgentMessages.length, }).toEqual({ title: "newer page", status: "running", usage: { inputTokens: 20, outputTokens: 8 }, permissions: ["current-permission"], archivePending: true, activity: "2026-07-12T12:00:00.000Z", sentMessages: 0, }); store.syncHosts([]); useSessionStore.getState().clearSession(host.serverId); }); it("re-subscribes agent directory updates when the surface requests after reconnect", async () => { const host = makeHost({ serverId: "srv_resubscribe", connections: [ { id: "direct:lan:6767", type: "directTcp", endpoint: "lan:6767", }, ], }); const fakeClient = new FakeDaemonClient(); fakeClient.setConnectionState({ status: "connected" }); const store = new HostRuntimeStore({ deps: { createClient: () => fakeClient as unknown as DaemonClient, connectToDaemon: async ({ host: hostProfile }) => ({ client: fakeClient as unknown as DaemonClient, serverId: hostProfile.serverId, hostname: hostProfile.label ?? null, }), getClientId: async () => "cid_test_runtime", }, }); useSessionStore .getState() .initializeSession(host.serverId, fakeClient as unknown as DaemonClient, 1); store.syncHosts([host]); await waitForHostOnline(store, host.serverId); await store.refreshAgentDirectory({ serverId: host.serverId, subscribe: {}, page: { limit: 200 }, }); await fakeClient.waitForFetches(1); fakeClient.setConnectionState({ status: "connected" }); await Promise.resolve(); await Promise.resolve(); expect(fakeClient.fetchAgentsCalls).toHaveLength(1); fakeClient.setConnectionState({ status: "disconnected", reason: "client_closed", }); fakeClient.setConnectionState({ status: "connected" }); await waitForHostOnline(store, host.serverId); await store.refreshAgentDirectory({ serverId: host.serverId, subscribe: {}, page: { limit: 200 }, }); await fakeClient.waitForFetches(2); expect(fakeClient.fetchAgentsCalls).toEqual([ { scope: "active", sort: [{ key: "updated_at", direction: "desc" }], subscribe: {}, page: { limit: 200 }, }, { scope: "active", sort: [{ key: "updated_at", direction: "desc" }], subscribe: {}, page: { limit: 200 }, }, ]); store.syncHosts([]); useSessionStore.getState().clearSession(host.serverId); }); it("replaces stale active session state when active bootstrap omits an agent", async () => { const host = makeHost({ serverId: "srv_archived_rehydrate", connections: [ { id: "direct:lan:6767", type: "directTcp", endpoint: "lan:6767", }, ], }); const fakeClient = new FakeDaemonClient(); fakeClient.setConnectionState({ status: "connected" }); fakeClient.fetchAgentsResponses.push( makeFetchAgentsPayload({ entries: [], subscriptionId: "app:srv_archived_rehydrate", }), ); const store = new HostRuntimeStore({ deps: { createClient: () => fakeClient as unknown as DaemonClient, connectToDaemon: async ({ host: hostProfile }) => ({ client: fakeClient as unknown as DaemonClient, serverId: hostProfile.serverId, hostname: hostProfile.label ?? null, }), getClientId: async () => "cid_test_runtime", }, }); useSessionStore .getState() .initializeSession(host.serverId, fakeClient as unknown as DaemonClient, 1); useSessionStore.getState().setAgents(host.serverId, () => { const stale = makeFetchAgentsEntry({ id: "agent-archived", cwd: "/workspaces/paseo", updatedAt: "2026-03-30T15:29:00.000Z", archivedAt: null, title: "Stale active copy", }).agent; const staleAgent: Agent = { ...stale, turn: stale.activeTurn ? { phase: "open", turnId: stale.activeTurn.turnId, startedAt: stale.activeTurn.startedAt ? new Date(stale.activeTurn.startedAt) : null, cancellationRequestId: null, } : { phase: "idle", cancellationRequestId: null }, serverId: host.serverId, createdAt: new Date(stale.createdAt), updatedAt: new Date(stale.updatedAt), lastUserMessageAt: null, lastActivityAt: new Date(stale.updatedAt), archivedAt: stale.archivedAt ? new Date(stale.archivedAt) : null, attentionTimestamp: stale.attentionTimestamp ? new Date(stale.attentionTimestamp) : null, parentAgentId: null, }; return new Map([[stale.id, staleAgent]]); }); store.syncHosts([host]); await waitForHostOnline(store, host.serverId); await store.refreshAgentDirectory({ serverId: host.serverId, subscribe: {}, page: { limit: 200 }, }); await fakeClient.waitForFetches(1); expect(useSessionStore.getState().sessions[host.serverId]?.agents.has("agent-archived")).toBe( false, ); store.syncHosts([]); useSessionStore.getState().clearSession(host.serverId); }); it("records unavailable startup probes when no connection can be established", async () => { const host = makeHost({ connections: [ { id: "direct:lan:6767", type: "directTcp", endpoint: "lan:6767", }, ], }); const store = new HostRuntimeStore({ deps: { createClient: () => { throw new Error("create client failed"); }, connectToDaemon: async () => { throw new Error("probe unavailable"); }, getClientId: async () => "cid_test_runtime", }, }); store.syncHosts([host]); let snapshot = store.getSnapshot(host.serverId); const timeoutAt = Date.now() + 100; while ( snapshot?.probeByConnectionId.get("direct:lan:6767")?.status !== "unavailable" && Date.now() < timeoutAt ) { await new Promise((resolve) => setTimeout(resolve, 0)); snapshot = store.getSnapshot(host.serverId); } expect(snapshot?.connectionStatus).toBe("connecting"); expect(snapshot?.lastError).toBeNull(); expect(snapshot?.probeByConnectionId.get("direct:lan:6767")).toEqual({ status: "unavailable", latencyMs: null, }); }); it("renameHost updates label in memory", async () => { const store = new HostRuntimeStore({ deps: { createClient: () => new FakeDaemonClient() as unknown as DaemonClient, connectToDaemon: async ({ host }) => ({ client: makeConnectedProbeClient(5) as unknown as DaemonClient, serverId: host.serverId, hostname: host.label ?? null, }), getClientId: async () => "cid_test_runtime", }, }); // upsertDirectConnection goes through setHostsAndSync, which both sets // this.hosts and syncs controllers — matching the real init path. await store.upsertDirectConnection({ serverId: "srv_rename", endpoint: "lan:6767", label: "old name", }); expect(store.getHosts().find((h) => h.serverId === "srv_rename")?.label).toBe("old name"); // persistHosts may throw in test env (no AsyncStorage/window), but the // in-memory state should still be updated by setHostsAndSync. await store.renameHost("srv_rename", "new name").catch(() => undefined); const renamed = store.getHosts().find((h) => h.serverId === "srv_rename"); expect(renamed?.label).toBe("new name"); store.syncHosts([]); }); it("preserves a manual host rename when desktop status re-advertises the daemon hostname", async () => { const advertisedHostname = "macbook-pro.local"; const store = new HostRuntimeStore({ deps: { createClient: () => new FakeDaemonClient() as unknown as DaemonClient, connectToDaemon: async ({ host }) => ({ client: makeConnectedProbeClient(5) as unknown as DaemonClient, serverId: host.serverId, hostname: advertisedHostname, }), getClientId: async () => "cid_test_runtime", }, storage: createMemoryHostRuntimeStorage(), }); try { await store.upsertConnectionFromListen({ listenAddress: "127.0.0.1:6767", serverId: "srv_desktop", hostname: advertisedHostname, }); await store.renameHost("srv_desktop", "mac-dev"); await store.upsertConnectionFromListen({ listenAddress: "127.0.0.1:6767", serverId: "srv_desktop", hostname: advertisedHostname, }); expect(store.getHosts().find((h) => h.serverId === "srv_desktop")?.label).toBe("mac-dev"); } finally { store.syncHosts([]); } }); it("upsertDirectConnection stores SSL on the connection and password on the host", async () => { const store = new HostRuntimeStore({ deps: { createClient: () => new FakeDaemonClient() as unknown as DaemonClient, connectToDaemon: async ({ host }) => ({ client: makeConnectedProbeClient(5) as unknown as DaemonClient, serverId: host.serverId, hostname: host.label ?? null, }), getClientId: async () => "cid_test_runtime", }, }); await store.upsertDirectConnection({ serverId: "srv_tls_password", endpoint: "example.paseo.test:7443", useTls: true, password: "shared-secret", label: "tls host", }); const host = store.getHosts().find((entry) => entry.serverId === "srv_tls_password"); expect(host?.connections).toEqual([ { id: "direct:example.paseo.test:7443", type: "directTcp", endpoint: "example.paseo.test:7443", useTls: true, }, ]); expect(host?.password).toBe("shared-secret"); store.syncHosts([]); }); it("probeAndUpsertConnection learns the real server id before storing a direct host", async () => { const connection: HostConnection = { id: "direct:lan:6767", type: "directTcp", endpoint: "lan:6767", }; const probeClient = makeConnectedProbeClient(5); const seenProbeHosts: string[] = []; const store = new HostRuntimeStore({ deps: { createClient: () => new FakeDaemonClient() as unknown as DaemonClient, connectToDaemon: async ({ host, connection: probedConnection }) => { seenProbeHosts.push(host.serverId); expect(probedConnection).toEqual(connection); return { client: probeClient as unknown as DaemonClient, serverId: "srv_real_direct", hostname: "mbp", }; }, getClientId: async () => "cid_test_runtime", }, }); const result = await store.probeAndUpsertConnection({ connection }); expect(result.serverId).toBe("srv_real_direct"); expect(result.hostname).toBe("mbp"); expect(seenProbeHosts).toEqual([""]); expect(probeClient.isDisposed()).toBe(false); expect(store.getHosts()).toMatchObject([ { serverId: "srv_real_direct", label: "mbp", connections: [connection], }, ]); store.syncHosts([]); }); it("probeAndUpsertConnection replaces a matching placeholder host with the real server id", async () => { const connection: HostConnection = { id: "direct:lan:6767", type: "directTcp", endpoint: "lan:6767", }; const store = new HostRuntimeStore({ deps: { createClient: () => new FakeDaemonClient() as unknown as DaemonClient, connectToDaemon: async () => ({ client: makeConnectedProbeClient(5) as unknown as DaemonClient, serverId: "srv_real_direct", hostname: "mbp", }), getClientId: async () => "cid_test_runtime", }, }); ( store as unknown as { hosts: HostProfile[]; } ).hosts = [ makeHost({ serverId: "local:lan:6767", label: "local:lan:6767", connections: [connection], preferredConnectionId: connection.id, }), ]; await store.probeAndUpsertConnection({ connection }); expect(store.getHosts().map((host) => host.serverId)).toEqual(["srv_real_direct"]); expect(store.getHosts()[0]?.label).toBe("mbp"); store.syncHosts([]); }); it("uses the advertised hostname when adding a relay host from a pairing offer", async () => { const store = createPairingStore({ hostname: "mbp" }); answerHostConfirmations(store, true); await store.beginLinkPairing().submit(encodeOfferUrl(makeOffer())); const pairedHost = store.getHosts().find((host) => host.serverId === "srv_offer"); expect(pairedHost?.label).toBe("mbp"); store.syncHosts([]); }); it("stores relay TLS from a pairing offer", async () => { const store = createPairingStore(); answerHostConfirmations(store, true); await store .beginLinkPairing() .submit( encodeOfferUrl(makeOffer({ relay: { endpoint: "relay.example.com:443", useTls: true } })), ); const pairedHost = store.getHosts().find((host) => host.serverId === "srv_offer"); expect(pairedHost?.connections).toEqual([ { id: "relay:wss:relay.example.com:443", type: "relay", relayEndpoint: "relay.example.com:443", useTls: true, daemonPublicKeyB64: "pk_test_offer", }, ]); store.syncHosts([]); }); it("uses TLS for old pairing URLs that omit relay TLS on port 443", async () => { const store = createPairingStore(); answerHostConfirmations(store, true); const oldPairingUrl = encodeOfferUrl({ v: 2, serverId: "srv_offer", daemonPublicKeyB64: "pk_test_offer", relay: { endpoint: "relay.paseo.sh:443" }, }); await store.importConnectionLink(oldPairingUrl, "openProject"); const pairedHost = store.getHosts().find((host) => host.serverId === "srv_offer"); expect(pairedHost?.connections).toEqual([ { id: "relay:wss:relay.paseo.sh:443", type: "relay", relayEndpoint: "relay.paseo.sh:443", useTls: true, daemonPublicKeyB64: "pk_test_offer", }, ]); store.syncHosts([]); }); it("probes a pairing link immediately and saves only after admission", async () => { const store = createPairingStore({ password: "correct-password" }); answerHostConfirmations(store, true); const offerUrl = encodeOfferUrl(makeOffer()); const pairing = store.beginLinkPairing(); await expect(pairing.submit(offerUrl)).rejects.toThrow("Password required"); expect(store.getHosts()).toHaveLength(0); const result = await pairing.submit(offerUrl, "correct-password"); expect(result).toMatchObject({ status: "connected", serverId: "srv_offer" }); expect(store.getHosts()[0]?.password).toBe("correct-password"); store.syncHosts([]); }); it("asks before a pairing link adds a new host, and Cancel saves and connects nothing", async () => { const connectAttempts: string[] = []; const store = createPairingStore({ connectAttempts }); const requests = answerHostConfirmations(store, false); const offerUrl = encodeOfferUrl(makeOffer()); const relayLink = "relay://relay.example:443/srv_relay?key=AAAA&ssl=true"; // `#offer=` in the app URL, then a QR scan, then the pair-link and add-host modals. await expect(store.importConnectionLink(offerUrl, "openProject")).resolves.toEqual({ status: "cancelled", }); await expect(store.importConnectionLink(offerUrl, "hostRoot")).resolves.toEqual({ status: "cancelled", }); await expect(store.beginLinkPairing().submit(offerUrl)).resolves.toEqual({ status: "cancelled", }); await expect(store.beginLinkPairing().submit(relayLink)).resolves.toEqual({ status: "cancelled", }); expect(requests).toEqual([ ...Array.from({ length: 3 }, () => ({ id: expect.any(Number), serverId: "srv_offer", keyFingerprint: "pkte stof fer", relayEndpoint: "relay.paseo.sh:443", kind: "newHost", })), { id: expect.any(Number), serverId: "srv_relay", keyFingerprint: "AAAA", relayEndpoint: "relay.example:443", kind: "newHost", }, ]); expect(store.getHosts()).toEqual([]); expect(connectAttempts).toEqual([]); store.syncHosts([]); }); it("connects a saved host from a matching pairing link without asking", async () => { const store = createPairingStore(); const requests = answerHostConfirmations(store, true); const offerUrl = encodeOfferUrl(makeOffer()); await store.beginLinkPairing().submit(offerUrl); expect(requests).toHaveLength(1); await expect(store.importConnectionLink(offerUrl, "openProject")).resolves.toEqual({ status: "connected", serverId: "srv_offer", }); await expect(store.importConnectionLink(offerUrl, "hostRoot")).resolves.toEqual({ status: "connected", serverId: "srv_offer", }); await expect(store.beginLinkPairing().submit(offerUrl)).resolves.toMatchObject({ status: "connected", }); expect(requests).toHaveLength(1); expect(store.getHosts()).toHaveLength(1); store.syncHosts([]); }); it("asks when a pairing link changes a saved host's key, relay, or TLS setting", async () => { const store = createPairingStore(); const requests = answerHostConfirmations(store, (_request, index) => index === 0); await store.beginLinkPairing().submit(encodeOfferUrl(makeOffer())); const savedHosts = store.getHosts(); const changedLinks = [ makeOffer({ daemonPublicKeyB64: "pk_other_key" }), makeOffer({ relay: { endpoint: "relay.example:443", useTls: false } }), makeOffer({ relay: { endpoint: "relay.paseo.sh:443", useTls: true } }), ].map(encodeOfferUrl); for (const link of changedLinks) { await expect(store.importConnectionLink(link, "openProject")).resolves.toEqual({ status: "cancelled", }); await expect(store.beginLinkPairing().submit(link)).resolves.toEqual({ status: "cancelled", }); } expect(requests.map((request) => request.kind)).toEqual([ "newHost", ...Array.from({ length: 6 }, () => "changedConnection"), ]); expect(store.getHosts()).toEqual(savedHosts); store.syncHosts([]); }); it("asks once per pairing flow and link when the host asks for a password", async () => { const store = createPairingStore({ password: "correct-password" }); const requests = answerHostConfirmations(store, (_request, index) => index < 2); const offerUrl = encodeOfferUrl(makeOffer()); const pairing = store.beginLinkPairing(); await expect(pairing.submit(offerUrl)).rejects.toThrow("Password required"); await expect(pairing.submit(offerUrl, "wrong-password")).rejects.toThrow(); expect(requests).toHaveLength(1); // A new pairing flow for the same link asks again. await expect(store.beginLinkPairing().submit(offerUrl, "wrong-password")).rejects.toThrow(); expect(requests).toHaveLength(2); await expect(pairing.submit(offerUrl, "correct-password")).resolves.toMatchObject({ status: "connected", }); expect(requests).toHaveLength(2); expect(store.getHosts()[0]).toMatchObject({ serverId: "srv_offer", password: "correct-password", }); // A different link in the same flow asks again. const otherKeyUrl = encodeOfferUrl(makeOffer({ daemonPublicKeyB64: "pk_other_key" })); await expect(pairing.submit(otherKeyUrl, "correct-password")).resolves.toEqual({ status: "cancelled", }); expect(requests).toHaveLength(3); store.syncHosts([]); }); it("continues a scanned link's password retry without asking again", async () => { const store = createPairingStore({ password: "correct-password" }); const requests = answerHostConfirmations(store, true); const link = "relay://relay.example:443/srv_pair?key=AAAA&ssl=true"; const outcome = await store.importConnectionLink(link, "hostRoot"); expect(outcome).toMatchObject({ status: "password_required", link }); expect(store.getHosts()).toHaveLength(0); if (outcome.status !== "password_required") throw new Error("expected password_required"); await outcome.pairing.submit(outcome.link, "correct-password"); expect(requests).toHaveLength(1); expect(store.getHosts()[0]).toMatchObject({ serverId: "srv_pair", password: "correct-password", }); store.syncHosts([]); }); it("keeps a host confirmation pending until it is answered, and a newer one cancels it", async () => { const store = createPairingStore(); const first = store.importConnectionLink(encodeOfferUrl(makeOffer()), "openProject"); await vi.waitFor(() => expect(store.getPendingHostConfirmation()).toMatchObject({ serverId: "srv_offer" }), ); const second = store.importConnectionLink( encodeOfferUrl(makeOffer({ serverId: "srv_second" })), "openProject", ); await expect(first).resolves.toEqual({ status: "cancelled" }); await vi.waitFor(() => expect(store.getPendingHostConfirmation()).toMatchObject({ serverId: "srv_second" }), ); store.answerHostConfirmation(requirePendingConfirmation(store).id, true); await expect(second).resolves.toEqual({ status: "connected", serverId: "srv_second" }); expect(store.getPendingHostConfirmation()).toBeNull(); expect(store.getHosts().map((host) => host.serverId)).toEqual(["srv_second"]); store.syncHosts([]); }); it("ignores an answer for a host confirmation that a newer one replaced", async () => { const store = createPairingStore(); const first = store.importConnectionLink(encodeOfferUrl(makeOffer()), "openProject"); await vi.waitFor(() => expect(store.getPendingHostConfirmation()).toMatchObject({ serverId: "srv_offer" }), ); const firstRequest = requirePendingConfirmation(store); const second = store.importConnectionLink( encodeOfferUrl(makeOffer({ serverId: "srv_second" })), "openProject", ); await expect(first).resolves.toEqual({ status: "cancelled" }); // Connect tapped on the first sheet as the second link arrives. store.answerHostConfirmation(firstRequest.id, true); const secondRequest = requirePendingConfirmation(store); expect(secondRequest.serverId).toBe("srv_second"); store.answerHostConfirmation(secondRequest.id, false); await expect(second).resolves.toEqual({ status: "cancelled" }); expect(store.getHosts()).toEqual([]); }); it("waits for saved hosts to load before deciding whether a pairing link asks", async () => { const savedHost = makeHost({ serverId: "srv_offer", connections: [ { id: "relay:relay.paseo.sh:443", type: "relay", relayEndpoint: "relay.paseo.sh:443", useTls: false, daemonPublicKeyB64: "pk_test_offer", }, ], }); const store = createPairingStore({ storage: createMemoryHostRuntimeStorage({ "@paseo:daemon-registry": JSON.stringify([savedHost]), "@paseo:e2e": "1", }), }); const requests = answerHostConfirmations(store, true); await expect( store.importConnectionLink(encodeOfferUrl(makeOffer()), "openProject"), ).resolves.toEqual({ status: "connected", serverId: "srv_offer" }); expect(requests).toEqual([]); expect(store.getHosts().map((host) => host.serverId)).toEqual(["srv_offer"]); store.syncHosts([]); }); it("adds hosts by address and the default localhost without asking", async () => { const store = createPairingStore({ serverIdForHost: (host) => host.connections[0]?.type === "directTcp" ? `srv_${host.connections[0].endpoint}` : "", }); const requests = answerHostConfirmations(store, false); await store.boot(); await store.probeAndUpsertDirectConnection({ endpoint: "10.0.0.5:6767" }); expect(requests).toEqual([]); expect(store.getHosts().map((host) => host.serverId)).toEqual([ "srv_localhost:6767", "srv_10.0.0.5:6767", ]); store.syncHosts([]); }); it("authenticates Remote SSH probes with the entered daemon password and stores it verbatim", async () => { const store = createPairingStore({ password: " s3cret ", serverIdForHost: (host) => (host.connections[0]?.type === "remoteSsh" ? "srv_ssh" : ""), }); answerHostConfirmations(store, false); await store.boot(); await store.probeAndUpsertRemoteSshConnection({ host: "deploy@example.com", password: " s3cret ", }); const host = store.getHosts().find((entry) => entry.serverId === "srv_ssh"); expect(host?.password).toBe(" s3cret "); expect(host?.connections[0]).toMatchObject({ type: "remoteSsh", host: "deploy@example.com", }); // The fix promises the saved password is sent on every connection: a later // cycle must authenticate from the stored profile alone. await store.runProbeCycleNow("srv_ssh"); await waitForHostOnline(store, "srv_ssh"); store.syncHosts([]); }); it("rejects Remote SSH probes without the daemon password, the way the daemon does", async () => { const store = createPairingStore({ password: "s3cret", serverIdForHost: (host) => (host.connections[0]?.type === "remoteSsh" ? "srv_ssh" : ""), }); answerHostConfirmations(store, false); await store.boot(); const failure = await store .probeAndUpsertRemoteSshConnection({ host: "deploy@example.com" }) .then( () => null, (error: unknown) => error, ); expect(failure).toBeInstanceOf(DaemonAuthenticationError); expect((failure as DaemonAuthenticationError).reason).toBe("password_required"); expect(store.getHosts().find((entry) => entry.serverId === "srv_ssh")).toBeUndefined(); store.syncHosts([]); }); it("preserves the existing host label when re-pairing an existing relay host", async () => { const store = createPairingStore({ hostname: "mbp" }); answerHostConfirmations(store, true); await store.beginLinkPairing().submit(encodeOfferUrl(makeOffer())); await store.renameHost("srv_offer", "Custom name"); await store.beginLinkPairing().submit(encodeOfferUrl(makeOffer())); const pairedHost = store.getHosts().find((host) => host.serverId === "srv_offer"); expect(pairedHost?.label).toBe("Custom name"); store.syncHosts([]); }); }); describe("readInitialDaemonConnectionHint", () => { it("returns null when no hint is present", () => { expect(readInitialDaemonConnectionHint({ isWebRuntime: true })).toBeNull(); }); it("parses a valid listen-only hint", () => { (globalThis as Record).__PASEO_INITIAL_DAEMON_CONNECTION__ = { listen: "localhost:6767", }; expect(readInitialDaemonConnectionHint({ isWebRuntime: true })).toEqual({ listen: "localhost:6767", useTls: false, }); }); it("preserves useTls when explicitly true", () => { (globalThis as Record).__PASEO_INITIAL_DAEMON_CONNECTION__ = { listen: "paseo.example.com:443", useTls: true, }; expect(readInitialDaemonConnectionHint({ isWebRuntime: true })).toEqual({ listen: "paseo.example.com:443", useTls: true, }); }); it("ignores invalid shapes", () => { (globalThis as Record).__PASEO_INITIAL_DAEMON_CONNECTION__ = "localhost:6767"; expect(readInitialDaemonConnectionHint({ isWebRuntime: true })).toBeNull(); (globalThis as Record).__PASEO_INITIAL_DAEMON_CONNECTION__ = { useTls: true, }; expect(readInitialDaemonConnectionHint({ isWebRuntime: true })).toBeNull(); }); }); describe("HostRuntimeStore initial connection hint bootstrap", () => { it("starts saved remote-host probes while desktop daemon startup is still pending", async () => { const host = makeHost({ serverId: "srv_remote_saved", connections: [ { id: "direct:remote.example:6799", type: "directTcp", endpoint: "remote.example:6799" }, ], }); const storage = createMemoryHostRuntimeStorage(); await storage.setItem("@paseo:daemon-registry", JSON.stringify([host])); let remoteProbeStarted = false; let finishDesktopStart: (() => void) | undefined; const desktopStartPending = new Promise((resolve) => { finishDesktopStart = resolve; }); const store = new HostRuntimeStore({ storage, deps: { createClient: () => new FakeDaemonClient() as unknown as DaemonClient, connectToDaemon: async () => { remoteProbeStarted = true; return { client: makeConnectedProbeClient(5) as unknown as DaemonClient, serverId: host.serverId, hostname: host.label, }; }, getClientId: async () => "cid_remote_saved", }, }); startHostRuntimeBootstrap({ store, shouldStartDaemon: true, daemonStartService: { startIfEnabled: async () => { await desktopStartPending; return { ok: true }; }, }, }); try { await vi.waitFor(() => expect(remoteProbeStarted).toBe(true), { timeout: 400 }); expect(store.isHostRegistryLoaded()).toBe(true); } finally { finishDesktopStart?.(); await store.boot(); store.syncHosts([]); } }); it("resolves a local credential for a reopened saved desktop host connection", async () => { const host = makeHost({ serverId: "srv_desktop_reopened", connections: [{ id: "direct:localhost:6799", type: "directTcp", endpoint: "localhost:6799" }], }); const storage = createMemoryHostRuntimeStorage(); await storage.setItem("@paseo:daemon-registry", JSON.stringify([host])); const credentialRequests: string[] = []; const store = new HostRuntimeStore({ storage, deps: { createClient: () => new FakeDaemonClient() as unknown as DaemonClient, connectToDaemon: async ({ host: probeHost, connection }) => { expect(probeHost.password).toBeUndefined(); const credential = await readDesktopManagedLocalCredential(connection, async (listen) => { credentialRequests.push(listen); return listen === "localhost:6799" ? "local-token" : null; }); expect(credential).toBe("local-token"); return { client: makeConnectedProbeClient(5) as unknown as DaemonClient, serverId: host.serverId, hostname: host.label, }; }, getClientId: async () => "cid_desktop_reopened", }, }); startHostRuntimeBootstrap({ store, shouldStartDaemon: true, daemonStartService: { startIfEnabled: async () => ({ ok: true }), }, }); await waitForHostOnline(store, host.serverId); expect(credentialRequests).toContain("localhost:6799"); store.syncHosts([]); }); it("requires a new password and stops reconnecting after a previously connected client is rejected", async () => { const client = makeConnectedProbeClient(5); const host = makeHost({ serverId: "srv_changed_password", password: "old-password", connections: [{ id: "direct:lan:6767", type: "directTcp", endpoint: "lan:6767" }], }); const store = new HostRuntimeStore({ storage: createMemoryHostRuntimeStorage(), deps: { createClient: () => client as unknown as DaemonClient, connectToDaemon: async () => ({ client: client as unknown as DaemonClient, serverId: host.serverId, hostname: host.label, }), getClientId: async () => "cid_changed_password", }, }); store.syncHosts([host], { initialConnectionByServerId: new Map([ [ host.serverId, { connectionId: host.connections[0]!.id, existingClient: client as unknown as DaemonClient, }, ], ]), }); await waitForHostOnline(store, host.serverId); client.authFailureReason = "incorrect_password"; client.setConnectionState({ status: "disconnected", reason: "Incorrect password" }); await onceHostSnapshotMatches( store, host.serverId, (snapshot) => snapshot?.authFailureReason === "incorrect_password", ); expect(store.getSnapshot(host.serverId)?.lastError).toBe("Incorrect password"); expect(client.reconnectEnabledChanges.at(-1)).toBe(false); store.syncHosts([]); }); it("tries another connection after the active connection rejects its password", async () => { const relayClient = makeConnectedProbeClient(5); const directClient = makeConnectedProbeClient(5); const host = makeHost({ serverId: "srv_rejected_relay_with_local_direct", password: "stale-password", preferredConnectionId: "relay:remote:443", connections: [ { id: "relay:remote:443", type: "relay", relayEndpoint: "remote:443", daemonPublicKeyB64: "pk_test_offer", }, { id: "direct:localhost:6799", type: "directTcp", endpoint: "localhost:6799" }, ], }); const probeAttempts: string[] = []; const store = new HostRuntimeStore({ storage: createMemoryHostRuntimeStorage(), deps: { createClient: () => new FakeDaemonClient() as unknown as DaemonClient, connectToDaemon: async ({ connection }) => { probeAttempts.push(connection.id); if (connection.type === "relay") throw new DaemonAuthenticationError("incorrect_password"); return { client: directClient as unknown as DaemonClient, serverId: host.serverId, hostname: host.label, }; }, getClientId: async () => "cid_rejected_relay_with_local_direct", }, }); store.syncHosts([host], { initialConnectionByServerId: new Map([ [ host.serverId, { connectionId: "relay:remote:443", existingClient: relayClient as unknown as DaemonClient, }, ], ]), }); await waitForHostOnline(store, host.serverId); relayClient.authFailureReason = "incorrect_password"; relayClient.setConnectionState({ status: "disconnected", reason: "Incorrect password" }); await onceHostSnapshotMatches( store, host.serverId, (snapshot) => snapshot?.authFailureReason === "incorrect_password", ); await store.runProbeCycleNow(host.serverId); await onceHostSnapshotMatches( store, host.serverId, (snapshot) => snapshot?.connectionStatus === "online" && snapshot.activeConnectionId === "direct:localhost:6799", ); expect(probeAttempts).toContain("direct:localhost:6799"); expect(probeAttempts).not.toContain("relay:remote:443"); store.syncHosts([]); }); it("keeps five rejected hosts red with independent password errors", async () => { let probeAttempts = 0; const store = new HostRuntimeStore({ storage: createMemoryHostRuntimeStorage(), deps: { createClient: () => new FakeDaemonClient() as unknown as DaemonClient, connectToDaemon: async () => { probeAttempts += 1; throw new DaemonAuthenticationError("password_required"); }, getClientId: async () => "cid_five_rejected_hosts", }, }); for (let index = 1; index <= 5; index += 1) { const serverId = `srv_rejected_${index}`; await store.upsertDirectConnection({ serverId, endpoint: `host-${index}:6767` }); await onceHostSnapshotMatches( store, serverId, (snapshot) => snapshot?.authFailureReason === "password_required", ); } for (let index = 1; index <= 5; index += 1) { const snapshot = store.getSnapshot(`srv_rejected_${index}`); expect(snapshot?.connectionStatus).toBe("error"); expect(snapshot?.lastError).toBe("Password required"); } const attemptsBeforeRetry = probeAttempts; await store.runProbeCycleNow("srv_rejected_1"); expect(probeAttempts).toBe(attemptsBeforeRetry); store.syncHosts([]); }); it("imports a deep link without waiting for admission, then leaves a rejected saved host red", async () => { const store = new HostRuntimeStore({ storage: createMemoryHostRuntimeStorage(), deps: { createClient: () => new FakeDaemonClient() as unknown as DaemonClient, connectToDaemon: async () => { throw new DaemonAuthenticationError("password_required"); }, getClientId: async () => "cid_deep_link", }, }); answerHostConfirmations(store, true); const link = "relay://relay.example:443/srv_deep?key=AAAA&ssl=true"; await expect(store.importConnectionLink(link, "openProject")).resolves.toEqual({ status: "connected", serverId: "srv_deep", }); expect(store.getHosts()).toHaveLength(1); await onceHostSnapshotMatches( store, "srv_deep", (snapshot) => snapshot?.authFailureReason === "password_required", ); expect(store.getSnapshot("srv_deep")?.connectionStatus).toBe("error"); store.syncHosts([]); }); it("stops connecting to a host removed while its first probe is pending", async () => { useHostRuntimeClock(); const firstProbe = createDeferred(); let connectCalls = 0; const store = new HostRuntimeStore({ storage: createMemoryHostRuntimeStorage(), deps: { createClient: () => new FakeDaemonClient() as unknown as DaemonClient, connectToDaemon: async ({ host }) => { connectCalls += 1; if (connectCalls === 1) await firstProbe.promise; return { client: makeConnectedProbeClient(5) as unknown as DaemonClient, serverId: host.serverId, hostname: host.label ?? null, }; }, getClientId: async () => "cid_test_removed_host", }, }); await store.upsertDirectConnection({ serverId: "srv_removed", endpoint: "lan:6767" }); await vi.waitFor(() => expect(connectCalls).toBe(1)); await store.removeHost("srv_removed"); firstProbe.resolve(); await vi.advanceTimersByTimeAsync(60_000); expect(connectCalls).toBe(1); expect(store.getSnapshot("srv_removed")).toBeNull(); expect(useSessionStore.getState().sessions["srv_removed"]).toBeUndefined(); }); it("saves and reconnects a rejected saved host after changing its password", async () => { const store = new HostRuntimeStore({ storage: createMemoryHostRuntimeStorage(), deps: { createClient: () => new FakeDaemonClient() as unknown as DaemonClient, connectToDaemon: async ({ host }) => { if (host.password !== "correct-password") throw new DaemonAuthenticationError("password_required"); return { client: makeConnectedProbeClient(5) as unknown as DaemonClient, serverId: host.serverId, hostname: host.label, }; }, getClientId: async () => "cid_test_password_setting", }, }); await store.upsertDirectConnection({ serverId: "srv_test", endpoint: "lan:6767" }); await onceHostSnapshotMatches( store, "srv_test", (snapshot) => snapshot?.authFailureReason === "password_required", ); expect(store.getSnapshot("srv_test")?.connectionStatus).toBe("error"); await store.setHostPassword("srv_test", "correct-password"); await waitForHostOnline(store, "srv_test"); expect(store.getHosts()[0]?.password).toBe("correct-password"); expect(store.getSnapshot("srv_test")?.authFailureReason).toBeNull(); await store.setHostPassword("srv_test", ""); expect(store.getHosts()[0]?.password).toBeUndefined(); store.syncHosts([]); }); it("attempts the explicit initial connection hint before default localhost bootstrap", async () => { const seenProbes: { endpoint: string; useTls?: boolean }[] = []; const store = new HostRuntimeStore({ deps: { createClient: () => new FakeDaemonClient() as unknown as DaemonClient, connectToDaemon: async ({ connection }) => { if (connection.type === "directTcp") { seenProbes.push({ endpoint: connection.endpoint, useTls: connection.useTls }); } return { client: makeConnectedProbeClient(5) as unknown as DaemonClient, serverId: "srv_hint", hostname: "hint host", }; }, getClientId: async () => "cid_test_runtime", readInitialConnectionHint: () => ({ listen: "daemon-origin:6767", useTls: true, }), }, storage: createMemoryHostRuntimeStorage(), }); const hostAdded = onceHostListMatches(store, () => store.getHosts().length > 0); store.boot(); await hostAdded; expect(seenProbes).toContainEqual({ endpoint: "daemon-origin:6767", useTls: true }); const host = store.getHosts()[0]; expect(host?.serverId).toBe("srv_hint"); expect(host?.connections).toEqual( expect.arrayContaining([ expect.objectContaining({ endpoint: "daemon-origin:6767", useTls: true }), ]), ); store.syncHosts([]); }); it("does not infer window.location.host when no explicit hint is present", async () => { const seenProbes: { endpoint: string; useTls?: boolean }[] = []; const firstProbe = createDeferred(); const store = new HostRuntimeStore({ deps: { createClient: () => new FakeDaemonClient() as unknown as DaemonClient, connectToDaemon: async ({ connection }) => { if (connection.type === "directTcp") { seenProbes.push({ endpoint: connection.endpoint, useTls: connection.useTls }); } firstProbe.resolve(); throw new Error("probe unavailable"); }, getClientId: async () => "cid_test_runtime", readInitialConnectionHint: () => null, }, storage: createMemoryHostRuntimeStorage(), }); (globalThis as { window?: unknown }).window = { location: { host: "metro-host:8081", protocol: "http:" }, }; store.boot(); await firstProbe.promise; expect(seenProbes).not.toContainEqual(expect.objectContaining({ endpoint: "metro-host:8081" })); expect(store.getHosts()).toHaveLength(0); }); });