From 5f3122cd1248b94b32d878496f468e13f56d282a Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Wed, 23 Apr 2025 13:54:01 +1000 Subject: [PATCH 1/5] feat: add progress messages when creating sync sessions --- .../Preview Content/PreviewFileSync.swift | 1 + .../Preview Content/PreviewVPN.swift | 20 +++++++++---------- .../Coder-Desktop/VPN/MenuState.swift | 17 ++++++++-------- .../Views/FileSync/FileSyncSessionModal.swift | 5 ++++- .../Coder-Desktop/Views/VPN/VPNMenuItem.swift | 9 ++++----- .../VPNLib/FileSync/FileSyncDaemon.swift | 3 +++ .../VPNLib/FileSync/FileSyncPrompting.swift | 3 +++ 7 files changed, 34 insertions(+), 24 deletions(-) diff --git a/Coder-Desktop/Coder-Desktop/Preview Content/PreviewFileSync.swift b/Coder-Desktop/Coder-Desktop/Preview Content/PreviewFileSync.swift index 1253e42..8da6b5f 100644 --- a/Coder-Desktop/Coder-Desktop/Preview Content/PreviewFileSync.swift +++ b/Coder-Desktop/Coder-Desktop/Preview Content/PreviewFileSync.swift @@ -3,6 +3,7 @@ import VPNLib @MainActor final class PreviewFileSync: FileSyncDaemon { var logFile: URL = .init(filePath: "~/log.txt")! + var lastPromptMessage: String? var sessionState: [VPNLib.FileSyncSession] = [] diff --git a/Coder-Desktop/Coder-Desktop/Preview Content/PreviewVPN.swift b/Coder-Desktop/Coder-Desktop/Preview Content/PreviewVPN.swift index a3ef51e..2c6e8d0 100644 --- a/Coder-Desktop/Coder-Desktop/Preview Content/PreviewVPN.swift +++ b/Coder-Desktop/Coder-Desktop/Preview Content/PreviewVPN.swift @@ -6,25 +6,25 @@ final class PreviewVPN: Coder_Desktop.VPNService { @Published var state: Coder_Desktop.VPNServiceState = .connected @Published var menuState: VPNMenuState = .init(agents: [ UUID(): Agent(id: UUID(), name: "dev", status: .error, hosts: ["asdf.coder"], wsName: "dogfood2", - wsID: UUID()), + wsID: UUID(), primaryHost: "asdf.coder"), UUID(): Agent(id: UUID(), name: "dev", status: .okay, hosts: ["asdf.coder"], - wsName: "testing-a-very-long-name", wsID: UUID()), + wsName: "testing-a-very-long-name", wsID: UUID(), primaryHost: "asdf.coder"), UUID(): Agent(id: UUID(), name: "dev", status: .warn, hosts: ["asdf.coder"], wsName: "opensrc", - wsID: UUID()), + wsID: UUID(), primaryHost: "asdf.coder"), UUID(): Agent(id: UUID(), name: "dev", status: .off, hosts: ["asdf.coder"], wsName: "gvisor", - wsID: UUID()), + wsID: UUID(), primaryHost: "asdf.coder"), UUID(): Agent(id: UUID(), name: "dev", status: .off, hosts: ["asdf.coder"], wsName: "example", - wsID: UUID()), + wsID: UUID(), primaryHost: "asdf.coder"), UUID(): Agent(id: UUID(), name: "dev", status: .error, hosts: ["asdf.coder"], wsName: "dogfood2", - wsID: UUID()), + wsID: UUID(), primaryHost: "asdf.coder"), UUID(): Agent(id: UUID(), name: "dev", status: .okay, hosts: ["asdf.coder"], - wsName: "testing-a-very-long-name", wsID: UUID()), + wsName: "testing-a-very-long-name", wsID: UUID(), primaryHost: "asdf.coder"), UUID(): Agent(id: UUID(), name: "dev", status: .warn, hosts: ["asdf.coder"], wsName: "opensrc", - wsID: UUID()), + wsID: UUID(), primaryHost: "asdf.coder"), UUID(): Agent(id: UUID(), name: "dev", status: .off, hosts: ["asdf.coder"], wsName: "gvisor", - wsID: UUID()), + wsID: UUID(), primaryHost: "asdf.coder"), UUID(): Agent(id: UUID(), name: "dev", status: .off, hosts: ["asdf.coder"], wsName: "example", - wsID: UUID()), + wsID: UUID(), primaryHost: "asdf.coder"), ], workspaces: [:]) let shouldFail: Bool let longError = "This is a long error to test the UI with long error messages" diff --git a/Coder-Desktop/Coder-Desktop/VPN/MenuState.swift b/Coder-Desktop/Coder-Desktop/VPN/MenuState.swift index 9c15aca..c7f1674 100644 --- a/Coder-Desktop/Coder-Desktop/VPN/MenuState.swift +++ b/Coder-Desktop/Coder-Desktop/VPN/MenuState.swift @@ -18,8 +18,7 @@ struct Agent: Identifiable, Equatable, Comparable, Hashable { return lhs.wsName.localizedCompare(rhs.wsName) == .orderedAscending } - // Hosts arrive sorted by length, the shortest looks best in the UI. - var primaryHost: String? { hosts.first } + let primaryHost: String } enum AgentStatus: Int, Equatable, Comparable { @@ -69,6 +68,9 @@ struct VPNMenuState { invalidAgents.append(agent) return } + // Remove trailing dot if present + let nonEmptyHosts = agent.fqdn.map { $0.hasSuffix(".") ? String($0.dropLast()) : $0 } + // An existing agent with the same name, belonging to the same workspace // is from a previous workspace build, and should be removed. agents.filter { $0.value.name == agent.name && $0.value.wsID == wsID } @@ -81,10 +83,11 @@ struct VPNMenuState { name: agent.name, // If last handshake was not within last five minutes, the agent is unhealthy status: agent.lastHandshake.date > Date.now.addingTimeInterval(-300) ? .okay : .warn, - // Remove trailing dot if present - hosts: agent.fqdn.map { $0.hasSuffix(".") ? String($0.dropLast()) : $0 }, + hosts: nonEmptyHosts, wsName: workspace.name, - wsID: wsID + wsID: wsID, + // Hosts arrive sorted by length, the shortest looks best in the UI. + primaryHost: nonEmptyHosts.first!, ) } @@ -135,9 +138,7 @@ struct VPNMenuState { return items.sorted() } - var onlineAgents: [Agent] { - agents.map(\.value).filter { $0.primaryHost != nil } - } + var onlineAgents: [Agent] { agents.map(\.value) } mutating func clear() { agents.removeAll() diff --git a/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncSessionModal.swift b/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncSessionModal.swift index 66b20ba..86294ff 100644 --- a/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncSessionModal.swift +++ b/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncSessionModal.swift @@ -40,7 +40,7 @@ struct FileSyncSessionModal: View { Section { Picker("Workspace", selection: $remoteHostname) { ForEach(agents, id: \.id) { agent in - Text(agent.primaryHost!).tag(agent.primaryHost!) + Text(agent.primaryHost).tag(agent.primaryHost) } // HACK: Silence error logs for no-selection. Divider().tag(nil as String?) @@ -62,6 +62,9 @@ struct FileSyncSessionModal: View { Divider() HStack { Spacer() + if let lastMessage = fileSync.lastPromptMessage { + Text(lastMessage).foregroundStyle(.secondary) + } Button("Cancel", action: { dismiss() }).keyboardShortcut(.cancelAction) Button(existingSession == nil ? "Add" : "Save") { Task { await submit() }} .keyboardShortcut(.defaultAction) diff --git a/Coder-Desktop/Coder-Desktop/Views/VPN/VPNMenuItem.swift b/Coder-Desktop/Coder-Desktop/Views/VPN/VPNMenuItem.swift index 700cefa..1bc0b98 100644 --- a/Coder-Desktop/Coder-Desktop/Views/VPN/VPNMenuItem.swift +++ b/Coder-Desktop/Coder-Desktop/Views/VPN/VPNMenuItem.swift @@ -66,7 +66,7 @@ struct MenuItemView: View { private var itemName: AttributedString { let name = switch item { - case let .agent(agent): agent.primaryHost ?? "\(item.wsName).\(state.hostnameSuffix)" + case let .agent(agent): agent.primaryHost case .offlineWorkspace: "\(item.wsName).\(state.hostnameSuffix)" } @@ -103,10 +103,10 @@ struct MenuItemView: View { } Spacer() }.buttonStyle(.plain) - if case let .agent(agent) = item, let copyableDNS = agent.primaryHost { + if case let .agent(agent) = item { Button { NSPasteboard.general.clearContents() - NSPasteboard.general.setString(copyableDNS, forType: .string) + NSPasteboard.general.setString(agent.primaryHost, forType: .string) } label: { Image(systemName: "doc.on.doc") .symbolVariant(.fill) @@ -143,7 +143,6 @@ struct MenuItemView: View { // If this menu item is an agent, and the user is logged in if case let .agent(agent) = item, let client = state.client, - let host = agent.primaryHost, let baseAccessURL = state.baseAccessURL, // Like the CLI, we'll re-use the existing session token to populate the URL let sessionToken = state.sessionToken @@ -166,7 +165,7 @@ struct MenuItemView: View { .flatMap(\.self) .first(where: { $0.id == agent.id }) { - apps = agentToApps(logger, wsAgent, host, baseAccessURL, sessionToken) + apps = agentToApps(logger, wsAgent, agent.primaryHost, baseAccessURL, sessionToken) } else { logger.error("Could not find agent '\(agent.id)' in workspace '\(item.wsName)' resources") } diff --git a/Coder-Desktop/VPNLib/FileSync/FileSyncDaemon.swift b/Coder-Desktop/VPNLib/FileSync/FileSyncDaemon.swift index 7f300fb..1b842e1 100644 --- a/Coder-Desktop/VPNLib/FileSync/FileSyncDaemon.swift +++ b/Coder-Desktop/VPNLib/FileSync/FileSyncDaemon.swift @@ -11,6 +11,7 @@ public protocol FileSyncDaemon: ObservableObject { var state: DaemonState { get } var sessionState: [FileSyncSession] { get } var logFile: URL { get } + var lastPromptMessage: String? { get } func tryStart() async func stop() async func refreshSessions() async @@ -47,6 +48,8 @@ public class MutagenDaemon: FileSyncDaemon { public let logFile: URL + @Published public var lastPromptMessage: String? + // Managing sync sessions could take a while, especially with prompting let sessionMgmtReqTimeout: TimeAmount = .seconds(15) diff --git a/Coder-Desktop/VPNLib/FileSync/FileSyncPrompting.swift b/Coder-Desktop/VPNLib/FileSync/FileSyncPrompting.swift index d5a49b4..dbd5cea 100644 --- a/Coder-Desktop/VPNLib/FileSync/FileSyncPrompting.swift +++ b/Coder-Desktop/VPNLib/FileSync/FileSyncPrompting.swift @@ -28,6 +28,7 @@ extension MutagenDaemon { try initResp.ensureValid(first: true, allowPrompts: allowPrompts) Task.detached(priority: .background) { + defer { Task { @MainActor in self.lastPromptMessage = nil } } do { while let msg = try await iter.next() { try msg.ensureValid(first: false, allowPrompts: allowPrompts) @@ -39,6 +40,8 @@ extension MutagenDaemon { } // Any other messages that require a non-empty response will // cause the create op to fail, showing an error. This is ok for now. + } else { + Task { @MainActor in self.lastPromptMessage = msg.message } } try await stream.requestStream.send(reply) } From 10631551f261a315c41ac7759df90794a02b2f71 Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Wed, 23 Apr 2025 14:05:32 +1000 Subject: [PATCH 2/5] fixup tests --- .../Preview Content/PreviewFileSync.swift | 8 ++++++-- Coder-Desktop/Coder-Desktop/VPN/MenuState.swift | 2 +- .../Views/FileSync/FileSyncSessionModal.swift | 13 ++++++++++--- Coder-Desktop/Coder-DesktopTests/AgentsTests.swift | 3 ++- Coder-Desktop/Coder-DesktopTests/Util.swift | 7 ++++++- Coder-Desktop/VPNLib/FileSync/FileSyncDaemon.swift | 8 ++++---- .../VPNLib/FileSync/FileSyncManagement.swift | 7 +++++-- .../VPNLib/FileSync/FileSyncPrompting.swift | 8 +++++--- 8 files changed, 39 insertions(+), 17 deletions(-) diff --git a/Coder-Desktop/Coder-Desktop/Preview Content/PreviewFileSync.swift b/Coder-Desktop/Coder-Desktop/Preview Content/PreviewFileSync.swift index 8da6b5f..fa64475 100644 --- a/Coder-Desktop/Coder-Desktop/Preview Content/PreviewFileSync.swift +++ b/Coder-Desktop/Coder-Desktop/Preview Content/PreviewFileSync.swift @@ -3,7 +3,6 @@ import VPNLib @MainActor final class PreviewFileSync: FileSyncDaemon { var logFile: URL = .init(filePath: "~/log.txt")! - var lastPromptMessage: String? var sessionState: [VPNLib.FileSyncSession] = [] @@ -21,7 +20,12 @@ final class PreviewFileSync: FileSyncDaemon { state = .stopped } - func createSession(arg _: CreateSyncSessionRequest) async throws(DaemonError) {} + func createSession( + arg _: CreateSyncSessionRequest, + promptCallback _: ( + @MainActor (String) -> Void + )? + ) async throws(DaemonError) {} func deleteSessions(ids _: [String]) async throws(VPNLib.DaemonError) {} diff --git a/Coder-Desktop/Coder-Desktop/VPN/MenuState.swift b/Coder-Desktop/Coder-Desktop/VPN/MenuState.swift index c7f1674..59dfae0 100644 --- a/Coder-Desktop/Coder-Desktop/VPN/MenuState.swift +++ b/Coder-Desktop/Coder-Desktop/VPN/MenuState.swift @@ -87,7 +87,7 @@ struct VPNMenuState { wsName: workspace.name, wsID: wsID, // Hosts arrive sorted by length, the shortest looks best in the UI. - primaryHost: nonEmptyHosts.first!, + primaryHost: nonEmptyHosts.first! ) } diff --git a/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncSessionModal.swift b/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncSessionModal.swift index 86294ff..ca579e1 100644 --- a/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncSessionModal.swift +++ b/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncSessionModal.swift @@ -15,6 +15,8 @@ struct FileSyncSessionModal: View { @State private var createError: DaemonError? @State private var pickingRemote: Bool = false + @State private var lastPromptMessage: String? + var body: some View { let agents = vpn.menuState.onlineAgents VStack(spacing: 0) { @@ -62,8 +64,11 @@ struct FileSyncSessionModal: View { Divider() HStack { Spacer() - if let lastMessage = fileSync.lastPromptMessage { - Text(lastMessage).foregroundStyle(.secondary) + if let msg = lastPromptMessage { + Text(msg).foregroundStyle(.secondary) + } + if loading { + ProgressView().controlSize(.small) } Button("Cancel", action: { dismiss() }).keyboardShortcut(.cancelAction) Button(existingSession == nil ? "Add" : "Save") { Task { await submit() }} @@ -106,8 +111,10 @@ struct FileSyncSessionModal: View { arg: .init( alpha: .init(path: localPath, protocolKind: .local), beta: .init(path: remotePath, protocolKind: .ssh(host: remoteHostname)) - ) + ), + promptCallback: { lastPromptMessage = $0 }, ) + lastPromptMessage = nil } catch { createError = error return diff --git a/Coder-Desktop/Coder-DesktopTests/AgentsTests.swift b/Coder-Desktop/Coder-DesktopTests/AgentsTests.swift index ac98bd3..fc53e04 100644 --- a/Coder-Desktop/Coder-DesktopTests/AgentsTests.swift +++ b/Coder-Desktop/Coder-DesktopTests/AgentsTests.swift @@ -27,7 +27,8 @@ struct AgentsTests { status: status, hosts: ["a\($0).coder"], wsName: "ws\($0)", - wsID: UUID() + wsID: UUID(), + primaryHost: "a\($0).coder", ) return (agent.id, agent) }) diff --git a/Coder-Desktop/Coder-DesktopTests/Util.swift b/Coder-Desktop/Coder-DesktopTests/Util.swift index c5239a9..6c7bc20 100644 --- a/Coder-Desktop/Coder-DesktopTests/Util.swift +++ b/Coder-Desktop/Coder-DesktopTests/Util.swift @@ -31,6 +31,8 @@ class MockVPNService: VPNService, ObservableObject { class MockFileSyncDaemon: FileSyncDaemon { var logFile: URL = .init(filePath: "~/log.txt") + var lastPromptMessage: String? + var sessionState: [VPNLib.FileSyncSession] = [] func refreshSessions() async {} @@ -47,7 +49,10 @@ class MockFileSyncDaemon: FileSyncDaemon { [] } - func createSession(arg _: CreateSyncSessionRequest) async throws(DaemonError) {} + func createSession( + arg _: CreateSyncSessionRequest, + promptCallback _: (@MainActor (String) -> Void)? + ) async throws(DaemonError) {} func pauseSessions(ids _: [String]) async throws(VPNLib.DaemonError) {} diff --git a/Coder-Desktop/VPNLib/FileSync/FileSyncDaemon.swift b/Coder-Desktop/VPNLib/FileSync/FileSyncDaemon.swift index 1b842e1..f8f1dc7 100644 --- a/Coder-Desktop/VPNLib/FileSync/FileSyncDaemon.swift +++ b/Coder-Desktop/VPNLib/FileSync/FileSyncDaemon.swift @@ -11,11 +11,13 @@ public protocol FileSyncDaemon: ObservableObject { var state: DaemonState { get } var sessionState: [FileSyncSession] { get } var logFile: URL { get } - var lastPromptMessage: String? { get } func tryStart() async func stop() async func refreshSessions() async - func createSession(arg: CreateSyncSessionRequest) async throws(DaemonError) + func createSession( + arg: CreateSyncSessionRequest, + promptCallback: (@MainActor (String) -> Void)? + ) async throws(DaemonError) func deleteSessions(ids: [String]) async throws(DaemonError) func pauseSessions(ids: [String]) async throws(DaemonError) func resumeSessions(ids: [String]) async throws(DaemonError) @@ -48,8 +50,6 @@ public class MutagenDaemon: FileSyncDaemon { public let logFile: URL - @Published public var lastPromptMessage: String? - // Managing sync sessions could take a while, especially with prompting let sessionMgmtReqTimeout: TimeAmount = .seconds(15) diff --git a/Coder-Desktop/VPNLib/FileSync/FileSyncManagement.swift b/Coder-Desktop/VPNLib/FileSync/FileSyncManagement.swift index aaf86b1..80fa76f 100644 --- a/Coder-Desktop/VPNLib/FileSync/FileSyncManagement.swift +++ b/Coder-Desktop/VPNLib/FileSync/FileSyncManagement.swift @@ -17,7 +17,10 @@ public extension MutagenDaemon { sessionState = sessions.sessionStates.map { FileSyncSession(state: $0) } } - func createSession(arg: CreateSyncSessionRequest) async throws(DaemonError) { + func createSession( + arg: CreateSyncSessionRequest, + promptCallback: (@MainActor (String) -> Void)? = nil + ) async throws(DaemonError) { if case .stopped = state { do throws(DaemonError) { try await start() @@ -26,7 +29,7 @@ public extension MutagenDaemon { throw error } } - let (stream, promptID) = try await host() + let (stream, promptID) = try await host(promptCallback: promptCallback) defer { stream.cancel() } let req = Synchronization_CreateRequest.with { req in req.prompter = promptID diff --git a/Coder-Desktop/VPNLib/FileSync/FileSyncPrompting.swift b/Coder-Desktop/VPNLib/FileSync/FileSyncPrompting.swift index dbd5cea..7b8307a 100644 --- a/Coder-Desktop/VPNLib/FileSync/FileSyncPrompting.swift +++ b/Coder-Desktop/VPNLib/FileSync/FileSyncPrompting.swift @@ -3,7 +3,10 @@ import GRPC extension MutagenDaemon { typealias PromptStream = GRPCAsyncBidirectionalStreamingCall - func host(allowPrompts: Bool = true) async throws(DaemonError) -> (PromptStream, identifier: String) { + func host( + allowPrompts: Bool = true, + promptCallback: (@MainActor (String) -> Void)? = nil + ) async throws(DaemonError) -> (PromptStream, identifier: String) { let stream = client!.prompt.makeHostCall() do { @@ -28,7 +31,6 @@ extension MutagenDaemon { try initResp.ensureValid(first: true, allowPrompts: allowPrompts) Task.detached(priority: .background) { - defer { Task { @MainActor in self.lastPromptMessage = nil } } do { while let msg = try await iter.next() { try msg.ensureValid(first: false, allowPrompts: allowPrompts) @@ -41,7 +43,7 @@ extension MutagenDaemon { // Any other messages that require a non-empty response will // cause the create op to fail, showing an error. This is ok for now. } else { - Task { @MainActor in self.lastPromptMessage = msg.message } + Task { @MainActor in promptCallback?(msg.message) } } try await stream.requestStream.send(reply) } From 65e7b785901d4c28526fdb0a2910da5953d47ecf Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Wed, 23 Apr 2025 17:44:40 +1000 Subject: [PATCH 3/5] tests --- .../Coder-DesktopTests/FileSyncDaemonTests.swift | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Coder-Desktop/Coder-DesktopTests/FileSyncDaemonTests.swift b/Coder-Desktop/Coder-DesktopTests/FileSyncDaemonTests.swift index 916faf6..85c0bcf 100644 --- a/Coder-Desktop/Coder-DesktopTests/FileSyncDaemonTests.swift +++ b/Coder-Desktop/Coder-DesktopTests/FileSyncDaemonTests.swift @@ -61,6 +61,7 @@ class FileSyncDaemonTests { #expect(statesEqual(daemon.state, .stopped)) #expect(daemon.sessionState.count == 0) + var promptMessages: [String] = [] try await daemon.createSession( arg: .init( alpha: .init( @@ -71,9 +72,16 @@ class FileSyncDaemonTests { path: mutagenBetaDirectory.path(), protocolKind: .local ) - ) + ), + promptCallback: { + promptMessages.append($0) + } ) + // There should be at least one prompt message + // Usually "Creating session..." + #expect(promptMessages.count > 0) + // Daemon should have started itself #expect(statesEqual(daemon.state, .running)) #expect(daemon.sessionState.count == 1) From c39a2ced02ce9d07918b3ed3bcbf8942576139a8 Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Wed, 23 Apr 2025 17:55:02 +1000 Subject: [PATCH 4/5] owned swift 6.1 update again --- .../Coder-Desktop/Views/FileSync/FileSyncSessionModal.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncSessionModal.swift b/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncSessionModal.swift index ca579e1..3e48ffd 100644 --- a/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncSessionModal.swift +++ b/Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncSessionModal.swift @@ -112,7 +112,7 @@ struct FileSyncSessionModal: View { alpha: .init(path: localPath, protocolKind: .local), beta: .init(path: remotePath, protocolKind: .ssh(host: remoteHostname)) ), - promptCallback: { lastPromptMessage = $0 }, + promptCallback: { lastPromptMessage = $0 } ) lastPromptMessage = nil } catch { From c43a960037e53ecc8187862e4158b88339f6729e Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Wed, 23 Apr 2025 18:22:57 +1000 Subject: [PATCH 5/5] owned by swift 6.1 update again --- Coder-Desktop/Coder-DesktopTests/AgentsTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Coder-Desktop/Coder-DesktopTests/AgentsTests.swift b/Coder-Desktop/Coder-DesktopTests/AgentsTests.swift index fc53e04..62c1607 100644 --- a/Coder-Desktop/Coder-DesktopTests/AgentsTests.swift +++ b/Coder-Desktop/Coder-DesktopTests/AgentsTests.swift @@ -28,7 +28,7 @@ struct AgentsTests { hosts: ["a\($0).coder"], wsName: "ws\($0)", wsID: UUID(), - primaryHost: "a\($0).coder", + primaryHost: "a\($0).coder" ) return (agent.id, agent) })