From add17f8e539fc205bfe363bc9980c3aabf6193ec Mon Sep 17 00:00:00 2001 From: Jacob Fu <141651335+FuJacob@users.noreply.github.com> Date: Fri, 17 Jul 2026 00:30:36 -0700 Subject: [PATCH] Remove dead code and stale diagnostics --- .github/assets/readme/demo-email.png | Bin 80556 -> 0 bytes .../Coordinators/EmojiPickerController.swift | 10 +-- .../SuggestionCoordinator+Acceptance.swift | 41 ++-------- .../SuggestionCoordinator+Lifecycle.swift | 10 --- .../SuggestionCoordinator+Prediction.swift | 38 +--------- .../Coordinators/SuggestionCoordinator.swift | 29 ++------ Cotabby/App/Core/CotabbyAppEnvironment.swift | 1 - .../Models/Context/VisualContextModels.swift | 14 ---- Cotabby/Models/Emoji/EmojiPickerModels.swift | 11 +-- Cotabby/Models/Focus/FocusModels.swift | 19 +---- .../Onboarding/OnboardingTemplate.swift | 1 - .../Models/Permissions/PermissionModels.swift | 9 --- .../Models/Runtime/HuggingFaceModels.swift | 3 - .../Models/Runtime/LlamaRuntimeModels.swift | 3 - .../OpenAICompatibleEndpointModels.swift | 13 ---- .../Runtime/RuntimeBootstrapModel.swift | 9 --- .../Settings/CompletionRenderMode.swift | 4 +- .../Settings/SuggestionSettingsModel.swift | 8 -- .../Suggestion/ActiveSuggestionSession.swift | 4 - .../Suggestion/FocusedInputContext.swift | 7 -- .../Suggestion/SuggestionConfiguration.swift | 33 --------- .../SuggestionPresentationModels.swift | 59 --------------- .../SuggestionSubsystemContracts.swift | 2 +- .../Focus/FocusSnapshotResolver.swift | 52 ++++--------- Cotabby/Services/Focus/FocusTracker.swift | 3 +- .../Permission/PermissionManager.swift | 14 ---- .../EmojiPickerPanelController.swift | 3 +- .../FocusDebugOverlayController.swift | 2 - .../Runtime/LlamaRuntimeManager.swift | 10 --- .../Suggestion/SuggestionDebugLogger.swift | 15 ---- Cotabby/Support/Emoji/EmojiCatalog.swift | 7 +- .../Focus/FocusCapabilityResolver.swift | 57 +------------- Cotabby/Support/Logging/RequestID.swift | 9 --- .../Onboarding/OnboardingFlowSteps.swift | 10 --- .../Presentation/SuggestionAnchorCache.swift | 4 - .../Runtime/BundledRuntimeLocator.swift | 10 --- .../Support/Runtime/DecodeStopPolicy.swift | 12 --- .../Runtime/HardwareCapabilityProbe.swift | 16 +--- .../Settings/SettingsAttentionEvaluator.swift | 36 +-------- Cotabby/Support/Spelling/SymSpell.swift | 1 - .../SuggestionSessionReconciler.swift | 2 - .../Suggestion/SuggestionTextNormalizer.swift | 11 --- .../UI/InlineFeatures/EmojiPickerView.swift | 3 - Cotabby/UI/MenuBar/MenuBarView.swift | 9 --- .../UI/Settings/Panes/WritingPaneView.swift | 7 -- .../UI/Settings/SettingsContainerView.swift | 1 - .../EmojiPickerControllerTests.swift | 14 ++-- ...SuggestionCoordinatorAcceptanceTests.swift | 17 +---- .../SuggestionCoordinatorInputTests.swift | 12 +-- .../SuggestionCoordinatorLifecycleTests.swift | 36 --------- ...SuggestionCoordinatorPredictionTests.swift | 10 +-- .../SuggestionStateHelperTests.swift | 2 +- .../PermissionAndContextModelTests.swift | 28 ------- .../Models/Emoji/EmojiPickerModelsTests.swift | 11 +-- .../Models/Focus/FocusModelsTests.swift | 26 +------ .../Runtime/HuggingFaceModelsTests.swift | 4 +- .../Runtime/RuntimeBootstrapModelTests.swift | 18 ----- .../SuggestionSettingsModelTests.swift | 1 - .../ModelAndPresentationValueTests.swift | 70 +++++------------- .../FocusSnapshotResolverLiveTests.swift | 4 +- .../OpenAICompatibleAPIClientTests.swift | 13 ++-- .../Emoji/EmojiCatalogMatcherTests.swift | 6 +- .../Support/Emoji/EmojiCatalogTests.swift | 19 ++--- .../Support/Emoji/EmojiRecentsTests.swift | 2 +- .../Emoji/EmojiVariantResolverTests.swift | 14 ++-- .../FocusCapabilityFlickerGateTests.swift | 15 ++-- .../Focus/FocusCapabilityResolverTests.swift | 58 ++------------- .../Focus/TerminalAppDetectorTests.swift | 12 +-- .../Support/Logging/RequestIDTests.swift | 9 --- .../Logging/SuggestionDebugLoggerTests.swift | 40 +--------- .../Onboarding/OnboardingFlowStepTests.swift | 18 +---- .../OnboardingTemplateRecommenderTests.swift | 5 +- .../SuggestionAnchorCacheTests.swift | 6 -- .../Runtime/BundledRuntimeLocatorTests.swift | 14 ++-- .../Runtime/DecodeStopPolicyTests.swift | 26 +++---- .../HardwareCapabilityProbeTests.swift | 15 +--- .../SettingsAttentionEvaluatorTests.swift | 61 +-------------- .../Support/Spelling/SymSpellTests.swift | 2 +- ...shotExternalApplicationIdentityTests.swift | 9 +-- ...SuggestionAvailabilityEvaluatorTests.swift | 14 ++-- .../SuggestionTextNormalizerTests.swift | 17 +++++ .../TestSupport/CotabbyTestFixtures.swift | 16 ++++ .../SuggestionCoordinatorTestSupport.swift | 3 +- 83 files changed, 202 insertions(+), 1067 deletions(-) delete mode 100644 .github/assets/readme/demo-email.png diff --git a/.github/assets/readme/demo-email.png b/.github/assets/readme/demo-email.png deleted file mode 100644 index 43804ed0f108f22f08826ea810409ceb0a52d352..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 80556 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 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)