Files
Termind/TFMTests/HostShellIntegrationSessionTests.swift
T
Kaede Akino 8e3f1128da feat(sessions): add tmux persistence and terminal context tracking
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.
2026-10-16 01:04:12 +08:00

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"
)
}
}