REPLACED: three worthless tests that only tested test code, not production: - statusMessageUpToDateFormat built the expected string locally and matched it - statusMessageUpdateReadyFormat same self-referential test - statusChannelsAreIndependent was literally #expect(true, ...) ADDED: four real tests that drive production code: - dubaiTimeCheckTimeFormat: assert DubaiTime.checkTime() formats as HH:MM Dubai - manualCheckUpToDateIncludesTimestamp: inject stub appcast via testAppcastJSON seam, drive UpdateChecker.checkNow(manual: true), verify statusMessage matches exact format - automaticCheckUpToDateLeavesMessageNil: verify automatic check (manual: false) leaves statusMessage nil when up-to-date - statusChannelsAreIndependent: construct real AppModel via AppDelegate.makeLaunchModel(), assert setStatus() does NOT affect updateStatusMessage, setUpdateStatus() does NOT affect statusLine, and vice versa. PROVES the defect is caught: test fails with 3 issues if updateStatusMessage is reverted to an alias of statusLine. ADDED: testAppcastJSON seam to UpdateChecker for test injection of appcast data. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012ZiTXPbPCSjzPVsfoweAbp
119 lines
4.7 KiB
Swift
119 lines
4.7 KiB
Swift
import Foundation
|
|
import Testing
|
|
@testable import Shotdeck
|
|
@testable import ShotdeckCore
|
|
|
|
@Test("DubaiTime.checkTime formats as HH:MM Dubai")
|
|
func dubaiTimeCheckTimeFormat() {
|
|
let now = Date()
|
|
let result = DubaiTime.checkTime(now)
|
|
|
|
// Dubai timezone format: HH:MM Dubai
|
|
let pattern = "^[0-9]{2}:[0-9]{2} Dubai$"
|
|
let regex = try? NSRegularExpression(pattern: pattern, options: [])
|
|
let range = NSRange(result.startIndex..<result.endIndex, in: result)
|
|
let matches = regex?.matches(in: result, options: [], range: range) ?? []
|
|
#expect(!matches.isEmpty, "checkTime should format as HH:MM Dubai, got: \(result)")
|
|
}
|
|
|
|
@Test("Status message: up-to-date manual check includes Dubai timestamp")
|
|
@MainActor
|
|
func manualCheckUpToDateIncludesTimestamp() async {
|
|
let checker = UpdateChecker()
|
|
|
|
// Inject a stub appcast showing the current version (no update available)
|
|
let currentVersion = UpdateChecker.currentVersion()
|
|
let stubAppcast = """
|
|
{
|
|
"version": "\(currentVersion)",
|
|
"zipURL": "https://example.com/dummy.zip",
|
|
"sha256": "0000000000000000000000000000000000000000000000000000000000000000"
|
|
}
|
|
"""
|
|
checker.testAppcastJSON = stubAppcast
|
|
|
|
// Capture the status message
|
|
var capturedStatus: String?
|
|
checker.onChecked = {
|
|
capturedStatus = checker.statusMessage
|
|
}
|
|
|
|
// Run the manual check
|
|
await checker.checkNow(manual: true)
|
|
|
|
// Verify the message matches the expected format and includes a timestamp
|
|
guard let status = capturedStatus else {
|
|
#expect(false, "statusMessage should not be nil for manual check finding no update")
|
|
return
|
|
}
|
|
|
|
// Message should be "Redline X.Y.Z is up to date, checked HH:MM Dubai"
|
|
let pattern = "^Redline [0-9]+\\.[0-9]+\\.[0-9]+ is up to date, checked [0-9]{2}:[0-9]{2} Dubai$"
|
|
let regex = try? NSRegularExpression(pattern: pattern, options: [])
|
|
let range = NSRange(status.startIndex..<status.endIndex, in: status)
|
|
let matches = regex?.matches(in: status, options: [], range: range) ?? []
|
|
#expect(!matches.isEmpty, "Manual check up-to-date message should match format, got: \(status)")
|
|
}
|
|
|
|
@Test("Status message: automatic check doesn't set message when up-to-date")
|
|
@MainActor
|
|
func automaticCheckUpToDateLeavesMessageNil() async {
|
|
let checker = UpdateChecker()
|
|
|
|
// Inject a stub appcast showing the current version (no update available)
|
|
let currentVersion = UpdateChecker.currentVersion()
|
|
let stubAppcast = """
|
|
{
|
|
"version": "\(currentVersion)",
|
|
"zipURL": "https://example.com/dummy.zip",
|
|
"sha256": "0000000000000000000000000000000000000000000000000000000000000000"
|
|
}
|
|
"""
|
|
checker.testAppcastJSON = stubAppcast
|
|
|
|
// Capture the status message
|
|
var capturedStatus: String?
|
|
checker.onChecked = {
|
|
capturedStatus = checker.statusMessage
|
|
}
|
|
|
|
// Run an AUTOMATIC check (manual: false)
|
|
await checker.checkNow(manual: false)
|
|
|
|
// For automatic checks finding no update, statusMessage should be nil
|
|
#expect(capturedStatus == nil,
|
|
"Automatic check finding no update should leave statusMessage nil, got: \(capturedStatus ?? "(nil)")")
|
|
}
|
|
|
|
@Test("Update status and general status are independent channels")
|
|
@MainActor
|
|
func statusChannelsAreIndependent() async throws {
|
|
let fm = FileManager.default
|
|
let appSupportRoot = fm.temporaryDirectory
|
|
.appendingPathComponent("update-checker-channel-test-\(UUID().uuidString)", isDirectory: true)
|
|
defer { try? fm.removeItem(at: appSupportRoot) }
|
|
|
|
// Create a real AppModel using the standard launch pattern
|
|
let model = AppDelegate.makeLaunchModel(appSupportRoot: appSupportRoot)
|
|
defer { model.hotkeys.unregisterAll() }
|
|
|
|
// Test 1: Setting statusLine should NOT affect updateStatusMessage
|
|
model.setStatus("General status: captured 3")
|
|
#expect(model.statusLine == "General status: captured 3", "statusLine should be set")
|
|
#expect(model.updateStatusMessage == nil, "updateStatusMessage should remain nil")
|
|
|
|
// Test 2: Setting updateStatus should NOT affect statusLine
|
|
model.setUpdateStatus("Update to 9.9.9 is ready")
|
|
#expect(model.statusLine == "General status: captured 3", "statusLine should remain unchanged")
|
|
#expect(model.updateStatusMessage == "Update to 9.9.9 is ready", "updateStatusMessage should be set")
|
|
|
|
// Test 3: Clearing statusLine leaves updateStatus intact
|
|
model.setStatus(nil)
|
|
#expect(model.statusLine == nil, "statusLine should be cleared")
|
|
#expect(model.updateStatusMessage == "Update to 9.9.9 is ready", "updateStatusMessage should persist")
|
|
|
|
// Test 4: Clearing updateStatus leaves other state unaffected
|
|
model.setUpdateStatus(nil)
|
|
#expect(model.updateStatusMessage == nil, "updateStatusMessage should be cleared")
|
|
}
|