From 4e9a4582b7f607b91889b388f86cb42ecc6896da Mon Sep 17 00:00:00 2001 From: "paseo-bot[bot]" <266920839+paseo-bot[bot]@users.noreply.github.com> Date: Wed, 30 Sep 2026 02:12:03 +0200 Subject: [PATCH] fix(app): stop connecting to a host removed during its first probe (#5722) Closes #4815 Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com> --- packages/app/src/runtime/host-runtime.test.ts | 32 +++++++++++++++++++ packages/app/src/runtime/host-runtime.ts | 7 ++++ 2 files changed, 39 insertions(+) diff --git a/packages/app/src/runtime/host-runtime.test.ts b/packages/app/src/runtime/host-runtime.test.ts index 280eae793e..fbf7764f31 100644 --- a/packages/app/src/runtime/host-runtime.test.ts +++ b/packages/app/src/runtime/host-runtime.test.ts @@ -4017,6 +4017,38 @@ describe("HostRuntimeStore initial connection hint bootstrap", () => { 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(), diff --git a/packages/app/src/runtime/host-runtime.ts b/packages/app/src/runtime/host-runtime.ts index 7328e7e5b4..14668aa0b5 100644 --- a/packages/app/src/runtime/host-runtime.ts +++ b/packages/app/src/runtime/host-runtime.ts @@ -677,8 +677,15 @@ export class HostRuntimeController { connectionId: options.initialConnection.connectionId, existingClient: options.initialConnection.existingClient, }); + if (!this.started) { + return; + } } await this.runProbeCycleNow(); + // stop() can run while startup is pending; a stopped controller never probes again. + if (!this.started) { + return; + } if (options?.autoProbe !== false) { this.probeIntervalHandle = setInterval(() => { void this.runProbeCycleNow();