diff --git a/.github/assets/readme/demo-email.png b/.github/assets/readme/demo-email.png deleted file mode 100644 index 43804ed0..00000000 Binary files a/.github/assets/readme/demo-email.png and /dev/null differ diff --git a/Cotabby/App/Coordinators/EmojiPickerController.swift b/Cotabby/App/Coordinators/EmojiPickerController.swift index fcaf70a5..1ea3a162 100644 --- a/Cotabby/App/Coordinators/EmojiPickerController.swift +++ b/Cotabby/App/Coordinators/EmojiPickerController.swift @@ -38,8 +38,6 @@ final class EmojiPickerController { private let isEnabled: () -> Bool /// Live emoji-customization preferences (skin tone / gender / neutral variant), read at match time. private let emojiPreferences: () -> EmojiVariantPreferences - /// The accept-word key label shown as a keycap on the highlighted row; `nil` hides the hint. - private let acceptKeyLabel: () -> String? /// Live personal usage snapshot, read at match time to rank favorites and seed the bare-`:` panel. /// `@MainActor`: it reads main-actor `EmojiUsageStore` state, matching where the picker runs. private let emojiUsage: @MainActor () -> EmojiUsageSnapshot @@ -85,7 +83,6 @@ final class EmojiPickerController { inserter: any EmojiTextInserting, isEnabled: @escaping () -> Bool, emojiPreferences: @escaping () -> EmojiVariantPreferences, - acceptKeyLabel: @escaping () -> String?, emojiUsage: @MainActor @escaping () -> EmojiUsageSnapshot, recordEmojiUsage: @MainActor @escaping (String) -> Void ) { @@ -96,7 +93,6 @@ final class EmojiPickerController { self.inserter = inserter self.isEnabled = isEnabled self.emojiPreferences = emojiPreferences - self.acceptKeyLabel = acceptKeyLabel self.emojiUsage = emojiUsage self.recordEmojiUsage = recordEmojiUsage } @@ -354,8 +350,7 @@ final class EmojiPickerController { query: currentQuery, matches: matches, selectedIndex: selectedIndex, - caretRect: caretRect, - acceptKeyLabel: acceptKeyLabel() + caretRect: caretRect ) } @@ -426,8 +421,7 @@ final class EmojiPickerController { query: currentQuery, matches: matches, selectedIndex: selectedIndex, - caretRect: context.caretRect, - acceptKeyLabel: acceptKeyLabel() + caretRect: context.caretRect ) } diff --git a/Cotabby/App/Coordinators/SuggestionCoordinator+Acceptance.swift b/Cotabby/App/Coordinators/SuggestionCoordinator+Acceptance.swift index b1a06fdd..bb6a5f71 100644 --- a/Cotabby/App/Coordinators/SuggestionCoordinator+Acceptance.swift +++ b/Cotabby/App/Coordinators/SuggestionCoordinator+Acceptance.swift @@ -162,7 +162,6 @@ extension SuggestionCoordinator { latestGenerationNumber = liveContext.generation clearSuggestion(clearDiagnostics: false) hideOverlay(reason: "Overlay hidden because \(keyName) accepted the final suggestion chunk.") - latestAcceptanceAction = "Accepted final chunk with \(keyName)." state = .idle // Remember what we just committed and the text it followed. `apply` consumes this to drop // a regeneration that only re-proposes the same tail before the host publishes the insert @@ -214,10 +213,6 @@ extension SuggestionCoordinator { schedulePostInsertionRefresh() let workID = currentWorkID deferAcceptanceBookkeeping { [weak self] in - self?.applySessionDiagnostics( - advancedSession, - acceptanceAction: "Accepted next chunk with \(keyName)." - ) self?.logStage( "\(keyName)-accepted-chunk", workID: workID, @@ -407,7 +402,6 @@ extension SuggestionCoordinator { let workID = currentWorkID deferAcceptanceBookkeeping { [weak self] in self?.recordAcceptedWords(from: replacement.replacementText) - self?.latestAcceptanceAction = "Accepted typo correction with \(keyName)." self?.logStage( "\(keyName)-accepted-correction", workID: workID, @@ -536,7 +530,6 @@ extension SuggestionCoordinator { } cancelPredictionWork() - applySessionDiagnostics(advancedSession, acceptanceAction: "User typed the next expected characters.") if advancedSession.isExhausted { completeActiveSuggestion( @@ -544,8 +537,7 @@ extension SuggestionCoordinator { scheduleNextPrediction: true, awaitHostPublish: true, stage: "typed-match-exhausted", - message: "The user typed the remaining suggestion characters exactly.", - acceptanceAction: "User typed through the rest of the suggestion." + message: "The user typed the remaining suggestion characters exactly." ) return true } @@ -595,12 +587,10 @@ extension SuggestionCoordinator { scheduleNextPrediction: Bool, awaitHostPublish: Bool = false, stage: String, - message: String, - acceptanceAction: String + message: String ) { let generation = latestGenerationNumber clearSuggestion(clearDiagnostics: false) - latestAcceptanceAction = acceptanceAction hideOverlay(reason: reason) state = .idle logStage(stage, workID: currentWorkID, generation: generation, message: message) @@ -619,17 +609,6 @@ extension SuggestionCoordinator { } } - func applySessionDiagnostics(_ session: ActiveSuggestionSession, acceptanceAction: String?) { - latestSuggestionPreview = session.remainingText - latestFullSuggestionPreview = session.fullText - latestRemainingSuggestionPreview = session.remainingText - latestAcceptedCharacterCount = session.acceptedCount - latestRemainingCharacterCount = session.remainingCount - if let acceptanceAction { - latestAcceptanceAction = acceptanceAction - } - } - /// Marks the session's suggestion accepted in the quality counters, once per suggestion: only /// the first chunk counts, so word-by-word walks of one suggestion add nothing further and the /// acceptance rate stays suggestions-accepted over suggestions-shown. @@ -767,13 +746,11 @@ extension SuggestionCoordinator { isCorrection: isCorrection, resolvedFieldStyle: context.resolvedFieldStyle ) - if let message = overlayPresenter.present( + _ = overlayPresenter.present( text: text, geometry: geometry, previousState: overlayState - ) { - latestOverlayMessage = message - } + ) } /// Repairs untrustworthy caret anchors with a hidden-text-layout estimate before presentation. @@ -913,8 +890,7 @@ extension SuggestionCoordinator { /// joined to the exact gate decision via `request_id`. The metadata deliberately carries the /// estimate-vs-AX vertical delta and which host measurements calibrated the layout, because /// field reports of "ghost is N lines off" are only diagnosable from those numbers. - /// Deliberately not routed through `logStage`: that helper also mutates the UI-facing - /// `latestStageMessage`, and a per-present geometry detail should not overwrite the + /// Deliberately not routed through `logStage`: this is a geometry-specific event rather than a /// user-visible pipeline stage. private func logCaretLayoutRepair( anchor: LayoutRepairedAnchor, @@ -982,7 +958,7 @@ extension SuggestionCoordinator { } func hideOverlay(reason: String) { - latestOverlayMessage = overlayPresenter.hide(reason: reason) + _ = overlayPresenter.hide(reason: reason) } func logStage( @@ -994,11 +970,6 @@ extension SuggestionCoordinator { rawOutput: String? = nil, normalizedOutput: String? = nil ) { - // Repeated keystrokes produce identical stage messages; republishing the same string - // would still fire `objectWillChange` and re-render every coordinator observer. - if latestStageMessage != message { - latestStageMessage = message - } logger.logStage( stage, workID: workID, diff --git a/Cotabby/App/Coordinators/SuggestionCoordinator+Lifecycle.swift b/Cotabby/App/Coordinators/SuggestionCoordinator+Lifecycle.swift index f167521d..8bd4dfa4 100644 --- a/Cotabby/App/Coordinators/SuggestionCoordinator+Lifecycle.swift +++ b/Cotabby/App/Coordinators/SuggestionCoordinator+Lifecycle.swift @@ -38,7 +38,6 @@ extension SuggestionCoordinator { clearSuggestion(clearDiagnostics: true) hideOverlay(reason: "Overlay hidden because the runtime model is switching.") state = .idle - latestStageMessage = "Idle: runtime model switching reset active suggestion state." } // MARK: - Settings @@ -51,7 +50,6 @@ extension SuggestionCoordinator { } CotabbyLogger.suggestion.info("Settings changed, resetting suggestion state") - let previousSnapshot = settingsSnapshot settingsSnapshot = snapshot cancelPredictionWork() resetCachedGenerationContext() @@ -59,14 +57,6 @@ extension SuggestionCoordinator { hideOverlay(reason: "Overlay hidden because autocomplete settings changed.") state = .idle - if previousSnapshot.selectedEngine != snapshot.selectedEngine { - latestStageMessage = "Updated autocomplete engine to \(snapshot.selectedEngine.displayLabel)." - } else if previousSnapshot.selectedWordCountPreset != snapshot.selectedWordCountPreset { - latestStageMessage = "Updated suggestion length to \(snapshot.selectedWordCountPreset.displayLabel)." - } else { - latestStageMessage = "Updated autocomplete settings." - } - // Cancel any obsolete context, then restart only when the subsystem is not disabled. visualContextCoordinator.cancel(resetState: true) if let focusedSnapshot = focusModel.snapshot.context, diff --git a/Cotabby/App/Coordinators/SuggestionCoordinator+Prediction.swift b/Cotabby/App/Coordinators/SuggestionCoordinator+Prediction.swift index 0a3e2112..bd436b0c 100644 --- a/Cotabby/App/Coordinators/SuggestionCoordinator+Prediction.swift +++ b/Cotabby/App/Coordinators/SuggestionCoordinator+Prediction.swift @@ -122,8 +122,6 @@ extension SuggestionCoordinator { visualContextSummary: visualContextSummary ) latestGenerationNumber = context.generation - latestPromptPreview = requestBuildResult.promptPreview - latestRawModelOutput = nil let request = requestBuildResult.request latestRequestID = request.requestID @@ -169,13 +167,11 @@ extension SuggestionCoordinator { lastAcceptedTail = nil latestGenerationNumber = context.generation - latestLatencyMilliseconds = 0 let session = interactionState.startSession( fullText: remainder, liveContext: context, latency: 0 ) - applySessionDiagnostics(session, acceptanceAction: "Restored a cached suggestion.") state = .ready(text: session.remainingText, latency: session.latency) presentOverlay( text: session.remainingText, @@ -242,8 +238,6 @@ extension SuggestionCoordinator { visualContextSummary: visualContextSummary ) latestGenerationNumber = context.generation - latestPromptPreview = requestBuildResult.promptPreview - latestRawModelOutput = nil let request = requestBuildResult.request latestRequestID = request.requestID @@ -544,7 +538,6 @@ extension SuggestionCoordinator { cancelPredictionWork() clearSuggestion(clearDiagnostics: false) hideOverlay(reason: "Overlay hidden because Cotabby automatically fixed a typo.") - latestAcceptanceAction = "Automatically corrected \"\(typoWord)\" to \"\(correctedWord)\"." state = .idle logStage( "typo-auto-corrected", @@ -570,14 +563,12 @@ extension SuggestionCoordinator { ) { let liveContext = interactionState.materializeContext(from: rawContext) latestGenerationNumber = liveContext.generation - latestLatencyMilliseconds = 0 let session = interactionState.startSession( fullText: correctedWord, liveContext: liveContext, latency: 0, kind: .correction(typoWord: typoWord) ) - applySessionDiagnostics(session, acceptanceAction: "Offered a correction for \"\(typoWord)\".") state = .ready(text: session.remainingText, latency: 0) presentOverlay( text: session.remainingText, @@ -677,7 +668,6 @@ extension SuggestionCoordinator { guard isPaidOffSpeculation || liveContext.generation == result.generation else { - latestRawModelOutput = SuggestionDebugLogger.debugPreview(result.rawText) // Lifecycle discards are counted under their own reasons so `generated` always equals // `shown` plus the suppression histogram; without this, every drop here silently // inflated the generated count against the others. @@ -694,8 +684,6 @@ extension SuggestionCoordinator { return } - latestRawModelOutput = SuggestionDebugLogger.debugPreview(result.rawText) - guard !result.text.isEmpty else { discardEmptyResult(result, workID: workID) return @@ -766,7 +754,6 @@ extension SuggestionCoordinator { return } - latestLatencyMilliseconds = Int(result.latency * 1000) latestGenerationNumber = liveContext.generation // One shown event per suggestion: this is the only place a fresh generation becomes // visible (re-presentations after partial accepts reuse the same session). @@ -781,7 +768,6 @@ extension SuggestionCoordinator { liveContext: liveContext, latency: result.latency ) - applySessionDiagnostics(session, acceptanceAction: "Generated new suggestion.") state = .ready(text: session.remainingText, latency: session.latency) presentOverlay( @@ -890,15 +876,13 @@ extension SuggestionCoordinator { advancement: SuggestionSessionAdvancement? ) { latestGenerationNumber = liveContext.generation - applySessionDiagnostics(reconciledSession, acceptanceAction: advancement?.actionSummary ?? latestAcceptanceAction) if reconciledSession.isExhausted { completeActiveSuggestion( reason: "Overlay hidden because the active suggestion was fully consumed.", scheduleNextPrediction: true, stage: advancement?.exhaustionStage ?? "session-exhausted", - message: advancement?.exhaustionMessage ?? "The active suggestion was fully consumed.", - acceptanceAction: advancement?.actionSummary ?? "Suggestion tail was fully consumed." + message: advancement?.exhaustionMessage ?? "The active suggestion was fully consumed." ) return } @@ -1013,7 +997,6 @@ extension SuggestionCoordinator { clearSuggestion(clearDiagnostics: true) hideOverlay(reason: reason) state = .disabled(reason) - latestStageMessage = "Disabled: \(reason)" } /// Disables predictions without tearing down the visual context session. @@ -1033,7 +1016,6 @@ extension SuggestionCoordinator { clearSuggestion(clearDiagnostics: true) hideOverlay(reason: reason) state = .disabled(reason) - latestStageMessage = "Disabled: \(reason)" } /// True when a previous teardown already disabled the pipeline for this exact reason and @@ -1049,13 +1031,10 @@ extension SuggestionCoordinator { } /// True when the no-session clear path still has anything to tear down. With no active - /// session, most keystrokes arrive with the overlay already hidden and the published - /// suggestion state already nil; assigning nil to an already-nil `@Published` property still - /// fires `objectWillChange`, so skipping the redundant clear avoids re-rendering every - /// coordinator observer on every key. `.disabled` counts as nothing-to-clear because entering - /// it already ran the full teardown. + /// session, most keystrokes arrive with the overlay already hidden and the state already idle. + /// `.disabled` counts as nothing-to-clear because entering it already ran the full teardown. var hasSuggestionArtifactsToClear: Bool { - if overlayState.isVisible || latestSuggestionPreview != nil || latestPromptPreview != nil { + if overlayState.isVisible || interactionState.activeSession != nil { return true } @@ -1074,18 +1053,9 @@ extension SuggestionCoordinator { lastAcceptedTail = nil // Stream bookkeeping follows the session it was rendering for. suggestionStreamingState.clearSession() - latestSuggestionPreview = nil - latestFullSuggestionPreview = nil - latestRemainingSuggestionPreview = nil - latestAcceptedCharacterCount = nil - latestRemainingCharacterCount = nil - latestAcceptanceAction = nil - latestLatencyMilliseconds = nil interactionState.clearSuggestion() if clearDiagnostics { - latestPromptPreview = nil - latestRawModelOutput = nil latestGenerationNumber = nil // Clear so the next session's terminal logStage doesn't carry the previous // request_id forward. `+Acceptance.logStage` falls back to "req_none" on nil, diff --git a/Cotabby/App/Coordinators/SuggestionCoordinator.swift b/Cotabby/App/Coordinators/SuggestionCoordinator.swift index e7499066..b207e042 100644 --- a/Cotabby/App/Coordinators/SuggestionCoordinator.swift +++ b/Cotabby/App/Coordinators/SuggestionCoordinator.swift @@ -12,22 +12,11 @@ import Foundation /// mutate them. @MainActor final class SuggestionCoordinator: ObservableObject { - /// The first group is user-facing and debug-facing state surfaced in the menu UI. - /// Keep treating these as coordinator-owned even though they are not `private(set)`. - @Published var state: SuggestionDebugState = .idle - @Published var overlayState: OverlayState = .hidden(reason: "Overlay idle.") - @Published var latestSuggestionPreview: String? - @Published var latestFullSuggestionPreview: String? - @Published var latestRemainingSuggestionPreview: String? - @Published var latestAcceptedCharacterCount: Int? - @Published var latestRemainingCharacterCount: Int? - @Published var latestAcceptanceAction: String? - @Published var latestLatencyMilliseconds: Int? - @Published var latestStageMessage = "Idle" - @Published var latestOverlayMessage = "Overlay idle." - @Published var latestPromptPreview: String? - @Published var latestRawModelOutput: String? - @Published var latestGenerationNumber: UInt64? + /// Coordinator-owned state machine values. Tests inspect these directly, but no UI subscribes + /// to them, so keeping them as plain properties avoids unrelated menu re-renders. + var state: SuggestionDebugState = .idle + var overlayState: OverlayState = .hidden(reason: "Overlay idle.") + var latestGenerationNumber: UInt64? @Published var visualContextStatus: VisualContextStatus = .idle @Published var latestVisualContextText: String? @Published var totalTabAcceptedWordCount: Int = 0 @@ -77,10 +66,9 @@ final class SuggestionCoordinator: ObservableObject { // Async work and active-session storage now live in dedicated collaborators below. var cancellables = Set() var settingsSnapshot: SuggestionSettingsSnapshot - /// Last completed round trip per backend, retained across keystrokes. `latestLatencyMilliseconds` - /// belongs to the currently displayed suggestion and is cleared as soon as the user types, so it - /// cannot drive adaptive scheduling. Keeping this separate lets a slow HTTP backend retain an - /// appropriate debounce without leaking that latency into the in-process engines. + /// Last completed round trip per backend, retained across keystrokes. Keeping this separate lets + /// a slow HTTP backend retain an appropriate debounce without leaking that latency into the + /// in-process engines. var lastLatencyByEngine: [SuggestionEngineKind: Int] = [:] // Synchronous input/focus callbacks cannot directly `await`, so resets are represented as a // barrier task that the next generation must cross before it can ask the runtime for output. @@ -200,7 +188,6 @@ final class SuggestionCoordinator: ObservableObject { latestVisualContextText = visualContextCoordinator.latestExcerpt overlayState = overlayController.state - latestOverlayMessage = overlayController.state.detail focusModel.snapshotPublisher .sink { [weak self] snapshot in diff --git a/Cotabby/App/Core/CotabbyAppEnvironment.swift b/Cotabby/App/Core/CotabbyAppEnvironment.swift index 04725258..33a4df83 100644 --- a/Cotabby/App/Core/CotabbyAppEnvironment.swift +++ b/Cotabby/App/Core/CotabbyAppEnvironment.swift @@ -275,7 +275,6 @@ final class CotabbyAppEnvironment { inserter: suggestionInserter, isEnabled: { suggestionSettings.isEmojiPickerEnabled }, emojiPreferences: { suggestionSettings.emojiVariantPreferences }, - acceptKeyLabel: { suggestionSettings.emojiPickerAcceptKeyLabel }, emojiUsage: { emojiUsageStore.snapshot() }, recordEmojiUsage: { emojiUsageStore.record(alias: $0) } ) diff --git a/Cotabby/Models/Context/VisualContextModels.swift b/Cotabby/Models/Context/VisualContextModels.swift index 7838e5b0..f486c164 100644 --- a/Cotabby/Models/Context/VisualContextModels.swift +++ b/Cotabby/Models/Context/VisualContextModels.swift @@ -45,20 +45,6 @@ nonisolated enum VisualContextStatus: Equatable, Sendable { case unavailable(String) case failed(String) - var detail: String { - switch self { - case .idle: - return "Waiting for a supported text input." - case .capturing: - return "Capturing nearby screen content." - case .extractingText: - return "Extracting visible text from the screenshot." - case .ready: - return "Nearby visible text is ready." - case let .unavailable(reason), let .failed(reason): - return reason - } - } } /// The final visual-context excerpt eventually injected into the completion prompt. diff --git a/Cotabby/Models/Emoji/EmojiPickerModels.swift b/Cotabby/Models/Emoji/EmojiPickerModels.swift index 15ea238d..fd890dad 100644 --- a/Cotabby/Models/Emoji/EmojiPickerModels.swift +++ b/Cotabby/Models/Emoji/EmojiPickerModels.swift @@ -5,8 +5,8 @@ import Foundation /// and free of AppKit/Accessibility/CGEvent dependencies so the catalog, matcher, and trigger /// state machine stay pure and easy to unit test. UI and runtime wiring live elsewhere. /// -/// The decoded dataset row mirrors the bundled `Resources/Emoji/emoji.json` schema exactly, so the -/// default `Decodable` synthesis can read it without custom `CodingKeys`. +/// The decoded dataset row keeps only the fields Cotabby uses. `Decodable` ignores extra dataset +/// keys, so metadata that does not affect matching or presentation stays out of the runtime model. /// One emoji record decoded from the bundled dataset. /// @@ -17,8 +17,6 @@ nonisolated struct EmojiEntry: Equatable, Decodable { let name: String let aliases: [String] let keywords: [String] - let group: String - let unicodeVersion: String } /// A single ranked search result surfaced in the picker panel. @@ -43,9 +41,6 @@ nonisolated struct EmojiMatch: Equatable, Identifiable { var id: String { displayGlyph } var glyph: String { displayGlyph } - /// Label shown next to the glyph. Falls back to the human description when an entry somehow has - /// no aliases, so a row is never blank. - var primaryAlias: String { entry.aliases.first ?? entry.name } } /// User-selectable skin tone applied to emoji that support Fitzpatrick modifiers. @@ -110,8 +105,6 @@ enum EmojiGender: String, CaseIterable, Equatable, Sendable { struct EmojiVariantPreferences: Equatable, Sendable { let skinTone: EmojiSkinTone let gender: EmojiGender - - static let `default` = EmojiVariantPreferences(skinTone: .neutral, gender: .neutral) } // MARK: - Trigger state machine vocabulary diff --git a/Cotabby/Models/Focus/FocusModels.swift b/Cotabby/Models/Focus/FocusModels.swift index 89bf5a3c..7bfdf42e 100644 --- a/Cotabby/Models/Focus/FocusModels.swift +++ b/Cotabby/Models/Focus/FocusModels.swift @@ -104,17 +104,6 @@ enum FocusCapability: Equatable { } } -/// Operator-facing debug information for how the resolver interpreted the AX tree. -struct FocusInspectionSnapshot: Equatable { - let focusedElementIdentifier: String - let focusedRole: String - let focusedSubrole: String? - let resolvedElementIdentifier: String? - let resolvedRole: String? - let resolvedSubrole: String? - let missingCapabilities: [FocusCapabilityRequirement] -} - /// Visual style of the focused field's own text, resolved from Accessibility so ghost text can be /// rendered to match it instead of always using the system font and a fixed gray. /// @@ -304,20 +293,14 @@ struct FocusSnapshot: Equatable { let bundleIdentifier: String? let capability: FocusCapability let context: FocusedInputSnapshot? - let inspection: FocusInspectionSnapshot? static let inactive = FocusSnapshot( applicationName: "No active application", bundleIdentifier: nil, capability: .unsupported("No focused text input"), - context: nil, - inspection: nil + context: nil ) - var capabilitySummary: String { - capability.summary - } - /// Returns the app identity that user-facing controls should target. /// /// Opening Cotabby's menu bar window can briefly make Cotabby the focused app. Treating Cotabby's own diff --git a/Cotabby/Models/Onboarding/OnboardingTemplate.swift b/Cotabby/Models/Onboarding/OnboardingTemplate.swift index 294ee6b8..86421bf7 100644 --- a/Cotabby/Models/Onboarding/OnboardingTemplate.swift +++ b/Cotabby/Models/Onboarding/OnboardingTemplate.swift @@ -160,7 +160,6 @@ struct OnboardingTemplateAvailability: Equatable, Sendable { /// A snapshot of the host's capability relevant to model selection. struct HardwareCapability: Equatable, Sendable { let physicalMemoryBytes: UInt64 - let isAppleSilicon: Bool /// Binary gigabytes (GiB), matching how macOS reports installed memory. var physicalMemoryGigabytes: Double { diff --git a/Cotabby/Models/Permissions/PermissionModels.swift b/Cotabby/Models/Permissions/PermissionModels.swift index 4fb47009..a3a5753b 100644 --- a/Cotabby/Models/Permissions/PermissionModels.swift +++ b/Cotabby/Models/Permissions/PermissionModels.swift @@ -70,15 +70,6 @@ enum CotabbyPermissionKind: String, CaseIterable, Identifiable, Sendable { } } - var guidanceHint: String { - switch guidanceStyle { - case .guidedOverlay: - "Cotabby will open System Settings and show a drag helper anchored to the correct list." - case .settingsOnly: - "Opens the matching System Settings pane so you can grant it manually." - } - } - var guidanceStyle: PermissionGuidanceStyle { switch self { case .accessibility, .inputMonitoring, .screenRecording: diff --git a/Cotabby/Models/Runtime/HuggingFaceModels.swift b/Cotabby/Models/Runtime/HuggingFaceModels.swift index a2da9803..6d310e10 100644 --- a/Cotabby/Models/Runtime/HuggingFaceModels.swift +++ b/Cotabby/Models/Runtime/HuggingFaceModels.swift @@ -6,17 +6,14 @@ import Foundation /// One result from `GET /api/models?filter=gguf&search=...&sort=downloads`. nonisolated struct HFModelSearchResult: Codable, Identifiable, Equatable { let id: String - let modelId: String let downloads: Int let likes: Int - let tags: [String] } /// One file entry from `GET /api/models//tree/main`. struct HFRepoFile: Codable, Equatable, Identifiable { let path: String let size: Int64 - let type: String var id: String { path } diff --git a/Cotabby/Models/Runtime/LlamaRuntimeModels.swift b/Cotabby/Models/Runtime/LlamaRuntimeModels.swift index 1755e434..9dbb2d73 100644 --- a/Cotabby/Models/Runtime/LlamaRuntimeModels.swift +++ b/Cotabby/Models/Runtime/LlamaRuntimeModels.swift @@ -216,9 +216,6 @@ struct LlamaGenerationOutput: Equatable, Sendable { /// True when the completion was withheld because `averageLogprob` fell below the floor. let suppressedByLowConfidence: Bool - static func text(_ text: String) -> LlamaGenerationOutput { - LlamaGenerationOutput(text: text, averageLogprob: nil, suppressedByLowConfidence: false) - } } /// The concrete runtime assets selected during bootstrap after checking available model files. diff --git a/Cotabby/Models/Runtime/OpenAICompatibleEndpointModels.swift b/Cotabby/Models/Runtime/OpenAICompatibleEndpointModels.swift index 3be0ca17..1562391c 100644 --- a/Cotabby/Models/Runtime/OpenAICompatibleEndpointModels.swift +++ b/Cotabby/Models/Runtime/OpenAICompatibleEndpointModels.swift @@ -12,13 +12,6 @@ nonisolated enum OpenAICompatibleAPIMode: String, CaseIterable, Codable, Identif var id: String { rawValue } - var displayLabel: String { - switch self { - case .completions: return "Completions" - case .chatCompletions: return "Chat Completions" - } - } - var route: String { switch self { case .completions: return "completions" @@ -30,12 +23,6 @@ nonisolated enum OpenAICompatibleAPIMode: String, CaseIterable, Codable, Identif /// One model identifier returned by `GET /v1/models`. nonisolated struct OpenAICompatibleModelOption: Decodable, Equatable, Identifiable, Sendable { let id: String - let ownedBy: String? - - private enum CodingKeys: String, CodingKey { - case id - case ownedBy = "owned_by" - } } /// Chooses the durable model identifier after endpoint discovery finishes. diff --git a/Cotabby/Models/Runtime/RuntimeBootstrapModel.swift b/Cotabby/Models/Runtime/RuntimeBootstrapModel.swift index a4e4de6a..bd9d6906 100644 --- a/Cotabby/Models/Runtime/RuntimeBootstrapModel.swift +++ b/Cotabby/Models/Runtime/RuntimeBootstrapModel.swift @@ -138,15 +138,6 @@ final class RuntimeBootstrapModel: ObservableObject { runtimeManager.stop() } - /// Cancels pending startup work and waits for the runtime manager to release native resources. - /// Normal UI shutdown can be fire-and-forget, but uninstall deletes the model directory and must - /// not race llama.cpp cleanup. - func stopAndWait() async { - runtimeTask?.cancel() - runtimeTask = nil - await runtimeManager.stopAndWait() - } - /// Synchronously releases the runtime on the calling thread, bounded by `timeoutSeconds`. /// Used by `applicationWillTerminate` so llama Metal resources are torn down before `exit()` /// runs C++ static destructors. See `LlamaRuntimeManager.shutdownSync` for rationale. diff --git a/Cotabby/Models/Settings/CompletionRenderMode.swift b/Cotabby/Models/Settings/CompletionRenderMode.swift index d301a204..14d56cec 100644 --- a/Cotabby/Models/Settings/CompletionRenderMode.swift +++ b/Cotabby/Models/Settings/CompletionRenderMode.swift @@ -19,8 +19,8 @@ nonisolated enum CompletionRenderMode: Equatable, Sendable { /// only; the rendering pipeline does not branch on it. case mirror(reason: MirrorReason) - /// Why mirror mode was chosen for this presentation. Surfaced in the focus debug overlay and - /// in `OverlayState.detail` so operators can confirm the policy is firing as expected. + /// Why mirror mode was chosen for this presentation. Surfaced in the focus debug overlay so + /// operators can confirm the policy is firing as expected. nonisolated enum MirrorReason: String, Equatable, Sendable { /// Caret quality came back `.estimated`, meaning the host did not expose `AXBoundsForRange` /// or any of the derived geometry paths. Inline rendering would land at a guessed X that diff --git a/Cotabby/Models/Settings/SuggestionSettingsModel.swift b/Cotabby/Models/Settings/SuggestionSettingsModel.swift index 847b09b8..1624cd35 100644 --- a/Cotabby/Models/Settings/SuggestionSettingsModel.swift +++ b/Cotabby/Models/Settings/SuggestionSettingsModel.swift @@ -164,15 +164,12 @@ final class SuggestionSettingsModel: ObservableObject { static let defaultFullAcceptanceKeyLabel = SuggestionSettingsStore.defaultFullAcceptanceKeyLabel static let minimumGhostTextOpacity = SuggestionSettingsStore.minimumGhostTextOpacity static let maximumGhostTextOpacity = SuggestionSettingsStore.maximumGhostTextOpacity - static let defaultGhostTextOpacity = SuggestionSettingsStore.defaultGhostTextOpacity static let ghostTextOpacityStep = SuggestionSettingsStore.ghostTextOpacityStep static let minimumGhostTextSizeMultiplier = SuggestionSettingsStore.minimumGhostTextSizeMultiplier static let maximumGhostTextSizeMultiplier = SuggestionSettingsStore.maximumGhostTextSizeMultiplier - static let defaultGhostTextSizeMultiplier = SuggestionSettingsStore.defaultGhostTextSizeMultiplier static let ghostTextSizeMultiplierStep = SuggestionSettingsStore.ghostTextSizeMultiplierStep static let minimumFadeInDuration = SuggestionSettingsStore.minimumFadeInDuration static let maximumFadeInDuration = SuggestionSettingsStore.maximumFadeInDuration - static let defaultFadeInDuration = SuggestionSettingsStore.defaultFadeInDuration static let fadeInDurationStep = SuggestionSettingsStore.fadeInDurationStep static let maximumExtendedContextCharacters = SuggestionSettingsStore.maximumExtendedContextCharacters @@ -343,11 +340,6 @@ final class SuggestionSettingsModel: ObservableObject { schedulePauseExpirationIfNeeded() } - /// Legacy compatibility shim. Reads through to `showIndicator`. - var showCaretIndicator: Bool { - showIndicator - } - /// Cohesive read model for non-UI consumers and persistence-oriented tests. /// /// SwiftUI-facing properties remain individually `@Published` for source compatibility. This diff --git a/Cotabby/Models/Suggestion/ActiveSuggestionSession.swift b/Cotabby/Models/Suggestion/ActiveSuggestionSession.swift index 6514bc2e..6fb9c8d2 100644 --- a/Cotabby/Models/Suggestion/ActiveSuggestionSession.swift +++ b/Cotabby/Models/Suggestion/ActiveSuggestionSession.swift @@ -32,10 +32,6 @@ struct ActiveSuggestionSession: Equatable, Sendable { self.kind = kind } - var acceptedText: String { - fullText.leadingCharacters(consumedCharacterCount) - } - var remainingText: String { fullText.droppingLeadingCharacters(consumedCharacterCount) } diff --git a/Cotabby/Models/Suggestion/FocusedInputContext.swift b/Cotabby/Models/Suggestion/FocusedInputContext.swift index f8409990..3c5ec61c 100644 --- a/Cotabby/Models/Suggestion/FocusedInputContext.swift +++ b/Cotabby/Models/Suggestion/FocusedInputContext.swift @@ -75,13 +75,6 @@ struct FocusedInputContext: Equatable, Sendable { self.generation = generation } - var identity: FocusedInputIdentity { - FocusedInputIdentity( - elementIdentifier: elementIdentifier, - focusChangeSequence: focusChangeSequence - ) - } - /// True when the caret is at the end of its line (only whitespace, if anything, before the next /// line break). Derived from `trailingText` via `CaretLinePosition`; used to decide when a /// mid-line completion strategy like fill-in-middle applies versus a plain forward continuation. diff --git a/Cotabby/Models/Suggestion/SuggestionConfiguration.swift b/Cotabby/Models/Suggestion/SuggestionConfiguration.swift index cc2863fe..998fad46 100644 --- a/Cotabby/Models/Suggestion/SuggestionConfiguration.swift +++ b/Cotabby/Models/Suggestion/SuggestionConfiguration.swift @@ -24,7 +24,6 @@ struct SuggestionWordRange: Equatable, Hashable, Sendable { return SuggestionWordRange(lowWords: clampedLow, highWords: clampedHigh) } - var displayLabel: String { "\(lowWords)-\(highWords) words" } var compactLabel: String { "\(lowWords)-\(highWords) w" } var promptInstruction: String { "Return only the next \(lowWords) to \(highWords) words." } } @@ -43,25 +42,6 @@ enum SuggestionWordCountPreset: String, CaseIterable, Equatable, Hashable, Senda "\(rawValue) words" } - /// Compact labels are useful in tight menu-bar controls where the full descriptive copy - /// would dominate the layout. - var compactLabel: String { - "\(rawValue) w" - } - - var promptInstruction: String { - switch self { - case .twoToFour: - return "Return only the next 2 to 4 words." - case .fourToSeven: - return "Return only the next 4 to 7 words." - case .sevenToTwelve: - return "Return only the next 7 to 12 words." - case .twelveToTwenty: - return "Return only the next 12 to 20 words." - } - } - /// The shared `SuggestionWordRange` shape so the prompt builder and token-budget math can /// consume presets and custom ranges identically. var range: SuggestionWordRange { @@ -77,19 +57,6 @@ enum SuggestionWordCountPreset: String, CaseIterable, Equatable, Hashable, Senda } } - /// Token budget is the sole governor of completion length on the local model (the in-prompt - /// word-range cue was removed), so it must track the upper word bound closely. Now derived from - /// the language-aware factor in `LanguageCatalog`; this convenience returns the English-default - /// number for callers/tests that don't have a language context handy. History: a ~1.5x sizing - /// (6/11/18/30) still overran because real prose uses fewer tokens per word than that assumed, - /// and an earlier 50% bump overran further, e.g. ~12 words on the shortest preset (#271). When - /// unsure, bias shorter; a clipped suggestion is cheaper than one that blows past the setting. - var suggestedPredictionTokenBudget: Int { - SuggestionWordRange.predictionTokenBudget( - highWords: range.highWords, - tokensPerWord: LanguageCatalog.fallbackTokensPerWord - ) - } } extension SuggestionWordRange { diff --git a/Cotabby/Models/Suggestion/SuggestionPresentationModels.swift b/Cotabby/Models/Suggestion/SuggestionPresentationModels.swift index 9b853456..48e2d96d 100644 --- a/Cotabby/Models/Suggestion/SuggestionPresentationModels.swift +++ b/Cotabby/Models/Suggestion/SuggestionPresentationModels.swift @@ -12,37 +12,6 @@ enum SuggestionDebugState: Equatable { case ready(text: String, latency: TimeInterval) case failed(String) - var shortLabel: String { - switch self { - case .idle: - return "Idle" - case .disabled: - return "Disabled" - case .debouncing: - return "Debouncing" - case .generating: - return "Generating" - case .ready: - return "Ready" - case .failed: - return "Failed" - } - } - - var detail: String? { - switch self { - case .idle: - return "No active suggestion is currently available." - case let .disabled(reason), let .failed(reason): - return reason - case .debouncing: - return "Waiting for typing to settle." - case .generating: - return "Requesting a completion from the active suggestion backend." - case .ready: - return "Ready means Cotabby has buffered a non-empty normalized completion for this field and can render it as ghost text." - } - } } /// Geometry needed to render ghost text in the same visual line box as the host editor. @@ -135,26 +104,6 @@ enum OverlayState: Equatable { case hidden(reason: String) case visible(text: String, geometry: SuggestionOverlayGeometry, mode: CompletionRenderMode) - var shortLabel: String { - switch self { - case .hidden: - return "Hidden" - case .visible: - return "Visible" - } - } - - var detail: String { - switch self { - case let .hidden(reason): - return reason - case let .visible(text, geometry, mode): - return "Showing \(text.count) characters near " + - "(\(Int(geometry.caretRect.minX)), \(Int(geometry.caretRect.minY))) " + - "using \(geometry.caretQuality.label) caret geometry (\(mode.label))." - } - } - var isVisible: Bool { if case .visible = self { return true @@ -163,14 +112,6 @@ enum OverlayState: Equatable { return false } - var visibleText: String? { - guard case let .visible(text, _, _) = self else { - return nil - } - - return text - } - var visibleMode: CompletionRenderMode? { guard case let .visible(_, _, mode) = self else { return nil diff --git a/Cotabby/Models/Suggestion/SuggestionSubsystemContracts.swift b/Cotabby/Models/Suggestion/SuggestionSubsystemContracts.swift index ecb1fc23..5ae1021c 100644 --- a/Cotabby/Models/Suggestion/SuggestionSubsystemContracts.swift +++ b/Cotabby/Models/Suggestion/SuggestionSubsystemContracts.swift @@ -226,7 +226,7 @@ protocol EmojiPickerPanelPresenting: AnyObject { var onSelectIndex: ((Int) -> Void)? { get set } var onClickOutside: (() -> Void)? { get set } - func show(query: String, matches: [EmojiMatch], selectedIndex: Int, caretRect: CGRect, acceptKeyLabel: String?) + func show(query: String, matches: [EmojiMatch], selectedIndex: Int, caretRect: CGRect) func setSelectedIndex(_ index: Int) func hide() } diff --git a/Cotabby/Services/Focus/FocusSnapshotResolver.swift b/Cotabby/Services/Focus/FocusSnapshotResolver.swift index 5ceded5d..c58adbaf 100644 --- a/Cotabby/Services/Focus/FocusSnapshotResolver.swift +++ b/Cotabby/Services/Focus/FocusSnapshotResolver.swift @@ -105,17 +105,6 @@ struct FocusSnapshotResolver { focusChangeSequence: focusChangeSequence ) let resolution = candidateResolution.resolution - let diagnosticCandidate = candidateResolution.diagnosticCandidate - let inspection = FocusInspectionSnapshot( - focusedElementIdentifier: focusedElementIdentifier, - focusedRole: focusedRole, - focusedSubrole: focusedSubrole, - resolvedElementIdentifier: diagnosticCandidate?.elementIdentifier, - resolvedRole: diagnosticCandidate?.role, - resolvedSubrole: diagnosticCandidate?.subrole, - missingCapabilities: resolution.resolvedCandidate == nil - ? resolution.missingCapabilities : [] - ) guard let resolvedCandidate = candidateResolution.resolvedCandidate else { CotabbyLogger.focus.trace("Focus unsupported in \(applicationName): \(resolution.unsupportedReason)") @@ -123,8 +112,7 @@ struct FocusSnapshotResolver { applicationName: applicationName, bundleIdentifier: bundleIdentifier, capability: .unsupported(resolution.unsupportedReason), - context: nil, - inspection: inspection + context: nil ) } @@ -133,8 +121,7 @@ struct FocusSnapshotResolver { applicationName: applicationName, bundleIdentifier: bundleIdentifier, capability: .unsupported("Selection range is unavailable."), - context: nil, - inspection: inspection + context: nil ) } @@ -143,8 +130,7 @@ struct FocusSnapshotResolver { applicationName: applicationName, bundleIdentifier: bundleIdentifier, capability: .unsupported("Selection range is invalid."), - context: nil, - inspection: inspection + context: nil ) } @@ -156,8 +142,7 @@ struct FocusSnapshotResolver { applicationName: applicationName, bundleIdentifier: bundleIdentifier, capability: .unsupported("Selection range exceeds the current field value."), - context: nil, - inspection: inspection + context: nil ) } @@ -208,8 +193,7 @@ struct FocusSnapshotResolver { applicationName: applicationName, bundleIdentifier: bundleIdentifier, capability: .unsupported("Caret bounds are unavailable."), - context: nil, - inspection: inspection + context: nil ) } let caretRect = caret.rect @@ -321,8 +305,7 @@ struct FocusSnapshotResolver { applicationName: applicationName, bundleIdentifier: bundleIdentifier, capability: .blocked("Secure text input is active."), - context: context, - inspection: inspection + context: context ) } @@ -331,8 +314,7 @@ struct FocusSnapshotResolver { applicationName: applicationName, bundleIdentifier: bundleIdentifier, capability: .blocked("Text is currently selected."), - context: context, - inspection: inspection + context: context ) } @@ -340,8 +322,7 @@ struct FocusSnapshotResolver { applicationName: applicationName, bundleIdentifier: bundleIdentifier, capability: .supported, - context: context, - inspection: inspection + context: context ) } @@ -366,12 +347,10 @@ struct FocusSnapshotResolver { deepDescendants: Bool, focusChangeSequence: UInt64 ) -> FocusCandidateResolution { - var bestPartial: (candidate: AXFocusCandidate, evaluation: FocusCapabilityCandidateEvaluation)? - var inspectedCount = 0 + var bestPartial: FocusCapabilityCandidateEvaluation? func winner(in elements: [AXUIElement]) -> FocusCandidateResolution? { for element in elements { - inspectedCount += 1 let candidate = candidateSnapshot( for: element, bundleIdentifier: bundleIdentifier, @@ -383,16 +362,14 @@ struct FocusSnapshotResolver { if evaluation.hasFullCapabilities { return FocusCandidateResolution( resolvedCandidate: candidate, - diagnosticCandidate: candidate, resolution: FocusCapabilityResolution( - selectedEvaluation: evaluation, - inspectedCandidateCount: inspectedCount + selectedEvaluation: evaluation ) ) } - if bestPartial == nil || evaluation.score > bestPartial!.evaluation.score { - bestPartial = (candidate, evaluation) + if bestPartial == nil || evaluation.score > bestPartial!.score { + bestPartial = evaluation } } @@ -423,10 +400,8 @@ struct FocusSnapshotResolver { return FocusCandidateResolution( resolvedCandidate: nil, - diagnosticCandidate: bestPartial?.candidate, resolution: FocusCapabilityResolution( - selectedEvaluation: bestPartial?.evaluation, - inspectedCandidateCount: inspectedCount + selectedEvaluation: bestPartial ) ) } @@ -985,7 +960,6 @@ struct FocusSnapshotResolver { private struct FocusCandidateResolution { let resolvedCandidate: AXFocusCandidate? - let diagnosticCandidate: AXFocusCandidate? let resolution: FocusCapabilityResolution } diff --git a/Cotabby/Services/Focus/FocusTracker.swift b/Cotabby/Services/Focus/FocusTracker.swift index 911744c6..98922099 100644 --- a/Cotabby/Services/Focus/FocusTracker.swift +++ b/Cotabby/Services/Focus/FocusTracker.swift @@ -455,8 +455,7 @@ final class FocusTracker { applicationName: applicationName, bundleIdentifier: bundleIdentifier, capability: capability, - context: nil, - inspection: nil + context: nil ), didChangeFocusedInput: clearFocusedInputSignatureIfNeeded() ) diff --git a/Cotabby/Services/Permission/PermissionManager.swift b/Cotabby/Services/Permission/PermissionManager.swift index 83306b6e..73e464c6 100644 --- a/Cotabby/Services/Permission/PermissionManager.swift +++ b/Cotabby/Services/Permission/PermissionManager.swift @@ -177,20 +177,6 @@ final class PermissionManager: ObservableObject { NSWorkspace.shared.open(permission.settingsURL) } - /// Opens System Settings directly to the Accessibility pane so the user can grant access. - func openAccessibilitySettings() { - openSettings(for: .accessibility) - } - - /// Opens System Settings directly to the Input Monitoring pane so the user can grant access. - func openInputMonitoringSettings() { - openSettings(for: .inputMonitoring) - } - - /// Opens System Settings directly to the Screen Recording pane for visual context capture. - func openScreenRecordingSettings() { - openSettings(for: .screenRecording) - } } extension PermissionManager: SuggestionPermissionProviding { diff --git a/Cotabby/Services/Presentation/EmojiPickerPanelController.swift b/Cotabby/Services/Presentation/EmojiPickerPanelController.swift index ad4e26e5..9b160d1a 100644 --- a/Cotabby/Services/Presentation/EmojiPickerPanelController.swift +++ b/Cotabby/Services/Presentation/EmojiPickerPanelController.swift @@ -55,11 +55,10 @@ final class EmojiPickerPanelController: EmojiPickerPanelPresenting { /// Shows or repositions the panel for the current query and matches. Recomputes the panel frame /// because the match count (and therefore the height) can change between calls. - func show(query: String, matches: [EmojiMatch], selectedIndex: Int, caretRect: CGRect, acceptKeyLabel: String?) { + func show(query: String, matches: [EmojiMatch], selectedIndex: Int, caretRect: CGRect) { model.query = query model.matches = matches model.selectedIndex = selectedIndex - model.acceptKeyLabel = acceptKeyLabel let contentSize = EmojiPickerMetrics.contentSize(matchCount: matches.count) let visibleFrame = targetVisibleFrame(for: caretRect) diff --git a/Cotabby/Services/Presentation/FocusDebugOverlayController.swift b/Cotabby/Services/Presentation/FocusDebugOverlayController.swift index 2046ccdd..24b99516 100644 --- a/Cotabby/Services/Presentation/FocusDebugOverlayController.swift +++ b/Cotabby/Services/Presentation/FocusDebugOverlayController.swift @@ -11,8 +11,6 @@ import SwiftUI /// headless and testable. @MainActor final class FocusDebugOverlayController { - static let launchArgument = CotabbyDebugOptions.launchArgument - static var isEnabled: Bool { CotabbyDebugOptions.isEnabled } diff --git a/Cotabby/Services/Runtime/LlamaRuntimeManager.swift b/Cotabby/Services/Runtime/LlamaRuntimeManager.swift index af02d8e5..3c1fc66c 100644 --- a/Cotabby/Services/Runtime/LlamaRuntimeManager.swift +++ b/Cotabby/Services/Runtime/LlamaRuntimeManager.swift @@ -212,16 +212,6 @@ final class LlamaRuntimeManager: ObservableObject { } } - /// Cancels runtime work and waits until native llama resources are released. - /// Destructive flows such as uninstall need this stronger guarantee before deleting model files - /// that may have been memory-mapped by the runtime. - func stopAndWait() async { - prepareForStop() - await Task.detached { [core] in - core.shutdown() - }.value - } - /// Synchronously releases the llama runtime on the current thread, bounded by `timeoutSeconds`. /// This is the termination-time path: C++ static destructors during `exit()` tear down the Metal /// device, so llama contexts must be released first to avoid `ggml_metal_rsets_free`. Returning diff --git a/Cotabby/Services/Suggestion/SuggestionDebugLogger.swift b/Cotabby/Services/Suggestion/SuggestionDebugLogger.swift index 44c1a1eb..1d236ea9 100644 --- a/Cotabby/Services/Suggestion/SuggestionDebugLogger.swift +++ b/Cotabby/Services/Suggestion/SuggestionDebugLogger.swift @@ -114,21 +114,6 @@ final class SuggestionDebugLogger { } } - /// Produces an escaped single-line preview suitable for compact logs and menu summaries. - static func debugPreview(_ text: String) -> String { - if text.isEmpty { - return "" - } - - let escaped = text.debugDescription - if escaped.count <= 160 { - return escaped - } - - let index = escaped.index(escaped.startIndex, offsetBy: 160) - return "\(escaped[.. FocusCapabilityResolution { - var bestPartial: FocusCapabilityCandidateEvaluation? - - for (index, candidate) in candidates.enumerated() { - let evaluation = evaluate(candidate) - - if evaluation.hasFullCapabilities { - return FocusCapabilityResolution( - selectedEvaluation: evaluation, - inspectedCandidateCount: index + 1 - ) - } - - if shouldReplace(bestPartial, with: evaluation) { - bestPartial = evaluation - } - } - - return FocusCapabilityResolution( - selectedEvaluation: bestPartial, - inspectedCandidateCount: candidates.count - ) - } - /// Computes the capability gaps and heuristic score for a single candidate element. static func evaluate(_ candidate: FocusCapabilityCandidate) -> FocusCapabilityCandidateEvaluation { let missingCapabilities = FocusCapabilityRequirement.allCases.filter { requirement in @@ -109,16 +66,4 @@ enum FocusCapabilityResolver { score: score ) } - - /// Breaks ties between two scored candidates using editability strength and support completeness. - private static func shouldReplace( - _ currentBest: FocusCapabilityCandidateEvaluation?, - with candidate: FocusCapabilityCandidateEvaluation - ) -> Bool { - guard let currentBest else { - return true - } - - return candidate.score > currentBest.score - } } diff --git a/Cotabby/Support/Logging/RequestID.swift b/Cotabby/Support/Logging/RequestID.swift index 23779796..01a8ad12 100644 --- a/Cotabby/Support/Logging/RequestID.swift +++ b/Cotabby/Support/Logging/RequestID.swift @@ -1,5 +1,4 @@ import Foundation -import Logging /// File overview: /// Short, human-readable correlation IDs for inline-completion requests. @@ -45,11 +44,3 @@ enum RequestID { return output } } - -extension Logger.Metadata { - /// Convenience for the common "stamp a `request_id` field on one log line" pattern. - /// Returns a fresh metadata dict so callers can pass `Logger.Metadata.requestID(id)` inline. - static func requestID(_ id: String) -> Logger.Metadata { - ["request_id": .string(id)] - } -} diff --git a/Cotabby/Support/Onboarding/OnboardingFlowSteps.swift b/Cotabby/Support/Onboarding/OnboardingFlowSteps.swift index 98c582ec..0d6e5be3 100644 --- a/Cotabby/Support/Onboarding/OnboardingFlowSteps.swift +++ b/Cotabby/Support/Onboarding/OnboardingFlowSteps.swift @@ -21,16 +21,6 @@ enum WelcomeStep: Int, CaseIterable, Comparable, Sendable { lhs.rawValue < rhs.rawValue } - /// The flow is strictly linear, so navigation is derived from case order instead of hand-wired - /// per step. `nil` past either end keeps the terminal steps terminal. - var next: WelcomeStep? { - WelcomeStep(rawValue: rawValue + 1) - } - - var previous: WelcomeStep? { - WelcomeStep(rawValue: rawValue - 1) - } - /// Number of steps shown in the progress indicator (the middle, non-terminal steps). static let totalProgressSteps = 4 diff --git a/Cotabby/Support/Presentation/SuggestionAnchorCache.swift b/Cotabby/Support/Presentation/SuggestionAnchorCache.swift index 53bd283f..c3efc36a 100644 --- a/Cotabby/Support/Presentation/SuggestionAnchorCache.swift +++ b/Cotabby/Support/Presentation/SuggestionAnchorCache.swift @@ -100,10 +100,6 @@ nonisolated struct SuggestionAnchorCache { return best?.remainder } - mutating func removeAll() { - entries.removeAll() - } - /// `k` such that liveTail == anchorTail + fullText.prefix(k) (tail-bounded comparison), with /// `0 <= k < fullText.count`; nil when the live text is not on the anchor's path. /// diff --git a/Cotabby/Support/Runtime/BundledRuntimeLocator.swift b/Cotabby/Support/Runtime/BundledRuntimeLocator.swift index d4be1adc..655612c5 100644 --- a/Cotabby/Support/Runtime/BundledRuntimeLocator.swift +++ b/Cotabby/Support/Runtime/BundledRuntimeLocator.swift @@ -64,11 +64,6 @@ struct BundledRuntimeLocator { UserDefaults.standard.bool(forKey: lmStudioSourceEnabledKey) } - /// Persists the LM Studio additive-source toggle. - static func setLMStudioSourceEnabled(_ enabled: Bool) { - UserDefaults.standard.set(enabled, forKey: lmStudioSourceEnabledKey) - } - /// The LM Studio models directory only when the user enabled the source *and* it exists on disk. /// A stale toggle (enabled, but LM Studio later uninstalled) resolves to nil so callers never /// scan a missing directory. @@ -135,11 +130,6 @@ struct BundledRuntimeLocator { return results } - /// Finds the first preferred local model that exists and returns the fully resolved runtime asset paths. - func resolve(configuration: LlamaRuntimeConfiguration) throws -> ResolvedLlamaRuntime { - try resolve(configuration: configuration, selectedModelFilename: nil) - } - /// Resolves a specific model when selected explicitly, or the default preferred model order otherwise. func resolve( configuration: LlamaRuntimeConfiguration, diff --git a/Cotabby/Support/Runtime/DecodeStopPolicy.swift b/Cotabby/Support/Runtime/DecodeStopPolicy.swift index 5714aba9..9dee41ce 100644 --- a/Cotabby/Support/Runtime/DecodeStopPolicy.swift +++ b/Cotabby/Support/Runtime/DecodeStopPolicy.swift @@ -42,18 +42,6 @@ nonisolated enum DecodeStopPolicy { return SentenceBoundaryClassifier.endsSentence(accumulated) ? .sentenceBoundary : nil } - static func shouldStop( - accumulated: String, - tokensGenerated: Int, - minimumTokens: Int = 2 - ) -> Bool { - verdict( - accumulated: accumulated, - tokensGenerated: tokensGenerated, - minimumTokens: minimumTokens - ) != nil - } - private static func containsScaffoldingStopMarker(_ text: String) -> Bool { // Cheap pre-filter: every stop marker starts with "<", so most prose never reaches the // per-marker scan. diff --git a/Cotabby/Support/Runtime/HardwareCapabilityProbe.swift b/Cotabby/Support/Runtime/HardwareCapabilityProbe.swift index 55e1846f..e794c4ed 100644 --- a/Cotabby/Support/Runtime/HardwareCapabilityProbe.swift +++ b/Cotabby/Support/Runtime/HardwareCapabilityProbe.swift @@ -1,26 +1,14 @@ import Foundation /// File overview: -/// Reads the host's installed memory and CPU architecture so onboarding can recommend and gate +/// Reads the host's installed memory so onboarding can recommend and gate /// model templates. Kept as a tiny seam (rather than reading `ProcessInfo` inline in the view) so /// `OnboardingTemplateRecommender` stays a pure function of a `HardwareCapability` value and can be /// exercised with synthetic hardware in tests. enum HardwareCapabilityProbe { static func current() -> HardwareCapability { HardwareCapability( - physicalMemoryBytes: ProcessInfo.processInfo.physicalMemory, - isAppleSilicon: isAppleSilicon + physicalMemoryBytes: ProcessInfo.processInfo.physicalMemory ) } - - /// Compile-time architecture is sufficient here: the app ships a single universal binary and we - /// only need to distinguish Apple Silicon from Intel for model-fit messaging. A Rosetta-translated - /// run reporting `x86_64` is an acceptable, conservative misread for that purpose. - private static var isAppleSilicon: Bool { - #if arch(arm64) - return true - #else - return false - #endif - } } diff --git a/Cotabby/Support/Settings/SettingsAttentionEvaluator.swift b/Cotabby/Support/Settings/SettingsAttentionEvaluator.swift index 4fdacba7..c919ee58 100644 --- a/Cotabby/Support/Settings/SettingsAttentionEvaluator.swift +++ b/Cotabby/Support/Settings/SettingsAttentionEvaluator.swift @@ -2,14 +2,13 @@ import Foundation /// File overview: /// Pure decision for which `SettingsCategory` rows in the redesigned Settings sidebar should -/// render an attention dot, and what callout (if any) belongs at the top of that pane. +/// render an attention dot. /// /// Why this lives in `Support/`: /// The legacy settings window puts a single attention banner at the top of one giant form. The /// redesign surfaces attention per pane: a sidebar dot signals "look in here," and the affected -/// pane carries an inline callout next to the controls that fix the underlying problem. Keeping -/// the rule outside the view layer makes it unit-testable without AppKit and keeps the sidebar -/// view free of state-mapping logic. +/// Keeping the rule outside the view layer makes it unit-testable without AppKit and keeps the +/// sidebar view free of state-mapping logic. enum SettingsAttentionEvaluator { /// Snapshot of the app state the evaluator inspects. Keeping inputs as a flat value type means /// callers can build it from whatever observables they hold without dragging the model graph @@ -19,7 +18,6 @@ enum SettingsAttentionEvaluator { let permissionsGranted: Bool let selectedEngine: SuggestionEngineKind let foundationModelAvailable: Bool - let foundationModelMessage: String let llamaRuntimeFailedReason: String? let endpointConfigurationError: String? let endpointConnectionFailedReason: String? @@ -51,32 +49,4 @@ enum SettingsAttentionEvaluator { return categories } - /// Returns the callout that belongs at the top of `category`'s pane, or `nil` if the pane is - /// healthy. Each pane already owns its own callout binding today; centralizing the message - /// keeps the wording consistent across the sidebar dot tooltip and the pane callout, and - /// makes a future copy-edit a one-file change. - static func calloutMessage(for category: SettingsCategory, inputs: Inputs) -> String? { - switch category { - case .permissions: - guard !inputs.permissionsGranted else { return nil } - return "Cotabby needs more access to run. Grant the permissions below to enable autocomplete." - - case .engineAndModel: - switch inputs.selectedEngine { - case .appleIntelligence: - guard !inputs.foundationModelAvailable, - !inputs.foundationModelMessage.isEmpty else { return nil } - return inputs.foundationModelMessage - case .llamaOpenSource: - guard let reason = inputs.llamaRuntimeFailedReason, - !reason.isEmpty else { return nil } - return reason - case .openAICompatible: - return inputs.endpointConfigurationError ?? inputs.endpointConnectionFailedReason - } - - case .home, .general, .appearance, .emoji, .writing, .context, .shortcuts, .apps, .performance, .about: - return nil - } - } } diff --git a/Cotabby/Support/Spelling/SymSpell.swift b/Cotabby/Support/Spelling/SymSpell.swift index bde26541..d67b9c4b 100644 --- a/Cotabby/Support/Spelling/SymSpell.swift +++ b/Cotabby/Support/Spelling/SymSpell.swift @@ -60,7 +60,6 @@ nonisolated final class SymSpell { self.prefixLength = prefixLength } - var isEmpty: Bool { wordsList.isEmpty } var wordCount: Int { wordsList.count } // MARK: - Dictionary loading diff --git a/Cotabby/Support/Suggestion/SuggestionSessionReconciler.swift b/Cotabby/Support/Suggestion/SuggestionSessionReconciler.swift index 7d01f94a..33962d46 100644 --- a/Cotabby/Support/Suggestion/SuggestionSessionReconciler.swift +++ b/Cotabby/Support/Suggestion/SuggestionSessionReconciler.swift @@ -10,7 +10,6 @@ import Foundation struct SuggestionSessionAdvancement: Equatable, Sendable { let stage: String let message: String - let actionSummary: String let exhaustionStage: String let exhaustionMessage: String } @@ -127,7 +126,6 @@ enum SuggestionSessionReconciler { message: reconciledSession.isExhausted ? "The live field state caught up with the fully consumed suggestion." : "The live field state consumed \(advancedBy) additional suggestion characters.", - actionSummary: "Suggestion tail advanced from live editor state.", exhaustionStage: "session-exhausted", exhaustionMessage: "The live field state fully consumed the active suggestion." ) diff --git a/Cotabby/Support/Suggestion/SuggestionTextNormalizer.swift b/Cotabby/Support/Suggestion/SuggestionTextNormalizer.swift index 8a90df82..7c6c5a23 100644 --- a/Cotabby/Support/Suggestion/SuggestionTextNormalizer.swift +++ b/Cotabby/Support/Suggestion/SuggestionTextNormalizer.swift @@ -39,17 +39,6 @@ struct SuggestionNormalizationResult: Equatable, Sendable { } enum SuggestionTextNormalizer { - /// Convenience wrapper returning only the ghost text. Callers that want to know *why* an empty - /// result came back (for diagnostics / on-device decode evaluation) should call - /// `normalizeDetailed` instead, which is the single source of truth this delegates to. - static func normalize( - _ rawSuggestion: String, - for request: SuggestionRequest, - promptEchoCandidates: [String] = [] - ) -> String { - normalizeDetailed(rawSuggestion, for: request, promptEchoCandidates: promptEchoCandidates).text - } - /// Normalizes one raw completion and, when the result is empty, attributes the suppression to a /// specific cause. This is the distinction the logs need to tell "the model produced nothing /// usable" apart from "a safety/echo filter dropped a real completion" — without it, every empty diff --git a/Cotabby/UI/InlineFeatures/EmojiPickerView.swift b/Cotabby/UI/InlineFeatures/EmojiPickerView.swift index 03ac1f7d..fc6eec8f 100644 --- a/Cotabby/UI/InlineFeatures/EmojiPickerView.swift +++ b/Cotabby/UI/InlineFeatures/EmojiPickerView.swift @@ -15,9 +15,6 @@ final class EmojiPickerViewModel: ObservableObject { @Published var query: String = "" @Published var matches: [EmojiMatch] = [] @Published var selectedIndex: Int = 0 - /// Retained for the panel contract, but deliberately not published: the minimal ribbon does not - /// draw a keycap, so updates should not invalidate the SwiftUI view hierarchy. - var acceptKeyLabel: String? } struct EmojiPickerView: View { diff --git a/Cotabby/UI/MenuBar/MenuBarView.swift b/Cotabby/UI/MenuBar/MenuBarView.swift index 9b306fe0..362ed14a 100644 --- a/Cotabby/UI/MenuBar/MenuBarView.swift +++ b/Cotabby/UI/MenuBar/MenuBarView.swift @@ -401,15 +401,6 @@ struct MenuBarView: View { ) } - private var selectedWordCountPresetBinding: Binding { - Binding( - get: { suggestionSettings.selectedWordCountPreset }, - set: { preset in - suggestionSettings.selectWordCountPreset(preset) - } - ) - } - /// One of the curated presets or the user's custom range. Backed by two pieces of state /// (`selectedWordCountPreset` + `isUsingCustomWordCountRange`) so the menu can render and /// mutate both with a single picker. diff --git a/Cotabby/UI/Settings/Panes/WritingPaneView.swift b/Cotabby/UI/Settings/Panes/WritingPaneView.swift index c7762021..0d8b510a 100644 --- a/Cotabby/UI/Settings/Panes/WritingPaneView.swift +++ b/Cotabby/UI/Settings/Panes/WritingPaneView.swift @@ -170,13 +170,6 @@ struct WritingPaneView: View { } } - private var selectedWordCountPresetBinding: Binding { - Binding( - get: { suggestionSettings.selectedWordCountPreset }, - set: { suggestionSettings.selectWordCountPreset($0) } - ) - } - private var autoAcceptTrailingPunctuationBinding: Binding { Binding( get: { suggestionSettings.autoAcceptTrailingPunctuation }, diff --git a/Cotabby/UI/Settings/SettingsContainerView.swift b/Cotabby/UI/Settings/SettingsContainerView.swift index fd1ce87e..e65917cc 100644 --- a/Cotabby/UI/Settings/SettingsContainerView.swift +++ b/Cotabby/UI/Settings/SettingsContainerView.swift @@ -98,7 +98,6 @@ struct SettingsContainerView: View { permissionsGranted: permissionManager.requiredPermissionsGranted, selectedEngine: suggestionSettings.selectedEngine, foundationModelAvailable: foundationModelAvailabilityService.isAvailable, - foundationModelMessage: foundationModelAvailabilityService.userVisibleMessage, llamaRuntimeFailedReason: runtimeModel.state.failureDetail, endpointConfigurationError: endpointConfigurationError, endpointConnectionFailedReason: openAICompatibleConnectionModel.state.failureDetail diff --git a/CotabbyTests/App/InlineFeatures/EmojiPickerControllerTests.swift b/CotabbyTests/App/InlineFeatures/EmojiPickerControllerTests.swift index a3d8eda6..74a860dd 100644 --- a/CotabbyTests/App/InlineFeatures/EmojiPickerControllerTests.swift +++ b/CotabbyTests/App/InlineFeatures/EmojiPickerControllerTests.swift @@ -153,9 +153,7 @@ final class EmojiPickerControllerTests: XCTestCase { glyph: "😄", name: "smiling face", aliases: ["smile"], - keywords: [], - group: "Smileys & Emotion", - unicodeVersion: "6.0" + keywords: [] ) ]) focus = FakeFocus(precedingText: precedingText, focusChangeSequence: 1) @@ -170,8 +168,9 @@ final class EmojiPickerControllerTests: XCTestCase { inputMonitor: monitor, inserter: inserter, isEnabled: { true }, - emojiPreferences: { .default }, - acceptKeyLabel: { "⇥" }, + emojiPreferences: { + EmojiVariantPreferences(skinTone: .neutral, gender: .neutral) + }, emojiUsage: { usageRecorder.snapshot }, recordEmojiUsage: { usageRecorder.recorded.append($0) } ) @@ -231,8 +230,7 @@ private final class FakeFocus: SuggestionFocusProviding { applicationName: "TestApp", bundleIdentifier: "com.test.app", capability: .supported, - context: context, - inspection: nil + context: context ) } } @@ -279,7 +277,7 @@ private final class FakePanel: EmojiPickerPanelPresenting { var onClickOutside: (() -> Void)? private(set) var isHidden = false - func show(query: String, matches: [EmojiMatch], selectedIndex: Int, caretRect: CGRect, acceptKeyLabel: String?) { + func show(query: String, matches: [EmojiMatch], selectedIndex: Int, caretRect: CGRect) { isHidden = false } diff --git a/CotabbyTests/App/Suggestion/SuggestionCoordinatorAcceptanceTests.swift b/CotabbyTests/App/Suggestion/SuggestionCoordinatorAcceptanceTests.swift index 0c40f4af..ded8765e 100644 --- a/CotabbyTests/App/Suggestion/SuggestionCoordinatorAcceptanceTests.swift +++ b/CotabbyTests/App/Suggestion/SuggestionCoordinatorAcceptanceTests.swift @@ -19,7 +19,7 @@ final class SuggestionCoordinatorAcceptanceTests: XCTestCase { } func test_acceptCurrentSuggestionAllowsVisibleSessionWhileDebugStateIsDebouncing() { - let coordinator: SuggestionCoordinator = runOnMainActor { + runOnMainActor { let snapshot = CotabbyTestFixtures.focusedInputSnapshot(precedingText: "Hello") let context = FocusedInputContext(snapshot: snapshot, generation: 7) let interactionState = SuggestionInteractionState() @@ -56,18 +56,6 @@ final class SuggestionCoordinatorAcceptanceTests: XCTestCase { } else { XCTFail("Partial acceptance should leave the remaining suggestion ready.") } - return coordinator - } - - // Acceptance bookkeeping (diagnostics publishes, counters, stage logs) lands one runloop - // hop after the gating tap callback returns (`deferAcceptanceBookkeeping`); drain that hop - // before asserting on it. The session math and overlay state above are still synchronous. - let bookkeepingDrained = expectation(description: "acceptance bookkeeping hop") - DispatchQueue.main.async { bookkeepingDrained.fulfill() } - wait(for: [bookkeepingDrained], timeout: 1.0) - - runOnMainActor { - XCTAssertEqual(coordinator.latestAcceptanceAction, "Accepted next chunk with Tab.") } } @@ -499,8 +487,7 @@ final class SuggestionCoordinatorAcceptanceTests: XCTestCase { applicationName: snapshot.applicationName, bundleIdentifier: snapshot.bundleIdentifier, capability: .supported, - context: snapshot, - inspection: nil + context: snapshot ) let settingsProvider = StubSuggestionSettingsProvider() settingsProvider.snapshot = settingsSnapshot diff --git a/CotabbyTests/App/Suggestion/SuggestionCoordinatorInputTests.swift b/CotabbyTests/App/Suggestion/SuggestionCoordinatorInputTests.swift index d7b2038d..0700c5cc 100644 --- a/CotabbyTests/App/Suggestion/SuggestionCoordinatorInputTests.swift +++ b/CotabbyTests/App/Suggestion/SuggestionCoordinatorInputTests.swift @@ -119,8 +119,7 @@ final class SuggestionCoordinatorInputTests: XCTestCase { applicationName: typedSnapshot.applicationName, bundleIdentifier: typedSnapshot.bundleIdentifier, capability: .supported, - context: typedSnapshot, - inspection: nil + context: typedSnapshot ) await waitUntil("Divergent typing never rescheduled generation") { rig.coordinator.state == .debouncing || rig.engine.requests.count == 1 @@ -185,8 +184,7 @@ final class SuggestionCoordinatorInputTests: XCTestCase { applicationName: typedSnapshot.applicationName, bundleIdentifier: typedSnapshot.bundleIdentifier, capability: .supported, - context: typedSnapshot, - inspection: nil + context: typedSnapshot ) await waitUntil("Keystroke never rescheduled generation") { rig.coordinator.state == .debouncing || !rig.engine.requests.isEmpty @@ -257,8 +255,7 @@ final class SuggestionCoordinatorInputTests: XCTestCase { applicationName: otherApp.applicationName, bundleIdentifier: otherApp.bundleIdentifier, capability: .supported, - context: otherApp, - inspection: nil + context: otherApp ) rig.coordinator.handleSupportedSnapshot(otherFocus) @@ -279,8 +276,7 @@ final class SuggestionCoordinatorInputTests: XCTestCase { applicationName: "TestApp", bundleIdentifier: "com.example.TestApp", capability: .unsupported("No focused text input"), - context: nil, - inspection: nil + context: nil ) rig.coordinator.handleSupportedSnapshot(bareSnapshot) diff --git a/CotabbyTests/App/Suggestion/SuggestionCoordinatorLifecycleTests.swift b/CotabbyTests/App/Suggestion/SuggestionCoordinatorLifecycleTests.swift index 50925370..f2784a7c 100644 --- a/CotabbyTests/App/Suggestion/SuggestionCoordinatorLifecycleTests.swift +++ b/CotabbyTests/App/Suggestion/SuggestionCoordinatorLifecycleTests.swift @@ -60,7 +60,6 @@ final class SuggestionCoordinatorLifecycleTests: XCTestCase { XCTAssertFalse(rig.coordinator.overlayState.isVisible) XCTAssertEqual(rig.coordinator.state, .idle) XCTAssertEqual(rig.visualContext.cancelCalls, [true]) - XCTAssertTrue(rig.coordinator.latestStageMessage.contains("model switching")) } func test_settingsChange_identicalSnapshotIsANoOp() { @@ -89,41 +88,6 @@ final class SuggestionCoordinatorLifecycleTests: XCTestCase { XCTAssertEqual(rig.coordinator.state, .debouncing) } - func test_settingsChange_stageMessagesNameTheEngineOrLengthChange() { - // Use a focus environment that cannot schedule a prediction, so the settings-change - // message is not immediately overwritten by the "debouncing" stage log. - let rig = retained(makeCoordinatorRig(capability: .unsupported("No focused text input"))) - - rig.coordinator.handleSuggestionSettingsChange( - CotabbyTestFixtures.settingsSnapshot(selectedEngine: .appleIntelligence, debounceMilliseconds: 1) - ) - XCTAssertTrue( - rig.coordinator.latestStageMessage.contains(SuggestionEngineKind.appleIntelligence.displayLabel) - ) - - rig.coordinator.handleSuggestionSettingsChange( - CotabbyTestFixtures.settingsSnapshot( - selectedEngine: .appleIntelligence, - selectedWordCountPreset: .twelveToTwenty, - debounceMilliseconds: 1 - ) - ) - XCTAssertTrue( - rig.coordinator.latestStageMessage.contains(SuggestionWordCountPreset.twelveToTwenty.displayLabel) - ) - - // Any other field change gets the generic message. - rig.coordinator.handleSuggestionSettingsChange( - CotabbyTestFixtures.settingsSnapshot( - selectedEngine: .appleIntelligence, - selectedWordCountPreset: .twelveToTwenty, - isClipboardContextEnabled: false, - debounceMilliseconds: 1 - ) - ) - XCTAssertEqual(rig.coordinator.latestStageMessage, "Updated autocomplete settings.") - } - func test_settingsChange_disablingGloballyDoesNotRestartThePipeline() { let rig = retained(makeCoordinatorRig()) diff --git a/CotabbyTests/App/Suggestion/SuggestionCoordinatorPredictionTests.swift b/CotabbyTests/App/Suggestion/SuggestionCoordinatorPredictionTests.swift index 82f9edd7..4d8b91c2 100644 --- a/CotabbyTests/App/Suggestion/SuggestionCoordinatorPredictionTests.swift +++ b/CotabbyTests/App/Suggestion/SuggestionCoordinatorPredictionTests.swift @@ -233,7 +233,6 @@ final class SuggestionCoordinatorPredictionTests: XCTestCase { XCTAssertEqual(rig.inserter.replacements.count, 1) XCTAssertEqual(rig.coordinator.state, .idle) - XCTAssertEqual(rig.coordinator.latestAcceptanceAction?.hasPrefix("Automatically corrected") ?? false, true) XCTAssertTrue(rig.engine.requests.isEmpty) } @@ -293,8 +292,7 @@ final class SuggestionCoordinatorPredictionTests: XCTestCase { applicationName: typedSnapshot.applicationName, bundleIdentifier: typedSnapshot.bundleIdentifier, capability: .supported, - context: typedSnapshot, - inspection: nil + context: typedSnapshot ) rig.coordinator.reconcileActiveSession(with: rig.focusProvider.snapshot) @@ -328,8 +326,7 @@ final class SuggestionCoordinatorPredictionTests: XCTestCase { applicationName: editedSnapshot.applicationName, bundleIdentifier: editedSnapshot.bundleIdentifier, capability: .supported, - context: editedSnapshot, - inspection: nil + context: editedSnapshot ) rig.coordinator.reconcileActiveSession(with: editedFocus) XCTAssertNil(rig.interactionState.activeSession) @@ -428,8 +425,7 @@ final class SuggestionCoordinatorPredictionTests: XCTestCase { applicationName: publishedSnapshot.applicationName, bundleIdentifier: publishedSnapshot.bundleIdentifier, capability: .supported, - context: publishedSnapshot, - inspection: nil + context: publishedSnapshot ) rig.coordinator.reconcileActiveSession(with: rig.focusProvider.snapshot) diff --git a/CotabbyTests/App/Suggestion/SuggestionStateHelperTests.swift b/CotabbyTests/App/Suggestion/SuggestionStateHelperTests.swift index cf35f659..6e16624c 100644 --- a/CotabbyTests/App/Suggestion/SuggestionStateHelperTests.swift +++ b/CotabbyTests/App/Suggestion/SuggestionStateHelperTests.swift @@ -286,7 +286,7 @@ final class SuggestionInteractionStateTests: XCTestCase { } XCTAssertEqual(session.remainingText, " again") XCTAssertEqual(state.activeSession?.remainingText, " again") - XCTAssertEqual(advancement?.actionSummary, "Suggestion tail advanced from live editor state.") + XCTAssertNotNil(advancement) } } diff --git a/CotabbyTests/Models/Context/PermissionAndContextModelTests.swift b/CotabbyTests/Models/Context/PermissionAndContextModelTests.swift index 13da86ea..5453096d 100644 --- a/CotabbyTests/Models/Context/PermissionAndContextModelTests.swift +++ b/CotabbyTests/Models/Context/PermissionAndContextModelTests.swift @@ -67,15 +67,6 @@ final class CotabbyPermissionKindTests: XCTestCase { XCTAssertFalse(CotabbyPermissionKind.inputMonitoring.isOptionalEnhancement) } - func test_guidanceHint_isNonEmptyForAllCases() { - for kind in CotabbyPermissionKind.allCases { - XCTAssertFalse( - kind.guidanceHint.isEmpty, - "\(kind) should have a non-empty guidanceHint" - ) - } - } - func test_id_isTheCaseItself() { for kind in CotabbyPermissionKind.allCases { XCTAssertEqual(kind.id, kind) @@ -93,25 +84,6 @@ final class CotabbyPermissionKindTests: XCTestCase { final class VisualContextModelTests: XCTestCase { - func test_status_detail_returnsNonEmptyStringForEachCase() { - let cases: [VisualContextStatus] = [ - .idle, .capturing, .extractingText, .ready, - .unavailable("no permission"), .failed("timeout") - ] - let details = cases.map(\.detail) - for detail in details { - XCTAssertFalse(detail.isEmpty) - } - // All distinct - XCTAssertEqual(Set(details).count, details.count, "Each status case should have a unique detail") - } - - func test_status_unavailableAndFailed_includeAssociatedReasonInDetail() { - let reason = "Screen recording denied" - XCTAssertEqual(VisualContextStatus.unavailable(reason).detail, reason) - XCTAssertEqual(VisualContextStatus.failed(reason).detail, reason) - } - func test_defaultConfiguration_hasExpectedValues() { let config = VisualContextConfiguration.default XCTAssertEqual(config.snapshotDimension, 700) diff --git a/CotabbyTests/Models/Emoji/EmojiPickerModelsTests.swift b/CotabbyTests/Models/Emoji/EmojiPickerModelsTests.swift index 66bc65d6..49e0a88f 100644 --- a/CotabbyTests/Models/Emoji/EmojiPickerModelsTests.swift +++ b/CotabbyTests/Models/Emoji/EmojiPickerModelsTests.swift @@ -11,7 +11,6 @@ final class EmojiPickerModelsTests: XCTestCase { XCTAssertEqual(match.displayGlyph, "\u{1F44B}") XCTAssertEqual(match.glyph, "\u{1F44B}") XCTAssertEqual(match.id, "\u{1F44B}") - XCTAssertEqual(match.primaryAlias, "wave") } func test_emojiMatch_variantOverrideChangesIdentityButKeepsSourceEntry() { @@ -23,12 +22,6 @@ final class EmojiPickerModelsTests: XCTestCase { XCTAssertEqual(toned.id, "\u{1F44B}\u{1F3FD}") XCTAssertEqual(toned.glyph, "\u{1F44B}\u{1F3FD}") XCTAssertEqual(toned.entry, base) - XCTAssertEqual(toned.primaryAlias, "wave") - } - - func test_emojiMatch_primaryAliasFallsBackToNameWhenEntryHasNoAliases() { - let match = EmojiMatch(entry: makeEntry(aliases: [])) - XCTAssertEqual(match.primaryAlias, "waving hand") } func test_emojiSkinTone_displayNamesArePinnedSettingsCopy() { @@ -67,9 +60,7 @@ final class EmojiPickerModelsTests: XCTestCase { glyph: "\u{1F44B}", name: "waving hand", aliases: aliases, - keywords: ["hello"], - group: "People & Body", - unicodeVersion: "6.0" + keywords: ["hello"] ) } } diff --git a/CotabbyTests/Models/Focus/FocusModelsTests.swift b/CotabbyTests/Models/Focus/FocusModelsTests.swift index d55826d6..3545faed 100644 --- a/CotabbyTests/Models/Focus/FocusModelsTests.swift +++ b/CotabbyTests/Models/Focus/FocusModelsTests.swift @@ -2,8 +2,8 @@ import Foundation import XCTest @testable import Cotabby -/// Tests for the pure focus value models: resolved field style emptiness, menu-facing capability -/// summaries, and the polling-event change label. +/// Tests for the pure focus value models: resolved field style emptiness and the polling-event +/// change label. final class FocusModelsTests: XCTestCase { func test_resolvedFieldStyle_isEmptyWhenNoRenderableAttributeIsPresent() { let empty = ResolvedFieldStyle(fontName: nil, fontPointSize: nil, colorHex: nil) @@ -23,28 +23,6 @@ final class FocusModelsTests: XCTestCase { XCTAssertTrue(sizeOnly.isEmpty) } - func test_focusSnapshot_capabilitySummaryForwardsTheCapabilityReason() { - XCTAssertEqual(FocusSnapshot.inactive.capabilitySummary, "No focused text input") - - let supported = FocusSnapshot( - applicationName: "Notes", - bundleIdentifier: "com.apple.Notes", - capability: .supported, - context: nil, - inspection: nil - ) - XCTAssertEqual(supported.capabilitySummary, "Supported") - - let blocked = FocusSnapshot( - applicationName: "Safari", - bundleIdentifier: "com.apple.Safari", - capability: .blocked("Secure text field"), - context: nil, - inspection: nil - ) - XCTAssertEqual(blocked.capabilitySummary, "Secure text field") - } - func test_focusPollingEvent_changeSummaryLabelsReflectFocusChange() { XCTAssertEqual(makePollingEvent(didChange: true).changeSummary, "changed") XCTAssertEqual(makePollingEvent(didChange: false).changeSummary, "unchanged") diff --git a/CotabbyTests/Models/Runtime/HuggingFaceModelsTests.swift b/CotabbyTests/Models/Runtime/HuggingFaceModelsTests.swift index f68c7ee9..6780dd6c 100644 --- a/CotabbyTests/Models/Runtime/HuggingFaceModelsTests.swift +++ b/CotabbyTests/Models/Runtime/HuggingFaceModelsTests.swift @@ -19,10 +19,8 @@ final class HuggingFaceModelsTests: XCTestCase { let result = try JSONDecoder().decode(HFModelSearchResult.self, from: json) XCTAssertEqual(result.id, "org/model-GGUF") - XCTAssertEqual(result.modelId, "org/model-GGUF") XCTAssertEqual(result.downloads, 1200) XCTAssertEqual(result.likes, 7) - XCTAssertEqual(result.tags, ["gguf", "text-generation"]) } func test_hfRepoFile_idIsThePathWithinTheRepo() { @@ -68,6 +66,6 @@ final class HuggingFaceModelsTests: XCTestCase { } private func makeFile(path: String = "model.gguf", size: Int64 = 1_073_741_824) -> HFRepoFile { - HFRepoFile(path: path, size: size, type: "file") + HFRepoFile(path: path, size: size) } } diff --git a/CotabbyTests/Models/Runtime/RuntimeBootstrapModelTests.swift b/CotabbyTests/Models/Runtime/RuntimeBootstrapModelTests.swift index ae7065a0..dd15319a 100644 --- a/CotabbyTests/Models/Runtime/RuntimeBootstrapModelTests.swift +++ b/CotabbyTests/Models/Runtime/RuntimeBootstrapModelTests.swift @@ -322,24 +322,6 @@ final class RuntimeBootstrapModelTests: XCTestCase { } } - func test_stopAndWait_completesWhenNothingWasLoaded() throws { - let directory = try makeModelDirectory(filenames: ["alpha.gguf"]) - let userDefaults = makeUserDefaults() - let model = runOnMainActor { makeModel(modelDirectory: directory, userDefaults: userDefaults) } - - let done = expectation(description: "stopAndWait returned") - Task { @MainActor in - await model.stopAndWait() - done.fulfill() - } - wait(for: [done], timeout: 10) - - runOnMainActor { - XCTAssertEqual(model.state, .idle) - XCTAssertEqual(model.diagnostics.lastLoadStatus, "Stopped") - } - } - func test_shutdownSync_returnsPromptlyWhenRuntimeNeverLoaded() throws { let directory = try makeModelDirectory(filenames: ["alpha.gguf"]) let userDefaults = makeUserDefaults() diff --git a/CotabbyTests/Models/Settings/SuggestionSettingsModelTests.swift b/CotabbyTests/Models/Settings/SuggestionSettingsModelTests.swift index cf7b1cc8..1ac8a26c 100644 --- a/CotabbyTests/Models/Settings/SuggestionSettingsModelTests.swift +++ b/CotabbyTests/Models/Settings/SuggestionSettingsModelTests.swift @@ -115,7 +115,6 @@ final class SuggestionSettingsModelTests: XCTestCase { XCTAssertEqual(reloaded.acceptanceGranularity, .phrase) XCTAssertTrue(reloaded.suggestInIntegratedTerminals) XCTAssertFalse(reloaded.showIndicator) - XCTAssertFalse(reloaded.showCaretIndicator) XCTAssertFalse(reloaded.showAcceptanceHint) XCTAssertEqual(reloaded.userName, "Ada") XCTAssertEqual(reloaded.extendedContext, "Glossary: cotabby means tea whisk") diff --git a/CotabbyTests/Models/Suggestion/ModelAndPresentationValueTests.swift b/CotabbyTests/Models/Suggestion/ModelAndPresentationValueTests.swift index 2ca33ee0..80d27d7d 100644 --- a/CotabbyTests/Models/Suggestion/ModelAndPresentationValueTests.swift +++ b/CotabbyTests/Models/Suggestion/ModelAndPresentationValueTests.swift @@ -38,20 +38,16 @@ final class SuggestionTextColorCodecTests: XCTestCase { } final class SuggestionModelValueTests: XCTestCase { - func test_wordCountPresetsExposeMatchingPromptInstructionsAndTokenBudgets() { - // Budgets are now derived from upper word count * fallback (English) tokens-per-word, rounded - // up. Per-language scaling lives in SuggestionRequestFactory and is exercised separately. - XCTAssertEqual(SuggestionWordCountPreset.twoToFour.promptInstruction, "Return only the next 2 to 4 words.") - XCTAssertEqual(SuggestionWordCountPreset.twoToFour.suggestedPredictionTokenBudget, 6) - - XCTAssertEqual(SuggestionWordCountPreset.fourToSeven.promptInstruction, "Return only the next 4 to 7 words.") - XCTAssertEqual(SuggestionWordCountPreset.fourToSeven.suggestedPredictionTokenBudget, 10) - - XCTAssertEqual(SuggestionWordCountPreset.sevenToTwelve.promptInstruction, "Return only the next 7 to 12 words.") - XCTAssertEqual(SuggestionWordCountPreset.sevenToTwelve.suggestedPredictionTokenBudget, 16) - - XCTAssertEqual(SuggestionWordCountPreset.twelveToTwenty.promptInstruction, "Return only the next 12 to 20 words.") - XCTAssertEqual(SuggestionWordCountPreset.twelveToTwenty.suggestedPredictionTokenBudget, 26) + func test_wordCountPresetsExposeMatchingRanges() { + XCTAssertEqual( + SuggestionWordCountPreset.allCases.map(\.range), + [ + SuggestionWordRange(lowWords: 2, highWords: 4), + SuggestionWordRange(lowWords: 4, highWords: 7), + SuggestionWordRange(lowWords: 7, highWords: 12), + SuggestionWordRange(lowWords: 12, highWords: 20) + ] + ) } func test_languageCatalog_effectiveTokensPerWord_fallsBackToEnglishForMultiOrUnknown() { @@ -95,7 +91,7 @@ final class SuggestionModelValueTests: XCTestCase { XCTAssertTrue(session.isExhausted) } - func test_overlayStateDetailIncludesTextCountCaretPositionAndQuality() { + func test_overlayStateVisibleExposesRenderMode() { let state = OverlayState.visible( text: "hello", geometry: CotabbyTestFixtures.overlayGeometry( @@ -105,12 +101,7 @@ final class SuggestionModelValueTests: XCTestCase { mode: .inline ) - XCTAssertEqual(state.shortLabel, "Visible") - XCTAssertEqual( - state.detail, - "Showing 5 characters near (12, 40) using derived caret geometry (inline)." - ) - XCTAssertEqual(state.visibleText, "hello") + XCTAssertTrue(state.isVisible) XCTAssertEqual(state.visibleMode, .inline) } @@ -152,41 +143,17 @@ final class SuggestionModelValueTests: XCTestCase { XCTAssertLessThan(layout.topLineCenterOffsetFromCaret, 0) } - func test_suggestionDebugStateLabelsAndDetailsAreStable() { - XCTAssertEqual(SuggestionDebugState.idle.shortLabel, "Idle") - XCTAssertEqual(SuggestionDebugState.disabled("No permission").shortLabel, "Disabled") - XCTAssertEqual(SuggestionDebugState.failed("Runtime failed").detail, "Runtime failed") - XCTAssertEqual(SuggestionDebugState.ready(text: "hello", latency: 0.2).shortLabel, "Ready") - } - - func test_suggestionWordRange_labelsRenderLowAndHighBounds() { + func test_suggestionWordRange_compactLabelRendersLowAndHighBounds() { let range = SuggestionWordRange(lowWords: 5, highWords: 15) - XCTAssertEqual(range.displayLabel, "5-15 words") XCTAssertEqual(range.compactLabel, "5-15 w") } - func test_wordCountPreset_idsAndCompactLabelsStayInSyncWithRawValues() { + func test_wordCountPreset_idsStayInSyncWithRawValues() { XCTAssertEqual( SuggestionWordCountPreset.allCases.map(\.id), ["2-4", "4-7", "7-12", "12-20"] ) - XCTAssertEqual( - SuggestionWordCountPreset.allCases.map(\.compactLabel), - ["2-4 w", "4-7 w", "7-12 w", "12-20 w"] - ) - } - - func test_focusedInputContext_identityPairsElementWithFocusSequence() { - let context = CotabbyTestFixtures.focusedInputContext( - elementIdentifier: "field-a", - focusChangeSequence: 7 - ) - - XCTAssertEqual( - context.identity, - FocusedInputIdentity(elementIdentifier: "field-a", focusChangeSequence: 7) - ) } func test_focusedInputContext_contentSignatureMirrorsSnapshotAndTagsSecureFields() { @@ -244,13 +211,10 @@ final class SuggestionModelValueTests: XCTestCase { XCTAssertEqual(advanced.resolvedFieldStyle, style) } - func test_overlayState_hiddenExposesNoVisibleTextOrMode() { + func test_overlayState_hiddenExposesNoVisibleMode() { let hidden = OverlayState.hidden(reason: "No suggestion buffered") - XCTAssertEqual(hidden.shortLabel, "Hidden") - XCTAssertEqual(hidden.detail, "No suggestion buffered") XCTAssertFalse(hidden.isVisible) - XCTAssertNil(hidden.visibleText) XCTAssertNil(hidden.visibleMode) } @@ -460,7 +424,7 @@ final class GhostTextOpacitySettingsTests: XCTestCase { func test_defaultOpacityIsFullyOpaqueOnFreshInstall() { runOnMainActor { - XCTAssertEqual(makeModel().ghostTextOpacity, SuggestionSettingsModel.defaultGhostTextOpacity) + XCTAssertEqual(makeModel().ghostTextOpacity, SuggestionSettingsStore.defaultGhostTextOpacity) } } @@ -539,7 +503,7 @@ final class GhostTextSizeSettingsTests: XCTestCase { runOnMainActor { XCTAssertEqual( makeModel().ghostTextSizeMultiplier, - SuggestionSettingsModel.defaultGhostTextSizeMultiplier + SuggestionSettingsStore.defaultGhostTextSizeMultiplier ) } } diff --git a/CotabbyTests/Services/Focus/FocusSnapshotResolverLiveTests.swift b/CotabbyTests/Services/Focus/FocusSnapshotResolverLiveTests.swift index 1a8bd3b9..c984313d 100644 --- a/CotabbyTests/Services/Focus/FocusSnapshotResolverLiveTests.swift +++ b/CotabbyTests/Services/Focus/FocusSnapshotResolverLiveTests.swift @@ -145,7 +145,7 @@ final class FocusSnapshotResolverLiveTests: XCTestCase { XCTAssertEqual(snapshot.context?.isSecure, true) } - func test_resolveSnapshot_nonEditableElementIsUnsupportedWithInspection() throws { + func test_resolveSnapshot_nonEditableElementHasDeterministicCapability() throws { _ = try requireElements() let appElement = AXUIElementCreateApplication(ProcessInfo.processInfo.processIdentifier) @@ -170,7 +170,7 @@ final class FocusSnapshotResolverLiveTests: XCTestCase { case .supported: XCTAssertNotNil(snapshot.context) case .blocked, .unsupported: - XCTAssertNotNil(snapshot.inspection) + XCTAssertNil(snapshot.context) } } diff --git a/CotabbyTests/Services/Runtime/OpenAICompatibleAPIClientTests.swift b/CotabbyTests/Services/Runtime/OpenAICompatibleAPIClientTests.swift index 0845a378..4ca79f02 100644 --- a/CotabbyTests/Services/Runtime/OpenAICompatibleAPIClientTests.swift +++ b/CotabbyTests/Services/Runtime/OpenAICompatibleAPIClientTests.swift @@ -8,8 +8,8 @@ import XCTest final class OpenAICompatibleAPIClientTests: XCTestCase { func test_modelSelectionResolver_choosesFirstDiscoveredModelForEmptySelection() { let models = [ - OpenAICompatibleModelOption(id: "alpha", ownedBy: nil), - OpenAICompatibleModelOption(id: "beta", ownedBy: nil) + OpenAICompatibleModelOption(id: "alpha"), + OpenAICompatibleModelOption(id: "beta") ] XCTAssertEqual( @@ -23,8 +23,8 @@ final class OpenAICompatibleAPIClientTests: XCTestCase { func test_modelSelectionResolver_preservesAvailableSelection() { let models = [ - OpenAICompatibleModelOption(id: "alpha", ownedBy: nil), - OpenAICompatibleModelOption(id: "beta", ownedBy: nil) + OpenAICompatibleModelOption(id: "alpha"), + OpenAICompatibleModelOption(id: "beta") ] XCTAssertEqual( @@ -38,8 +38,8 @@ final class OpenAICompatibleAPIClientTests: XCTestCase { func test_modelSelectionResolver_replacesStaleSelectionWithFirstDiscoveredModel() { let models = [ - OpenAICompatibleModelOption(id: "alpha", ownedBy: nil), - OpenAICompatibleModelOption(id: "beta", ownedBy: nil) + OpenAICompatibleModelOption(id: "alpha"), + OpenAICompatibleModelOption(id: "beta") ] XCTAssertEqual( @@ -152,7 +152,6 @@ final class OpenAICompatibleAPIClientTests: XCTestCase { ) XCTAssertEqual(models.map(\.id), ["alpha", "zeta"]) - XCTAssertEqual(models.first?.ownedBy, "library") } func test_completionGeneration_postsStandardPayloadAndAccumulatesSSE() async throws { diff --git a/CotabbyTests/Support/Emoji/EmojiCatalogMatcherTests.swift b/CotabbyTests/Support/Emoji/EmojiCatalogMatcherTests.swift index 008bca06..5ada13a2 100644 --- a/CotabbyTests/Support/Emoji/EmojiCatalogMatcherTests.swift +++ b/CotabbyTests/Support/Emoji/EmojiCatalogMatcherTests.swift @@ -19,9 +19,7 @@ final class EmojiCatalogMatcherTests: XCTestCase { glyph: glyph, name: name, aliases: aliases, - keywords: keywords, - group: "Test", - unicodeVersion: "1.0" + keywords: keywords ) } @@ -171,7 +169,7 @@ final class EmojiCatalogMatcherTests: XCTestCase { func test_bundledCatalogLoadsAndDecodes() { let catalog = EmojiCatalog.bundled() - XCTAssertFalse(catalog.isEmpty, "Bundled emoji.json should be packaged and decode") + XCTAssertFalse(catalog.indexed.isEmpty, "Bundled emoji.json should be packaged and decode") let matcher = EmojiMatcher(catalog: catalog) XCTAssertEqual(matcher.matches(for: "grinning").first?.glyph, "😀") } diff --git a/CotabbyTests/Support/Emoji/EmojiCatalogTests.swift b/CotabbyTests/Support/Emoji/EmojiCatalogTests.swift index da34f34d..7fbb65e6 100644 --- a/CotabbyTests/Support/Emoji/EmojiCatalogTests.swift +++ b/CotabbyTests/Support/Emoji/EmojiCatalogTests.swift @@ -30,8 +30,8 @@ final class EmojiCatalogTests: XCTestCase { let catalog = EmojiCatalog.bundled(in: bundle) - XCTAssertEqual(catalog.count, 2) - XCTAssertFalse(catalog.isEmpty) + XCTAssertEqual(catalog.indexed.count, 2) + XCTAssertFalse(catalog.indexed.isEmpty) // The loaded catalog must resolve stored aliases case-insensitively, as recents/popularity // lookups rely on. XCTAssertEqual(catalog.entry(forAlias: "Grinning")?.glyph, "😀") @@ -43,8 +43,7 @@ final class EmojiCatalogTests: XCTestCase { let catalog = EmojiCatalog.bundled(in: bundle) - XCTAssertTrue(catalog.isEmpty, "A missing resource must disable the picker, not crash") - XCTAssertEqual(catalog.count, 0) + XCTAssertTrue(catalog.indexed.isEmpty, "A missing resource must disable the picker, not crash") } func test_bundled_returnsEmptyCatalogWhenJSONIsMalformed() throws { @@ -52,23 +51,21 @@ final class EmojiCatalogTests: XCTestCase { let catalog = EmojiCatalog.bundled(in: bundle) - XCTAssertTrue(catalog.isEmpty, "An undecodable resource must disable the picker, not crash") + XCTAssertTrue(catalog.indexed.isEmpty, "An undecodable resource must disable the picker, not crash") } - func test_count_reportsNumberOfIndexedEntries() { + func test_indexedEntriesReflectTheInputCatalog() { let entries = [ EmojiEntry( glyph: "🐱", name: "cat face", aliases: ["cat"], - keywords: ["pet"], - group: "Animals & Nature", - unicodeVersion: "6.0" + keywords: ["pet"] ) ] - XCTAssertEqual(EmojiCatalog(entries: entries).count, 1) - XCTAssertEqual(EmojiCatalog(entries: []).count, 0) + XCTAssertEqual(EmojiCatalog(entries: entries).indexed.count, 1) + XCTAssertEqual(EmojiCatalog(entries: []).indexed.count, 0) } /// Builds a flat directory bundle. Foundation treats a plain directory as an unbundled layout diff --git a/CotabbyTests/Support/Emoji/EmojiRecentsTests.swift b/CotabbyTests/Support/Emoji/EmojiRecentsTests.swift index 863409b5..907b52ea 100644 --- a/CotabbyTests/Support/Emoji/EmojiRecentsTests.swift +++ b/CotabbyTests/Support/Emoji/EmojiRecentsTests.swift @@ -5,7 +5,7 @@ import XCTest /// resolved against the catalog. final class EmojiRecentsTests: XCTestCase { private func entry(_ glyph: String, _ alias: String) -> EmojiEntry { - EmojiEntry(glyph: glyph, name: alias, aliases: [alias], keywords: [], group: "Test", unicodeVersion: "1.0") + EmojiEntry(glyph: glyph, name: alias, aliases: [alias], keywords: []) } private func sampleCatalog() -> EmojiCatalog { diff --git a/CotabbyTests/Support/Emoji/EmojiVariantResolverTests.swift b/CotabbyTests/Support/Emoji/EmojiVariantResolverTests.swift index 0696edc4..0ece7aa6 100644 --- a/CotabbyTests/Support/Emoji/EmojiVariantResolverTests.swift +++ b/CotabbyTests/Support/Emoji/EmojiVariantResolverTests.swift @@ -10,9 +10,7 @@ final class EmojiVariantResolverTests: XCTestCase { glyph: glyph, name: name, aliases: aliases, - keywords: [], - group: "People & Body", - unicodeVersion: "1.0" + keywords: [] ) ) } @@ -40,7 +38,10 @@ final class EmojiVariantResolverTests: XCTestCase { func test_skinTone_neutralLeavesGlyphsUnchanged() { let wave = match(glyph: "\u{1F44B}", name: "waving hand", aliases: ["wave"]) - let resolved = EmojiVariantResolver.resolve([wave], preferences: .default) + let resolved = EmojiVariantResolver.resolve( + [wave], + preferences: EmojiVariantPreferences(skinTone: .neutral, gender: .neutral) + ) XCTAssertEqual(resolved.map(\.glyph), ["\u{1F44B}"]) } @@ -71,7 +72,10 @@ final class EmojiVariantResolverTests: XCTestCase { ["\u{1F468}\u{200D}\u{1F692}"] ) XCTAssertEqual( - EmojiVariantResolver.resolve(family, preferences: .default).map(\.glyph), + EmojiVariantResolver.resolve( + family, + preferences: EmojiVariantPreferences(skinTone: .neutral, gender: .neutral) + ).map(\.glyph), ["\u{1F9D1}\u{200D}\u{1F692}"] ) } diff --git a/CotabbyTests/Support/Focus/FocusCapabilityFlickerGateTests.swift b/CotabbyTests/Support/Focus/FocusCapabilityFlickerGateTests.swift index 18fab1d7..893e12e3 100644 --- a/CotabbyTests/Support/Focus/FocusCapabilityFlickerGateTests.swift +++ b/CotabbyTests/Support/Focus/FocusCapabilityFlickerGateTests.swift @@ -83,8 +83,7 @@ final class FocusCapabilityFlickerGateTests: XCTestCase { applicationName: "Calendar", bundleIdentifier: "com.apple.iCal", capability: .supported, - context: nil, - inspection: nil + context: nil ) XCTAssertEqual(gate.evaluate(supportedWithoutContext), .apply) XCTAssertEqual(gate.evaluate(blockedSnapshot(elementID: "field-A")), .apply) @@ -100,8 +99,7 @@ final class FocusCapabilityFlickerGateTests: XCTestCase { applicationName: "Calendar", bundleIdentifier: "com.apple.iCal", capability: .blocked("Text is currently selected."), - context: nil, - inspection: nil + context: nil ) XCTAssertEqual(gate.evaluate(blockedWithoutContext), .apply) } @@ -113,8 +111,7 @@ final class FocusCapabilityFlickerGateTests: XCTestCase { applicationName: "Calendar", bundleIdentifier: "com.apple.iCal", capability: .supported, - context: CotabbyTestFixtures.focusedInputSnapshot(elementIdentifier: elementID), - inspection: nil + context: CotabbyTestFixtures.focusedInputSnapshot(elementIdentifier: elementID) ) } @@ -123,8 +120,7 @@ final class FocusCapabilityFlickerGateTests: XCTestCase { applicationName: "Calendar", bundleIdentifier: "com.apple.iCal", capability: .blocked("Text is currently selected."), - context: CotabbyTestFixtures.focusedInputSnapshot(elementIdentifier: elementID), - inspection: nil + context: CotabbyTestFixtures.focusedInputSnapshot(elementIdentifier: elementID) ) } @@ -133,8 +129,7 @@ final class FocusCapabilityFlickerGateTests: XCTestCase { applicationName: "Finder", bundleIdentifier: "com.apple.finder", capability: .unsupported("No focused text input"), - context: nil, - inspection: nil + context: nil ) } } diff --git a/CotabbyTests/Support/Focus/FocusCapabilityResolverTests.swift b/CotabbyTests/Support/Focus/FocusCapabilityResolverTests.swift index 327c0242..2e34de35 100644 --- a/CotabbyTests/Support/Focus/FocusCapabilityResolverTests.swift +++ b/CotabbyTests/Support/Focus/FocusCapabilityResolverTests.swift @@ -6,39 +6,6 @@ import XCTest /// These tests stay below real AX APIs. The resolver's job is to score already-observed candidate /// facts, so pure value tests give us deterministic coverage of the heuristic policy. final class FocusCapabilityResolverTests: XCTestCase { - func test_resolve_selectsFirstCandidateWithFullCapabilities() { - let partial = CotabbyTestFixtures.focusCapabilityCandidate( - elementIdentifier: "partial", - hasCaretBounds: false - ) - let full = CotabbyTestFixtures.focusCapabilityCandidate( - elementIdentifier: "full", - editableHintScore: 20 - ) - - let resolution = FocusCapabilityResolver.resolve(candidates: [partial, full]) - - XCTAssertEqual(resolution.resolvedCandidate?.elementIdentifier, "full") - XCTAssertEqual(resolution.inspectedCandidateCount, 2) - XCTAssertTrue(resolution.missingCapabilities.isEmpty) - } - - func test_resolve_stopsAtFirstFullCandidateBecauseSearchOrderCarriesMeaning() { - let firstFull = CotabbyTestFixtures.focusCapabilityCandidate( - elementIdentifier: "first", - editableHintScore: 0 - ) - let laterFull = CotabbyTestFixtures.focusCapabilityCandidate( - elementIdentifier: "later", - editableHintScore: 50 - ) - - let resolution = FocusCapabilityResolver.resolve(candidates: [firstFull, laterFull]) - - XCTAssertEqual(resolution.resolvedCandidate?.elementIdentifier, "first") - XCTAssertEqual(resolution.inspectedCandidateCount, 1) - } - func test_evaluate_reportsMissingCapabilitiesInStableRequirementOrder() { let candidate = CotabbyTestFixtures.focusCapabilityCandidate( hasStrongEditabilitySignal: false, @@ -71,32 +38,19 @@ final class FocusCapabilityResolverTests: XCTestCase { XCTAssertEqual(evaluation.score, 307) } - func test_resolve_returnsBestPartialCandidateForDiagnostics() { - let weakerPartial = CotabbyTestFixtures.focusCapabilityCandidate( - elementIdentifier: "weaker", - hasSelectionRange: false, - hasCaretBounds: false - ) - let strongerPartial = CotabbyTestFixtures.focusCapabilityCandidate( - elementIdentifier: "stronger", + func test_resolution_usesTheFirstMissingCapabilityAsItsUnsupportedReason() { + let candidate = CotabbyTestFixtures.focusCapabilityCandidate( hasStrongEditabilitySignal: false ) + let evaluation = FocusCapabilityResolver.evaluate(candidate) + let resolution = FocusCapabilityResolution(selectedEvaluation: evaluation) - let resolution = FocusCapabilityResolver.resolve( - candidates: [weakerPartial, strongerPartial] - ) - - XCTAssertNil(resolution.resolvedCandidate) - XCTAssertEqual(resolution.bestDiagnosticCandidate?.elementIdentifier, "stronger") XCTAssertEqual(resolution.unsupportedReason, "Missing editable target.") } - func test_resolve_emptyCandidateListReportsGenericUnsupportedReason() { - let resolution = FocusCapabilityResolver.resolve(candidates: []) + func test_resolution_withoutACandidateReportsGenericUnsupportedReason() { + let resolution = FocusCapabilityResolution(selectedEvaluation: nil) - XCTAssertNil(resolution.resolvedCandidate) - XCTAssertNil(resolution.bestDiagnosticCandidate) - XCTAssertEqual(resolution.missingCapabilities, FocusCapabilityRequirement.allCases) XCTAssertEqual( resolution.unsupportedReason, "No nearby text target exposed the required Accessibility capabilities." diff --git a/CotabbyTests/Support/Focus/TerminalAppDetectorTests.swift b/CotabbyTests/Support/Focus/TerminalAppDetectorTests.swift index 5a02311c..5ba294ef 100644 --- a/CotabbyTests/Support/Focus/TerminalAppDetectorTests.swift +++ b/CotabbyTests/Support/Focus/TerminalAppDetectorTests.swift @@ -88,8 +88,7 @@ final class TerminalAppDetectorTests: XCTestCase { applicationName: "Terminal", bundleIdentifier: "com.apple.Terminal", capability: .supported, - context: nil, - inspection: nil + context: nil ) let reason = SuggestionAvailabilityEvaluator.disabledReason( @@ -106,8 +105,7 @@ final class TerminalAppDetectorTests: XCTestCase { applicationName: "Safari", bundleIdentifier: "com.apple.Safari", capability: .supported, - context: nil, - inspection: nil + context: nil ) let reason = SuggestionAvailabilityEvaluator.disabledReason( @@ -124,8 +122,7 @@ final class TerminalAppDetectorTests: XCTestCase { applicationName: "iTerm2", bundleIdentifier: "com.googlecode.iterm2", capability: .supported, - context: nil, - inspection: nil + context: nil ) XCTAssertFalse( @@ -142,8 +139,7 @@ final class TerminalAppDetectorTests: XCTestCase { applicationName: "Terminal", bundleIdentifier: "com.apple.Terminal", capability: .supported, - context: nil, - inspection: nil + context: nil ) let reason = SuggestionAvailabilityEvaluator.disabledReason( diff --git a/CotabbyTests/Support/Logging/RequestIDTests.swift b/CotabbyTests/Support/Logging/RequestIDTests.swift index 38cc682d..a1ec5407 100644 --- a/CotabbyTests/Support/Logging/RequestIDTests.swift +++ b/CotabbyTests/Support/Logging/RequestIDTests.swift @@ -38,13 +38,4 @@ final class RequestIDTests: XCTestCase { XCTAssertEqual(ids.count, 1_000) } - func test_metadataRequestID_buildsTheSingleStampedField() { - // OSLogHandler's metadata property gives us a typed `Logger.Metadata` context without the - // test target needing its own swift-log dependency. - var handler = OSLogHandler(label: "com.cotabby.test-request-id") - handler.metadata = .requestID("req_a3f9k2lq") - - XCTAssertEqual(handler.metadata.count, 1) - XCTAssertEqual(handler.metadata["request_id"], .string("req_a3f9k2lq")) - } } diff --git a/CotabbyTests/Support/Logging/SuggestionDebugLoggerTests.swift b/CotabbyTests/Support/Logging/SuggestionDebugLoggerTests.swift index ba9cbfb7..5c805624 100644 --- a/CotabbyTests/Support/Logging/SuggestionDebugLoggerTests.swift +++ b/CotabbyTests/Support/Logging/SuggestionDebugLoggerTests.swift @@ -1,9 +1,7 @@ import XCTest @testable import Cotabby -/// Tests for `SuggestionDebugLogger`: the escaped single-line preview used by compact logs and -/// menu summaries, plus the instance-level console block formatting (safe to instantiate since -/// the type grew its `nonisolated deinit`). +/// Tests for `SuggestionDebugLogger`'s console block routing and formatting. @MainActor final class SuggestionDebugLoggerTests: XCTestCase { // MARK: - Instance logging paths @@ -38,40 +36,4 @@ final class SuggestionDebugLoggerTests: XCTestCase { logger.logStage("failed", workID: 9, generation: 1, message: "boom") } - func test_debugPreview_emptyTextReturnsPlaceholder() async { - XCTAssertEqual(SuggestionDebugLogger.debugPreview(""), "") - } - - func test_debugPreview_shortTextReturnsQuotedEscapedDescription() async { - XCTAssertEqual(SuggestionDebugLogger.debugPreview("hello"), "\"hello\"") - } - - func test_debugPreview_escapesControlCharactersIntoOneLine() async { - let preview = SuggestionDebugLogger.debugPreview("line1\nline2\ttabbed") - - XCTAssertEqual(preview, "\"line1\\nline2\\ttabbed\"") - XCTAssertFalse(preview.contains("\n"), "A preview must never break the log line it is embedded in") - } - - func test_debugPreview_truncatesLongEscapedTextWithEllipsis() async { - let text = String(repeating: "a", count: 200) - - let preview = SuggestionDebugLogger.debugPreview(text) - - // The escaped form is 202 characters (two quotes), so the preview keeps the first 160 - // escaped characters and appends the ellipsis. - XCTAssertEqual(preview, "\"" + String(repeating: "a", count: 159) + "...") - XCTAssertEqual(preview.count, 163) - } - - func test_debugPreview_keepsTextWhoseEscapedFormFitsTheLimit() async { - // 158 characters plus the surrounding quotes is exactly 160 escaped characters: the - // boundary case must pass through untouched. - let text = String(repeating: "a", count: 158) - - let preview = SuggestionDebugLogger.debugPreview(text) - - XCTAssertEqual(preview, text.debugDescription) - XCTAssertFalse(preview.hasSuffix("...")) - } } diff --git a/CotabbyTests/Support/Onboarding/OnboardingFlowStepTests.swift b/CotabbyTests/Support/Onboarding/OnboardingFlowStepTests.swift index ae3e1b9c..b648c8b4 100644 --- a/CotabbyTests/Support/Onboarding/OnboardingFlowStepTests.swift +++ b/CotabbyTests/Support/Onboarding/OnboardingFlowStepTests.swift @@ -1,8 +1,8 @@ import XCTest @testable import Cotabby -/// Tests for the pure onboarding flow model: step ordering, linear navigation, progress indices, -/// and window sizing. `WelcomeCoordinator` persists raw values as the wizard's resume point, so +/// Tests for the pure onboarding flow model: step ordering, progress indices, and window sizing. +/// `WelcomeCoordinator` persists raw values as the wizard's resume point, so /// several of these pin the numbering scheme itself; if one fails because steps were reordered or /// inserted, the coordinator's progress key must move to a fresh name at the same time. final class OnboardingFlowStepTests: XCTestCase { @@ -23,20 +23,6 @@ final class OnboardingFlowStepTests: XCTestCase { XCTAssertFalse(WelcomeStep.done < .welcome) } - func test_navigation_isLinearAndTerminalAtBothEnds() { - // Every step's next is the following case; every step's previous is the prior one. - for (index, step) in WelcomeStep.allCases.enumerated() { - if index + 1 < WelcomeStep.allCases.count { - XCTAssertEqual(step.next, WelcomeStep.allCases[index + 1]) - } - if index > 0 { - XCTAssertEqual(step.previous, WelcomeStep.allCases[index - 1]) - } - } - XCTAssertNil(WelcomeStep.welcome.previous) - XCTAssertNil(WelcomeStep.done.next) - } - func test_progressIndices_coverOneThroughTotalExactlyOnce() { let indices = WelcomeStep.allCases.compactMap(\.progressIndex) diff --git a/CotabbyTests/Support/Onboarding/OnboardingTemplateRecommenderTests.swift b/CotabbyTests/Support/Onboarding/OnboardingTemplateRecommenderTests.swift index de8eafc3..8606b6b1 100644 --- a/CotabbyTests/Support/Onboarding/OnboardingTemplateRecommenderTests.swift +++ b/CotabbyTests/Support/Onboarding/OnboardingTemplateRecommenderTests.swift @@ -7,10 +7,9 @@ import XCTest /// Apple Intelligence downloads nothing, Open Source maps each tier to its local GGUF. Each case /// pins one product decision so a future tweak has to update an obvious assertion. final class OnboardingTemplateRecommenderTests: XCTestCase { - private func hardware(gigabytes: Double, appleSilicon: Bool = true) -> HardwareCapability { + private func hardware(gigabytes: Double) -> HardwareCapability { HardwareCapability( - physicalMemoryBytes: UInt64(gigabytes * 1_073_741_824), - isAppleSilicon: appleSilicon + physicalMemoryBytes: UInt64(gigabytes * 1_073_741_824) ) } diff --git a/CotabbyTests/Support/Presentation/SuggestionAnchorCacheTests.swift b/CotabbyTests/Support/Presentation/SuggestionAnchorCacheTests.swift index af334a27..ecca8847 100644 --- a/CotabbyTests/Support/Presentation/SuggestionAnchorCacheTests.swift +++ b/CotabbyTests/Support/Presentation/SuggestionAnchorCacheTests.swift @@ -86,10 +86,4 @@ final class SuggestionAnchorCacheTests: XCTestCase { XCTAssertEqual(cache.remainder(identityKey: 1, precedingText: longPrefix + " and"), " more") } - func testRemoveAllEmptiesTheCache() { - var cache = makeCache() - cache.record(identityKey: 1, precedingText: "Hello", fullText: " world") - cache.removeAll() - XCTAssertNil(cache.remainder(identityKey: 1, precedingText: "Hello")) - } } diff --git a/CotabbyTests/Support/Runtime/BundledRuntimeLocatorTests.swift b/CotabbyTests/Support/Runtime/BundledRuntimeLocatorTests.swift index 31b36bef..15596f76 100644 --- a/CotabbyTests/Support/Runtime/BundledRuntimeLocatorTests.swift +++ b/CotabbyTests/Support/Runtime/BundledRuntimeLocatorTests.swift @@ -21,7 +21,7 @@ final class BundledRuntimeLocatorTests: XCTestCase { let config = makeConfig(runtimePath: dir.path, preferred: ["beta.gguf"]) let locator = BundledRuntimeLocator() - let resolved = try locator.resolve(configuration: config) + let resolved = try locator.resolve(configuration: config, selectedModelFilename: nil) XCTAssertEqual(resolved.modelFileURL.lastPathComponent, "beta.gguf") } @@ -32,7 +32,7 @@ final class BundledRuntimeLocatorTests: XCTestCase { let config = makeConfig(runtimePath: dir.path, preferred: ["nonexistent.gguf"]) let locator = BundledRuntimeLocator() - let resolved = try locator.resolve(configuration: config) + let resolved = try locator.resolve(configuration: config, selectedModelFilename: nil) XCTAssertEqual(resolved.modelFileURL.lastPathComponent, "alpha.gguf") } @@ -72,7 +72,7 @@ final class BundledRuntimeLocatorTests: XCTestCase { ) let locator = BundledRuntimeLocator() - XCTAssertThrowsError(try locator.resolve(configuration: config)) { error in + XCTAssertThrowsError(try locator.resolve(configuration: config, selectedModelFilename: nil)) { error in guard case BundledRuntimeLocatorError.runtimeDirectoryMissing = error else { XCTFail("Expected runtimeDirectoryMissing, got \(error)") return @@ -85,7 +85,7 @@ final class BundledRuntimeLocatorTests: XCTestCase { let config = makeConfig(runtimePath: dir.path, preferred: []) let locator = BundledRuntimeLocator() - XCTAssertThrowsError(try locator.resolve(configuration: config)) { error in + XCTAssertThrowsError(try locator.resolve(configuration: config, selectedModelFilename: nil)) { error in guard case BundledRuntimeLocatorError.modelMissing = error else { XCTFail("Expected modelMissing, got \(error)") return @@ -154,7 +154,7 @@ final class BundledRuntimeLocatorTests: XCTestCase { let config = makeConfig(runtimePath: dir.path, preferred: []) let locator = BundledRuntimeLocator() - let resolved = try locator.resolve(configuration: config) + let resolved = try locator.resolve(configuration: config, selectedModelFilename: nil) XCTAssertTrue( resolved.runtimeDirectoryURL.path.hasPrefix(dir.path), "Should resolve from the explicit runtime directory" @@ -255,10 +255,10 @@ final class BundledRuntimeLocatorTests: XCTestCase { let original = UserDefaults.standard.object(forKey: key) defer { UserDefaults.standard.set(original, forKey: key) } - BundledRuntimeLocator.setLMStudioSourceEnabled(true) + UserDefaults.standard.set(true, forKey: key) XCTAssertTrue(BundledRuntimeLocator.isLMStudioSourceEnabled()) - BundledRuntimeLocator.setLMStudioSourceEnabled(false) + UserDefaults.standard.set(false, forKey: key) XCTAssertFalse(BundledRuntimeLocator.isLMStudioSourceEnabled()) // Disabled means no LM Studio directory is ever returned, even if it exists on disk. XCTAssertNil(BundledRuntimeLocator.enabledLMStudioModelsDirectory()) diff --git a/CotabbyTests/Support/Runtime/DecodeStopPolicyTests.swift b/CotabbyTests/Support/Runtime/DecodeStopPolicyTests.swift index 1d00c2ec..c181155a 100644 --- a/CotabbyTests/Support/Runtime/DecodeStopPolicyTests.swift +++ b/CotabbyTests/Support/Runtime/DecodeStopPolicyTests.swift @@ -7,47 +7,47 @@ import XCTest final class DecodeStopPolicyTests: XCTestCase { func testBelowMinimumTokensDoesNotStop() { // Even a complete sentence should not stop before the minimum token count. - XCTAssertFalse(DecodeStopPolicy.shouldStop(accumulated: "Hello there.", tokensGenerated: 1)) + XCTAssertNil(DecodeStopPolicy.verdict(accumulated: "Hello there.", tokensGenerated: 1)) } func testStopsAtSentenceEnd() { - XCTAssertTrue(DecodeStopPolicy.shouldStop(accumulated: "Hello there.", tokensGenerated: 3)) + XCTAssertNotNil(DecodeStopPolicy.verdict(accumulated: "Hello there.", tokensGenerated: 3)) } func testStopsOnQuestionMark() { - XCTAssertTrue(DecodeStopPolicy.shouldStop(accumulated: "Are you sure?", tokensGenerated: 3)) + XCTAssertNotNil(DecodeStopPolicy.verdict(accumulated: "Are you sure?", tokensGenerated: 3)) } func testStopsOnExclamation() { - XCTAssertTrue(DecodeStopPolicy.shouldStop(accumulated: "That works!", tokensGenerated: 2)) + XCTAssertNotNil(DecodeStopPolicy.verdict(accumulated: "That works!", tokensGenerated: 2)) } func testStopsWithTrailingWhitespaceAfterPeriod() { - XCTAssertTrue(DecodeStopPolicy.shouldStop(accumulated: "All done. ", tokensGenerated: 3)) + XCTAssertNotNil(DecodeStopPolicy.verdict(accumulated: "All done. ", tokensGenerated: 3)) } func testDoesNotStopOnAbbreviation() { - XCTAssertFalse(DecodeStopPolicy.shouldStop(accumulated: "Please meet Dr.", tokensGenerated: 3)) + XCTAssertNil(DecodeStopPolicy.verdict(accumulated: "Please meet Dr.", tokensGenerated: 3)) } func testDoesNotStopOnInitialism() { - XCTAssertFalse(DecodeStopPolicy.shouldStop(accumulated: "Made in the U.S.", tokensGenerated: 4)) + XCTAssertNil(DecodeStopPolicy.verdict(accumulated: "Made in the U.S.", tokensGenerated: 4)) } func testDoesNotStopMidDecimal() { - XCTAssertFalse(DecodeStopPolicy.shouldStop(accumulated: "Pi is about 3.14", tokensGenerated: 4)) + XCTAssertNil(DecodeStopPolicy.verdict(accumulated: "Pi is about 3.14", tokensGenerated: 4)) } func testDoesNotStopWithoutTerminator() { - XCTAssertFalse(DecodeStopPolicy.shouldStop(accumulated: "still going strong", tokensGenerated: 5)) + XCTAssertNil(DecodeStopPolicy.verdict(accumulated: "still going strong", tokensGenerated: 5)) } func testRespectsCustomMinimum() { - XCTAssertFalse( - DecodeStopPolicy.shouldStop(accumulated: "Hi.", tokensGenerated: 2, minimumTokens: 3) + XCTAssertNil( + DecodeStopPolicy.verdict(accumulated: "Hi.", tokensGenerated: 2, minimumTokens: 3) ) - XCTAssertTrue( - DecodeStopPolicy.shouldStop(accumulated: "Hi.", tokensGenerated: 3, minimumTokens: 3) + XCTAssertNotNil( + DecodeStopPolicy.verdict(accumulated: "Hi.", tokensGenerated: 3, minimumTokens: 3) ) } diff --git a/CotabbyTests/Support/Runtime/HardwareCapabilityProbeTests.swift b/CotabbyTests/Support/Runtime/HardwareCapabilityProbeTests.swift index f335a4d8..c89f1467 100644 --- a/CotabbyTests/Support/Runtime/HardwareCapabilityProbeTests.swift +++ b/CotabbyTests/Support/Runtime/HardwareCapabilityProbeTests.swift @@ -2,8 +2,8 @@ import XCTest @testable import Cotabby /// The probe is the seam between real host hardware and the pure -/// `OnboardingTemplateRecommender`, so these tests pin its two outputs to the -/// authoritative sources they must mirror. +/// `OnboardingTemplateRecommender`, so these tests pin its memory output to the authoritative +/// source it must mirror. final class HardwareCapabilityProbeTests: XCTestCase { func test_current_reportsHostPhysicalMemoryExactly() { let capability = HardwareCapabilityProbe.current() @@ -25,15 +25,4 @@ final class HardwareCapabilityProbeTests: XCTestCase { ) } - func test_current_reportsCompileTimeArchitecture() { - let capability = HardwareCapabilityProbe.current() - - // The probe intentionally answers from compile-time architecture; the test target builds - // for the same architecture, so the same condition is the ground truth here. - #if arch(arm64) - XCTAssertTrue(capability.isAppleSilicon) - #else - XCTAssertFalse(capability.isAppleSilicon) - #endif - } } diff --git a/CotabbyTests/Support/Settings/SettingsAttentionEvaluatorTests.swift b/CotabbyTests/Support/Settings/SettingsAttentionEvaluatorTests.swift index a5e0cbff..c929d6a4 100644 --- a/CotabbyTests/Support/Settings/SettingsAttentionEvaluatorTests.swift +++ b/CotabbyTests/Support/Settings/SettingsAttentionEvaluatorTests.swift @@ -1,15 +1,14 @@ import XCTest @testable import Cotabby -/// Tests for the pure attention-decision rule that drives sidebar dots and per-pane callouts in -/// the redesigned Settings window. Each test pins one real-world condition so a future change to +/// Tests for the pure attention-decision rule that drives sidebar dots in the redesigned Settings +/// window. Each test pins one real-world condition so a future change to /// the rule has to update an obvious assertion rather than slip through. final class SettingsAttentionEvaluatorTests: XCTestCase { private func makeInputs( permissionsGranted: Bool = true, selectedEngine: SuggestionEngineKind = .llamaOpenSource, foundationModelAvailable: Bool = true, - foundationModelMessage: String = "Apple Intelligence is available.", llamaRuntimeFailedReason: String? = nil, endpointConfigurationError: String? = nil, endpointConnectionFailedReason: String? = nil @@ -18,7 +17,6 @@ final class SettingsAttentionEvaluatorTests: XCTestCase { permissionsGranted: permissionsGranted, selectedEngine: selectedEngine, foundationModelAvailable: foundationModelAvailable, - foundationModelMessage: foundationModelMessage, llamaRuntimeFailedReason: llamaRuntimeFailedReason, endpointConfigurationError: endpointConfigurationError, endpointConnectionFailedReason: endpointConnectionFailedReason @@ -43,8 +41,7 @@ final class SettingsAttentionEvaluatorTests: XCTestCase { let categories = SettingsAttentionEvaluator.categoriesNeedingAttention( makeInputs( selectedEngine: .appleIntelligence, - foundationModelAvailable: false, - foundationModelMessage: "Apple Intelligence is turned off in System Settings." + foundationModelAvailable: false ) ) XCTAssertEqual(categories, [.engineAndModel]) @@ -77,57 +74,5 @@ final class SettingsAttentionEvaluatorTests: XCTestCase { endpointConfigurationError: "Choose a model." ) XCTAssertEqual(SettingsAttentionEvaluator.categoriesNeedingAttention(inputs), [.engineAndModel]) - XCTAssertEqual( - SettingsAttentionEvaluator.calloutMessage(for: .engineAndModel, inputs: inputs), - "Choose a model." - ) - } - - func test_callout_permissions_returnsActionableMessage() { - let message = SettingsAttentionEvaluator.calloutMessage( - for: .permissions, - inputs: makeInputs(permissionsGranted: false) - ) - XCTAssertNotNil(message) - XCTAssertTrue(message?.contains("more access") ?? false) - } - - func test_callout_engineAndModel_appleIntelligence_echoesAvailabilityMessage() { - let inputs = makeInputs( - selectedEngine: .appleIntelligence, - foundationModelAvailable: false, - foundationModelMessage: "This Mac is not eligible for Apple Intelligence." - ) - let message = SettingsAttentionEvaluator.calloutMessage(for: .engineAndModel, inputs: inputs) - XCTAssertEqual(message, "This Mac is not eligible for Apple Intelligence.") - } - - func test_callout_engineAndModel_openSource_echoesRuntimeFailureReason() { - let inputs = makeInputs( - selectedEngine: .llamaOpenSource, - llamaRuntimeFailedReason: "Couldn't open the GGUF file." - ) - let message = SettingsAttentionEvaluator.calloutMessage(for: .engineAndModel, inputs: inputs) - XCTAssertEqual(message, "Couldn't open the GGUF file.") - } - - func test_callout_engineAndModel_healthyEngine_isNil() { - let inputs = makeInputs( - selectedEngine: .appleIntelligence, - foundationModelAvailable: true - ) - XCTAssertNil( - SettingsAttentionEvaluator.calloutMessage(for: .engineAndModel, inputs: inputs) - ) - } - - func test_callout_paneWithoutAttention_isNil() { - let inputs = makeInputs() - for category in [SettingsCategory.general, .writing, .shortcuts, .apps, .about] { - XCTAssertNil( - SettingsAttentionEvaluator.calloutMessage(for: category, inputs: inputs), - "\(category) should never carry a callout" - ) - } } } diff --git a/CotabbyTests/Support/Spelling/SymSpellTests.swift b/CotabbyTests/Support/Spelling/SymSpellTests.swift index 783b1c77..e8da8c7d 100644 --- a/CotabbyTests/Support/Spelling/SymSpellTests.swift +++ b/CotabbyTests/Support/Spelling/SymSpellTests.swift @@ -55,7 +55,7 @@ final class SymSpellTests: XCTestCase { func test_emptyDictionaryReturnsNil() { let symSpell = SymSpell() - XCTAssertTrue(symSpell.isEmpty) + XCTAssertEqual(symSpell.wordCount, 0) XCTAssertNil(symSpell.bestSuggestion(for: "teh")) } diff --git a/CotabbyTests/Support/Suggestion/FocusSnapshotExternalApplicationIdentityTests.swift b/CotabbyTests/Support/Suggestion/FocusSnapshotExternalApplicationIdentityTests.swift index 57ec10da..b3c650b8 100644 --- a/CotabbyTests/Support/Suggestion/FocusSnapshotExternalApplicationIdentityTests.swift +++ b/CotabbyTests/Support/Suggestion/FocusSnapshotExternalApplicationIdentityTests.swift @@ -12,8 +12,7 @@ final class FocusSnapshotExternalApplicationIdentityTests: XCTestCase { applicationName: "Google Chrome", bundleIdentifier: "com.google.Chrome", capability: .supported, - context: nil, - inspection: nil + context: nil ) XCTAssertEqual( @@ -30,8 +29,7 @@ final class FocusSnapshotExternalApplicationIdentityTests: XCTestCase { applicationName: "Cotabby", bundleIdentifier: "com.jacobfu.tabby", capability: .blocked("Cotabby is focused."), - context: nil, - inspection: nil + context: nil ) XCTAssertNil( @@ -44,8 +42,7 @@ final class FocusSnapshotExternalApplicationIdentityTests: XCTestCase { applicationName: "Unknown", bundleIdentifier: nil, capability: .unsupported("No active application."), - context: nil, - inspection: nil + context: nil ) XCTAssertNil( diff --git a/CotabbyTests/Support/Suggestion/SuggestionAvailabilityEvaluatorTests.swift b/CotabbyTests/Support/Suggestion/SuggestionAvailabilityEvaluatorTests.swift index 4bf7d114..fadf050c 100644 --- a/CotabbyTests/Support/Suggestion/SuggestionAvailabilityEvaluatorTests.swift +++ b/CotabbyTests/Support/Suggestion/SuggestionAvailabilityEvaluatorTests.swift @@ -9,9 +9,8 @@ import XCTest /// that contract in. final class SuggestionAvailabilityEvaluatorTests: XCTestCase { - // Build a FocusSnapshot with only the capability varied — none of the gate - // logic we're testing here touches `context` or `inspection`, so leaving - // them nil keeps each test focused on the single axis under test. + // Build a FocusSnapshot with only the capability varied. Leaving context nil keeps each test + // focused on the single gate axis under test. private func makeSnapshot( applicationName: String = "TestApp", bundleIdentifier: String? = "app.test", @@ -21,8 +20,7 @@ final class SuggestionAvailabilityEvaluatorTests: XCTestCase { applicationName: applicationName, bundleIdentifier: bundleIdentifier, capability: capability, - context: nil, - inspection: nil + context: nil ) } @@ -56,8 +54,7 @@ final class SuggestionAvailabilityEvaluatorTests: XCTestCase { applicationName: "TestApp", bundleIdentifier: "app.test", capability: .supported, - context: context, - inspection: nil + context: context ) } @@ -75,8 +72,7 @@ final class SuggestionAvailabilityEvaluatorTests: XCTestCase { applicationName: "Code", bundleIdentifier: "com.microsoft.VSCode", capability: .supported, - context: context, - inspection: nil + context: context ) } diff --git a/CotabbyTests/Support/Suggestion/SuggestionTextNormalizerTests.swift b/CotabbyTests/Support/Suggestion/SuggestionTextNormalizerTests.swift index 9fb33ee7..31702175 100644 --- a/CotabbyTests/Support/Suggestion/SuggestionTextNormalizerTests.swift +++ b/CotabbyTests/Support/Suggestion/SuggestionTextNormalizerTests.swift @@ -380,3 +380,20 @@ final class SuggestionTextNormalizerTests: XCTestCase { XCTAssertEqual(result.suppression, .normalizedToEmpty) } } + +/// Most normalization tests care about the public insertion text rather than suppression +/// attribution. Keep that convenience in the test target instead of shipping a second production +/// entry point that no runtime caller uses. +private extension SuggestionTextNormalizer { + static func normalize( + _ rawSuggestion: String, + for request: SuggestionRequest, + promptEchoCandidates: [String] = [] + ) -> String { + normalizeDetailed( + rawSuggestion, + for: request, + promptEchoCandidates: promptEchoCandidates + ).text + } +} diff --git a/CotabbyTests/TestSupport/CotabbyTestFixtures.swift b/CotabbyTests/TestSupport/CotabbyTestFixtures.swift index e82b926e..429a5253 100644 --- a/CotabbyTests/TestSupport/CotabbyTestFixtures.swift +++ b/CotabbyTests/TestSupport/CotabbyTestFixtures.swift @@ -295,3 +295,19 @@ enum CotabbyTestFixtures { ) } } + +/// Acceptance-focused tests inspect the consumed prefix frequently. Keeping this projection in +/// the test module avoids expanding the production session API for assertion convenience alone. +extension ActiveSuggestionSession { + var acceptedText: String { + String(fullText.prefix(max(consumedCharacterCount, 0))) + } +} + +/// Runtime fakes use the common successful-output shape across several test files. Defining the +/// shorthand in the test module keeps production callers on the explicit initializer. +extension LlamaGenerationOutput { + static func text(_ text: String) -> LlamaGenerationOutput { + LlamaGenerationOutput(text: text, averageLogprob: nil, suppressedByLowConfidence: false) + } +} diff --git a/CotabbyTests/TestSupport/SuggestionCoordinatorTestSupport.swift b/CotabbyTests/TestSupport/SuggestionCoordinatorTestSupport.swift index a3ff9016..df96d813 100644 --- a/CotabbyTests/TestSupport/SuggestionCoordinatorTestSupport.swift +++ b/CotabbyTests/TestSupport/SuggestionCoordinatorTestSupport.swift @@ -228,8 +228,7 @@ func makeCoordinatorRig( applicationName: snapshot.applicationName, bundleIdentifier: snapshot.bundleIdentifier, capability: capability, - context: snapshot, - inspection: nil + context: snapshot ) let permissionProvider = RigPermissionProvider() let focusProvider = RigFocusProvider(snapshot: focusSnapshot)