vibetunnel/mac/VibeTunnelTests/TerminalManagerTests.swift
Peter Steinberger 1dd8a55fc6 lint
2025-06-20 13:28:49 +02:00

462 lines
14 KiB
Swift

import Foundation
import Testing
@testable import VibeTunnel
// MARK: - Mock Process for Testing
final class MockProcess: Process, @unchecked Sendable {
var mockIsRunning = false
var mockProcessIdentifier: Int32 = 12_345
var mockShouldFailToRun = false
var runCalled = false
var terminateCalled = false
override var isRunning: Bool {
mockIsRunning
}
override var processIdentifier: Int32 {
mockProcessIdentifier
}
override func run() throws {
runCalled = true
if mockShouldFailToRun {
throw CocoaError(.fileNoSuchFile)
}
mockIsRunning = true
}
override func terminate() {
terminateCalled = true
mockIsRunning = false
}
}
// MARK: - Mock Terminal Manager
actor MockTerminalManager {
var mockSessions: [UUID: TunnelSession] = [:]
var mockProcesses: [UUID: MockProcess] = [:]
var createSessionShouldFail = false
var executeCommandShouldFail = false
var executeCommandOutput = ("", "")
func createSession(request: CreateSessionRequest) throws -> TunnelSession {
if createSessionShouldFail {
throw TunnelError.invalidRequest
}
let session = TunnelSession()
mockSessions[session.id] = session
let process = MockProcess()
process.mockProcessIdentifier = Int32.random(in: 1_000...9_999)
mockProcesses[session.id] = process
return session
}
func executeCommand(sessionId: UUID, command: String) async throws -> (output: String, error: String) {
if executeCommandShouldFail {
throw TunnelError.commandExecutionFailed("Mock failure")
}
guard mockSessions[sessionId] != nil else {
throw TunnelError.sessionNotFound
}
return executeCommandOutput
}
func listSessions() -> [TunnelSession] {
Array(mockSessions.values)
}
func getSession(id: UUID) -> TunnelSession? {
mockSessions[id]
}
func closeSession(id: UUID) {
mockProcesses[id]?.terminate()
mockProcesses.removeValue(forKey: id)
mockSessions.removeValue(forKey: id)
}
func reset() {
mockSessions = [:]
mockProcesses = [:]
createSessionShouldFail = false
executeCommandShouldFail = false
executeCommandOutput = ("", "")
}
func setCreateSessionShouldFail(_ value: Bool) {
createSessionShouldFail = value
}
func setExecuteCommandOutput(_ value: (String, String)) {
executeCommandOutput = value
}
}
// MARK: - Terminal Manager Tests
@Suite("Terminal Manager Tests")
struct TerminalManagerTests {
// MARK: - Terminal Detection Tests
@Test("Detecting installed terminals", arguments: [
"/bin/bash",
"/bin/zsh",
"/bin/sh"
])
func terminalDetection(shell: String) throws {
// Verify common shells exist on the system
let shellExists = FileManager.default.fileExists(atPath: shell)
if shellExists {
#expect(FileManager.default.isExecutableFile(atPath: shell))
}
}
@Test("Default terminal selection")
func defaultTerminal() async throws {
let manager = MockTerminalManager()
// Create session with default shell
let request = CreateSessionRequest()
let session = try await manager.createSession(request: request)
#expect(session.id != UUID())
#expect(session.isActive)
#expect(await manager.mockSessions.count == 1)
}
// MARK: - Session Creation Tests
@Test("Create terminal session with custom shell", arguments: [
"/bin/bash",
"/bin/zsh",
"/usr/bin/env"
])
func createSessionWithShell(shell: String) async throws {
let manager = MockTerminalManager()
let request = CreateSessionRequest(shell: shell)
let session = try await manager.createSession(request: request)
#expect(session.isActive)
#expect(session.createdAt <= Date())
#expect(session.lastActivity >= session.createdAt)
}
@Test("Create session with working directory")
func createSessionWithWorkingDirectory() async throws {
let manager = MockTerminalManager()
let tempDir = FileManager.default.temporaryDirectory.path
let request = CreateSessionRequest(workingDirectory: tempDir)
let session = try await manager.createSession(request: request)
#expect(session.isActive)
#expect(await manager.getSession(id: session.id) != nil)
}
@Test("Create session with environment variables")
func createSessionWithEnvironment() async throws {
let manager = MockTerminalManager()
let env = [
"CUSTOM_VAR": "test_value",
"PATH": "/custom/path:/usr/bin"
]
let request = CreateSessionRequest(environment: env)
let session = try await manager.createSession(request: request)
#expect(session.isActive)
}
@Test("Session creation failure")
func sessionCreationFailure() async throws {
let manager = MockTerminalManager()
await manager.reset()
await manager.setCreateSessionShouldFail(true)
await #expect(throws: TunnelError.invalidRequest) {
_ = try await manager.createSession(request: CreateSessionRequest())
}
#expect(await manager.mockSessions.isEmpty)
}
// MARK: - Command Execution Tests
@Test("Execute command in session", arguments: [
"ls -la",
"pwd",
"echo 'Hello, World!'",
"date"
])
func commandExecution(command: String) async throws {
let manager = MockTerminalManager()
// Create session
let session = try await manager.createSession(request: CreateSessionRequest())
// Set expected output
await manager.setExecuteCommandOutput(("Command output\n", ""))
// Execute command
let (output, error) = try await manager.executeCommand(
sessionId: session.id,
command: command
)
#expect(output == "Command output\n")
#expect(error.isEmpty)
}
@Test("Execute command with error output")
func commandWithError() async throws {
let manager = MockTerminalManager()
let session = try await manager.createSession(request: CreateSessionRequest())
await manager.setExecuteCommandOutput(("", "Command not found\n"))
let (output, error) = try await manager.executeCommand(
sessionId: session.id,
command: "nonexistent-command"
)
#expect(output.isEmpty)
#expect(error == "Command not found\n")
}
@Test("Execute command in non-existent session")
func commandInNonExistentSession() async throws {
let manager = MockTerminalManager()
let fakeId = UUID()
await #expect(throws: TunnelError.sessionNotFound) {
_ = try await manager.executeCommand(
sessionId: fakeId,
command: "ls"
)
}
}
@Test("Command execution timeout")
func commandTimeout() async throws {
// Test that timeout is handled properly
let error = TunnelError.timeout
#expect(error.errorDescription == "Operation timed out")
}
// MARK: - Session Management Tests
@Test("List all sessions")
func testListSessions() async throws {
let manager = MockTerminalManager()
// Create multiple sessions
let session1 = try await manager.createSession(request: CreateSessionRequest())
let session2 = try await manager.createSession(request: CreateSessionRequest())
let session3 = try await manager.createSession(request: CreateSessionRequest())
let sessions = await manager.listSessions()
#expect(sessions.count == 3)
#expect(sessions.map(\.id).contains(session1.id))
#expect(sessions.map(\.id).contains(session2.id))
#expect(sessions.map(\.id).contains(session3.id))
}
@Test("Get specific session")
func testGetSession() async throws {
let manager = MockTerminalManager()
let session = try await manager.createSession(request: CreateSessionRequest())
let retrieved = await manager.getSession(id: session.id)
#expect(retrieved?.id == session.id)
#expect(retrieved?.isActive == true)
// Non-existent session
let nonExistent = await manager.getSession(id: UUID())
#expect(nonExistent == nil)
}
@Test("Close session")
func testCloseSession() async throws {
let manager = MockTerminalManager()
let session = try await manager.createSession(request: CreateSessionRequest())
#expect(await manager.mockSessions.count == 1)
await manager.closeSession(id: session.id)
#expect(await manager.mockSessions.isEmpty)
#expect(await manager.getSession(id: session.id) == nil)
// Verify process was terminated
let process = await manager.mockProcesses[session.id]
#expect(process == nil)
}
@Test("Close non-existent session")
func closeNonExistentSession() async throws {
let manager = MockTerminalManager()
let fakeId = UUID()
// Should not throw, just silently do nothing
await manager.closeSession(id: fakeId)
#expect(await manager.mockSessions.isEmpty)
}
// MARK: - Session Cleanup Tests
@Test("Cleanup inactive sessions")
func testCleanupInactiveSessions() async throws {
let manager = TerminalManager()
// This test documents expected behavior
// In real implementation, sessions older than specified minutes would be cleaned up
await manager.cleanupInactiveSessions(olderThan: 30)
// After cleanup, only active/recent sessions should remain
let remainingSessions = await manager.listSessions()
for session in remainingSessions {
#expect(session.lastActivity > Date().addingTimeInterval(-30 * 60))
}
}
// MARK: - Concurrent Operations Tests
@Test("Concurrent session creation", .tags(.concurrency))
func concurrentSessionCreation() async throws {
let manager = MockTerminalManager()
let sessionIds = await withTaskGroup(of: UUID?.self) { group in
for i in 0..<5 {
group.addTask {
do {
let request = CreateSessionRequest(
workingDirectory: "/tmp/session-\(i)"
)
let session = try await manager.createSession(request: request)
return session.id
} catch {
return nil
}
}
}
var ids: [UUID] = []
for await id in group {
if let id {
ids.append(id)
}
}
return ids
}
#expect(sessionIds.count == 5)
#expect(Set(sessionIds).count == 5) // All unique
#expect(await manager.mockSessions.count == 5)
}
@Test("Concurrent command execution", .tags(.concurrency))
func concurrentCommandExecution() async throws {
let manager = MockTerminalManager()
// Create a session
let session = try await manager.createSession(request: CreateSessionRequest())
await manager.setExecuteCommandOutput(("OK\n", ""))
// Execute multiple commands concurrently
let results = await withTaskGroup(of: Result<String, Error>.self) { group in
for i in 0..<3 {
group.addTask {
do {
let (output, _) = try await manager.executeCommand(
sessionId: session.id,
command: "echo \(i)"
)
return .success(output)
} catch {
return .failure(error)
}
}
}
var outputs: [String] = []
for await result in group {
if case .success(let output) = result {
outputs.append(output)
}
}
return outputs
}
#expect(results.count == 3)
#expect(results.allSatisfy { $0 == "OK\n" })
}
// MARK: - Error Handling Tests
@Test("Terminal error types")
func errorTypes() throws {
let errors: [TunnelError] = [
.sessionNotFound,
.commandExecutionFailed("Test failure"),
.timeout,
.invalidRequest
]
for error in errors {
#expect(error.errorDescription != nil)
#expect(!error.errorDescription!.isEmpty)
}
}
// MARK: - Integration Tests
@Test("Full session lifecycle", .tags(.integration))
func fullSessionLifecycle() async throws {
let manager = MockTerminalManager()
// 1. Create session
let request = CreateSessionRequest(
workingDirectory: "/tmp",
environment: ["TEST": "value"],
shell: "/bin/bash"
)
let session = try await manager.createSession(request: request)
// 2. Verify session exists
let retrieved = await manager.getSession(id: session.id)
#expect(retrieved != nil)
#expect(retrieved?.isActive == true)
// 3. Execute commands
await manager.setExecuteCommandOutput(("test output\n", ""))
let (output1, _) = try await manager.executeCommand(
sessionId: session.id,
command: "echo test"
)
#expect(output1 == "test output\n")
// 4. List sessions
let sessions = await manager.listSessions()
#expect(sessions.count == 1)
// 5. Close session
await manager.closeSession(id: session.id)
// 6. Verify cleanup
#expect(await manager.getSession(id: session.id) == nil)
#expect(await manager.listSessions().isEmpty)
}
}