fix(app): preserve active host connection choices (#2905)
Settings lost the workspace host when entering the app-wide route, while desktop bootstrap could restore a removed local connection before the saved registry was ready. Use the remembered workspace host for Settings and only register the managed daemon when its server is unknown after registry hydration.
This commit is contained in:
@@ -2,6 +2,8 @@ import { describe, expect, it, vi } from "vitest";
|
||||
import { DaemonStartService, upsertDesktopDaemonConnection } from "./daemon-start-service";
|
||||
import type { HostRuntimeStore } from "./host-runtime";
|
||||
import type { DesktopDaemonStatus } from "@/desktop/daemon/desktop-daemon";
|
||||
import { defaultHostAppearance } from "@/hosts/appearance";
|
||||
import type { HostProfile } from "@/types/host-connection";
|
||||
|
||||
interface RecordedUpsert {
|
||||
listenAddress: string;
|
||||
@@ -9,12 +11,13 @@ interface RecordedUpsert {
|
||||
hostname: string | null;
|
||||
}
|
||||
|
||||
function createFakeStore(): {
|
||||
store: Pick<HostRuntimeStore, "upsertConnectionFromListen">;
|
||||
function createFakeStore(hosts: HostProfile[] = []): {
|
||||
store: Pick<HostRuntimeStore, "getHosts" | "upsertConnectionFromListen">;
|
||||
upserts: RecordedUpsert[];
|
||||
} {
|
||||
const upserts: RecordedUpsert[] = [];
|
||||
const store = {
|
||||
getHosts: () => hosts,
|
||||
upsertConnectionFromListen: async (input: RecordedUpsert) => {
|
||||
upserts.push(input);
|
||||
return {} as Awaited<ReturnType<HostRuntimeStore["upsertConnectionFromListen"]>>;
|
||||
@@ -38,6 +41,26 @@ function makeStatus(overrides: Partial<DesktopDaemonStatus> = {}): DesktopDaemon
|
||||
};
|
||||
}
|
||||
|
||||
function makeRelayOnlyHost(serverId: string): HostProfile {
|
||||
return {
|
||||
serverId,
|
||||
label: "Relay host",
|
||||
appearance: defaultHostAppearance(),
|
||||
lifecycle: {},
|
||||
connections: [
|
||||
{
|
||||
id: "relay:relay.example.com",
|
||||
type: "relay",
|
||||
relayEndpoint: "relay.example.com",
|
||||
daemonPublicKeyB64: "public-key",
|
||||
},
|
||||
],
|
||||
preferredConnectionId: "relay:relay.example.com",
|
||||
createdAt: "2026-01-01T00:00:00.000Z",
|
||||
updatedAt: "2026-01-01T00:00:00.000Z",
|
||||
};
|
||||
}
|
||||
|
||||
describe("DaemonStartService", () => {
|
||||
it("upserts the connection on a successful daemon start", async () => {
|
||||
const fake = createFakeStore();
|
||||
@@ -291,6 +314,15 @@ describe("upsertDesktopDaemonConnection", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("does not add localhost when desktop bootstrap finds its server id already registered", async () => {
|
||||
const fake = createFakeStore([makeRelayOnlyHost("srv_desktop")]);
|
||||
|
||||
const result = await upsertDesktopDaemonConnection(fake.store, makeStatus());
|
||||
|
||||
expect(result).toEqual({ ok: true });
|
||||
expect(fake.upserts).toEqual([]);
|
||||
});
|
||||
|
||||
it("rejects a missing listen address without upserting", async () => {
|
||||
const fake = createFakeStore();
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ export interface StartDaemonIfEnabledInput {
|
||||
shouldStart: DaemonStartCondition;
|
||||
}
|
||||
|
||||
type DaemonConnectionStore = Pick<HostRuntimeStore, "upsertConnectionFromListen">;
|
||||
type DaemonConnectionStore = Pick<HostRuntimeStore, "getHosts" | "upsertConnectionFromListen">;
|
||||
|
||||
export interface DaemonStartServiceDeps {
|
||||
store: DaemonConnectionStore;
|
||||
@@ -20,14 +20,18 @@ export async function upsertDesktopDaemonConnection(
|
||||
store: DaemonConnectionStore,
|
||||
daemon: DesktopDaemonStatus,
|
||||
): Promise<DaemonStartResult> {
|
||||
const listenAddress = daemon.listen?.trim() ?? "";
|
||||
const serverId = daemon.serverId.trim();
|
||||
if (!listenAddress) {
|
||||
return { ok: false, error: "Desktop daemon did not return a listen address." };
|
||||
}
|
||||
if (!serverId) {
|
||||
return { ok: false, error: "Desktop daemon did not return a server id." };
|
||||
}
|
||||
if (store.getHosts().some((host) => host.serverId === serverId)) {
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
const listenAddress = daemon.listen?.trim() ?? "";
|
||||
if (!listenAddress) {
|
||||
return { ok: false, error: "Desktop daemon did not return a listen address." };
|
||||
}
|
||||
if (!connectionFromListen(listenAddress)) {
|
||||
return {
|
||||
ok: false,
|
||||
|
||||
@@ -1358,7 +1358,7 @@ export class HostRuntimeStore {
|
||||
private queuedAgentDrainInFlight = new Set<string>();
|
||||
private directorySyncByServer = new Map<string, DirectorySync>();
|
||||
private configuredOverrideBootstrapInFlight: Promise<void> | null = null;
|
||||
private bootStarted = false;
|
||||
private bootPromise: Promise<void> | null = null;
|
||||
private storage: HostRuntimeStorage;
|
||||
private replicaCache: ReplicaCache;
|
||||
|
||||
@@ -1393,12 +1393,11 @@ export class HostRuntimeStore {
|
||||
return this.hostRegistryLoaded;
|
||||
}
|
||||
|
||||
boot(): void {
|
||||
if (this.bootStarted) {
|
||||
return;
|
||||
boot(): Promise<void> {
|
||||
if (!this.bootPromise) {
|
||||
this.bootPromise = this.runBoot();
|
||||
}
|
||||
this.bootStarted = true;
|
||||
void this.runBoot();
|
||||
return this.bootPromise;
|
||||
}
|
||||
|
||||
private async runBoot(): Promise<void> {
|
||||
|
||||
Reference in New Issue
Block a user