import AgentKit import Foundation import TermindApplication import TermindDomain import TermindInfrastructure import TermindPersistence import TermindPresentation import TermindRemoteInfrastructure import Testing import struct TermindDomain.Host @testable import Termind @Suite @MainActor struct QuickOpenTests { @Test func telnetUsesItsOwnSymbolInQuickOpenAndNativeHostMenus() throws { let host = Host( name: "Legacy Console", host: "console.example.invalid", telnetEnabled: true) let store = HostLibraryModel(hosts: [host]) let app = AppModel(hosts: store) let window = WindowModel() let commands = app.commands.commands(app: app, window: window) let ssh = try #require(commands.first { $0.target == .connectHost(host.id, .sshSession) }) let telnet = try #require(commands.first { $0.target == .connectHost(host.id, .telnetSession) }) #expect(ssh.symbol == MenuSymbol.ssh) #expect(telnet.symbol == MenuSymbol.telnet) #expect(ssh.symbol != telnet.symbol) let entries = HostMenuEntries.entries(for: host, app: app, window: window) let connect = try #require(entries.first { $0.title == String(localized: "Connect") }) let telnetEntry = try #require( connect.children.first { $0.title == String(localized: "with Telnet") }) #expect(telnetEntry.symbol == MenuSymbol.telnet) #expect( entries.map(\.title) == [ String(localized: "Quick Connect"), String(localized: "Connect"), String(localized: "Copy SSH Command"), "", String(localized: "Edit"), ]) let historyEntries = HostMenuEntries.entries( for: host, app: app, window: window, includesCopySSHCommand: false) #expect(!historyEntries.contains { $0.title == String(localized: "Copy SSH Command") }) } @Test func hostConnectMenusUseTheProtocolUnionAndFilterEachActionTargets() throws { let ssh = Host( name: "SSH", host: "ssh.example.invalid", sshEnabled: true, telnetEnabled: false) let telnet = Host( name: "Telnet", host: "telnet.example.invalid", sshEnabled: false, telnetEnabled: true) let both = Host( name: "Both", host: "both.example.invalid", sshEnabled: true, telnetEnabled: true) let neither = Host( name: "Neither", host: "neither.example.invalid", sshEnabled: false, telnetEnabled: false) #expect(HostConnectAction.available(for: [ssh]) == [.ssh, .sftp]) #expect(HostConnectAction.available(for: [telnet]) == [.telnet]) #expect(HostConnectAction.available(for: [both]) == [.ssh, .telnet, .sftp]) #expect(HostConnectAction.available(for: [neither]).isEmpty) #expect(HostConnectAction.available(for: [ssh, telnet]) == [.ssh, .telnet, .sftp]) #expect(HostConnectAction.ssh.matchingHosts(in: [ssh, telnet, both]).map(\.id) == [ssh.id, both.id]) #expect(HostConnectAction.telnet.matchingHosts(in: [ssh, telnet, both]).map(\.id) == [telnet.id, both.id]) #expect(HostConnectAction.sftp.matchingHosts(in: [ssh, telnet, both]).map(\.id) == [ssh.id, both.id]) let app = AppModel(hosts: HostLibraryModel(hosts: [ssh, telnet, both, neither])) let window = WindowModel() let sshEntries = HostMenuEntries.entries(for: ssh, app: app, window: window) let sshConnect = try #require(sshEntries.first { $0.title == String(localized: "Connect") }) #expect(sshConnect.children.map(\.title) == [String(localized: "with SSH"), String(localized: "with SFTP")]) #expect(!sshConnect.children.contains { !$0.isEnabled }) let telnetEntries = HostMenuEntries.entries(for: telnet, app: app, window: window) let telnetConnect = try #require(telnetEntries.first { $0.title == String(localized: "Connect") }) #expect(telnetConnect.children.map(\.title) == [String(localized: "with Telnet")]) #expect(!telnetConnect.children.contains { !$0.isEnabled }) #expect( !HostMenuEntries.entries(for: neither, app: app, window: window) .contains { $0.title == String(localized: "Connect") } ) } @Test func multiHostQuickConnectUsesOneWorkspace() throws { let ssh = Host(name: "SSH", host: "ssh.example.invalid") let telnet = Host( name: "Telnet", host: "telnet.example.invalid", sshEnabled: false, telnetEnabled: true) let app = AppModel(hosts: HostLibraryModel(hosts: [ssh, telnet])) let window = WindowModel() let layout = HostMenuPolicy.quickConnectLayout(for: [ssh, telnet]) #expect(layout == .oneWorkspace) #expect(HostMenuPolicy.quickConnectLayout(for: [ssh]) == .separateWorkspaces) app.open([ssh, telnet], as: .preferredTerminal, layout: layout, in: window) #expect(window.workspaces.count == 1) #expect(window.workspaces[0].panes.map(\.hostID) == [ssh.id, telnet.id]) } @Test func registryCombinesManagementWorkspacesHostsAndActions() { let host = Host(name: "Production", host: "prod.example", tags: ["critical"]) let template = WorkspaceTemplate( name: "Production Layout", layout: .pane(.ssh(hostID: host.id)) ) let store = HostLibraryModel(hosts: [host], workspaceTemplates: [template]) let app = AppModel(hosts: store) let workspace = Workspace.sshSession(host: host) let window = WindowModel(workspaces: [workspace]) let commands = app.commands.commands(app: app, window: window) #expect(commands.contains { $0.target == .vaultPage(.hosts) }) #expect(commands.contains { $0.target == .vault }) #expect(commands.contains { $0.target == .assistant }) #expect(commands.contains { $0.target == .settings }) #expect(commands.contains { $0.target == .workspace(workspace.id) }) #expect(commands.contains { $0.target == .workspaceTemplate(template.id) }) #expect(commands.contains { $0.target == .connectHost(host.id, .sshSession) }) #expect(commands.contains { $0.target == .newChat }) #expect(commands.contains { $0.target == .showAssistant }) #expect(commands.filter { $0.matches("critical") }.count == 2) #expect( commands.filter { $0.section == .navigation }.allSatisfy { switch $0.target { case .vaultPage, .vault, .assistant, .settings: true default: false } }) #expect( commands.filter { $0.section == .workspaces }.allSatisfy { switch $0.target { case .workspace, .workspaceTemplate: true default: false } }) #expect(commands.filter { $0.section == .conversations }.isEmpty) #expect( commands.filter { $0.section == .hosts }.allSatisfy { if case .connectHost = $0.target { true } else { false } }) #expect( commands.filter { $0.section == .actions }.allSatisfy { switch $0.target { case .newChat, .newHost, .showAssistant, .newWindow, .moveWorkspaceToNewWindow: true default: false } }) } @Test func registryListsSearchesAndOpensWorkspaceTemplates() throws { let production = Host(name: "Production API", host: "prod.example") let staging = Host(name: "Staging Database", host: "staging.example") let fleetID = UUID(uuidString: "00000000-0000-0000-0000-000000000002")! let adminID = UUID(uuidString: "00000000-0000-0000-0000-000000000001")! let fleet = WorkspaceTemplate( id: fleetID, name: "Fleet", layout: .split( axis: .horizontal, fraction: 0.5, first: .pane(.ssh(hostID: production.id)), second: .pane(.sftp(hostID: staging.id)) ) ) let admin = WorkspaceTemplate( id: adminID, name: "Admin", layout: .pane(.empty) ) let store = HostLibraryModel( hosts: [production, staging], workspaceTemplates: [fleet, admin] ) let app = AppModel(hosts: store) let window = WindowModel() window.hostStore = store let templateCommands = app.commands.commands(app: app, window: window).filter { if case .workspaceTemplate = $0.target { true } else { false } } #expect(templateCommands.map(\.title) == ["Admin", "Fleet"]) #expect( templateCommands.map(\.id) == [ "workspace-template.\(adminID.uuidString)", "workspace-template.\(fleetID.uuidString)", ]) #expect(Set(templateCommands.map(\.id)).count == templateCommands.count) #expect(templateCommands.allSatisfy { $0.subtitle == "Workspace Template" }) #expect(templateCommands.allSatisfy { $0.symbol == "rectangle.split.2x1" }) #expect( QuickOpenResults(commands: templateCommands, query: "staging database") .commands.map(\.target) == [.workspaceTemplate(fleet.id)] ) app.commands.perform(.workspaceTemplate(fleet.id), app: app, window: window) let opened = try #require(window.selectedWorkspace) #expect(opened.sourceTemplateID == fleet.id) #expect(opened.panes.map(\.kind) == [.sshSession, .sftpChannel]) } @Test func registryIncludesActiveConversationsInLibraryOrderAndSearchesVisibleContent() throws { let pinned = ConversationModel( messages: [ ChatMessage(role: .assistant, text: "The nginx upstream is healthy.") ], sortIndex: 20) pinned.customTitle = "Pinned deployment" pinned.isPinned = true let firstChat = ConversationModel( messages: [ ChatMessage(role: .user, text: "Inspect the database vacuum schedule") ], sortIndex: 1) firstChat.customTitle = "Database maintenance" let secondChat = ConversationModel( messages: [ ChatMessage( role: .assistant, text: "The proxy is ready.", reasoning: [AgentReasoningBlock(text: "hidden-token")] ) ], sortIndex: 2) secondChat.customTitle = "NGINX rollout" let archived = ConversationModel( messages: [ ChatMessage(role: .user, text: "Archived nginx notes") ], sortIndex: 0) archived.customTitle = "Archived nginx chat" archived.archivedAt = .now let library = AssistantLibraryModel( store: AssistantConversationStore( conversations: [secondChat, archived, pinned, firstChat] )) let app = AppModel(hosts: HostLibraryModel()) let window = WindowModel(assistantLibrary: library) let conversationCommands = app.commands.commands(app: app, window: window).filter { $0.section == .conversations } #expect( conversationCommands.map(\.title) == [ "Pinned deployment", "Database maintenance", "NGINX rollout", ]) #expect(Set(conversationCommands.map(\.id)).count == conversationCommands.count) #expect( conversationCommands.map(\.target) == [ .assistantConversation(pinned.id), .assistantConversation(firstChat.id), .assistantConversation(secondChat.id), ]) #expect( !conversationCommands.contains { command in command.target == .assistantConversation(archived.id) }) let nginx = QuickOpenResults(commands: conversationCommands, query: "nginx") #expect( nginx.commands.map(\.target) == [ .assistantConversation(pinned.id), .assistantConversation(secondChat.id), ]) #expect(QuickOpenResults(commands: conversationCommands, query: "NGINX").commands == nginx.commands) #expect(QuickOpenResults(commands: conversationCommands, query: "hidden-token").commands.isEmpty) } @Test func resultsGroupMatchesAndKeyboardSelectionSkipsHeaders() throws { let host = Host(name: "Production", host: "prod.example", tags: ["critical"]) let store = HostLibraryModel(hosts: [host]) let app = AppModel(hosts: store) let workspace = Workspace.sshSession(host: host) let window = WindowModel(workspaces: [workspace]) let commands = app.commands.commands(app: app, window: window) let results = QuickOpenResults(commands: commands, query: "") #expect(results.sections == [.navigation, .workspaces, .hosts, .actions]) #expect(sectionItems(in: results.listItems(selectedID: nil)) == results.sections) let first = try #require(results.commands.first) let second = try #require(results.commands.dropFirst().first) let last = try #require(results.commands.last) #expect(results.selection(movingFrom: nil, by: 1) == first.id) #expect(results.selection(movingFrom: first.id, by: -1) == first.id) #expect(results.selection(movingFrom: first.id, by: 1) == second.id) #expect(results.selection(movingFrom: last.id, by: 1) == last.id) let filtered = QuickOpenResults(commands: commands, query: "critical") #expect(filtered.sections == [.hosts]) #expect(filtered.commands.count == 2) #expect( sectionItems(in: filtered.listItems(selectedID: filtered.commands.first?.id)) == [ .hosts ]) #expect(filtered.listItems(selectedID: filtered.commands.first?.id).filter(\.isSelected).count == 1) } @Test func registryRoutesWithoutDuplicatingNavigationLogic() { let store = HostLibraryModel() let app = AppModel(hosts: store) let window = WindowModel() #expect( !app.commands.commands(app: app, window: window).contains { $0.target == .showAssistant } ) app.commands.perform(.settings, app: app, window: window) #expect(window.selectedDestination == .settings) #expect(window.settingsRoute == .index) #expect(window.workspaces.isEmpty) app.commands.perform(.newChat, app: app, window: window) #expect(window.selectedDestination == .assistant) #expect(window.workspaces.isEmpty) #expect(window.assistantLibrary.conversations.isEmpty) #expect(window.assistantLibrary.selectedConversationID == nil) } @Test func registryOpensConversationInMainAssistantAndLeavesArchive() { let conversation = ConversationModel(messages: [ ChatMessage(role: .user, text: "Review deployment") ]) let library = AssistantLibraryModel( store: AssistantConversationStore( conversations: [conversation] )) let app = AppModel(hosts: HostLibraryModel()) let window = WindowModel(assistantLibrary: library) window.showAssistantArchive() app.commands.perform( .assistantConversation(conversation.id), app: app, window: window ) #expect(window.selectedDestination == .assistant) #expect(window.assistantLibrary.selectedConversationID == conversation.id) #expect(!window.isAssistantArchivePresented) } private func sectionItems(in items: [QuickOpenListItem]) -> [AppCommandSection] { items.compactMap { item in guard case .section(let section) = item else { return nil } return section } } }