Files
Jarvis/App/Tests/AppTests/WizardStateTests.swift
james.maes 795c3bf5c9 feat(app): v0.1 UX surfaces — startup panels + InputMonitoring banner + PTT (#87, #88, #89, #156)
#89: new AppDelegate.startupSurfacesMode enum (.minimal/.diagnostic). Debug defaults .diagnostic; Release defaults .minimal. openStartupSurfacesIfDiagnostic() runs at tail of bootHealthTask (skipped under XCTest or when wizard is open). Status panel top-right + Dev Overlay bottom-right of NSScreen.main.visibleFrame. DevOverlay opens to Logs tab.

#88: BannerContent.inputMonitoringDenied is now nonDismissible: true. New HUDBannerCoordinator.dismiss(id:) clears a banner programmatically without writing dismissedThisLaunch. AppDelegate.reprobeInputMonitoring fires on NSApplication.didBecomeActiveNotification; rebinds the global hotkey on re-grant. Uses IOHIDCheckAccess (query-only, no prompt) per CLAUDE.md guidance. 3 new coordinator tests.

#87: WizardState.pttHotkey persisted under Jarvis.pttHotkey (independent of summon hotkey). Ships unbound per CLAUDE.md hotkey hygiene. AppDelegate.rebindPTTFromState() called from installVoice, foreground TCC re-grant, Settings Save/Clear. SettingsView extended with PTT hotkey section. Audio-graph isolation: NSEvent monitors only, no audio-graph surgery — safe alongside fix/voice-crit-v0.1. 3 new WizardState tests.

DevOverlay 14 + Voice 26 tests pass; all 17 grep boundary gates green (incl. single-writer-hudstate + no-modal-presentation).
2026-05-13 06:32:16 -05:00

101 lines
4.1 KiB
Swift

import XCTest
@testable import Jarvis
@testable import Keychain
@testable import Shell
@MainActor
final class WizardStateTests: XCTestCase {
/// `@unchecked Sendable`: fields are `let`, but `KeychainStore` requires
/// `Sendable` conformance on an otherwise-struct type that our fake
/// implements as a class is unnecessary. Struct is fine.
private struct FakeKeychain: KeychainStore {
let apiKeyStored: Bool
func set(_ value: String, for item: KeychainItem) throws {}
func get(_ item: KeychainItem) throws -> String {
guard apiKeyStored else { throw KeychainError.itemNotFound }
return "sk-ant-fake"
}
func delete(_ item: KeychainItem) throws {}
}
func test_firstUnresolvedStageIsAPIKeyWhenEmpty() {
let state = WizardState(keychain: FakeKeychain(apiKeyStored: false))
XCTAssertEqual(state.firstUnresolvedStage, .apiKey)
}
func test_firstUnresolvedStageIsTCCWhenAPIKeyStored() {
let state = WizardState(keychain: FakeKeychain(apiKeyStored: true))
state.inputMonitoringProbed = false
XCTAssertEqual(state.firstUnresolvedStage, .tcc)
}
func test_firstUnresolvedStageIsHotkeyWhenTCCDone() {
let state = WizardState(keychain: FakeKeychain(apiKeyStored: true))
state.inputMonitoringProbed = true
state.hotkey = nil
XCTAssertEqual(state.firstUnresolvedStage, .hotkey)
}
func test_firstUnresolvedStageIsCompleteWhenAllDone() {
let state = WizardState(keychain: FakeKeychain(apiKeyStored: true))
state.inputMonitoringProbed = true
state.hotkey = Shell.KeyboardShortcut(keyCode: 38, modifiers: [.command, .shift])
XCTAssertEqual(state.firstUnresolvedStage, .complete)
}
// MARK: - #87 / VOICE-13 — PTT hotkey persistence
/// PTT hotkey defaults to nil (ships unbound per CLAUDE.md hotkey
/// hygiene — Cmd+Shift+J collides with Chrome/Slack/VSCode and
/// Option+Space collides with Alfred/Raycast).
func test_pttHotkeyDefaultsToNil() {
let defaults = UserDefaults(suiteName: "PTTHotkeyTest-\(UUID().uuidString)")!
defaults.removeObject(forKey: "Jarvis.pttHotkey")
let state = WizardState(
keychain: FakeKeychain(apiKeyStored: false),
userDefaults: defaults
)
XCTAssertNil(state.pttHotkey, "#87: PTT hotkey ships unbound")
}
/// PTT hotkey persists to UserDefaults under its own key, independent
/// of the summon-Jarvis hotkey.
func test_pttHotkeyPersistsAndReloads() {
let suite = "PTTHotkeyTest-\(UUID().uuidString)"
let defaults = UserDefaults(suiteName: suite)!
let shortcut = Shell.KeyboardShortcut(keyCode: 49, modifiers: [.control, .option])
let writer = WizardState(
keychain: FakeKeychain(apiKeyStored: true),
userDefaults: defaults
)
writer.pttHotkey = shortcut
// Distinct WizardState instance reads the same UserDefaults suite
// and must see the persisted PTT hotkey.
let reader = WizardState(
keychain: FakeKeychain(apiKeyStored: true),
userDefaults: defaults
)
XCTAssertEqual(reader.pttHotkey, shortcut, "#87: PTT hotkey must round-trip through UserDefaults")
}
/// PTT and summon-Jarvis hotkeys are independent — clearing one does
/// not affect the other.
func test_pttHotkeyIsIndependentOfSummonHotkey() {
let suite = "PTTHotkeyTest-\(UUID().uuidString)"
let defaults = UserDefaults(suiteName: suite)!
let state = WizardState(
keychain: FakeKeychain(apiKeyStored: true),
userDefaults: defaults
)
let summon = Shell.KeyboardShortcut(keyCode: 38, modifiers: [.command, .shift])
let ptt = Shell.KeyboardShortcut(keyCode: 49, modifiers: [.control, .option])
state.hotkey = summon
state.pttHotkey = ptt
state.hotkey = nil
XCTAssertNil(state.hotkey)
XCTAssertEqual(state.pttHotkey, ptt, "#87: clearing summon hotkey must not affect PTT hotkey")
}
}