refactor(history): remove redundant closed workspace state

This commit is contained in:
Kaede Akino committed 2026-10-06 15:16:19 +08:00
1 parent 231be54e08
commit 59fbd11030
18 files changed
+126 -147

No files matched your search

@@ -19,7 +19,6 @@ struct ConnectionHistoryMenuTests {
let local = UUID()
let repository = InMemoryConnectionHistoryRepository()
let history = ConnectionHistoryModel(repository: repository, sourceDeviceID: local)
let devices = DeviceDirectoryModel(currentDeviceID: local)
let library = HostLibraryModel(hosts: [host])
let first = try #require(history.record(kind: .ssh, for: host.id, at: now))
let second = try #require(history.record(kind: .sftp, for: host.id, at: now.addingTimeInterval(1)))
@@ -29,13 +28,13 @@ struct ConnectionHistoryMenuTests {
history.reload()
let menu = ConnectionHistoryMenuSnapshot(
history: history, library: library, devices: devices, now: now, calendar: calendar)
history: history, library: library, now: now, calendar: calendar)
#expect(menu.recentToday.map(\.id) == [synced.id, second.id, first.id])
#expect(menu.days.isEmpty)
let restarted = ConnectionHistoryModel(repository: repository, sourceDeviceID: local)
let afterRestart = ConnectionHistoryMenuSnapshot(
history: restarted, library: library, devices: devices, now: now, calendar: calendar)
history: restarted, library: library, now: now, calendar: calendar)
#expect(afterRestart.recentToday.map(\.id) == [synced.id, second.id, first.id])
#expect(afterRestart.days.isEmpty)
}
@@ -49,7 +48,7 @@ struct ConnectionHistoryMenuTests {
try #require(history.record(kind: .ssh, for: host.id, at: today.addingTimeInterval(Double(index + 1))))
}
let menu = ConnectionHistoryMenuSnapshot(
history: history, library: HostLibraryModel(hosts: [host]), devices: DeviceDirectoryModel(),
history: history, library: HostLibraryModel(hosts: [host]),
now: today.addingTimeInterval(3600), calendar: calendar)
#expect(menu.recentToday.map(\.id) == entries.suffix(15).reversed().map(\.id))
#expect(menu.days.map(\.isToday) == [true, false])
@@ -68,10 +67,9 @@ struct ConnectionHistoryMenuTests {
let missing = ConnectionHistoryEntry(
hostID: UUID(), kind: .ssh, connectedAt: today.addingTimeInterval(-200_000))
let history = ConnectionHistoryModel(entries: [earlier, missing, recent])
let devices = DeviceDirectoryModel()
let library = HostLibraryModel(hosts: [host])
let menu = ConnectionHistoryMenuSnapshot(
history: history, library: library, devices: devices, now: now, calendar: calendar)
history: history, library: library, now: now, calendar: calendar)
#expect(menu.recentToday.map(\.id) == [recent.id])
#expect(menu.days.map(\.isToday) == [false])
#expect(menu.days.flatMap(\.rows).map(\.id) == [earlier.id])
@@ -79,11 +77,11 @@ struct ConnectionHistoryMenuTests {
var shifted = calendar
shifted.timeZone = TimeZone(secondsFromGMT: 8 * 3600)!
let localMenu = ConnectionHistoryMenuSnapshot(
history: history, library: library, devices: devices, now: today.addingTimeInterval(120), calendar: shifted)
history: history, library: library, now: today.addingTimeInterval(120), calendar: shifted)
#expect(localMenu.days.isEmpty)
#expect(localMenu.recentToday.map(\.id) == [recent.id, earlier.id])
let tomorrow = ConnectionHistoryMenuSnapshot(
history: history, library: library, devices: devices,
history: history, library: library,
now: today.addingTimeInterval(86_400), calendar: calendar)
#expect(tomorrow.recentToday.isEmpty)
#expect(tomorrow.days.allSatisfy { !$0.isToday })
@@ -96,15 +94,14 @@ struct ConnectionHistoryMenuTests {
hostID: host.id, kind: .sftp, connectedAt: calendar.startOfDay(for: now).addingTimeInterval(-60))
let history = ConnectionHistoryModel(entries: [old])
let library = HostLibraryModel(hosts: [host])
let devices = DeviceDirectoryModel()
_ = try #require(history.record(kind: .ssh, for: host.id, at: now))
let before = ConnectionHistoryMenuSnapshot(
history: history, library: library, devices: devices, now: now, calendar: calendar)
history: history, library: library, now: now, calendar: calendar)
#expect(before.recentToday.first?.canOpen == false)
#expect(before.days.first?.rows.first?.canOpen == false)
history.clearAll()
let after = ConnectionHistoryMenuSnapshot(
history: history, library: library, devices: devices, now: now, calendar: calendar)
history: history, library: library, now: now, calendar: calendar)
#expect(after.recentToday.isEmpty && after.days.isEmpty)
#expect(library.host(host.id) != nil)
}
@@ -6,7 +6,6 @@ import struct TermindDomain.Host
public struct ConnectionHistoryMenuRow: Identifiable {
public let entry: ConnectionHistoryEntry
public let host: Host
public let deviceName: String?
public var id: UUID { entry.id }
public var canOpen: Bool {
switch entry.kind {
@@ -30,7 +29,7 @@ public struct ConnectionHistoryMenuSnapshot {
@MainActor
public init(
history: ConnectionHistoryModel, library: HostLibraryModel, devices: DeviceDirectoryModel,
history: ConnectionHistoryModel, library: HostLibraryModel,
now: Date? = nil, calendar: Calendar = .current
) {
let rows = history.entries().sorted {
@@ -38,9 +37,7 @@ public struct ConnectionHistoryMenuSnapshot {
return $0.id.uuidString < $1.id.uuidString
}.compactMap { entry -> ConnectionHistoryMenuRow? in
guard let host = library.host(entry.hostID) else { return nil }
return ConnectionHistoryMenuRow(
entry: entry, host: host,
deviceName: history.isLocal(entry) ? nil : devices.name(for: entry.sourceDeviceID))
return ConnectionHistoryMenuRow(entry: entry, host: host)
}
let referenceDate = now ?? history.menuReferenceDate
recentToday = Array(
@@ -83,7 +83,7 @@ public struct HistoryMenuRecords: View {
open(row)
} label: {
Label {
Text(verbatim: title(row))
Text(verbatim: row.host.name)
} icon: {
image(row.entry.kind.symbol)
}
@@ -91,6 +91,4 @@ public struct HistoryMenuRecords: View {
.disabled(!row.canOpen)
}
}
private func title(_ row: ConnectionHistoryMenuRow) -> String { row.host.name }
}
+1 -1
View File
@@ -468,8 +468,8 @@ final class MobileAppModel {
}
private func closeWorkspaces(for hostID: UUID) {
forgetClosedWorkspaces(for: hostID)
for session in workspaceSessions {
session.forgetClosedWorkspaces(for: hostID)
for item in session.orderedWorkspaceItems where item.descriptor.hostID == hostID {
session.close(item.id, rememberForReopen: false)
}
+1 -1
View File
@@ -69,7 +69,7 @@ final class MobileSceneModel {
var confirmsClearConnectionHistory = false
var historyMenuSnapshot: ConnectionHistoryMenuSnapshot {
ConnectionHistoryMenuSnapshot(history: app.connectionHistory, library: app.library, devices: app.devices)
ConnectionHistoryMenuSnapshot(history: app.connectionHistory, library: app.library)
}
var hasConnectionHistory: Bool { !app.connectionHistory.entries().isEmpty }
@@ -55,10 +55,6 @@ final class MobileWorkspaceSessionModel {
var canReopen: Bool { !isDiscarded && !app.closedWorkspaceRecords.isEmpty }
var reopensWindow: Bool { app.closedWorkspaceRecords.first?.isWindow == true }
var recentlyClosedRecords: [RecentlyClosedRecord<MobileWorkspaceDescriptor>] { app.closedWorkspaceRecords }
var recentlyClosedWorkspaces: [MobileWorkspaceDescriptor] {
guard !isDiscarded else { return [] }
return app.closedWorkspaceRecords.filter { !$0.isWindow }.flatMap(\.items)
}
private(set) var isDiscarded = false
@ObservationIgnored private unowned let app: MobileAppModel
@@ -301,10 +297,6 @@ final class MobileWorkspaceSessionModel {
}
}
func forgetClosedWorkspaces(for hostID: Host.ID) {
app.forgetClosedWorkspaces(for: hostID)
}
func reopenLastClosed() -> MobileWorkspace.ID? {
guard !isDiscarded, let record = app.closedWorkspaceRecords.first else { return nil }
if record.isWindow {
+21 -5
View File
@@ -26,14 +26,14 @@ struct MobileWorkspaceSessionTests {
let nextApp = try fixture.makeApp()
let window = MobileSceneModel(app: nextApp, sessionID: "second")
let other = MobileSceneModel(app: nextApp, sessionID: "other")
#expect(window.workspaceSession.recentlyClosedWorkspaces.map(\.id) == [closed.id])
#expect(window.workspaceSession.recentlyClosedRecords.flatMap(\.items).map(\.id) == [closed.id])
window.open(hostID: fixture.host.id, kind: .ssh)
window.activatePendingWorkspace()
let current = try #require(window.activeWorkspace)
window.closeActiveWorkspace()
#expect(window.workspaceSession.recentlyClosedWorkspaces.map(\.id) == [current.id, closed.id])
#expect(window.workspaceSession.recentlyClosedRecords.flatMap(\.items).map(\.id) == [current.id, closed.id])
window.reopenLastClosedWorkspace()
#expect(window.workspaceSession.recentlyClosedWorkspaces.map(\.id) == [closed.id])
#expect(window.workspaceSession.recentlyClosedRecords.flatMap(\.items).map(\.id) == [closed.id])
window.reopenLastClosedWorkspace()
window.activatePendingWorkspace()
#expect(window.activeWorkspace?.descriptor.kind == .sftp)
@@ -102,9 +102,9 @@ struct MobileWorkspaceSessionTests {
let app = try fixture.makeApp()
let session = app.workspaceSession(for: "owner")
session.closeAll()
#expect(session.recentlyClosedWorkspaces.map(\.id) == records.reversed().map(\.id))
#expect(session.recentlyClosedRecords.flatMap(\.items).map(\.id) == records.reversed().map(\.id))
#expect(session.reopenClosedWorkspace(records[0].id) == records[0].id)
#expect(session.recentlyClosedWorkspaces.map(\.id) == [records[2].id, records[1].id])
#expect(session.recentlyClosedRecords.flatMap(\.items).map(\.id) == [records[2].id, records[1].id])
#expect(session.reopenClosedWorkspace(records[0].id) == nil)
#expect(session.reopenLastClosed() == records[2].id)
}
@@ -231,6 +231,22 @@ struct MobileWorkspaceSessionTests {
#expect(session.workspaces.isEmpty)
}
@Test func deletingAHostBeforeBindingAWindowRemovesPersistedClosedWorkspaces() throws {
let fixture = try Fixture()
defer { fixture.cleanUp() }
let firstApp = try fixture.makeApp()
let session = firstApp.workspaceSession(for: "owner")
let id = try #require(session.open(hostID: fixture.host.id, kind: .ssh))
session.close(id)
let nextApp = try fixture.makeApp()
#expect(nextApp.recentlyClosedStore.entries.flatMap(\.items).map(\.id) == [id])
#expect(nextApp.deleteHost(fixture.host.id))
#expect(nextApp.recentlyClosedStore.entries.isEmpty)
let thirdApp = try fixture.makeApp()
#expect(thirdApp.recentlyClosedStore.entries.isEmpty)
}
@Test func agentForwardingStaysWithTheConversationScene() async throws {
let fixture = try Fixture()
defer { fixture.cleanUp() }
-8
View File
@@ -709,12 +709,6 @@ final class AppModel {
AssistantExecutionController.titleContext(for: conversation)
}
func forgetClosedWorkspaces(_ ids: Set<UUID>) {
for window in windows.values.compactMap(\.value) {
for id in ids { window.forgetClosedWorkspace(id) }
}
}
func unregisterWindow(_ window: WindowModel) {
windows[ObjectIdentifier(window)] = nil
onboarding.windowClosed(window)
@@ -725,12 +719,10 @@ final class AppModel {
configureConnectionHistory(in: window)
window.connectionCoordinator = connections
window.hostStore = hosts
window.closedHistory = { [weak self] in self?.closedWorkspaceRecords ?? [] }
window.rememberClosedWorkspace = { [weak self] in self?.rememberClosedWorkspaces([$0]) }
window.rememberClosedWindow = { [weak self] workspaces, selected in
self?.rememberClosedWorkspaces(workspaces, isWindow: true, selectedID: selected)
}
window.restoreClosedRecord = { [weak self, weak window] id in self?.reopenClosedRecord(id, in: window) }
window.metricsModel = serverMetrics
windows = windows.filter { $0.value.value != nil }
windows[ObjectIdentifier(window)] = WeakWindow(window)
-1
View File
@@ -49,7 +49,6 @@ extension AppModel {
window.select(restored[selected].id)
}
let removed = Set(items.map(\.id))
forgetClosedWorkspaces(removed)
record.items.removeAll { removed.contains($0.id) }
closedWorkspaceInstances[id]?.removeAll { removed.contains($0.id) }
if record.items.isEmpty {
+1 -1
View File
@@ -297,7 +297,7 @@ struct TermindCommands: Commands {
HistoryMenuRecords(
snapshot: ConnectionHistoryMenuSnapshot(
history: app.connectionHistory, library: app.hostLibrary, devices: app.devices),
history: app.connectionHistory, library: app.hostLibrary),
image: { Image(nsImage: MenuImageVisibilityController.markedImage(systemName: $0)) }
) { row in
guard let window, let host = app.hostLibrary.host(row.host.id) else { return }
+4 -46
View File
@@ -277,15 +277,6 @@ final class WindowModel {
}
}
/// Closed workspaces and individually closed panes, most recent last. A closed
/// pane is kept as a single-pane Workspace. ⇧⌘T pops from here. The Workspace retains
/// its layout, names, and conversation, while `prepareForReopen` replaces
/// terminal models because a dead SSH session cannot be revived.
private var reopenStack: [Workspace] {
get { workspaceDeck.reopenStack }
set { workspaceDeck.reopenStack = newValue }
}
var commandFocusChanged: (() -> Void)?
var configureCommandNotifications: ((Pane) -> Void)?
@@ -545,17 +536,8 @@ final class WindowModel {
}
}
var closedHistory: () -> [RecentlyClosedRecord<ClosedWorkspaceRecord>] = { [] }
var rememberClosedWorkspace: ((Workspace) -> Void)?
var rememberClosedWindow: (([Workspace], UUID?) -> Void)?
var restoreClosedRecord: ((UUID) -> Void)?
var canReopen: Bool {
!isClosed && (restoreClosedRecord == nil ? !reopenStack.isEmpty : !closedHistory().isEmpty)
}
var reopensWindow: Bool { closedHistory().first?.isWindow == true }
var recentlyClosedWorkspaces: [Workspace] { Array(reopenStack.reversed()) }
func forgetClosedWorkspace(_ id: UUID) { reopenStack.removeAll { $0.id == id } }
var confirmsClearConnectionHistory = false
var historyOperationError: String?
@@ -1397,7 +1379,6 @@ final class WindowModel {
workspace.lifecycleRouter = nil
}
workspaces.removeAll()
reopenStack.removeAll()
dragging = nil
presentations.route = nil
transcriptCache.removeAll()
@@ -1412,7 +1393,7 @@ final class WindowModel {
guard let index = workspaces.firstIndex(where: { $0.id == id }) else { return }
let removed = workspaces.remove(at: index)
removed.disconnectConnections()
rememberForReopen(removed)
rememberClosedWorkspace?(removed)
if selectedWorkspaceID == id {
// Select the neighbour that visually takes its place.
@@ -1436,7 +1417,7 @@ final class WindowModel {
func closeOthers(than id: Workspace.ID) {
for workspace in workspaces where workspace.id != id {
workspace.disconnectConnections()
rememberForReopen(workspace)
rememberClosedWorkspace?(workspace)
}
workspaces.removeAll { $0.id != id }
selectedWorkspaceID = id
@@ -1445,7 +1426,7 @@ final class WindowModel {
func closeAll() {
for workspace in workspaces {
workspace.disconnectConnections()
rememberForReopen(workspace)
rememberClosedWorkspace?(workspace)
}
workspaces.removeAll()
promoteVisibleSidebarConversationOrSelectVault()
@@ -1487,13 +1468,7 @@ final class WindowModel {
// session. prepareForReopen restores routing when it becomes live again.
pane.sshSession?.inputRouter = nil
pane.sftpChannel?.navigationRouter = nil
rememberForReopen(closedWorkspace)
}
private func rememberForReopen(_ workspace: Workspace) {
reopenStack.append(workspace)
rememberClosedWorkspace?(workspace)
if reopenStack.count > 15 { reopenStack.removeFirst(reopenStack.count - 15) }
rememberClosedWorkspace?(closedWorkspace)
}
func closeWorkspaces(for hostID: Host.ID) {
@@ -1509,23 +1484,6 @@ final class WindowModel {
}
}
func reopenLastClosed() {
let candidate = restoreClosedRecord == nil ? reopenStack.last?.id : closedHistory().first?.id
guard let id = candidate else { return }
reopenClosedWorkspace(id)
}
func reopenClosedWorkspace(_ id: Workspace.ID) {
if let restoreClosedRecord {
restoreClosedRecord(id)
return
}
guard let index = reopenStack.firstIndex(where: { $0.id == id }) else { return }
let workspace = reopenStack.remove(at: index)
workspace.prepareForReopen()
open(workspace)
}
// MARK: - Reordering
/// Drag-reorder in the Navigator: puts `id` at `index` in the Workspace
+2 -4
View File
@@ -3,13 +3,11 @@ import TermindApplication
import TermindDomain
import TermindPresentation
/// Window-scoped ownership for the dynamic workspace collection and its
/// recoverable close history. Navigation remains in `WindowModel`; workspace
/// lifetime no longer shares storage with Assistant and Vault state.
/// Window-scoped ownership for the dynamic workspace collection.
/// Navigation remains in `WindowModel`; recoverable close history belongs to the app.
@Observable
final class WorkspaceDeckModel {
var workspaces: [Workspace]
var reopenStack: [Workspace] = []
init(workspaces: [Workspace] = []) {
self.workspaces = workspaces
+36 -16
View File
@@ -11,6 +11,29 @@ import struct TermindDomain.Host
@Suite @MainActor
struct ClosedWorkspaceRestoreTests {
@Test func evictedHistoryReleasesWorkspacesAcrossWindows() {
let app = AppModel(hosts: HostLibraryModel())
let firstWindow = WindowModel()
let secondWindow = WindowModel()
app.registerWindow(firstWindow)
app.registerWindow(secondWindow)
weak var evicted: Workspace?
do {
let workspace = Workspace(pane: Pane(.empty))
evicted = workspace
firstWindow.open(workspace)
firstWindow.close(workspace.id)
}
#expect(evicted != nil)
for _ in 0..<15 {
let workspace = Workspace(pane: Pane(.empty))
secondWindow.open(workspace)
secondWindow.close(workspace.id)
}
#expect(app.closedWorkspaceRecords.count == 15)
#expect(evicted == nil)
}
@Test func restartKeepsClosedSplitsInOneRecentList() throws {
let name = "ClosedWorkspace.\(UUID())"
let defaults = try #require(UserDefaults(suiteName: name))
@@ -34,27 +57,24 @@ struct ClosedWorkspaceRestoreTests {
let nextApp = AppModel(hosts: hosts, dependencies: dependencies)
let window = WindowModel()
nextApp.registerWindow(window)
#expect(window.recentlyClosedWorkspaces.isEmpty)
#expect(window.closedHistory().flatMap(\.items).map(\.title) == ["Operations"])
#expect(window.canReopen)
#expect(nextApp.closedWorkspaceRecords.flatMap(\.items).map(\.title) == ["Operations"])
#expect(!nextApp.closedWorkspaceRecords.isEmpty)
let current = Workspace.sshSession(host: host)
window.open(current)
window.close(current.id)
#expect(window.closedHistory().count == 2)
window.reopenLastClosed()
#expect(nextApp.closedWorkspaceRecords.count == 2)
nextApp.reopenClosedRecord(try #require(nextApp.closedWorkspaceRecords.first?.id), in: window)
#expect(window.selectedWorkspace === current)
window.reopenLastClosed()
nextApp.reopenClosedRecord(try #require(nextApp.closedWorkspaceRecords.first?.id), in: window)
let restored = try #require(window.selectedWorkspace)
#expect(restored !== workspace)
#expect(restored.panes.map(\.kind) == [.sshSession, .sftpChannel])
#expect(restored.panes.map(\.customTitle) == ["Shell", "Files"])
#expect(restored.customTitle == "Operations")
#expect(restored.focusedPane.kind == .sftpChannel)
#expect(!window.canReopen)
#expect(nextApp.closedWorkspaceRecords.isEmpty)
let thirdApp = AppModel(hosts: hosts, dependencies: dependencies)
let thirdWindow = WindowModel()
thirdApp.registerWindow(thirdWindow)
#expect(!thirdWindow.canReopen)
#expect(thirdApp.closedWorkspaceRecords.isEmpty)
}
@Test func closingAWindowCreatesOneGroupAndReopenWaitsForANewWindow() throws {
@@ -73,11 +93,11 @@ struct ClosedWorkspaceRestoreTests {
#expect(app.closedWorkspaceRecords.count == 1)
let current = WindowModel()
app.registerWindow(current)
#expect(current.reopensWindow)
#expect(app.closedWorkspaceRecords.first?.isWindow == true)
var requests: [UUID] = []
app.mainWindows.createScene = { requests.append($0) }
current.reopenLastClosed()
current.reopenLastClosed()
app.reopenClosedRecord(record.id, in: current)
app.reopenClosedRecord(record.id, in: current)
#expect(requests.count == 1)
#expect(current.workspaces.isEmpty)
#expect(app.closedWorkspaceRecords.count == 1)
@@ -107,7 +127,7 @@ struct ClosedWorkspaceRestoreTests {
app.restoreClosedRecord(id, into: destination, itemID: first.id)
#expect(destination.selectedWorkspace === first)
#expect(app.closedWorkspaceRecords.first?.items.map(\.id) == [second.id])
#expect(destination.reopensWindow)
#expect(app.closedWorkspaceRecords.first?.isWindow == true)
}
@Test func missingHostsAreNotOfferedAfterRestart() throws {
@@ -117,8 +137,8 @@ struct ClosedWorkspaceRestoreTests {
let window = WindowModel()
app.registerWindow(window)
app.recentlyClosedStore.record(RecentlyClosedRecord(items: [record]))
#expect(!window.canReopen)
window.reopenClosedWorkspace(record.id)
#expect(app.closedWorkspaceRecords.isEmpty)
app.reopenClosedRecord(record.id, in: window)
#expect(window.workspaces.isEmpty)
}
}
+1 -1
View File
@@ -206,7 +206,7 @@ struct CommandNotificationTests {
other.close(moved.id)
#expect(app.commandNotificationTarget(firstID) == nil)
#expect(app.selectCommandNotificationTarget(firstID) == nil)
other.reopenLastClosed()
app.reopenClosedRecord(try #require(app.closedWorkspaceRecords.first?.id), in: other)
#expect(app.commandNotificationTarget(firstID) == nil)
#expect(first.commandSessionID != firstID)
}
+1 -1
View File
@@ -80,7 +80,7 @@ struct HistoryMenuRecordTests {
let history = ConnectionHistoryModel(entries: [earlier, yesterday] + recent)
let current = try #require(history.record(kind: .ssh, for: host.id, at: now))
let snapshot = ConnectionHistoryMenuSnapshot(
history: history, library: HostLibraryModel(hosts: [host]), devices: DeviceDirectoryModel(), now: now)
history: history, library: HostLibraryModel(hosts: [host]), now: now)
var opened: [UUID] = []
let menu = NSHostingMenu(
rootView: HistoryMenuRecords(
+5 -5
View File
@@ -42,7 +42,7 @@ struct MultiWindowTests {
app.mainWindows.register(targetNative, model: target, requestID: request)
#expect(source.workspaces.isEmpty)
#expect(!source.canReopen)
#expect(app.closedWorkspaceRecords.isEmpty)
#expect(!source.isClosed)
#expect(source.vaultDestination == .welcome)
#expect(target.selectedWorkspace === workspace)
@@ -122,7 +122,7 @@ struct MultiWindowTests {
#expect(session.status == .disconnected)
app.mainWindows.register(second, model: target, requestID: request)
#expect(target.workspaces.isEmpty)
#expect(!source.canReopen)
#expect(app.closedWorkspaceRecords.first?.isWindow == true)
source.closeForWindow()
session.status = .connecting
let phase = session.connectionJourney.phase
@@ -276,14 +276,14 @@ struct MultiWindowTests {
app.mainWindows.closeSelectionOrWindow(in: model)
#expect(model.workspaces.isEmpty)
#expect(model.canReopen)
#expect(app.closedWorkspaceRecords.count == 2)
#expect(firstSession.status == .disconnected)
#expect(!model.isClosed)
app.mainWindows.closeSelectionOrWindow(in: model)
#expect(model.isClosed)
#expect(!model.canReopen)
#expect(app.closedWorkspaceRecords.count == 2)
}
@Test(arguments: [WindowDestination.vault, .settings, .assistant, .metrics])
@@ -310,7 +310,7 @@ struct MultiWindowTests {
#expect(model.selectedDestination == destination)
#expect(model.workspaces.first === workspace)
#expect(session.status == .connected)
#expect(!model.canReopen)
#expect(app.closedWorkspaceRecords.isEmpty)
}
@Test(arguments: [false, true])
+37 -25
View File
@@ -33,17 +33,19 @@ struct WindowStateTests {
let first = Workspace(pane: Pane(.empty))
let second = Workspace(pane: Pane(.empty))
let window = WindowModel(workspaces: [first, second])
let app = AppModel(hosts: HostLibraryModel())
app.registerWindow(window)
window.close(first.id)
window.close(second.id)
#expect(window.recentlyClosedWorkspaces.map(\.id) == [second.id, first.id])
window.reopenClosedWorkspace(first.id)
#expect(app.closedWorkspaceRecords.flatMap(\.items).map(\.id) == [second.id, first.id])
app.reopenClosedRecord(first.id, in: window)
#expect(window.selectedWorkspace === first)
#expect(window.recentlyClosedWorkspaces.map(\.id) == [second.id])
window.reopenClosedWorkspace(first.id)
#expect(app.closedWorkspaceRecords.flatMap(\.items).map(\.id) == [second.id])
app.reopenClosedRecord(first.id, in: window)
#expect(window.workspaces.count == 1)
window.reopenLastClosed()
app.reopenClosedRecord(try #require(app.closedWorkspaceRecords.first?.id), in: window)
#expect(window.selectedWorkspace === second)
#expect(!window.canReopen)
#expect(app.closedWorkspaceRecords.isEmpty)
}
@Test(arguments: [WindowDestination.vault, .metrics, .settings, .assistant])
@@ -1277,12 +1279,14 @@ struct WindowStateTests {
pane.customTitle = "Logs"
let closedSession = try #require(pane.sshSession)
let window = WindowModel(workspaces: [workspace])
let app = AppModel(hosts: HostLibraryModel(hosts: [host]))
app.registerWindow(window)
window.close(workspace.id)
#expect(closedSession.status == .disconnected)
#expect(window.canReopen)
#expect(!app.closedWorkspaceRecords.isEmpty)
window.reopenLastClosed()
app.reopenClosedRecord(try #require(app.closedWorkspaceRecords.first?.id), in: window)
let reopenedPane = try #require(window.selectedWorkspace?.focusedPane)
let reopenedSession = try #require(reopenedPane.sshSession)
@@ -1291,7 +1295,7 @@ struct WindowStateTests {
#expect(reopenedSession.status == .connecting)
#expect(reopenedPane.customTitle == "Logs")
#expect(window.selectedWorkspace === workspace)
#expect(!window.canReopen)
#expect(app.closedWorkspaceRecords.isEmpty)
}
@Test(arguments: [false, true])
@@ -1308,6 +1312,8 @@ struct WindowStateTests {
workspace.splitFocusedPane(axis: .vertical, with: closedPane)
workspace.broadcastsInput = true
let window = WindowModel(workspaces: [workspace])
let app = AppModel(hosts: HostLibraryModel(hosts: [host]))
app.registerWindow(window)
if closeSelected {
window.closeSelected()
@@ -1315,11 +1321,11 @@ struct WindowStateTests {
window.closePane(closedPane.id)
}
#expect(window.canReopen)
#expect(!app.closedWorkspaceRecords.isEmpty)
#expect(closedSession.status == .disconnected)
#expect(workspace.panes.map(\.id) == [survivor.id])
window.reopenLastClosed()
app.reopenClosedRecord(try #require(app.closedWorkspaceRecords.first?.id), in: window)
let reopened = try #require(window.selectedWorkspace)
let reopenedSession = try #require(reopened.focusedPane.sshSession)
@@ -1335,7 +1341,7 @@ struct WindowStateTests {
#expect(survivorSession.status == .connected)
#expect(survivorSession.inputRouter === workspace)
#expect(!workspace.broadcastsInput && !reopened.broadcastsInput)
#expect(!window.canReopen)
#expect(app.closedWorkspaceRecords.isEmpty)
}
@Test
@@ -1347,30 +1353,34 @@ struct WindowStateTests {
workspace.splitFocusedPane(axis: .horizontal, with: second)
let other = Workspace.sshSession(host: host)
let window = WindowModel(workspaces: [workspace, other])
let app = AppModel(hosts: HostLibraryModel(hosts: [host]))
app.registerWindow(window)
window.closePane(second.id)
window.close(other.id)
window.closePane(first.id)
#expect(window.workspaces.isEmpty)
window.reopenLastClosed()
app.reopenClosedRecord(try #require(app.closedWorkspaceRecords.first?.id), in: window)
#expect(window.selectedWorkspace === workspace)
#expect(workspace.panes.map(\.id) == [first.id])
window.reopenLastClosed()
app.reopenClosedRecord(try #require(app.closedWorkspaceRecords.first?.id), in: window)
#expect(window.selectedWorkspace === other)
window.reopenLastClosed()
app.reopenClosedRecord(try #require(app.closedWorkspaceRecords.first?.id), in: window)
let reopened = try #require(window.selectedWorkspace)
#expect(reopened.panes.map(\.id) == [second.id])
#expect(reopened.focusedPane.sftpChannel?.path == "/var/log")
#expect(window.workspaces.count == 3)
#expect(!window.canReopen)
#expect(app.closedWorkspaceRecords.isEmpty)
}
@Test
func individuallyClosedPanesShareTheFifteenItemReopenLimit() {
func individuallyClosedPanesShareTheFifteenItemReopenLimit() throws {
let survivor = Pane(.empty)
let workspace = Workspace(pane: survivor)
let window = WindowModel(workspaces: [workspace])
let app = AppModel(hosts: HostLibraryModel())
app.registerWindow(window)
let closedPanes = (0..<16).map { _ in Pane(.empty) }
for pane in closedPanes {
workspace.splitFocusedPane(axis: .horizontal, with: pane)
@@ -1378,17 +1388,17 @@ struct WindowStateTests {
}
var reopenedIDs: [Pane.ID] = []
while window.canReopen {
window.reopenLastClosed()
while !app.closedWorkspaceRecords.isEmpty {
app.reopenClosedRecord(try #require(app.closedWorkspaceRecords.first?.id), in: window)
if let pane = window.selectedWorkspace?.focusedPane { reopenedIDs.append(pane.id) }
}
#expect(reopenedIDs == closedPanes.suffix(15).reversed().map(\.id))
#expect(workspace.panes.map(\.id) == [survivor.id])
window.closePane(reopenedIDs[0])
#expect(window.canReopen)
#expect(!app.closedWorkspaceRecords.isEmpty)
window.closeForWindow()
#expect(!window.canReopen)
#expect(app.closedWorkspaceRecords.first?.isWindow == true)
}
@Test
@@ -1480,7 +1490,7 @@ struct WindowStateTests {
}
@Test
func normalRemoteExitClosesOnlyItsSplitPane() {
func normalRemoteExitClosesOnlyItsSplitPane() throws {
let firstHost = Host(name: "First", host: "first.example")
let secondHost = Host(name: "Second", host: "second.example")
let workspace = Workspace.sshSession(host: firstHost)
@@ -1491,6 +1501,8 @@ struct WindowStateTests {
workspace.focus(firstPane.id)
workspace.broadcastsInput = true
let window = WindowModel(workspaces: [workspace])
let app = AppModel(hosts: HostLibraryModel(hosts: [firstHost, secondHost]))
app.registerWindow(window)
firstSSHSession.handleRemoteExit(.success(0))
@@ -1500,13 +1512,13 @@ struct WindowStateTests {
#expect(workspace.focusedPaneID == secondPane.id)
#expect(!workspace.broadcastsInput)
#expect(firstSSHSession.inputRouter == nil)
#expect(window.canReopen)
#expect(!app.closedWorkspaceRecords.isEmpty)
window.reopenLastClosed()
app.reopenClosedRecord(try #require(app.closedWorkspaceRecords.first?.id), in: window)
#expect(window.selectedWorkspace?.focusedPane.id == firstPane.id)
#expect(window.selectedWorkspace?.focusedPane.sshSession !== firstSSHSession)
#expect(!window.canReopen)
#expect(app.closedWorkspaceRecords.isEmpty)
}
@Test
+5 -5
View File
@@ -55,7 +55,7 @@ struct WorkspaceWindowMoveTests {
#expect(workspace.lifecycleRouter === source)
#expect(source.sidebar.sessionContext.workingDirectory == "/srv")
#expect(target.sidebar.sessionContext.workingDirectory == nil)
#expect(!source.canReopen && !target.canReopen)
#expect(app.closedWorkspaceRecords.isEmpty)
#expect(ssh.status == .connected && sftp.status == .connected)
#expect(sftp.path == "/srv")
#expect(Set(history.entries().map(\.id)) == historyIDs)
@@ -100,7 +100,7 @@ struct WorkspaceWindowMoveTests {
#expect(source.workspaces.isEmpty)
#expect(source.selectedDestination == .vault)
#expect(source.vaultDestination == .welcome)
#expect(!source.canReopen)
#expect(app.closedWorkspaceRecords.isEmpty)
}
@Test(arguments: [false, true])
@@ -134,10 +134,10 @@ struct WorkspaceWindowMoveTests {
#expect(session.status == .disconnected)
#expect(target.workspaces.isEmpty)
#expect(source.pane(moved.id) == nil)
#expect(source.canReopen == !closesWindow)
#expect(app.closedWorkspaceRecords.first?.isWindow == closesWindow)
#expect(app.mainWindows.movePane(moved.id, from: source) == nil)
if !closesWindow {
source.reopenLastClosed()
app.reopenClosedRecord(try #require(app.closedWorkspaceRecords.first?.id), in: source)
#expect(source.selectedWorkspace?.focusedPane.id == moved.id)
#expect(source.selectedWorkspace?.focusedPane.sshSession !== session)
#expect(target.workspaces.isEmpty)
@@ -197,7 +197,7 @@ struct WorkspaceWindowMoveTests {
#expect(first.selectedWorkspace?.focusedPane === firstPane)
#expect(second.selectedWorkspace?.focusedPane === secondPane)
#expect(source.workspaces.isEmpty)
#expect(!source.canReopen)
#expect(app.closedWorkspaceRecords.isEmpty)
}
@Test(arguments: [false, true])