Remove the transfer forwarding adapter, borrowed SFTP channel scheduling, legacy assistant send entry points, unreachable Telnet password state, unused Tailscale APIs, and unused host deletion impact projections. Migrate behavioral tests to current paths and cover SFTP passphrase saving. Validation: Swift formatting and shared-copy/localization checks passed. The macOS test run reported failures in ConnectionStatusIconLayoutTests and AgentRuntimeTests; investigation and iOS validation remain pending.
272 lines
13 KiB
Swift
272 lines
13 KiB
Swift
import Foundation
|
|
import GhosttyTerminal
|
|
import TermindApplication
|
|
import TermindDomain
|
|
import TermindPresentation
|
|
import Testing
|
|
|
|
@testable import Termind
|
|
|
|
@Suite @MainActor
|
|
struct CommandNotificationTests {
|
|
@Test(arguments: ["hidden", "navigation", "snippets", "history", "peer", "archive", "visible"])
|
|
func assistantNotificationOpensSidebarWithoutReplacingWorkspace(_ state: String) throws {
|
|
let app = AppModel(hosts: HostLibraryModel())
|
|
let window = WindowModel()
|
|
app.registerWindow(window)
|
|
let conversation = ConversationModel()
|
|
conversation.composer.draft = "Target"
|
|
conversation.sendDraft()
|
|
window.assistantLibrary.insertAndSelect(conversation)
|
|
let workspace = Workspace(pane: Pane(.empty))
|
|
workspace.splitFocusedPane(axis: .horizontal, with: Pane(.empty))
|
|
window.open(workspace)
|
|
let focusedPane = workspace.focusedPane
|
|
window.showAssistant()
|
|
switch state {
|
|
case "hidden": window.showsSidebar = false
|
|
case "navigation": window.sidebarMode = .navigation
|
|
case "snippets": window.sidebarMode = .snippets
|
|
case "history": window.sidebarMode = .history
|
|
case "peer":
|
|
let peer = ConversationModel()
|
|
peer.composer.draft = "Other conversation"
|
|
peer.sendDraft()
|
|
window.assistantLibrary.insertAndSelect(peer)
|
|
case "archive": window.showAssistantArchiveInSidebar()
|
|
default: break
|
|
}
|
|
#expect(window.isShowingAssistant(conversation.id) == (state == "visible"))
|
|
#expect(app.isAssistantNotificationFocused(conversation.id) == (state == "visible"))
|
|
window.isQuickOpenPresented = true
|
|
|
|
#expect(app.selectAssistantNotificationTarget(conversation.id) === window)
|
|
|
|
#expect(window.selectedWorkspace === workspace)
|
|
#expect(workspace.focusedPane === focusedPane)
|
|
#expect(window.showsSidebar && window.sidebarMode == .assistant)
|
|
#expect(window.isShowingAssistant(conversation.id))
|
|
#expect(!window.isAssistantArchivePresented)
|
|
#expect(!window.isQuickOpenPresented && !window.isToolbarFindFocused)
|
|
}
|
|
|
|
@Test(arguments: [true, false])
|
|
func assistantNotificationPrefersFocusThenStableOrderWithinMatchingWindows(_ visible: Bool) throws {
|
|
let app = AppModel(hosts: HostLibraryModel())
|
|
let windows = [WindowModel(), WindowModel()].sorted { $0.id.uuidString < $1.id.uuidString }
|
|
for window in windows { app.registerWindow(window) }
|
|
let conversation = ConversationModel()
|
|
conversation.composer.draft = "Target"
|
|
conversation.sendDraft()
|
|
windows[0].assistantLibrary.insertAndSelect(conversation)
|
|
for window in windows {
|
|
window.openAssistantFullScreen(conversation.id)
|
|
if !visible { window.selectVault() }
|
|
}
|
|
|
|
#expect(
|
|
app.selectAssistantNotificationTarget(conversation.id, windowIsFocused: { $0 === windows[1] })
|
|
=== windows[1])
|
|
if !visible { windows[1].selectVault() }
|
|
#expect(app.selectAssistantNotificationTarget(conversation.id, windowIsFocused: { _ in false }) === windows[0])
|
|
|
|
windows[0].closeForWindow()
|
|
#expect(app.selectAssistantNotificationTarget(conversation.id) === windows[1])
|
|
}
|
|
|
|
@Test func assistantNotificationReusesHiddenSelectionBeforeAnUnrelatedFocusedWindow() throws {
|
|
let app = AppModel(hosts: HostLibraryModel())
|
|
let owner = WindowModel()
|
|
let other = WindowModel()
|
|
app.registerWindow(owner)
|
|
app.registerWindow(other)
|
|
let conversation = ConversationModel()
|
|
conversation.composer.draft = "Target"
|
|
conversation.sendDraft()
|
|
owner.assistantLibrary.insertAndSelect(conversation)
|
|
owner.openAssistantFullScreen(conversation.id)
|
|
let workspace = Workspace(pane: Pane(.empty))
|
|
owner.open(workspace)
|
|
owner.showsSidebar = false
|
|
#expect(!app.isAssistantNotificationFocused(conversation.id))
|
|
|
|
#expect(app.selectAssistantNotificationTarget(conversation.id, windowIsFocused: { $0 === other }) === owner)
|
|
#expect(owner.selectedWorkspace === workspace)
|
|
#expect(owner.isShowingAssistant(conversation.id))
|
|
#expect(other.assistantLibrary.selectedConversationID == nil)
|
|
|
|
other.openAssistantFullScreen(conversation.id)
|
|
owner.showsSidebar = false
|
|
#expect(app.selectAssistantNotificationTarget(conversation.id, windowIsFocused: { $0 === owner }) === other)
|
|
#expect(!owner.showsSidebar)
|
|
}
|
|
|
|
@Test func notificationRouterUsesTheLatestClickAndConsumesItOnce() async throws {
|
|
let app = AppModel(hosts: HostLibraryModel())
|
|
let window = WindowModel()
|
|
app.registerWindow(window)
|
|
let first = ConversationModel()
|
|
let second = ConversationModel()
|
|
first.composer.draft = "First"
|
|
second.composer.draft = "Second"
|
|
first.sendDraft()
|
|
window.assistantLibrary.insertAndSelect(first)
|
|
second.sendDraft()
|
|
window.assistantLibrary.insertAndSelect(second)
|
|
let activations = AsyncStream<Void>.makeStream()
|
|
var count = 0
|
|
let router = DesktopNotificationRouter(app: app) { _ in
|
|
count += 1
|
|
activations.continuation.yield(())
|
|
}
|
|
router.open(.assistant(first.id))
|
|
router.open(.assistant(second.id))
|
|
var iterator = activations.stream.makeAsyncIterator()
|
|
_ = await iterator.next()
|
|
#expect(window.assistantLibrary.selectedConversationID == second.id)
|
|
router.resume()
|
|
#expect(count == 1)
|
|
router.stop()
|
|
router.open(.assistant(first.id))
|
|
router.resume()
|
|
#expect(window.assistantLibrary.selectedConversationID == second.id)
|
|
}
|
|
|
|
@Test func manuallyUnreadWorkspacesCountOnceWithoutRequiringATerminal() {
|
|
let app = AppModel(hosts: HostLibraryModel())
|
|
let window = WindowModel()
|
|
app.registerWindow(window)
|
|
let workspace = Workspace(pane: Pane(.empty))
|
|
window.open(workspace, select: false)
|
|
workspace.markCommandsUnread()
|
|
workspace.markCommandsUnread()
|
|
#expect(workspace.hasUnreadCommandCompletion)
|
|
#expect(window.unreadCompletionCount == 1)
|
|
#expect(app.unreadNotificationCount == 1)
|
|
workspace.markCommandsRead()
|
|
#expect(app.unreadNotificationCount == 0)
|
|
#expect(!Workspace(pane: Pane(.empty)).hasUnreadCommandCompletion)
|
|
}
|
|
|
|
@Test func appOwnsCommandNotificationsWithoutChangingThemeOrFontPrecedence() {
|
|
let state = TerminalViewState(configSource: .none)
|
|
let preferences = TerminalPreferences(
|
|
darkCustomEnabled: true,
|
|
darkCustomConfiguration: "notify-on-command-finish = always\nbackground = 010203")
|
|
state.applyPreferences(preferences)
|
|
#expect(state.controller.terminalConfiguration.rendered.contains("notify-on-command-finish = never"))
|
|
#expect(!state.theme.dark.rendered.contains("notify-on-command-finish = always"))
|
|
#expect(state.theme.dark.rendered.hasSuffix("background = 010203"))
|
|
}
|
|
|
|
@Test func workspaceFocusIncludesEveryPaneAndTargetsFollowMoves() throws {
|
|
let host = Host(name: "Build", host: "test.invalid")
|
|
let app = AppModel(hosts: HostLibraryModel(hosts: [host]))
|
|
let window = WindowModel()
|
|
let other = WindowModel()
|
|
app.registerWindow(window)
|
|
app.registerWindow(other)
|
|
let first = Pane.sshSession(host: host)
|
|
let second = Pane.sshSession(host: host)
|
|
let workspace = Workspace(pane: first)
|
|
workspace.splitFocusedPane(axis: .horizontal, with: second)
|
|
window.open(workspace)
|
|
let firstID = try #require(first.commandSessionID)
|
|
let secondID = try #require(second.commandSessionID)
|
|
#expect(workspace.focusedPane === second)
|
|
#expect(app.isCommandWorkspaceFocused(firstID, windowIsFocused: { $0 === window }))
|
|
#expect(app.isCommandWorkspaceFocused(secondID, windowIsFocused: { $0 === window }))
|
|
#expect(!app.isCommandWorkspaceFocused(firstID, windowIsFocused: { _ in false }))
|
|
window.selectVault()
|
|
#expect(app.selectCommandNotificationTarget(firstID) === window)
|
|
#expect(window.selectedWorkspace === workspace)
|
|
#expect(workspace.focusedPane === first)
|
|
window.selectVault()
|
|
#expect(!app.isCommandWorkspaceFocused(firstID, windowIsFocused: { _ in true }))
|
|
let monitor = try #require(first.commandMonitor)
|
|
monitor.beginConnection()
|
|
monitor.receiveOSC133(exitCode: 0, durationNanos: 0)
|
|
#expect(workspace.hasUnreadCommandCompletion)
|
|
#expect(window.unreadCompletionCount == 1)
|
|
#expect(other.unreadCompletionCount == 0)
|
|
let moved = try #require(window.takePaneForMove(first.id))
|
|
other.open(moved)
|
|
#expect(!workspace.hasUnreadCommandCompletion)
|
|
#expect(moved.hasUnreadCommandCompletion)
|
|
#expect(window.unreadCompletionCount == 0)
|
|
#expect(other.unreadCompletionCount == 1)
|
|
#expect(app.commandNotificationTarget(firstID)?.0 === other)
|
|
other.selectVault()
|
|
#expect(app.selectCommandNotificationTarget(firstID) === other)
|
|
#expect(other.selectedWorkspace === moved)
|
|
#expect(moved.focusedPane === first)
|
|
moved.markCommandsRead()
|
|
#expect(!moved.hasUnreadCommandCompletion)
|
|
#expect(other.unreadCompletionCount == 0)
|
|
other.close(moved.id)
|
|
#expect(app.commandNotificationTarget(firstID) == nil)
|
|
#expect(app.selectCommandNotificationTarget(firstID) == nil)
|
|
other.reopenLastClosed()
|
|
#expect(app.commandNotificationTarget(firstID) == nil)
|
|
#expect(first.commandSessionID != firstID)
|
|
}
|
|
|
|
@Test func assistantFocusIncludesVisibleSidebarAndNotificationTargetsReuseTheConversation() throws {
|
|
let app = AppModel(hosts: HostLibraryModel())
|
|
let window = WindowModel()
|
|
let other = WindowModel()
|
|
app.registerWindow(window)
|
|
app.registerWindow(other)
|
|
let conversation = ConversationModel()
|
|
conversation.composer.draft = "Test"
|
|
conversation.sendDraft()
|
|
window.assistantLibrary.insertAndSelect(conversation)
|
|
window.openAssistantFullScreen(conversation.id)
|
|
#expect(app.isAssistantNotificationFocused(conversation.id))
|
|
#expect(app.isAssistantNotificationFocused(UUID()))
|
|
window.open(Workspace(pane: Pane(.empty)))
|
|
window.sidebarMode = .assistant
|
|
window.showsSidebar = true
|
|
#expect(window.isShowingAssistant(conversation.id))
|
|
window.showsSidebar = false
|
|
#expect(!window.isShowingAssistant(conversation.id))
|
|
#expect(!app.isAssistantNotificationFocused(conversation.id))
|
|
other.openAssistantFullScreen(conversation.id)
|
|
#expect(app.selectAssistantNotificationTarget(conversation.id) === other)
|
|
#expect(other.assistantLibrary.selectedConversation === conversation)
|
|
let peer = ConversationModel()
|
|
peer.composer.draft = "Second conversation"
|
|
peer.sendDraft()
|
|
window.assistantLibrary.insertAndSelect(peer)
|
|
window.openAssistantInSidebar(peer.id)
|
|
window.showsSidebar = true
|
|
other.open(Workspace(pane: Pane(.empty)))
|
|
other.sidebarMode = .assistant
|
|
other.showsSidebar = true
|
|
#expect(app.isAssistantNotificationFocused(peer.id))
|
|
#expect(app.isAssistantNotificationFocused(conversation.id))
|
|
#expect(!app.isAssistantNotificationFocused(UUID()))
|
|
#expect(other.assistantLibrary.conversation(id: peer.id) === peer)
|
|
other.isAssistantArchivePresented = true
|
|
#expect(!other.isShowingAssistant(conversation.id))
|
|
let target = try #require(app.selectAssistantNotificationTarget(UUID()))
|
|
#expect(target.selectedDestination == .assistant)
|
|
#expect(target.assistantLibrary.selectedConversation == nil)
|
|
}
|
|
|
|
@Test func ghosttyValuesAlreadyPublishedAreNotReplayedWhenObserving() async {
|
|
let state = TerminalViewState(configSource: .none)
|
|
state.terminalDidFinishCommand(exitCode: 0, durationNanos: 1)
|
|
try? await Task.sleep(for: .milliseconds(30))
|
|
let monitor = TerminalCommandMonitor()
|
|
monitor.beginConnection()
|
|
let observation = TerminalCommandObservation(state: state, monitor: monitor)
|
|
#expect(!monitor.hasUnreadCompletion)
|
|
monitor.endConnection()
|
|
state.terminalDidFinishCommand(exitCode: 0, durationNanos: 1)
|
|
try? await Task.sleep(for: .milliseconds(30))
|
|
#expect(!monitor.hasUnreadCompletion)
|
|
withExtendedLifetime(observation) {}
|
|
}
|
|
}
|