Add extensible remote session backends, safe ownership and restore handling, and pane-scoped shell integration over the existing SSH connection. Keep iOS workspaces in memory and rebuild expired Activity targets on demand. Integrate session selection into Connection Journey with consistent controls, cancellation behavior, and localized counts.
129 lines
6.1 KiB
Swift
129 lines
6.1 KiB
Swift
import Foundation
|
|
import TermindApplication
|
|
import TermindDomain
|
|
import Testing
|
|
|
|
import struct TermindDomain.Host
|
|
|
|
@testable import Termind
|
|
|
|
@Suite(.serialized) @MainActor
|
|
struct HostShellIntegrationSessionTests {
|
|
@Test func runtimeContextGatesEnhancementsWithoutPersistence() throws {
|
|
let host = Host(name: "Server", host: "server.invalid", shellIntegrationMode: .enabled)
|
|
let session = SSHSessionModel(hostID: host.id, title: host.name)
|
|
session.connect(using: HostLibraryModel(hosts: [host]))
|
|
defer { session.disconnect() }
|
|
session.status = .connected
|
|
let context = TerminalShellContext(
|
|
identity: .init(backend: "test-multiplexer", server: "s", session: "s", pane: "a", shellInstance: "i"),
|
|
shell: "bash", directory: "/pane-a", home: "/home/test", promptReady: true, sequence: 1,
|
|
capabilities: [.directory, .commandLifecycle, .readDraft, .replaceDraft])
|
|
session.receiveShellIntegrationEvent(
|
|
.runtime(
|
|
.init(
|
|
status: .multiplexer, active: context.identity, contexts: [context])))
|
|
#expect(!host.sessionPersistence.enabled)
|
|
#expect(session.canGenerateFromCurrentDraft)
|
|
#expect(session.workingDirectory == "/pane-a")
|
|
let ticket = try #require(session.directoryContextTicket)
|
|
session.receiveShellIntegrationEvent(.runtime(.init(status: .unknown)))
|
|
#expect(session.workingDirectory == nil)
|
|
#expect(!session.canGenerateFromCurrentDraft)
|
|
#expect(!session.isDirectoryContextCurrent(ticket))
|
|
session.receiveShellIntegrationEvent(.available(shell: "bash", systemInfo: nil, homeDirectory: "/stale"))
|
|
session.receiveShellIntegrationEvent(.promptReady)
|
|
#expect(!session.canGenerateFromCurrentDraft)
|
|
#expect(session.workingDirectory == nil)
|
|
}
|
|
|
|
@Test(arguments: HostShellIntegrationMode.allCases, [false, true])
|
|
func connectionRetainsItsPreferenceUntilTheNextAttempt(mode: HostShellIntegrationMode, global: Bool) {
|
|
let defaults = UserDefaults.standard
|
|
let previous = defaults.object(forKey: SettingsKey.shellIntegration)
|
|
defer { defaults.set(previous, forKey: SettingsKey.shellIntegration) }
|
|
defaults.set(global, forKey: SettingsKey.shellIntegration)
|
|
var host = Host(name: "Server", host: "server.invalid", shellIntegrationMode: mode)
|
|
let library = HostLibraryModel(hosts: [host])
|
|
let session = SSHSessionModel(hostID: host.id, title: host.name)
|
|
let history = session.commandHistory
|
|
defer { session.disconnect() }
|
|
// The missing identity holds connection setup at authentication without opening a socket.
|
|
session.connect(using: library)
|
|
let expected = mode.isEnabled(globalSetting: global)
|
|
#expect(session.isShellIntegrationEnabled == expected)
|
|
session.status = .connected
|
|
let info = RemoteSystemInfo(kernelName: "Linux", kernelRelease: "6.8", architecture: "x86_64")
|
|
session.receiveShellIntegrationEvent(.available(shell: "bash", systemInfo: info, homeDirectory: "/home/test"))
|
|
session.receiveShellIntegrationEvent(.promptReady)
|
|
let pane = Pane(.sshSession(session))
|
|
#expect(pane.remoteSystemInfo == (expected ? info : nil))
|
|
#expect(session.canGenerateFromCurrentDraft == expected)
|
|
|
|
defaults.set(!global, forKey: SettingsKey.shellIntegration)
|
|
host.shellIntegrationMode = expected ? .disabled : .enabled
|
|
library.upsert(host)
|
|
#expect(session.isShellIntegrationEnabled == expected)
|
|
#expect(pane.remoteSystemInfo == (expected ? info : nil))
|
|
#expect(session.canGenerateFromCurrentDraft == expected)
|
|
session.receiveShellIntegrationEvent(.commandStarted("pwd"))
|
|
session.receiveShellIntegrationEvent(.commandFinished(0, duration: 0.1))
|
|
#expect(history.entries().count == (expected ? 1 : 0))
|
|
library.replacePersistedSnapshot(VaultSnapshot(hosts: [host]))
|
|
#expect(session.isShellIntegrationEnabled == expected)
|
|
|
|
session.disconnect()
|
|
session.connect(using: library)
|
|
#expect(session.isShellIntegrationEnabled == !expected)
|
|
#expect(session.remoteSystemInfo == nil)
|
|
|
|
host.shellIntegrationMode = .followGlobalSettings
|
|
library.replacePersistedSnapshot(VaultSnapshot(hosts: [host]))
|
|
session.disconnect()
|
|
session.connect(using: library)
|
|
#expect(session.isShellIntegrationEnabled == !global)
|
|
}
|
|
|
|
@Test func transientHostOverrideIsUsedAtConnectionStart() {
|
|
let host = Host(name: "Quick Connect", host: "quick.invalid", shellIntegrationMode: .disabled)
|
|
let session = SSHSessionModel(host: host)
|
|
defer { session.disconnect() }
|
|
session.connect(using: HostLibraryModel())
|
|
#expect(!session.isShellIntegrationEnabled)
|
|
}
|
|
|
|
@Test func startupSnippetUsesExplicitLaunchOnceAndResolvesLatestBodyOnReconnect() {
|
|
var startup = Snippet(title: "Startup", body: "echo first")
|
|
let host = Host(
|
|
name: "Server", host: "server.invalid", startupSnippetID: startup.id
|
|
)
|
|
let library = HostLibraryModel(hosts: [host], snippets: [startup])
|
|
let session = SSHSessionModel(
|
|
hostID: host.id, title: host.name, initialCommand: "echo explicit"
|
|
)
|
|
var submitted = Data()
|
|
session.inputSink = { submitted.append($0) }
|
|
defer { session.disconnect() }
|
|
|
|
session.connect(using: library)
|
|
session.status = .connected
|
|
session.receiveShellIntegrationEvent(
|
|
.available(shell: "bash", systemInfo: nil, homeDirectory: "/home/test")
|
|
)
|
|
#expect(String(decoding: submitted, as: UTF8.self) == "echo explicit\r")
|
|
|
|
session.disconnect()
|
|
startup.body = "echo latest"
|
|
library.upsert(startup)
|
|
session.connect(using: library)
|
|
session.status = .connected
|
|
session.receiveShellIntegrationEvent(
|
|
.available(shell: "bash", systemInfo: nil, homeDirectory: "/home/test")
|
|
)
|
|
#expect(
|
|
String(decoding: submitted, as: UTF8.self)
|
|
== "echo explicit\recho latest\r"
|
|
)
|
|
}
|
|
}
|