From 39e589b02f488d59a8e152f14b44dc9a94e1bbff Mon Sep 17 00:00:00 2001 From: lawrencecchen <54008264+lawrencecchen@users.noreply.github.com> Date: Sat, 31 Oct 2026 15:17:14 +0000 Subject: [PATCH] base red: a Forget's acpmux peer removal moves to CmuxNextServer (app_lines) #28799 (4a46cfa244c) added 75 CmuxNextApp Swift lines (SSHService.swift, AddDeviceServices+App.swift) with no exception entry; arch-ratchet's app_lines row refuses it alone (--base 4a46cfa244c^ --head 4a46cfa244c: +75), and the tip watcher's range 30806df8b37d..644491fdf9ef is +75. - SSHPeerForgets (new, CmuxNextServer/AddDevice, next to DeviceDirectory) owns the removal of a written Forget's acpmux peer: the default `acp host ls`/`acp host rm` through the app's cmux-tui, the marks of Forgets whose Undo still shows, the pending Forget record (Pending, was SSHService.PendingForget) and the exact-route match (reaches, sameRoute). CmuxNextServer now depends on CmuxNextRemote (ui -> service). - SSHService keeps the calls: hide, unhide, commitForget (with its "Forget still holds and no other session reaches the route" check), forgetMachines(reachingPeer:) and the connect's mark clear. - Reconnect's decision table moves to CmuxNextRemote as SSHConnectionMachine.Status.reconnectStep(daemonConnected:daemonFailed:) (it read only `daemon == .connected`), so the move takes out more App lines than #28799 added. CmuxNextApp -79; the watcher's range is -4. No behavior change. ForgetRemovesBothListsTests read the same cases through the moved code, now also check that a written Forget takes the SSH machine out of the list as well as its peer, and test SSHPeerForgets on its own. RemoteConnectRecoveryTests read the moved reconnect step. --- Packages/macOS/CmuxNext/Package.swift | 2 +- .../CmuxNextApp/Remote/SSHService.swift | 121 +++--------------- .../SSH/SSHConnectionMachine+Reconnect.swift | 27 ++++ .../AddDevice/SSHPeerForgets.swift | 80 ++++++++++++ .../ForgetRemovesBothListsTests.swift | 46 +++++-- .../RemoteConnectRecoveryTests.swift | 8 +- 6 files changed, 168 insertions(+), 116 deletions(-) create mode 100644 Packages/macOS/CmuxNext/Sources/CmuxNextRemote/SSH/SSHConnectionMachine+Reconnect.swift create mode 100644 Packages/macOS/CmuxNext/Sources/CmuxNextServer/AddDevice/SSHPeerForgets.swift diff --git a/Packages/macOS/CmuxNext/Package.swift b/Packages/macOS/CmuxNext/Package.swift index 02e490298fb7..85f0ad1f8224 100644 --- a/Packages/macOS/CmuxNext/Package.swift +++ b/Packages/macOS/CmuxNext/Package.swift @@ -1138,7 +1138,7 @@ let packageTargets: [Target] = uiLayerPreviewTargets + [ // projection of `server.status`. The App supplies the source. .target( name: "CmuxNextServer", - dependencies: ["CmuxNextCloud", "CmuxNextDesign", "CmuxNextIcons"], + dependencies: ["CmuxNextCloud", "CmuxNextDesign", "CmuxNextIcons", "CmuxNextRemote"], resources: [ .process("Resources"), ], diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Remote/SSHService.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Remote/SSHService.swift index 7464d188350d..7adc533a0fa0 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Remote/SSHService.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextApp/Remote/SSHService.swift @@ -31,17 +31,10 @@ final class SSHService { @ObservationIgnored let logger = Logger(subsystem: "com.cmuxterm.app.next", category: "app.ssh") /// Runs `remote.install` through the registry's confirmation (set by the handlers). @ObservationIgnored var offerInstall: ((SSHMachineSession) -> Void)? - /// Removes the acpmux peers whose ssh `[user@]host` the predicate picks: by default the app's own - /// cmux-tui (`acp host ls`, then `acp host rm` for each match); tests replace it. A written Forget - /// takes out the peer enroll added for the same host, so the machine leaves the agent list too - /// (HQ #4877 break 8). - @ObservationIgnored var removePeers: (((String) -> Bool) async -> Void)? + /// A written Forget's acpmux peer removal (HQ #4877 break 8); tests replace its `remove`. + @ObservationIgnored let peerForgets: SSHPeerForgets /// Forgotten machines, so a stale registry echo does not bring them back. @ObservationIgnored private var forgotten: Set = [] - /// Forgets still showing their Undo, by machine: their routes keep their acpmux peer, since an - /// Undo brings the machine back and must find its peer there. - /// Keyed by each Forget's own token, so an older Forget's write never clears a newer one's mark. - @ObservationIgnored private var undoable: [UUID: (machineID: String, route: SSHDestination)] = [:] /// Saved hosts on the home daemon's current connection (`SSHService+SavedHosts`). @ObservationIgnored var savedHosts: (connection: ObjectIdentifier, store: SavedHostsStore)? /// Machines with an install (and its handoffs) running from this app: one at a time (cx-3r9mt). @@ -60,19 +53,7 @@ final class SSHService { paths = SSHPaths.standard(bundleID: bundleID) binary = try? DaemonLauncher.resolveBinary(bundle: .main, environment: ProcessInfo.processInfo.environment) installer = RemoteInstaller(paths: paths) - removePeers = binary.map(Self.removePeers(cli:)) - } - - /// The default ``removePeers``: the app's own cmux-tui at `cli` lists the peers, then removes each - /// match, asking the predicate again right before each remove. - private static func removePeers(cli binary: URL) -> ((String) -> Bool) async -> Void { - let cli = HostCLI(binary: binary, environment: SSHService.environment) - return { matches in - guard let data = await cli.json(["acp", "host", "ls", "--json"], label: "host ls") else { return } - for peer in DeviceDirectory.sshPeers(data) where matches(peer.destination) { - _ = await cli.json(["acp", "host", "rm", peer.name, "--json"], label: "host rm") - } - } + peerForgets = SSHPeerForgets(cli: binary, environment: Self.environment) } /// Why SSH machines cannot connect in this build, or nil. @@ -196,7 +177,7 @@ final class SSHService { throw ActionFailure(message: RemoteStrings.invalidPath((error as? SSHHost.Invalid)?.field == "remote_state_dir" ? stateDir ?? "" : binary ?? "")) } forgotten.remove(host.machineID) - undoable = undoable.filter { $0.value.machineID != host.machineID } + peerForgets.connected(host.machineID) // The same machine (route, session, state directory) with another // cmux-tui path: the old link goes and the record takes the new // path, so a connect never runs the old binary again (cx-vuvc). @@ -243,7 +224,7 @@ final class SSHService { // A retry gets a fresh error from the new attempt. Do not let the // recovery sheet copy the previous attempt while the link is working. session.lastError = nil - switch Self.reconnectStep(link: session.linkStatus, daemon: session.daemon.startup, daemonFailed: session.daemonFailure != nil) { + switch session.linkStatus.reconnectStep(daemonConnected: session.daemon.startup == .connected, daemonFailed: session.daemonFailure != nil) { case .connect: session.connect() case .reconnectDaemon: session.reconnectDaemon() case .wake: session.wake(.user) @@ -254,50 +235,12 @@ final class SSHService { } } - /// What Reconnect does for a link in status `link` whose daemon startup is `daemon`. - enum ReconnectStep: Equatable { case connect, reconnectDaemon, wake, newAttempt, nothing } - - static func reconnectStep(link: SSHConnectionMachine.Status, daemon: DaemonStartupState, daemonFailed: Bool) -> ReconnectStep { - switch link { - case .offline, .unreachable, .failed: - // These failures leave the gate open for automatic backoff, so a user - // Retry must start a fresh attempt rather than only waking the loop. - return .connect - case .connected: - // A healthy transport has no reconnect to perform. If only the daemon - // handshake failed, retry that daemon attempt explicitly; otherwise a - // plain Reconnect must not arm a timeout on a live connection. - return daemonFailed ? .reconnectDaemon : .nothing - case .reconnecting where daemon == .connected: - // A transient interruption under an established daemon connection: the link may - // recover with its socket and connection intact, and no didConnect would then cancel - // a new startup deadline. Only wake the link. - return .wake - default: - return .newAttempt - } - } - func disconnect(_ session: SSHMachineSession) { session.disconnect() updateSavedHost(session) logger.info("disconnected \(session.host.destination.description, privacy: .public)") } - /// A Forget shown but not yet written: the machine is gone from this - /// run, while the saved list and the home session still hold it. - struct PendingForget { - let machineID: String - /// This Forget's own mark among the ones whose Undo still shows. - let token = UUID() - let label: String - /// Where it was reached: a written Forget also removes the acpmux peer at this route. - let destination: SSHDestination - /// The registry transport, with its `connect` flag. - let fields: [String: String] - let sessionID: String? - } - /// Disconnects and removes the machine from the saved list, with its /// personal organization (order, groups, pins) in the home session. func forget(_ session: SSHMachineSession) async { @@ -306,12 +249,11 @@ final class SSHService { /// Disconnects the machine and takes it out of this run at once; the /// writes wait for `commitForget` (the undo toast's end, `RemoteForgetUndo`). - func hide(_ session: SSHMachineSession) -> PendingForget { + func hide(_ session: SSHMachineSession) -> SSHPeerForgets.Pending { forgotten.insert(session.machineID) - let pending = PendingForget(machineID: session.machineID, label: session.host.label, destination: session.host.destination, - fields: Self.fields(Self.transport(session)) ?? [:], - sessionID: session.daemon.identity?.sessionID ?? recordID(for: session.host)) - undoable[pending.token] = (session.machineID, session.host.destination) + let pending = peerForgets.hide(machineID: session.machineID, label: session.host.label, destination: session.host.destination, + fields: Self.fields(Self.transport(session)) ?? [:], + sessionID: session.daemon.identity?.sessionID ?? recordID(for: session.host)) session.close() _ = machines.removeSSH(session.machineID) return pending @@ -319,17 +261,17 @@ final class SSHService { /// Undo of a hidden Forget: the machine comes back as it was saved, and /// connects again when it was set to connect. - func unhide(_ pending: PendingForget) { + func unhide(_ pending: SSHPeerForgets.Pending) { forgotten.remove(pending.machineID) - undoable[pending.token] = nil + peerForgets.ended(pending) restore(pending.fields) } /// Writes a hidden Forget: the saved list entry and the home session's /// record with its personal organization go. - func commitForget(_ pending: PendingForget) async { + func commitForget(_ pending: SSHPeerForgets.Pending) async { // A connect to the same machine while the toast showed took it back. - undoable[pending.token] = nil + peerForgets.ended(pending) guard stillForgotten(pending) else { return } forgetSavedHost(pending.machineID) if let sessionID = pending.sessionID, machines.local.supports(DaemonCapability.profiles.rawValue), @@ -340,49 +282,28 @@ final class SSHService { logger.error("forget-session \(sessionID, privacy: .public) failed: \(String(describing: error), privacy: .public)") } } - // Last, after every write, and checked again after the await above: the peer goes only while - // the Forget still holds and nothing else uses its route (another session there, or a Forget - // whose Undo still shows). - // The predicate runs again before each `host rm`, after every await of the removal. + // Last, after every write: the peer goes only while the Forget still holds and no other + // session reaches its route (checked again before each `host rm`). let route = pending.destination - await removePeers?({ [weak self] peer in - guard let self, self.stillForgotten(pending), !self.routeInUse(route) else { return false } - return Self.reaches(route, peer: peer) - }) + await peerForgets.removePeers(at: route) { [weak self] in + guard let self else { return false } + return stillForgotten(pending) && !machines.ssh.contains { SSHPeerForgets.sameRoute($0.host.destination, route) } + } } /// A connect to the same machine (while the toast showed, or during a write) takes it back. - private func stillForgotten(_ pending: PendingForget) -> Bool { + private func stillForgotten(_ pending: SSHPeerForgets.Pending) -> Bool { forgotten.contains(pending.machineID) && machines.sshSession(pending.machineID) == nil } - /// Whether a machine in the list, or a Forget still showing its Undo, reaches `route`. - private func routeInUse(_ route: SSHDestination) -> Bool { - machines.ssh.contains { Self.sameRoute($0.host.destination, route) } || undoable.values.contains { Self.sameRoute($0.route, route) } - } - /// Settings > Devices removed the acpmux peer at `peer` (`[user@]host`): the SSH machines that /// reach the same route leave too, written at once (Remove for Good was confirmed already). func forgetMachines(reachingPeer peer: String) async { - for session in machines.ssh where Self.reaches(session.host.destination, peer: peer) { + for session in machines.ssh where SSHPeerForgets.reaches(session.host.destination, peer: peer) { await forget(session) } } - /// Whether an SSH machine at `destination` and an acpmux peer whose `ssh://` URL dials `peer` - /// (`[user@]host`, as enroll adds it for that machine) are the same route: the same host (any - /// case) and the same user, and no ssh port on the machine (a peer URL's port is its daemon's, - /// never ssh's, so a machine on another ssh port is never taken for it). - static func reaches(_ destination: SSHDestination, peer: String) -> Bool { - guard destination.port == nil, let target = try? SSHDestination(parsing: peer), target.port == nil else { return false } - return sameRoute(destination, target) - } - - /// The same host (any case), user and port. - static func sameRoute(_ a: SSHDestination, _ b: SSHDestination) -> Bool { - a.host.caseInsensitiveCompare(b.host) == .orderedSame && a.user == b.user && a.port == b.port - } - /// The registry record's session id for `host`, when it was saved. private func recordID(for host: SSHHost) -> String? { machines.local.store.personal.sessions.first { record in diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextRemote/SSH/SSHConnectionMachine+Reconnect.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextRemote/SSH/SSHConnectionMachine+Reconnect.swift new file mode 100644 index 000000000000..0744c46ebf2a --- /dev/null +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextRemote/SSH/SSHConnectionMachine+Reconnect.swift @@ -0,0 +1,27 @@ +extension SSHConnectionMachine.Status { + /// What Reconnect does for a link in this status. + public enum ReconnectStep: Equatable, Sendable { case connect, reconnectDaemon, wake, newAttempt, nothing } + + /// What Reconnect does for a link in this status whose daemon startup is connected + /// (`daemonConnected`) and whose daemon handshake failed (`daemonFailed`). + public func reconnectStep(daemonConnected: Bool, daemonFailed: Bool) -> ReconnectStep { + switch self { + case .offline, .unreachable, .failed: + // These failures leave the gate open for automatic backoff, so a user + // Retry must start a fresh attempt rather than only waking the loop. + return .connect + case .connected: + // A healthy transport has no reconnect to perform. If only the daemon + // handshake failed, retry that daemon attempt explicitly; otherwise a + // plain Reconnect must not arm a timeout on a live connection. + return daemonFailed ? .reconnectDaemon : .nothing + case .reconnecting where daemonConnected: + // A transient interruption under an established daemon connection: the link may + // recover with its socket and connection intact, and no didConnect would then cancel + // a new startup deadline. Only wake the link. + return .wake + default: + return .newAttempt + } + } +} diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextServer/AddDevice/SSHPeerForgets.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextServer/AddDevice/SSHPeerForgets.swift new file mode 100644 index 000000000000..0b151c2942af --- /dev/null +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextServer/AddDevice/SSHPeerForgets.swift @@ -0,0 +1,80 @@ +public import CmuxNextRemote +public import Foundation + +/// A written Forget of an SSH machine takes out the acpmux peer that enroll added for the same +/// route, so the machine leaves the agent list too (HQ #4877 break 8). It keeps the peer while a +/// Forget of that route still shows its Undo (an Undo brings the machine back and must find its +/// peer there), and asks again right before each `acp host rm`. The app's SSHService owns one. +@MainActor +public final class SSHPeerForgets { + /// Removes the acpmux peers whose ssh `[user@]host` the predicate picks: by default the app's + /// own cmux-tui (`acp host ls`, then `acp host rm` for each match, the predicate asked again + /// right before each remove); tests replace it. + public var remove: (((String) -> Bool) async -> Void)? + /// Forgets still showing their Undo, each by its own token, so an older Forget's write never + /// clears a newer one's mark. + private var undoable: [UUID: (machineID: String, route: SSHDestination)] = [:] + + /// - Parameter cli: the app's own cmux-tui; nil (a build without it) removes no peer. + public init(cli binary: URL?, environment: @escaping @Sendable () async -> [String: String]) { + remove = binary.map { binary in + let cli = HostCLI(binary: binary, environment: environment) + return { matches in + guard let data = await cli.json(["acp", "host", "ls", "--json"], label: "host ls") else { return } + for peer in DeviceDirectory.sshPeers(data) where matches(peer.destination) { + _ = await cli.json(["acp", "host", "rm", peer.name, "--json"], label: "host rm") + } + } + } + } + + /// A Forget shown but not yet written: the machine is gone from this run, while the saved list + /// and the home session still hold it. + public struct Pending: Sendable { + public let machineID: String + /// This Forget's own mark among the ones whose Undo still shows. + public let token = UUID() + public let label: String + /// Where it was reached: a written Forget also removes the acpmux peer at this route. + public let destination: SSHDestination + /// The registry transport, with its `connect` flag. + public let fields: [String: String] + public let sessionID: String? + } + + /// A Forget of `machineID` at `destination` is hidden and shows its Undo: its route keeps its peer. + public func hide(machineID: String, label: String, destination: SSHDestination, fields: [String: String], sessionID: String?) -> Pending { + let pending = Pending(machineID: machineID, label: label, destination: destination, fields: fields, sessionID: sessionID) + undoable[pending.token] = (machineID, destination) + return pending + } + + /// The Forget `pending` was undone or written: its Undo no longer shows. + public func ended(_ pending: Pending) { undoable[pending.token] = nil } + + /// `machineID` connected again: no Forget of it holds its route any more. + public func connected(_ machineID: String) { undoable = undoable.filter { $0.value.machineID != machineID } } + + /// Removes the peers at `route` while `holds` (the Forget still holds and no machine in the list + /// reaches the route) and no Forget whose Undo still shows reaches it, asked before each remove. + public func removePeers(at route: SSHDestination, while holds: @escaping @MainActor () -> Bool) async { + await remove?({ [weak self] peer in + guard let self, holds(), !undoable.values.contains(where: { Self.sameRoute($0.route, route) }) else { return false } + return Self.reaches(route, peer: peer) + }) + } + + /// Whether an SSH machine at `destination` and an acpmux peer whose `ssh://` URL dials `peer` + /// (`[user@]host`, as enroll adds it for that machine) are the same route: the same host (any + /// case) and the same user, and no ssh port on the machine (a peer URL's port is its daemon's, + /// never ssh's, so a machine on another ssh port is never taken for it). + public nonisolated static func reaches(_ destination: SSHDestination, peer: String) -> Bool { + guard destination.port == nil, let target = try? SSHDestination(parsing: peer), target.port == nil else { return false } + return sameRoute(destination, target) + } + + /// The same host (any case), user and port. + public nonisolated static func sameRoute(_ a: SSHDestination, _ b: SSHDestination) -> Bool { + a.host.caseInsensitiveCompare(b.host) == .orderedSame && a.user == b.user && a.port == b.port + } +} diff --git a/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/ForgetRemovesBothListsTests.swift b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/ForgetRemovesBothListsTests.swift index 2066ca81b9c1..0156fa50338a 100644 --- a/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/ForgetRemovesBothListsTests.swift +++ b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/ForgetRemovesBothListsTests.swift @@ -24,14 +24,14 @@ import Testing @Test func aPeerIsTheMachinesOnlyOnAnExactRoute() throws { let leo = try SSHDestination(parsing: "leo@studio.example.ts.net") - #expect(SSHService.reaches(leo, peer: "leo@studio.example.ts.net")) - #expect(SSHService.reaches(leo, peer: "leo@Studio.Example.ts.net"), "host names compare without case") - #expect(!SSHService.reaches(leo, peer: "studio.example.ts.net"), "another user is another route") - #expect(!SSHService.reaches(leo, peer: "root@studio.example.ts.net")) - #expect(!SSHService.reaches(leo, peer: "leo@studio"), "a short name is not the same host") - #expect(!SSHService.reaches(try SSHDestination(parsing: "leo@studio.example.ts.net:2222"), peer: "leo@studio.example.ts.net"), + #expect(SSHPeerForgets.reaches(leo, peer: "leo@studio.example.ts.net")) + #expect(SSHPeerForgets.reaches(leo, peer: "leo@Studio.Example.ts.net"), "host names compare without case") + #expect(!SSHPeerForgets.reaches(leo, peer: "studio.example.ts.net"), "another user is another route") + #expect(!SSHPeerForgets.reaches(leo, peer: "root@studio.example.ts.net")) + #expect(!SSHPeerForgets.reaches(leo, peer: "leo@studio"), "a short name is not the same host") + #expect(!SSHPeerForgets.reaches(try SSHDestination(parsing: "leo@studio.example.ts.net:2222"), peer: "leo@studio.example.ts.net"), "a machine on another ssh port is never the peer") - #expect(SSHService.reaches(try SSHDestination(parsing: "leo@[fd7a::2]"), peer: "leo@[fd7a::2]")) + #expect(SSHPeerForgets.reaches(try SSHDestination(parsing: "leo@[fd7a::2]"), peer: "leo@[fd7a::2]")) } @Test func forgetRemovesThePeerOnlyWhenItIsWritten() async throws { @@ -39,7 +39,7 @@ import Testing let session = try Self.session("leo@studio.example.ts.net") ssh.machines.add(session) var picked: [[String]] = [] - ssh.removePeers = { matches in + ssh.peerForgets.remove = { matches in picked.append(["leo@studio.example.ts.net", "studio.example.ts.net", "leo@gpu-west"].filter(matches)) } let pending = ssh.hide(session) @@ -52,6 +52,30 @@ import Testing let again = ssh.hide(session) await ssh.commitForget(again) #expect(picked == [["leo@studio.example.ts.net"]], "the written Forget takes out the peer at its route, and only that one") + #expect(ssh.machines.sshSession(session.machineID) == nil, "and the SSH machine is out of the list too") + } + + /// The peer removal on its own (SSHPeerForgets, CmuxNextServer): a Forget whose Undo shows keeps + /// its route's peer until it ends; a connect of the machine clears its marks; only the route's + /// peer is picked, and only while the caller says the Forget holds. + @Test func thePeerRemovalKeepsAShownUndosPeerAndPicksOnlyItsRoute() async throws { + let forgets = SSHPeerForgets(cli: nil, environment: { [:] }) + #expect(forgets.remove == nil, "no cmux-tui: no peer is removed") + var picked: [String] = [] + forgets.remove = { matches in picked += ["leo@studio.example.ts.net", "leo@gpu-west"].filter(matches) } + let route = try SSHDestination(parsing: "leo@studio.example.ts.net") + let shown = forgets.hide(machineID: "m1", label: "studio", destination: route, fields: [:], sessionID: nil) + await forgets.removePeers(at: route) { true } + #expect(picked.isEmpty, "a Forget of that route still shows its Undo") + forgets.ended(shown) + await forgets.removePeers(at: route) { false } + #expect(picked.isEmpty, "the caller says the Forget no longer holds") + await forgets.removePeers(at: route) { true } + #expect(picked == ["leo@studio.example.ts.net"]) + _ = forgets.hide(machineID: "m1", label: "studio", destination: route, fields: [:], sessionID: nil) + forgets.connected("m1") + await forgets.removePeers(at: route) { true } + #expect(picked.count == 2, "a connect of the machine clears its Forget's mark") } @Test func forgetKeepsThePeerWhileAnotherSessionReachesTheSameRoute() async throws { @@ -61,7 +85,7 @@ import Testing ssh.machines.add(main) ssh.machines.add(work) var calls = 0 - ssh.removePeers = { matches in calls += ["leo@studio.example.ts.net"].filter(matches).count } + ssh.peerForgets.remove = { matches in calls += ["leo@studio.example.ts.net"].filter(matches).count } await ssh.forget(work) #expect(calls == 0, "the default session still uses that peer") await ssh.forget(main) @@ -77,7 +101,7 @@ import Testing ssh.machines.add(main) ssh.machines.add(work) var calls = 0 - ssh.removePeers = { matches in calls += ["leo@studio.example.ts.net"].filter(matches).count } + ssh.peerForgets.remove = { matches in calls += ["leo@studio.example.ts.net"].filter(matches).count } let first = ssh.hide(work) let second = ssh.hide(main) await ssh.commitForget(first) @@ -93,7 +117,7 @@ import Testing let session = try Self.session("leo@studio.example.ts.net") ssh.machines.add(session) var removed: [String] = [] - ssh.removePeers = { matches in + ssh.peerForgets.remove = { matches in ssh.machines.add(session) // the person connects it again while `host ls` runs removed += ["leo@studio.example.ts.net"].filter(matches) } diff --git a/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/RemoteConnectRecoveryTests.swift b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/RemoteConnectRecoveryTests.swift index bba1c0f0762b..ddc99f827ae3 100644 --- a/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/RemoteConnectRecoveryTests.swift +++ b/Packages/macOS/CmuxNext/Tests/CmuxNextAppTests/RemoteConnectRecoveryTests.swift @@ -74,10 +74,10 @@ import Testing } @Test func reconnectDuringATransientInterruptionKeepsAnEstablishedDaemonsStartup() { - #expect(SSHService.reconnectStep(link: .reconnecting, daemon: .connected, daemonFailed: false) == .wake) - #expect(SSHService.reconnectStep(link: .reconnecting, daemon: .connecting, daemonFailed: false) == .newAttempt) - #expect(SSHService.reconnectStep(link: .connected, daemon: .connected, daemonFailed: false) == .nothing) - #expect(SSHService.reconnectStep(link: .offline, daemon: .connected, daemonFailed: false) == .connect) + #expect(SSHConnectionMachine.Status.reconnecting.reconnectStep(daemonConnected: true, daemonFailed: false) == .wake) + #expect(SSHConnectionMachine.Status.reconnecting.reconnectStep(daemonConnected: false, daemonFailed: false) == .newAttempt) + #expect(SSHConnectionMachine.Status.connected.reconnectStep(daemonConnected: true, daemonFailed: false) == .nothing) + #expect(SSHConnectionMachine.Status.offline.reconnectStep(daemonConnected: true, daemonFailed: false) == .connect) } @Test func aRetryChangesTheDaemonObservationForAnIdenticalFailure() {