Files
Termind/TFMTests/QuickOpenTests.swift
T
Kaede Akino 9738605dd3 feat(terminal): add Mosh and Eternal Terminal transports
Add SSH-authenticated transports with independent settings, protocol-aware menus and snippets, and tmux session persistence.

Restore eligible iOS workspaces from encrypted transport checkpoints without adding a macOS recovery entry. Include schema migrations and regression coverage.
2026-10-16 08:36:56 +08:00

402 lines
17 KiB
Swift

import AgentKit
import Foundation
import TermindApplication
import TermindDomain
import TermindInfrastructure
import TermindPresentation
import TermindRemoteInfrastructure
import Testing
import struct TermindDomain.Host
@testable import Termind
@Suite
@MainActor
struct QuickOpenTests {
@Test
func resumableConnectActionsUseWithPrefix() {
let locale = Locale(identifier: "en")
#expect(AppLocalization.resolve(HostConnectAction.mosh.title, locale: locale) == "with Mosh")
#expect(
AppLocalization.resolve(HostConnectAction.eternalTerminal.title, locale: locale)
== "with Eternal Terminal")
}
@Test
func nativeHostMenuCanDuplicateWithoutChangingTheOriginalHost() throws {
let host = Host(name: "Production", host: "server.invalid", port: 2222)
let app = AppModel(hosts: HostLibraryModel(hosts: [host]))
defer { app.shutdownForTermination() }
let entries = HostMenuEntries.entries(
for: host, app: app, window: WindowModel(), includesDuplicate: true)
let duplicate = try #require(entries.first { $0.symbol == MenuSymbol.duplicate })
#expect(duplicate.isEnabled)
duplicate.action()
#expect(app.hostLibrary.hosts.count == 2)
let copy = try #require(app.hostLibrary.hosts.first { $0.id != host.id })
#expect(copy.name == String(localized: AppStrings.Vault.duplicatedHostName(host.name)))
#expect(copy.host == host.host)
#expect(copy.port == host.port)
#expect(app.hostLibrary.host(host.id)?.name == host.name)
}
@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(.showAssistant, app: app, window: window)
#expect(window.selectedDestination == .assistant)
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
}
}
}