264 lines
10 KiB
Swift
264 lines
10 KiB
Swift
// SPDX-FileCopyrightText: 2026 Kaede Akino
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
import Foundation
|
|
import SwiftData
|
|
import TermindApplication
|
|
import TermindDomain
|
|
import TermindInfrastructure
|
|
import TermindPersistence
|
|
import TermindPresentation
|
|
import TermindRemoteInfrastructure
|
|
import Testing
|
|
|
|
import struct TermindDomain.Host
|
|
|
|
@testable import Termind
|
|
|
|
@Suite
|
|
@MainActor
|
|
struct SnippetFeatureTests {
|
|
@Test
|
|
func snippetPanesUseEachHostsDefaultTransport() throws {
|
|
var mosh = Host(name: "Mosh", host: "mosh.invalid")
|
|
mosh.terminalTransports.moshEnabled = true
|
|
mosh.terminalTransports.defaultTransport = .mosh
|
|
var eternal = Host(name: "ET", host: "et.invalid")
|
|
eternal.terminalTransports.eternalTerminalEnabled = true
|
|
eternal.terminalTransports.defaultTransport = .eternalTerminal
|
|
let ssh = Host(name: "SSH", host: "ssh.invalid")
|
|
let window = WindowModel()
|
|
let workspace = try #require(window.openSnippetWorkspace(hosts: [mosh, eternal, ssh], command: "uptime"))
|
|
defer { for pane in workspace.panes { pane.sshSession?.disconnect() } }
|
|
|
|
#expect(workspace.panes.compactMap { $0.sshSession?.transport } == [.mosh, .eternalTerminal, .ssh])
|
|
#expect(workspace.panes.map(\.hostID) == [mosh.id, eternal.id, ssh.id])
|
|
for pane in workspace.panes {
|
|
let session = try #require(pane.sshSession)
|
|
var commands: [String] = []
|
|
session.inputSink = { data in commands.append(String(decoding: data, as: UTF8.self)) }
|
|
session.submitPendingInitialCommand()
|
|
session.submitPendingInitialCommand()
|
|
#expect(commands == ["uptime\r"])
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func snippetCardsReadCreationAndEditTimesFromTheVaultJournal() async throws {
|
|
let container = try TermindPersistence.makeContainer(inMemory: true)
|
|
let persistence = HostPersistence(context: container.mainContext)
|
|
let app = AppModel(hosts: HostLibraryModel(persistence: persistence))
|
|
defer { app.shutdownForTermination() }
|
|
var snippet = Snippet(title: "Status", body: "uptime")
|
|
app.snippets.upsert(snippet)
|
|
let createdAt = try #require(app.snippets.modifiedAt(for: snippet.id))
|
|
#expect(app.snippetModifiedAt(for: snippet.id) == createdAt)
|
|
app.snippets.upsert(snippet)
|
|
#expect(app.snippets.modifiedAt(for: snippet.id) == createdAt)
|
|
try? await Task.sleep(for: .milliseconds(10))
|
|
snippet.body = "uptime -p"
|
|
app.snippets.upsert(snippet)
|
|
#expect(app.snippetModifiedAt(for: snippet.id) > createdAt)
|
|
#expect(app.snippetModifiedAt(for: snippet.id) == app.snippets.modifiedAt(for: snippet.id))
|
|
}
|
|
|
|
@Test
|
|
func snippetMovePreservesBatchOrderAcrossPackages() {
|
|
let source = SnippetPackage(name: "Source")
|
|
let destination = SnippetPackage(name: "Destination")
|
|
let first = Snippet(title: "First", body: "one", packageID: source.id, sortIndex: 0)
|
|
let second = Snippet(title: "Second", body: "two", packageID: source.id, sortIndex: 1)
|
|
let before = Snippet(
|
|
title: "Before", body: "before", packageID: destination.id, sortIndex: 0
|
|
)
|
|
let after = Snippet(
|
|
title: "After", body: "after", packageID: destination.id, sortIndex: 1
|
|
)
|
|
let store = HostLibraryModel(
|
|
snippets: [first, second, before, after],
|
|
snippetPackages: [source, destination]
|
|
)
|
|
|
|
store.move(
|
|
snippetIDs: [second.id, first.id], into: destination.id,
|
|
before: after.id
|
|
)
|
|
|
|
#expect(
|
|
store.snippets(in: destination.id).map(\.id)
|
|
== [before.id, second.id, first.id, after.id]
|
|
)
|
|
}
|
|
|
|
@Test
|
|
func movingIntoNestedPackageExpandsItsAncestors() {
|
|
let root = SnippetPackage(name: "Root", isExpanded: false)
|
|
let child = SnippetPackage(
|
|
name: "Child", parentID: root.id, isExpanded: false
|
|
)
|
|
let snippet = Snippet(title: "Status", body: "uptime")
|
|
let store = HostLibraryModel(
|
|
snippets: [snippet], snippetPackages: [root, child]
|
|
)
|
|
|
|
store.move(snippetIDs: [snippet.id], into: child.id)
|
|
|
|
#expect(store.isSnippetPackageExpanded(root.id))
|
|
#expect(store.isSnippetPackageExpanded(child.id))
|
|
}
|
|
|
|
@Test
|
|
func deletingPackageCanKeepTreeSnippetsUngrouped() {
|
|
let existing = Snippet(title: "Existing", body: "pwd", sortIndex: 0)
|
|
let root = SnippetPackage(name: "Root")
|
|
let child = SnippetPackage(name: "Child", parentID: root.id)
|
|
let first = Snippet(
|
|
title: "First", body: "one", packageID: root.id, sortIndex: 0
|
|
)
|
|
let second = Snippet(
|
|
title: "Second", body: "two", packageID: child.id, sortIndex: 0
|
|
)
|
|
let store = HostLibraryModel(
|
|
snippets: [existing, first, second], snippetPackages: [root, child]
|
|
)
|
|
|
|
store.delete(snippetPackageID: root.id, keepSnippets: true)
|
|
|
|
#expect(store.snippetPackages.isEmpty)
|
|
#expect(store.snippets(in: nil).map(\.id) == [existing.id, first.id, second.id])
|
|
}
|
|
|
|
@Test
|
|
func deletingHostRemovesItFromEverySnippetTarget() {
|
|
let removed = Host(name: "Removed", host: "removed.example")
|
|
let kept = Host(name: "Kept", host: "kept.example")
|
|
let snippet = Snippet(
|
|
title: "Status", body: "uptime",
|
|
targetHostIDs: [removed.id, kept.id]
|
|
)
|
|
let store = HostLibraryModel(hosts: [removed, kept], snippets: [snippet])
|
|
|
|
store.projectHostDeletion(removed.id)
|
|
|
|
#expect(store.snippet(snippet.id)?.targetHostIDs == [kept.id])
|
|
}
|
|
|
|
@Test
|
|
func deletingSnippetsClearsOnlyMatchingHostStartupReferences() {
|
|
let package = SnippetPackage(name: "Startup")
|
|
let direct = Snippet(title: "Direct", body: "echo direct")
|
|
let packaged = Snippet(
|
|
title: "Packaged", body: "echo packaged", packageID: package.id
|
|
)
|
|
let retained = Snippet(title: "Retained", body: "echo retained")
|
|
let first = Host(
|
|
name: "First", host: "first.invalid", startupSnippetID: direct.id
|
|
)
|
|
let second = Host(
|
|
name: "Second", host: "second.invalid", startupSnippetID: packaged.id
|
|
)
|
|
let third = Host(
|
|
name: "Third", host: "third.invalid", startupSnippetID: retained.id
|
|
)
|
|
let store = HostLibraryModel(
|
|
hosts: [first, second, third], snippets: [direct, packaged, retained],
|
|
snippetPackages: [package]
|
|
)
|
|
|
|
store.delete(snippetIDs: [direct.id])
|
|
#expect(store.host(first.id)?.startupSnippetID == nil)
|
|
#expect(store.host(second.id)?.startupSnippetID == packaged.id)
|
|
#expect(store.host(third.id)?.startupSnippetID == retained.id)
|
|
|
|
store.delete(snippetPackageID: package.id, keepSnippets: false)
|
|
#expect(store.host(second.id)?.startupSnippetID == nil)
|
|
#expect(store.host(third.id)?.startupSnippetID == retained.id)
|
|
}
|
|
|
|
@Test
|
|
func snippetPersistenceRoundTripKeepsPackagesTargetsAndLocalExpansion() throws {
|
|
let configuration = localModelConfiguration(
|
|
"SnippetFeatureTests", schema: HostPersistence.schema,
|
|
isStoredInMemoryOnly: true
|
|
)
|
|
let container = try ModelContainer(
|
|
for: HostPersistence.schema,
|
|
configurations: [configuration]
|
|
)
|
|
let defaultsName = "SnippetFeatureTests.\(UUID().uuidString)"
|
|
let defaults = try #require(UserDefaults(suiteName: defaultsName))
|
|
defer { defaults.removePersistentDomain(forName: defaultsName) }
|
|
let store = HostLibraryModel(
|
|
modelContext: container.mainContext,
|
|
localDefaults: defaults
|
|
)
|
|
let host = Host(name: "Web", host: "web.example")
|
|
let package = SnippetPackage(name: "Operations", isExpanded: false)
|
|
let script = """
|
|
#!/bin/sh
|
|
systemctl status nginx
|
|
journalctl -u nginx -n 20
|
|
"""
|
|
let snippet = Snippet(
|
|
title: "Status", body: script,
|
|
packageID: package.id, targetHostIDs: [host.id]
|
|
)
|
|
|
|
store.upsert(host)
|
|
store.upsert(package)
|
|
store.setSnippetPackageExpanded(false, for: package.id)
|
|
store.upsert(snippet)
|
|
|
|
let reloaded = HostLibraryModel(
|
|
modelContext: container.mainContext,
|
|
localDefaults: defaults
|
|
)
|
|
#expect(!reloaded.isSnippetPackageExpanded(package.id))
|
|
#expect(reloaded.snippet(snippet.id)?.packageID == package.id)
|
|
#expect(reloaded.snippet(snippet.id)?.targetHostIDs == [host.id])
|
|
#expect(reloaded.snippet(snippet.id)?.body == script)
|
|
}
|
|
|
|
@Test
|
|
func executingSnippetsKeepsDefaultTitlesAndRequestOrder() {
|
|
let firstHost = Host(name: "A", host: "a.example")
|
|
let secondHost = Host(name: "B", host: "b.example")
|
|
let first = Snippet(
|
|
title: "First Workspace", body: "uptime",
|
|
targetHostIDs: [secondHost.id, firstHost.id]
|
|
)
|
|
let second = Snippet(
|
|
title: "Second Workspace", body: "whoami",
|
|
targetHostIDs: [firstHost.id]
|
|
)
|
|
let store = HostLibraryModel(hosts: [secondHost, firstHost], snippets: [first, second])
|
|
let app = AppModel(hosts: store)
|
|
let window = WindowModel()
|
|
|
|
app.execute([first, second], in: window)
|
|
|
|
#expect(window.workspaces.count == 2)
|
|
#expect(window.workspaces[0].title(in: store) == String(localized: "Workspace"))
|
|
#expect(window.workspaces[0].panes.map(\.hostID) == [firstHost.id, secondHost.id])
|
|
#expect(window.workspaces[1].title(in: store) == firstHost.name)
|
|
#expect(window.selectedWorkspaceID == window.workspaces[0].id)
|
|
}
|
|
|
|
@Test
|
|
func temporaryTargetOverrideDoesNotMutateSnippet() {
|
|
let saved = Host(name: "Saved", host: "saved.example")
|
|
let temporary = Host(name: "Temporary", host: "temporary.example")
|
|
let snippet = Snippet(
|
|
title: "Status", body: "uptime", targetHostIDs: [saved.id]
|
|
)
|
|
let store = HostLibraryModel(hosts: [saved, temporary], snippets: [snippet])
|
|
let app = AppModel(hosts: store)
|
|
let window = WindowModel()
|
|
|
|
app.execute([snippet], overrideHostIDs: [temporary.id], in: window)
|
|
|
|
#expect(window.workspaces.first?.panes.map(\.hostID) == [temporary.id])
|
|
#expect(store.snippet(snippet.id)?.targetHostIDs == [saved.id])
|
|
}
|
|
}
|