mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-15 12:55:52 +00:00
462 lines
14 KiB
Swift
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)
|
|
}
|
|
}
|