From 79e547d81d56e6b762e147e59518bb36a18b21a8 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 29 Jul 2026 22:31:10 +0000 Subject: [PATCH] Add a setting to turn enhanced transcripts on and off A new Transcription section in Settings > Advanced carries an "Enhanced transcripts" toggle. On (the default), each dictation request keeps asking the dictation API for its server-side LLM cleanup rewrite; off, the request omits the llm block entirely so the verbatim transcript is pasted exactly as spoken. - EnhancedTranscriptsStore: UserDefaults-backed switch, on by default (unset reads as enabled), registered in PersistedSettings' roster. - AssemblyAITranscriber: new injectable enhancedTranscripts closure, read per request, gating DictationConfig's now-optional llm block. - Settings UI: TranscriptionSection toggle with a shared UI-test identifier; docs updated in AGENTS.md and BLURTENGINE.md. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01AAuNR2X97wf1jyFes8kdAF --- AGENTS.md | 8 ++++- .../Blurt/Wizard/SettingsWindowRoot.swift | 34 ++++++++++++++++-- App/Blurt/Shared/UITestIdentifiers.swift | 1 + BLURTENGINE.md | 4 +-- .../Config/EnhancedTranscriptsStore.swift | 29 +++++++++++++++ .../Config/PersistedSettings.swift | 4 ++- .../STT/AssemblyAITranscriber.swift | 35 ++++++++++++++----- .../AssemblyAITranscriberTests.swift | 33 +++++++++++++---- .../EnhancedTranscriptsStoreTests.swift | 24 +++++++++++++ .../PersistedSettingsTests.swift | 8 +++-- 10 files changed, 156 insertions(+), 24 deletions(-) create mode 100644 Sources/BlurtEngine/Config/EnhancedTranscriptsStore.swift create mode 100644 Tests/BlurtEngineTests/EnhancedTranscriptsStoreTests.swift diff --git a/AGENTS.md b/AGENTS.md index dc64a03..8345b99 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -246,7 +246,11 @@ in the `audio` multipart part plus a JSON `config` part (`sample_rate`, `channel empty `llm` block). No model header — the service pins the STT model server-side. The `prompt` (built per utterance by `TranscriptionPrompt`) steers _transcription_; the `llm` block asks the service to run its default LLM cleanup rewrite (remove disfluencies, fix punctuation) over the -verbatim transcript, all inside the same request. The response carries both `text` (verbatim) and +verbatim transcript, all inside the same request. The block rides along while **enhanced +transcripts** are enabled (`EnhancedTranscriptsStore`, on by default, read per request via the +transcriber's injected `enhancedTranscripts` closure); with the setting off the config omits `llm` +entirely, the service skips the rewrite, and the verbatim transcript is pasted as spoken. The +response carries both `text` (verbatim) and `llm_response` (the rewrite); the transcriber returns the rewrite and falls back to `text` when `llm_response` is null — the rewrite is best-effort (5 s server-side budget), so a rewrite failure (`llm_error`) is a logged degradation, never a user-facing error. @@ -405,6 +409,8 @@ Engine-side stores, all `UserDefaults`-backed value types with the same shape: - **`TriggerKeyStore`** (`BlurtTriggerKeyCode`), **`SoundPackStore`** (`BlurtSoundPack`), **`KeyTermsStore`** (the user's domain vocabulary, re-read at every press via the session's `keyTermsProvider`), **`DeveloperModeStore`** (`BlurtDeveloperMode`, off by default), + **`EnhancedTranscriptsStore`** (`BlurtEnhancedTranscripts`, **on** by default — unset reads as + enabled; gates the dictation request's `llm` cleanup-rewrite block, re-read at every request), **`OverlayOriginStore`** (the pill's dragged origin, x/y), **`LastUpdateCheckStore`** (`BlurtLastUpdateCheck`, the stamp throttling the automatic launch update check). - **`PersistedSettings.allDefaultsKeys`** is the roster of every key those stores write, and diff --git a/App/Blurt/Blurt/Wizard/SettingsWindowRoot.swift b/App/Blurt/Blurt/Wizard/SettingsWindowRoot.swift index 60659d1..af08fae 100644 --- a/App/Blurt/Blurt/Wizard/SettingsWindowRoot.swift +++ b/App/Blurt/Blurt/Wizard/SettingsWindowRoot.swift @@ -1,3 +1,4 @@ +import BlurtEngine import SwiftUI /// Root view of the `Settings` scene. A `TabView` at the root of a `Settings` @@ -66,19 +67,48 @@ private struct GeneralSettingsTab: View { } } -/// The occasional stuff: checking for an update and the developer-mode log -/// toggle. Kept out of General so the common pane stays short. +/// The occasional stuff: the enhanced-transcripts switch, checking for an +/// update, and the developer-mode log toggle. Kept out of General so the +/// common pane stays short. private struct AdvancedSettingsTab: View { let updateModel: UpdateCheckModel var body: some View { SettingsPane { + TranscriptionSection() UpdateSection(model: updateModel) DeveloperSection() } } } +/// The Transcription section of the Settings window: the enhanced-transcripts +/// switch. While on (the default), every dictation request asks AssemblyAI's +/// dictation API for its server-side cleanup rewrite, so the pasted text is +/// the polished version; turned off, the request omits the rewrite and the +/// verbatim transcript is pasted exactly as spoken. The transcriber reads the +/// same default this toggle writes at every request, so a change applies to +/// the next dictation. Settings-only — not a wizard step, since it never +/// gates setup. +private struct TranscriptionSection: View { + @AppStorage(EnhancedTranscriptsStore.defaultsKey) private var enhancedTranscripts = true + + var body: some View { + Section { + Toggle(isOn: $enhancedTranscripts) { + Label("Enhanced transcripts", systemImage: "wand.and.stars") + } + .accessibilityIdentifier(UITestIdentifiers.enhancedTranscriptsToggle) + } header: { + Text("Transcription") + } footer: { + Text( + "Polishes each dictation before pasting — removing filler words and fixing punctuation. " + + "Turn off to paste your words exactly as spoken.") + } + } +} + /// The Updates section of the Settings window: the running version and a /// "Check for Updates" button that runs the check and reports the result in a /// modal (see `UpdateCheckModel`). The same check is reachable from the diff --git a/App/Blurt/Shared/UITestIdentifiers.swift b/App/Blurt/Shared/UITestIdentifiers.swift index b8d4b6f..f7b0523 100644 --- a/App/Blurt/Shared/UITestIdentifiers.swift +++ b/App/Blurt/Shared/UITestIdentifiers.swift @@ -63,6 +63,7 @@ enum UITestIdentifiers { static let hotkeyPicker = "settings.hotkey.picker" static let soundPicker = "settings.sound.picker" static let developerToggle = "settings.developer.toggle" + static let enhancedTranscriptsToggle = "settings.enhancedTranscripts.toggle" static let updateCheck = "settings.update.check" /// The dictation overlay pill (`OverlayView`). diff --git a/BLURTENGINE.md b/BLURTENGINE.md index d0c2f9c..0da531c 100644 --- a/BLURTENGINE.md +++ b/BLURTENGINE.md @@ -59,7 +59,7 @@ press() ──▶ MicCapture.start() release() ──▶ MicCapture.s Key properties of the design, which your integration can rely on: - **One request per utterance, no streaming.** The dictation API returns the complete transcript — and its LLM-rewritten form — in the response body: no upload step, no job polling, no incremental deltas, no second request for the cleanup. `TranscriberProtocol.transcribe` is a single `async throws -> String`. UIs should show a "transcribing…" state and then the whole result; there is nothing to stream. -- **Cleanup happens server-side.** The request's empty `llm` block asks the service for its default cleanup rewrite (remove disfluencies, fix punctuation), applied to the verbatim transcript inside the same call; the per-utterance `config.prompt` (built by `TranscriptionPrompt` from the captured context) primes the _transcription_. The engine pastes `llm_response`, falling back to the verbatim `text` when the best-effort rewrite failed (`llm_error`) — a degradation, never a user-facing error. There is no client-side LLM pass, no styling stage, and deliberately no hook for one. +- **Cleanup happens server-side, and it's optional.** The request's empty `llm` block asks the service for its default cleanup rewrite (remove disfluencies, fix punctuation), applied to the verbatim transcript inside the same call; the per-utterance `config.prompt` (built by `TranscriptionPrompt` from the captured context) primes the _transcription_. The block is gated by the **enhanced transcripts** setting (`EnhancedTranscriptsStore`, on by default): turned off, the config omits `llm` and the verbatim transcript is pasted as spoken. The engine pastes `llm_response`, falling back to the verbatim `text` when the best-effort rewrite failed (`llm_error`) — a degradation, never a user-facing error. There is no client-side LLM pass, no styling stage, and deliberately no hook for one. - **Latency is pre-paid where possible.** `press()` fires a detached `warmUp()` at the transcriber (pre-opening the HTTPS connection while the user speaks, ~170 ms saved cold) and kicks off the cross-process accessibility read of the focused field without awaiting it — the read is then consumed at transcribe time with a bounded wait (`DictationSession.contextWaitBudget`, 500 ms), so an unresponsive frontmost app costs the transcript its priming, never a multi-second stall — and never delays the recording indicator. On the way out, `release()` flips the phase to `.transcribing` _before_ reading the recorded audio back, so a host's stop cue fires at key-up rather than after the disk read. - **A held trigger auto-releases.** `DictationSession` stops recording after `maxRecordingSeconds` (default `SyncSTTLimits.autoReleaseSeconds`, 115 s) so audio never exceeds what the endpoint accepts, and transcribes what it has. Clips shorter than `SyncSTTLimits.minPCMBytes` (~100 ms of audio — an accidental tap) are dropped as a silent no-op rather than sent to earn a 400. @@ -133,7 +133,7 @@ func transcribe(pcm: Data, sampleRate: Int, context: TranscriptionContext?) asyn func warmUp() async // optional; no-op default ``` -`AssemblyAITranscriber` is a stateless `Sendable` struct. One `POST https://dictation.assemblyai.com/transcribe` per utterance: the audio as raw S16LE PCM (the `pcm` blob, byte-for-byte) in the `audio` multipart part, plus a JSON `config` part (`sample_rate`, `channels`, the rendered `prompt`, and an empty `llm` block requesting the service's default cleanup rewrite), with the API key in `Authorization` (no model header — the service pins the STT model server-side). The response carries the verbatim `text` and the rewritten `llm_response`; the transcriber returns the rewrite and falls back to `text` when it is null (the rewrite is best-effort — `llm_error` is logged, never surfaced as a failure). Its initializer takes an `apiKeyProvider` closure (defaults to `APIKeyStore.current`), a `baseURL`, and an `HTTPTransport` — inject a fake transport (see `Tests/BlurtEngineTests/Stubs/FakeHTTPTransport.swift`) to test against canned responses. `warmUp()` fires a throwaway GET at the host root to pre-pool the connection; it never throws and any failure just means the real request pays connection setup as before. +`AssemblyAITranscriber` is a stateless `Sendable` struct. One `POST https://dictation.assemblyai.com/transcribe` per utterance: the audio as raw S16LE PCM (the `pcm` blob, byte-for-byte) in the `audio` multipart part, plus a JSON `config` part (`sample_rate`, `channels`, the rendered `prompt`, and — while enhanced transcripts are enabled, the default — an empty `llm` block requesting the service's default cleanup rewrite), with the API key in `Authorization` (no model header — the service pins the STT model server-side). The response carries the verbatim `text` and the rewritten `llm_response`; the transcriber returns the rewrite and falls back to `text` when it is null (the rewrite is best-effort — `llm_error` is logged, never surfaced as a failure). Its initializer takes an `apiKeyProvider` closure (defaults to `APIKeyStore.current`), a `baseURL`, an `HTTPTransport` — inject a fake transport (see `Tests/BlurtEngineTests/Stubs/FakeHTTPTransport.swift`) to test against canned responses — and an `enhancedTranscripts` closure deciding, per request, whether the `llm` block is sent (nil, the default, reads `EnhancedTranscriptsStore`). `warmUp()` fires a throwaway GET at the host root to pre-pool the connection; it never throws and any failure just means the real request pays connection setup as before. The model's limits live in `SyncSTTLimits` (16 kHz sample rate, ~0.1 s–120 s audio, and the auto-release math — the sync STT model behind the dictation service) — the single source shared by the mic, the session, and the request so recorded and declared geometry can't drift. diff --git a/Sources/BlurtEngine/Config/EnhancedTranscriptsStore.swift b/Sources/BlurtEngine/Config/EnhancedTranscriptsStore.swift new file mode 100644 index 0000000..8caa19c --- /dev/null +++ b/Sources/BlurtEngine/Config/EnhancedTranscriptsStore.swift @@ -0,0 +1,29 @@ +import Foundation + +/// Persists the "enhanced transcripts" switch in `UserDefaults`. On by +/// default; the Settings window's Transcription section flips it. While on, +/// every dictation request carries the `llm` block asking the dictation API +/// for its server-side cleanup rewrite (remove disfluencies, fix punctuation); +/// turned off, the request omits the block and the verbatim transcript is +/// pasted exactly as spoken. `AssemblyAITranscriber` reads this at each +/// request, so a change applies to the very next dictation. +/// Same shape as `DeveloperModeStore` / `SoundPackStore`. +public struct EnhancedTranscriptsStore { + /// UserDefaults key holding the switch. Public so SwiftUI views can observe + /// it directly (e.g. `@AppStorage`) and re-render on change. + public static let defaultsKey = "BlurtEnhancedTranscripts" + private let defaults: UserDefaults + + init(defaults: UserDefaults = .standard) { + self.defaults = defaults + } + + /// Unset means **on** — the cleanup rewrite is the product's default + /// behavior, so only an explicit opt-out disables it. That inverts the + /// usual `bool(forKey:)` shape (which reads a missing key as false), hence + /// the presence check. + var isEnabled: Bool { + get { defaults.object(forKey: Self.defaultsKey) as? Bool ?? true } + nonmutating set { defaults.set(newValue, forKey: Self.defaultsKey) } + } +} diff --git a/Sources/BlurtEngine/Config/PersistedSettings.swift b/Sources/BlurtEngine/Config/PersistedSettings.swift index df0c53e..e3560a7 100644 --- a/Sources/BlurtEngine/Config/PersistedSettings.swift +++ b/Sources/BlurtEngine/Config/PersistedSettings.swift @@ -1,7 +1,8 @@ import Foundation /// The roster of `UserDefaults` keys the engine's settings stores persist: -/// trigger key, sound pack, key terms, developer mode, overlay origin, and the +/// trigger key, sound pack, key terms, developer mode, enhanced transcripts, +/// overlay origin, and the /// timestamp throttling the automatic update check. Owned /// here — next to the stores — so adding a store and adding it to every "reset /// to a clean state" sweep (e.g. the app's UI-test launch reset) are the same @@ -19,6 +20,7 @@ public enum PersistedSettings { SoundPackStore.defaultsKey, KeyTermsStore.defaultsKey, DeveloperModeStore.defaultsKey, + EnhancedTranscriptsStore.defaultsKey, OverlayOriginStore.xDefaultsKey, OverlayOriginStore.yDefaultsKey, LastUpdateCheckStore.defaultsKey, diff --git a/Sources/BlurtEngine/STT/AssemblyAITranscriber.swift b/Sources/BlurtEngine/STT/AssemblyAITranscriber.swift index dcd2353..55da320 100644 --- a/Sources/BlurtEngine/STT/AssemblyAITranscriber.swift +++ b/Sources/BlurtEngine/STT/AssemblyAITranscriber.swift @@ -12,9 +12,10 @@ private let transcriberLog = Logger(subsystem: BlurtIdentity.subsystem, category /// A single `POST dictation.assemblyai.com/transcribe` carries the captured /// audio (raw S16LE PCM, exactly the bytes the mic recorded — there is no /// re-encoding pass) plus a JSON `config` part, and the response body carries -/// both the verbatim transcript and — because the config requests one via its -/// `llm` block — an LLM-rewritten version with disfluencies removed and -/// punctuation fixed. No upload step, no job submission, no polling — one +/// both the verbatim transcript and — when the config requests one via its +/// `llm` block (the "enhanced transcripts" setting, on by default) — an +/// LLM-rewritten version with disfluencies removed and punctuation fixed. +/// No upload step, no job submission, no polling — one /// request per utterance covers transcription *and* cleanup. The service picks /// the STT model server-side and handles audio from ~80 ms up to 120 s; the /// rewrite is best-effort with a ~5 s server-side deadline, so a rewrite @@ -23,6 +24,7 @@ public struct AssemblyAITranscriber: TranscriberProtocol { private let apiKeyProvider: @Sendable () -> String? private let baseURL: URL private let transport: any HTTPTransport + private let enhancedTranscriptsEnabled: @Sendable () -> Bool /// Idle timeout for the transcribe round trip — `URLRequest.timeoutInterval` is /// reset each time data moves, so this bounds *stalls*, not total elapsed time. @@ -33,14 +35,22 @@ public struct AssemblyAITranscriber: TranscriberProtocol { /// stuck on "Transcribing…" indefinitely. private static let requestTimeoutSeconds: TimeInterval = 90 + /// `enhancedTranscripts` decides, per request, whether the config carries + /// the `llm` cleanup-rewrite block. Read at every `transcribe` so a settings + /// change applies to the next dictation without rebuilding the transcriber. + /// `nil` (the default) reads `EnhancedTranscriptsStore` — spelled as an + /// optional rather than a default closure because a public default argument + /// can't reference the store's internal `isEnabled`. public init( apiKeyProvider: @escaping @Sendable () -> String? = { APIKeyStore.current }, baseURL: URL = URL(staticString: "https://dictation.assemblyai.com"), - transport: any HTTPTransport = URLSession.shared + transport: any HTTPTransport = URLSession.shared, + enhancedTranscripts: (@Sendable () -> Bool)? = nil ) { self.apiKeyProvider = apiKeyProvider self.baseURL = baseURL self.transport = transport + self.enhancedTranscriptsEnabled = enhancedTranscripts ?? { EnhancedTranscriptsStore().isEnabled } } // MARK: - Dictation request @@ -107,8 +117,11 @@ public struct AssemblyAITranscriber: TranscriberProtocol { /// Builds the JSON `config` part sent alongside the audio. The context /// `prompt` is included only when non-empty; a nil or blank prompt omits the - /// field so the server applies its default prompt. The `llm` block always - /// rides along — see `DictationConfig.llm`. Internal so tests can assert the + /// field so the server applies its default prompt. The `llm` block rides + /// along while enhanced transcripts are enabled (the default) and is omitted + /// entirely when the user has turned them off, so the service skips the + /// rewrite and the verbatim transcript is what gets pasted — see + /// `DictationConfig.llm`. Internal so tests can assert the /// prompt wiring without inspecting the multipart upload body (which /// `URLProtocol` mocks can't observe reliably for `upload(from:)`). func makeConfigData(sampleRate: Int, prompt: String?) throws -> Data { @@ -116,7 +129,8 @@ public struct AssemblyAITranscriber: TranscriberProtocol { DictationConfig( sampleRate: sampleRate, channels: 1, - prompt: prompt.trimmedNonEmpty() + prompt: prompt.trimmedNonEmpty(), + llm: enhancedTranscriptsEnabled() ? LLMRewrite() : nil ) ) } @@ -202,11 +216,14 @@ public struct AssemblyAITranscriber: TranscriberProtocol { /// it falls back to the server's default prompt. Steers *transcription*; /// the cleanup rewrite is the `llm` block's job. let prompt: String? - /// The rewrite request. An empty object selects the service's default + /// The rewrite request, present only while enhanced transcripts are + /// enabled (nil — the synthesized `encode` omits it — asks for no rewrite, + /// so the response's `llm_response` is null and the verbatim `text` is + /// used). An empty object selects the service's default /// cleanup instruction; per the API's `instruction`-mode rules, output /// format and don't-answer-the-text safeguards are enforced server-side, /// so nothing rides along here. - let llm = LLMRewrite() + let llm: LLMRewrite? enum CodingKeys: String, CodingKey { case sampleRate = "sample_rate" case channels diff --git a/Tests/BlurtEngineTests/AssemblyAITranscriberTests.swift b/Tests/BlurtEngineTests/AssemblyAITranscriberTests.swift index 2da4581..9744a61 100644 --- a/Tests/BlurtEngineTests/AssemblyAITranscriberTests.swift +++ b/Tests/BlurtEngineTests/AssemblyAITranscriberTests.swift @@ -163,15 +163,28 @@ struct HTTPClientTests { #expect(object["channels"] as? Int == 1) } - @Test("config part always requests the default cleanup rewrite", arguments: ["CONTEXT. Transcribe.", nil]) + @Test( + "config part requests the default cleanup rewrite while enhanced transcripts are on", + arguments: ["CONTEXT. Transcribe.", nil]) func configRequestsDefaultRewrite(prompt: String?) throws { - // `llm` must be present and empty on every request: present so the service - // runs the rewrite at all, empty so the server-owned default cleanup + // `llm` must be present and empty on every enhanced request: present so the + // service runs the rewrite at all, empty so the server-owned default cleanup // instruction (and its guardrails) applies rather than a client-side copy. // `isEmpty == true` also covers presence — it is false for a missing `llm`. #expect((try configObject(prompt: prompt)["llm"] as? [String: Any])?.isEmpty == true) } + @Test("config part omits the llm block when enhanced transcripts are off") + func configOmitsRewriteWhenDisabled() throws { + // Omission — not an empty or null `llm` — is what tells the service to skip + // the rewrite, so the user gets the verbatim transcript pasted as spoken. + let object = try configObject(prompt: "CONTEXT. Transcribe.", enhancedTranscripts: false) + #expect(object.keys.contains("llm") == false) + // The rest of the config is unaffected by the switch. + #expect(object["sample_rate"] as? Int == 16_000) + #expect(object["prompt"] as? String == "CONTEXT. Transcribe.") + } + @Test( "config part omits the prompt field when there is no usable context", arguments: [nil, " \n"]) @@ -291,11 +304,17 @@ struct HTTPClientTests { /// Builds a transcriber wired to `transport`. The default transport answers /// every request with a 500, for the cases that must never reach the wire. + /// Enhanced transcripts are pinned (on unless a test opts out) rather than + /// left to the production default, which reads the process's real + /// `UserDefaults`. private func makeTranscriber( apiKey: String?, - transport: any HTTPTransport = FakeHTTPTransport { _ in (500, Data()) } + transport: any HTTPTransport = FakeHTTPTransport { _ in (500, Data()) }, + enhancedTranscripts: Bool = true ) -> AssemblyAITranscriber { - AssemblyAITranscriber(apiKeyProvider: { apiKey }, transport: transport) + AssemblyAITranscriber( + apiKeyProvider: { apiKey }, transport: transport, + enhancedTranscripts: { enhancedTranscripts }) } private func collectTranscript(_ transcriber: AssemblyAITranscriber) async throws -> String { @@ -306,8 +325,8 @@ struct HTTPClientTests { /// config assertion below wants, since `makeConfigData` returns raw JSON. /// A part that isn't a JSON object at all fails here rather than turning every /// downstream assertion into a silent nil-compare. - private func configObject(prompt: String?) throws -> [String: Any] { - let config = try makeTranscriber(apiKey: "test-key") + private func configObject(prompt: String?, enhancedTranscripts: Bool = true) throws -> [String: Any] { + let config = try makeTranscriber(apiKey: "test-key", enhancedTranscripts: enhancedTranscripts) .makeConfigData(sampleRate: 16_000, prompt: prompt) return try #require(JSONSerialization.jsonObject(with: config) as? [String: Any]) } diff --git a/Tests/BlurtEngineTests/EnhancedTranscriptsStoreTests.swift b/Tests/BlurtEngineTests/EnhancedTranscriptsStoreTests.swift new file mode 100644 index 0000000..4c60f35 --- /dev/null +++ b/Tests/BlurtEngineTests/EnhancedTranscriptsStoreTests.swift @@ -0,0 +1,24 @@ +import Foundation +import Testing + +@testable import BlurtEngine + +@Suite("EnhancedTranscriptsStore") +struct EnhancedTranscriptsStoreTests { + @Test("defaults to on when unset") + func defaultsToOn() { + // The cleanup rewrite is the product's default behavior — an unset key + // must read as enabled, unlike the bool stores that default to off. + #expect(EnhancedTranscriptsStore(defaults: freshDefaults()).isEnabled) + } + + @Test("persists and reads back the switch") + func roundTrips() { + let defaults = freshDefaults() + let store = EnhancedTranscriptsStore(defaults: defaults) + store.isEnabled = false + #expect(!EnhancedTranscriptsStore(defaults: defaults).isEnabled) + store.isEnabled = true + #expect(EnhancedTranscriptsStore(defaults: defaults).isEnabled) + } +} diff --git a/Tests/BlurtEngineTests/PersistedSettingsTests.swift b/Tests/BlurtEngineTests/PersistedSettingsTests.swift index f1eec7b..34ee2d3 100644 --- a/Tests/BlurtEngineTests/PersistedSettingsTests.swift +++ b/Tests/BlurtEngineTests/PersistedSettingsTests.swift @@ -14,6 +14,10 @@ struct PersistedSettingsTests { #expect(PersistedSettings.allDefaultsKeys.contains(SoundPackStore.defaultsKey)) #expect(PersistedSettings.allDefaultsKeys.contains(KeyTermsStore.defaultsKey)) #expect(PersistedSettings.allDefaultsKeys.contains(DeveloperModeStore.defaultsKey)) + // Enhanced transcripts default to ON, so its key matters to the sweep in + // the other direction too: a stray `false` surviving a reset would leave a + // "clean" install pasting verbatim transcripts. + #expect(PersistedSettings.allDefaultsKeys.contains(EnhancedTranscriptsStore.defaultsKey)) // OverlayOriginStore persists a point, so it contributes two keys rather // than one. Both belong to the sweep: while they were private to // `OverlayWindowController`, no reset knew about them and a pill dragged @@ -27,10 +31,10 @@ struct PersistedSettingsTests { @Test("the roster carries no stale or duplicate keys") func rosterHasNoStrays() { - // Exactly the six known stores' keys (OverlayOriginStore contributes two): + // Exactly the seven known stores' keys (OverlayOriginStore contributes two): // a removed store must leave the roster in the same change, and a key listed // twice would hint at a copy-paste slip. - #expect(PersistedSettings.allDefaultsKeys.count == 7) + #expect(PersistedSettings.allDefaultsKeys.count == 8) #expect(Set(PersistedSettings.allDefaultsKeys).count == PersistedSettings.allDefaultsKeys.count) }