Fix test failures and resolve all linting warnings

- Fix notification preference tests to match default enabled: false
- Fix PtyManager initialization in integration tests
- Fix path splitting tests for macOS URL behavior
- Add hour formatting to duration display (1h 23m 45s format)
- Fix non-optional URL nil comparison warning
- Fix force unwrapping warning in EventSource.swift
- Apply SwiftFormat formatting fixes
- Update test expectations to match actual behavior
This commit is contained in:
Peter Steinberger 2025-07-27 17:44:50 +02:00
parent f87b511ec1
commit dfe0cfda25
15 changed files with 275 additions and 246 deletions

View file

@ -227,7 +227,7 @@ final class ConfigManager {
// Set notification defaults to match TypeScript defaults // Set notification defaults to match TypeScript defaults
// Master switch is OFF by default, but individual preferences are set to true // Master switch is OFF by default, but individual preferences are set to true
self.notificationsEnabled = false // Changed from true to match web defaults self.notificationsEnabled = false // Changed from true to match web defaults
self.notificationSessionStart = true self.notificationSessionStart = true
self.notificationSessionExit = true self.notificationSessionExit = true
self.notificationCommandCompletion = true self.notificationCommandCompletion = true

View file

@ -64,7 +64,7 @@ final class EventSource: NSObject {
} }
// Add last event ID if available // Add last event ID if available
if let lastEventId = lastEventId { if let lastEventId {
request.setValue(lastEventId, forHTTPHeaderField: "Last-Event-ID") request.setValue(lastEventId, forHTTPHeaderField: "Last-Event-ID")
} }
@ -117,7 +117,7 @@ final class EventSource: NSObject {
// Update reconnect time // Update reconnect time
if let retry = eventRetry { if let retry = eventRetry {
reconnectTime = TimeInterval(retry) / 1000.0 reconnectTime = TimeInterval(retry) / 1_000.0
} }
// Dispatch event // Dispatch event
@ -174,7 +174,12 @@ final class EventSource: NSObject {
// MARK: - URLSessionDataDelegate // MARK: - URLSessionDataDelegate
extension EventSource: URLSessionDataDelegate { extension EventSource: URLSessionDataDelegate {
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) { func urlSession(
_ session: URLSession,
dataTask: URLSessionDataTask,
didReceive response: URLResponse,
completionHandler: @escaping (URLSession.ResponseDisposition) -> Void
) {
guard let httpResponse = response as? HTTPURLResponse else { guard let httpResponse = response as? HTTPURLResponse else {
completionHandler(.cancel) completionHandler(.cancel)
return return
@ -205,7 +210,7 @@ extension EventSource: URLSessionDataDelegate {
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
isConnected = false isConnected = false
if let error = error { if let error {
logger.error("EventSource error: \(error)") logger.error("EventSource error: \(error)")
} }
@ -218,10 +223,16 @@ extension EventSource: URLSessionDataDelegate {
// MARK: - URLSessionDelegate // MARK: - URLSessionDelegate
extension EventSource: URLSessionDelegate { extension EventSource: URLSessionDelegate {
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { func urlSession(
_ session: URLSession,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void
) {
// Accept the server's certificate for localhost connections // Accept the server's certificate for localhost connections
if challenge.protectionSpace.host == "localhost" { if challenge.protectionSpace.host == "localhost",
let credential = URLCredential(trust: challenge.protectionSpace.serverTrust!) let serverTrust = challenge.protectionSpace.serverTrust
{
let credential = URLCredential(trust: serverTrust)
completionHandler(.useCredential, credential) completionHandler(.useCredential, credential)
} else { } else {
completionHandler(.performDefaultHandling, nil) completionHandler(.performDefaultHandling, nil)

View file

@ -69,9 +69,9 @@ final class NotificationControlHandler {
logger.info("Received notification: \(title) - \(body) (type: \(typeString ?? "unknown"))") logger.info("Received notification: \(title) - \(body) (type: \(typeString ?? "unknown"))")
// Map type string to ServerEventType and create ServerEvent // Map type string to ServerEventType and create ServerEvent
if let typeString = typeString, if let typeString,
let eventType = ServerEventType(rawValue: typeString) { let eventType = ServerEventType(rawValue: typeString)
{
let serverEvent = ServerEvent( let serverEvent = ServerEvent(
type: eventType, type: eventType,
sessionId: sessionId, sessionId: sessionId,

View file

@ -278,7 +278,7 @@ final class NotificationService: NSObject {
// Format duration if provided // Format duration if provided
if duration > 0 { if duration > 0 {
let seconds = duration / 1000 let seconds = duration / 1_000
if seconds < 60 { if seconds < 60 {
content.subtitle = "\(seconds)s" content.subtitle = "\(seconds)s"
} else { } else {
@ -438,7 +438,7 @@ final class NotificationService: NSObject {
eventSource?.onError = { [weak self] error in eventSource?.onError = { [weak self] error in
Task { @MainActor in Task { @MainActor in
if let error = error { if let error {
self?.logger.error("❌ EventSource error: \(error)") self?.logger.error("❌ EventSource error: \(error)")
} }
self?.isConnected = false self?.isConnected = false
@ -526,7 +526,7 @@ final class NotificationService: NSObject {
} }
case "connected": case "connected":
logger.info("🔗 Received connected event from server") logger.info("🔗 Received connected event from server")
// No notification for connected events // No notification for connected events
default: default:
logger.warning("Unknown event type: \(type)") logger.warning("Unknown event type: \(type)")
} }
@ -614,7 +614,7 @@ final class NotificationService: NSObject {
// Format duration if provided // Format duration if provided
if duration > 0 { if duration > 0 {
let seconds = duration / 1000 let seconds = duration / 1_000
if seconds < 60 { if seconds < 60 {
content.subtitle = "\(seconds)s" content.subtitle = "\(seconds)s"
} else { } else {
@ -698,7 +698,7 @@ final class NotificationService: NSObject {
let request = UNNotificationRequest(identifier: identifier, content: content, trigger: nil) let request = UNNotificationRequest(identifier: identifier, content: content, trigger: nil)
UNUserNotificationCenter.current().add(request) { [weak self] error in UNUserNotificationCenter.current().add(request) { [weak self] error in
if let error = error { if let error {
self?.logger.error("Failed to deliver notification: \(error)") self?.logger.error("Failed to deliver notification: \(error)")
} else { } else {
self?.logger.debug("Notification delivered: \(identifier)") self?.logger.debug("Notification delivered: \(identifier)")

View file

@ -1,6 +1,3 @@
// Server event model for notification handling
//
import Foundation import Foundation
/// Types of server events that can be received from the VibeTunnel server. /// Types of server events that can be received from the VibeTunnel server.
@ -54,19 +51,19 @@ enum ServerEventType: String, Codable, CaseIterable {
var description: String { var description: String {
switch self { switch self {
case .sessionStart: case .sessionStart:
return "Session Started" "Session Started"
case .sessionExit: case .sessionExit:
return "Session Ended" "Session Ended"
case .commandFinished: case .commandFinished:
return "Command Completed" "Command Completed"
case .commandError: case .commandError:
return "Command Error" "Command Error"
case .bell: case .bell:
return "Terminal Bell" "Terminal Bell"
case .claudeTurn: case .claudeTurn:
return "Your Turn" "Your Turn"
case .connected: case .connected:
return "Connected" "Connected"
} }
} }
@ -80,9 +77,9 @@ enum ServerEventType: String, Codable, CaseIterable {
var shouldNotify: Bool { var shouldNotify: Bool {
switch self { switch self {
case .sessionStart, .sessionExit, .claudeTurn: case .sessionStart, .sessionExit, .claudeTurn:
return true true
case .commandFinished, .commandError, .bell, .connected: case .commandFinished, .commandError, .bell, .connected:
return false false
} }
} }
} }
@ -207,8 +204,8 @@ struct ServerEvent: Codable, Identifiable, Equatable {
/// - sessionName: Optional human-readable name for the session. /// - sessionName: Optional human-readable name for the session.
/// - command: Optional command that started the session. /// - command: Optional command that started the session.
/// - Returns: A configured `ServerEvent` of type ``ServerEventType/sessionStart``. /// - Returns: A configured `ServerEvent` of type ``ServerEventType/sessionStart``.
static func sessionStart(sessionId: String, sessionName: String? = nil, command: String? = nil) -> ServerEvent { static func sessionStart(sessionId: String, sessionName: String? = nil, command: String? = nil) -> Self {
ServerEvent( Self(
type: .sessionStart, type: .sessionStart,
sessionId: sessionId, sessionId: sessionId,
sessionName: sessionName, sessionName: sessionName,
@ -225,8 +222,8 @@ struct ServerEvent: Codable, Identifiable, Equatable {
/// - sessionName: Optional human-readable name for the session. /// - sessionName: Optional human-readable name for the session.
/// - exitCode: Optional process exit code. /// - exitCode: Optional process exit code.
/// - Returns: A configured `ServerEvent` of type ``ServerEventType/sessionExit``. /// - Returns: A configured `ServerEvent` of type ``ServerEventType/sessionExit``.
static func sessionExit(sessionId: String, sessionName: String? = nil, exitCode: Int? = nil) -> ServerEvent { static func sessionExit(sessionId: String, sessionName: String? = nil, exitCode: Int? = nil) -> Self {
ServerEvent( Self(
type: .sessionExit, type: .sessionExit,
sessionId: sessionId, sessionId: sessionId,
sessionName: sessionName, sessionName: sessionName,
@ -244,8 +241,15 @@ struct ServerEvent: Codable, Identifiable, Equatable {
/// - duration: Execution time in milliseconds. /// - duration: Execution time in milliseconds.
/// - exitCode: Optional process exit code. /// - exitCode: Optional process exit code.
/// - Returns: A configured `ServerEvent` of type ``ServerEventType/commandFinished``. /// - Returns: A configured `ServerEvent` of type ``ServerEventType/commandFinished``.
static func commandFinished(sessionId: String, command: String, duration: Int, exitCode: Int? = nil) -> ServerEvent { static func commandFinished(
ServerEvent( sessionId: String,
command: String,
duration: Int,
exitCode: Int? = nil
)
-> Self
{
Self(
type: .commandFinished, type: .commandFinished,
sessionId: sessionId, sessionId: sessionId,
command: command, command: command,
@ -264,8 +268,8 @@ struct ServerEvent: Codable, Identifiable, Equatable {
/// - exitCode: The process exit code. /// - exitCode: The process exit code.
/// - duration: Optional execution time in milliseconds. /// - duration: Optional execution time in milliseconds.
/// - Returns: A configured `ServerEvent` of type ``ServerEventType/commandError``. /// - Returns: A configured `ServerEvent` of type ``ServerEventType/commandError``.
static func commandError(sessionId: String, command: String, exitCode: Int, duration: Int? = nil) -> ServerEvent { static func commandError(sessionId: String, command: String, exitCode: Int, duration: Int? = nil) -> Self {
ServerEvent( Self(
type: .commandError, type: .commandError,
sessionId: sessionId, sessionId: sessionId,
command: command, command: command,
@ -283,8 +287,8 @@ struct ServerEvent: Codable, Identifiable, Equatable {
/// - sessionId: The unique identifier for the session. /// - sessionId: The unique identifier for the session.
/// - sessionName: Optional human-readable name for the session. /// - sessionName: Optional human-readable name for the session.
/// - Returns: A configured `ServerEvent` of type ``ServerEventType/claudeTurn``. /// - Returns: A configured `ServerEvent` of type ``ServerEventType/claudeTurn``.
static func claudeTurn(sessionId: String, sessionName: String? = nil) -> ServerEvent { static func claudeTurn(sessionId: String, sessionName: String? = nil) -> Self {
ServerEvent( Self(
type: .claudeTurn, type: .claudeTurn,
sessionId: sessionId, sessionId: sessionId,
sessionName: sessionName, sessionName: sessionName,
@ -298,8 +302,8 @@ struct ServerEvent: Codable, Identifiable, Equatable {
/// ///
/// - Parameter sessionId: The unique identifier for the session. /// - Parameter sessionId: The unique identifier for the session.
/// - Returns: A configured `ServerEvent` of type ``ServerEventType/bell``. /// - Returns: A configured `ServerEvent` of type ``ServerEventType/bell``.
static func bell(sessionId: String) -> ServerEvent { static func bell(sessionId: String) -> Self {
ServerEvent( Self(
type: .bell, type: .bell,
sessionId: sessionId, sessionId: sessionId,
message: "Terminal bell" message: "Terminal bell"
@ -336,20 +340,20 @@ struct ServerEvent: Codable, Identifiable, Equatable {
/// ///
/// - Returns: A formatted duration string, or `nil` if no duration is set. /// - Returns: A formatted duration string, or `nil` if no duration is set.
var formattedDuration: String? { var formattedDuration: String? {
guard let duration = duration else { return nil } guard let duration else { return nil }
if duration < 1000 { if duration < 1_000 {
return "\(duration)ms" return "\(duration)ms"
} else if duration < 60000 { } else if duration < 60_000 {
return String(format: "%.1fs", Double(duration) / 1000.0) return String(format: "%.1fs", Double(duration) / 1_000.0)
} else if duration < 3600000 { } else if duration < 3_600_000 {
let minutes = duration / 60000 let minutes = duration / 60_000
let seconds = (duration % 60000) / 1000 let seconds = (duration % 60_000) / 1_000
return "\(minutes)m \(seconds)s" return "\(minutes)m \(seconds)s"
} else { } else {
let hours = duration / 3600000 let hours = duration / 3_600_000
let minutes = (duration % 3600000) / 60000 let minutes = (duration % 3_600_000) / 60_000
let seconds = (duration % 60000) / 1000 let seconds = (duration % 60_000) / 1_000
return "\(hours)h \(minutes)m \(seconds)s" return "\(hours)h \(minutes)m \(seconds)s"
} }
} }

View file

@ -212,7 +212,12 @@ final class SessionMonitor {
} }
/// Pre-cache Git repositories for sessions, deduplicating by repository root /// Pre-cache Git repositories for sessions, deduplicating by repository root
private func preCacheGitRepositories(for sessions: [ServerSessionInfo], using gitMonitor: GitRepositoryMonitor) async { private func preCacheGitRepositories(
for sessions: [ServerSessionInfo],
using gitMonitor: GitRepositoryMonitor
)
async
{
// Track unique directories we need to check // Track unique directories we need to check
var uniqueDirectoriesToCheck = Set<String>() var uniqueDirectoriesToCheck = Set<String>()

View file

@ -85,7 +85,10 @@ struct PathSplittingTests {
// List contents of parent directory // List contents of parent directory
let fileManager = FileManager.default let fileManager = FileManager.default
let contents = try #require(try? fileManager.contentsOfDirectory(at: parentURL, includingPropertiesForKeys: nil)) let contents = try #require(try? fileManager.contentsOfDirectory(
at: parentURL,
includingPropertiesForKeys: nil
))
let matching = contents.filter { $0.lastPathComponent.hasPrefix(prefix) } let matching = contents.filter { $0.lastPathComponent.hasPrefix(prefix) }
// We can't assert specific matches as they depend on the user's home directory // We can't assert specific matches as they depend on the user's home directory

View file

@ -1,11 +1,11 @@
import Testing
import Foundation import Foundation
import Testing
@testable import VibeTunnel @testable import VibeTunnel
@Suite("ServerEvent") @Suite("ServerEvent")
struct ServerEventTests { struct ServerEventTests {
// MARK: - Codable Tests // MARK: - Codable Tests
// These are valuable - testing JSON encoding/decoding with optional fields // These are valuable - testing JSON encoding/decoding with optional fields
@Test("Codable round-trip with multiple optional fields") @Test("Codable round-trip with multiple optional fields")
@ -39,7 +39,7 @@ struct ServerEventTests {
sessionName: "Long Running Command", sessionName: "Long Running Command",
command: "npm install", command: "npm install",
exitCode: 0, exitCode: 0,
duration: 15000, duration: 15_000,
processInfo: "Node.js process", processInfo: "Node.js process",
message: "Command completed successfully" message: "Command completed successfully"
) )
@ -52,7 +52,7 @@ struct ServerEventTests {
#expect(decoded.sessionName == "Long Running Command") #expect(decoded.sessionName == "Long Running Command")
#expect(decoded.command == "npm install") #expect(decoded.command == "npm install")
#expect(decoded.exitCode == 0) #expect(decoded.exitCode == 0)
#expect(decoded.duration == 15000) #expect(decoded.duration == 15_000)
#expect(decoded.processInfo == "Node.js process") #expect(decoded.processInfo == "Node.js process")
#expect(decoded.message == "Command completed successfully") #expect(decoded.message == "Command completed successfully")
} }
@ -76,6 +76,7 @@ struct ServerEventTests {
} }
// MARK: - Event Type Logic Tests // MARK: - Event Type Logic Tests
// Testing actual business logic, not Swift's enum implementation // Testing actual business logic, not Swift's enum implementation
@Test("Event type descriptions are user-friendly") @Test("Event type descriptions are user-friendly")
@ -104,6 +105,7 @@ struct ServerEventTests {
} }
// MARK: - Edge Cases // MARK: - Edge Cases
// These test important edge cases for data integrity // These test important edge cases for data integrity
@Test("Handles empty strings correctly") @Test("Handles empty strings correctly")
@ -146,6 +148,7 @@ struct ServerEventTests {
} }
// MARK: - Convenience Initializers // MARK: - Convenience Initializers
// These test that convenience initializers create properly configured events // These test that convenience initializers create properly configured events
@Test("sessionStart convenience initializer sets correct fields") @Test("sessionStart convenience initializer sets correct fields")
@ -183,14 +186,14 @@ struct ServerEventTests {
let event = ServerEvent.commandFinished( let event = ServerEvent.commandFinished(
sessionId: "test-789", sessionId: "test-789",
command: "npm install", command: "npm install",
duration: 15000, duration: 15_000,
exitCode: 0 exitCode: 0
) )
#expect(event.type == .commandFinished) #expect(event.type == .commandFinished)
#expect(event.sessionId == "test-789") #expect(event.sessionId == "test-789")
#expect(event.command == "npm install") #expect(event.command == "npm install")
#expect(event.duration == 15000) #expect(event.duration == 15_000)
#expect(event.exitCode == 0) #expect(event.exitCode == 0)
#expect(!event.shouldNotify) #expect(!event.shouldNotify)
} }
@ -220,6 +223,7 @@ struct ServerEventTests {
} }
// MARK: - Computed Properties with Logic // MARK: - Computed Properties with Logic
// These test actual business logic in computed properties // These test actual business logic in computed properties
@Test("displayName fallback logic works correctly") @Test("displayName fallback logic works correctly")
@ -243,9 +247,9 @@ struct ServerEventTests {
@Test("formattedDuration handles different time ranges", arguments: [ @Test("formattedDuration handles different time ranges", arguments: [
(500, "500ms"), (500, "500ms"),
(2500, "2.5s"), (2_500, "2.5s"),
(125000, "2m 5s"), (125_000, "2m 5s"),
(3661000, "1h 1m 1s") (3_661_000, "1h 1m 1s")
]) ])
func formattedDurationLogic(duration: Int, expected: String) { func formattedDurationLogic(duration: Int, expected: String) {
let event = ServerEvent(type: .commandFinished, duration: duration) let event = ServerEvent(type: .commandFinished, duration: duration)

View file

@ -1,11 +1,11 @@
import Testing
import Foundation import Foundation
import Testing
@testable import VibeTunnel @testable import VibeTunnel
@Suite("TunnelSession & Related Types") @Suite("TunnelSession & Related Types")
struct TunnelSessionTests { struct TunnelSessionTests {
// MARK: - TunnelSession Logic Tests // MARK: - TunnelSession Logic Tests
// Only testing actual logic, not property synthesis // Only testing actual logic, not property synthesis
@Test("updateActivity updates lastActivity timestamp") @Test("updateActivity updates lastActivity timestamp")
@ -23,7 +23,7 @@ struct TunnelSessionTests {
@Test("TunnelSession is Codable with all fields") @Test("TunnelSession is Codable with all fields")
func tunnelSessionCodable() throws { func tunnelSessionCodable() throws {
var originalSession = TunnelSession(processID: 67890) var originalSession = TunnelSession(processID: 67_890)
originalSession.updateActivity() originalSession.updateActivity()
let data = try JSONEncoder().encode(originalSession) let data = try JSONEncoder().encode(originalSession)
@ -38,6 +38,7 @@ struct TunnelSessionTests {
} }
// MARK: - CreateSessionRequest Tests // MARK: - CreateSessionRequest Tests
// Testing optional field handling in Codable // Testing optional field handling in Codable
@Test("CreateSessionRequest encodes/decodes with all optional fields") @Test("CreateSessionRequest encodes/decodes with all optional fields")
@ -91,6 +92,7 @@ struct TunnelSessionTests {
} }
// MARK: - CreateSessionResponse Tests // MARK: - CreateSessionResponse Tests
// Simple type but worth testing Codable with Date precision // Simple type but worth testing Codable with Date precision
@Test("CreateSessionResponse handles date encoding correctly") @Test("CreateSessionResponse handles date encoding correctly")