From 0c5390b1459cfd8484bb166a58ab8b968874a43c Mon Sep 17 00:00:00 2001 From: Willy Date: Tue, 5 May 2026 20:55:57 -0500 Subject: [PATCH 1/5] fix: restore menu bar icon on macOS 26.4 (#805) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two changes addressing the invisible status item icon on macOS 26.4: 1. Replace compositingGroup + blendMode(.destinationOut) in UsageProgressBar with a single Canvas that draws track, fill, and pace-tip punch-out entirely inside Core Graphics. SwiftUI compositing modifiers (.compositingGroup, .blendMode as a view modifier) trigger Metal/RenderBox shader compilation; on macOS 26.4 that compilation can fail with a precondition error, making the NSStatusItem window invisible. Moving the blend to the GraphicsContext API inside the Canvas avoids the Metal path entirely. 2. Gate migrateLegacySecrets and migrateLegacyAccounts behind existing == nil in CodexBarConfigMigrator.loadOrMigrate(). These ran unconditionally on every launch, performing ~28 SecItemCopyMatching calls on the main thread. After the first launch the config is the source of truth and legacy stores are cleared, so subsequent calls all hit errSecItemNotFound — but macOS 26.4 Performance Diagnostics still emits a fault per call, adding startup jank. applyLegacyCookieSources (cheap UserDefaults reads) remains unconditional. Co-Authored-By: Claude Sonnet 4.6 --- .../Config/CodexBarConfigMigrator.swift | 14 ++- Sources/CodexBar/UsageProgressBar.swift | 110 ++++++++---------- 2 files changed, 58 insertions(+), 66 deletions(-) diff --git a/Sources/CodexBar/Config/CodexBarConfigMigrator.swift b/Sources/CodexBar/Config/CodexBarConfigMigrator.swift index a34629766..3f7cc7cb9 100644 --- a/Sources/CodexBar/Config/CodexBarConfigMigrator.swift +++ b/Sources/CodexBar/Config/CodexBarConfigMigrator.swift @@ -36,14 +36,20 @@ struct CodexBarConfigMigrator { var config = (existing ?? CodexBarConfig.makeDefault()).normalized() var state = MigrationState() + // applyLegacyCookieSources reads only UserDefaults, so it is cheap and runs unconditionally + // to pick up any newly-added cookie-source keys on every launch. + self.applyLegacyCookieSources(userDefaults: userDefaults, config: &config, state: &state) + + // The heavy Keychain + file migrations only need to run once: on the very first launch before a + // config file exists. After that the config is the source of truth, and legacy stores have been + // cleared. Running them on every launch caused ~28 unnecessary SecItemCopyMatching calls on the + // main thread, which macOS 26.4 Performance Diagnostics flags as faults (issue #805). if existing == nil { self.applyLegacyOrderAndToggles(userDefaults: userDefaults, config: &config, state: &state) + self.migrateLegacySecrets(userDefaults: userDefaults, stores: stores, config: &config, state: &state) + self.migrateLegacyAccounts(stores: stores, config: &config, state: &state) } - self.applyLegacyCookieSources(userDefaults: userDefaults, config: &config, state: &state) - self.migrateLegacySecrets(userDefaults: userDefaults, stores: stores, config: &config, state: &state) - self.migrateLegacyAccounts(stores: stores, config: &config, state: &state) - if state.didUpdate { do { try configStore.save(config) diff --git a/Sources/CodexBar/UsageProgressBar.swift b/Sources/CodexBar/UsageProgressBar.swift index d2bc2d844..58d95475e 100644 --- a/Sources/CodexBar/UsageProgressBar.swift +++ b/Sources/CodexBar/UsageProgressBar.swift @@ -39,80 +39,66 @@ struct UsageProgressBar: View { } var body: some View { - GeometryReader { proxy in + // Draw the entire progress bar — track, fill, and pace-tip punch-out — in a single Canvas. + // A single Canvas uses Core Graphics internally and avoids the SwiftUI compositing modifiers + // (.compositingGroup, .blendMode) that trigger Metal/RenderBox shader compilation on macOS 26.x, + // which caused the status item icon to disappear (issue #805). + Canvas { context, size in let scale = max(self.displayScale, 1) - let fillWidth = proxy.size.width * self.clamped / 100 - let paceWidth = proxy.size.width * Self.clampedPercent(self.pacePercent) / 100 - let tipWidth = max(25, proxy.size.height * 6.5) + let fillWidth = size.width * self.clamped / 100 + let paceWidth = size.width * Self.clampedPercent(self.pacePercent) / 100 + let tipWidth = max(25, size.height * 6.5) let stripeInset = 1 / scale let tipOffset = paceWidth - tipWidth + (Self.paceStripeSpan(for: scale) / 2) + stripeInset let showTip = self.pacePercent != nil && tipWidth > 0.5 - let needsPunchCompositing = showTip - let bar = ZStack(alignment: .leading) { - Capsule() - .fill(MenuHighlightStyle.progressTrack(self.isHighlighted)) - self.actualBar(width: fillWidth) - if showTip { - self.paceTip(width: tipWidth) - .offset(x: tipOffset) - } - } - .clipped() - if self.isHighlighted { - bar - .compositingGroup() - } else if needsPunchCompositing { - bar - .compositingGroup() - } else { - bar - } - } - .frame(height: 6) - .accessibilityLabel(self.accessibilityLabel) - .accessibilityValue("\(Int(self.clamped)) percent") - } - - private func actualBar(width: CGFloat) -> some View { - Capsule() - .fill(MenuHighlightStyle.progressTint(self.isHighlighted, fallback: self.tint)) - .frame(width: width) - .contentShape(Rectangle()) - .allowsHitTesting(false) - } - private func paceTip(width: CGFloat) -> some View { - let isDeficit = self.paceOnTop == false - let useDeficitRed = isDeficit && self.isHighlighted == false - return GeometryReader { proxy in - let size = proxy.size + let cornerRadius = size.height / 2 + let cornerSize = CGSize(width: cornerRadius, height: cornerRadius) let rect = CGRect(origin: .zero, size: size) - let scale = max(self.displayScale, 1) - let stripes = Self.paceStripePaths(size: size, scale: scale) - let stripeColor: Color = if self.isHighlighted { - .white - } else if useDeficitRed { - .red - } else { - .green + + context.clip(to: Path(rect)) + + // Track + let trackPath = Path { p in p.addRoundedRect(in: rect, cornerSize: cornerSize) } + context.fill(trackPath, with: .color(MenuHighlightStyle.progressTrack(self.isHighlighted))) + + // Fill + if fillWidth > 0 { + let fillRect = CGRect(x: 0, y: 0, width: min(fillWidth, size.width), height: size.height) + let fillPath = Path { p in p.addRoundedRect(in: fillRect, cornerSize: cornerSize) } + context.fill( + fillPath, + with: .color(MenuHighlightStyle.progressTint(self.isHighlighted, fallback: self.tint))) } - ZStack { - Canvas { context, _ in - context.clip(to: Path(rect)) - context.fill(stripes.punched, with: .color(.white.opacity(0.9))) + // Pace tip: punch-out + center stripe drawn within the canvas context using Core Graphics + // blend modes so no SwiftUI compositing modifier (.blendMode, .compositingGroup) is needed. + if showTip { + let isDeficit = self.paceOnTop == false + let useDeficitRed = isDeficit && self.isHighlighted == false + let stripeColor: Color = if self.isHighlighted { + .white + } else if useDeficitRed { + .red + } else { + .green } - .blendMode(.destinationOut) - Canvas { context, _ in - context.clip(to: Path(rect)) - context.fill(stripes.center, with: .color(stripeColor)) - } + let tipSize = CGSize(width: tipWidth, height: size.height) + let stripes = Self.paceStripePaths(size: tipSize, scale: scale) + let shift = CGAffineTransform(translationX: tipOffset, y: 0) + + // Punch out of the accumulated track+fill pixels. + context.blendMode = .destinationOut + context.fill(stripes.punched.applying(shift), with: .color(.white.opacity(0.9))) + context.blendMode = .normal + + context.fill(stripes.center.applying(shift), with: .color(stripeColor)) } } - .frame(width: width) - .contentShape(Rectangle()) - .allowsHitTesting(false) + .frame(height: 6) + .accessibilityLabel(self.accessibilityLabel) + .accessibilityValue("\(Int(self.clamped)) percent") } private static func paceStripePaths(size: CGSize, scale: CGFloat) -> (punched: Path, center: Path) { From 0be7a6eb574131b779f82d62e0b62346132be576 Mon Sep 17 00:00:00 2001 From: Willy Date: Tue, 5 May 2026 21:22:45 -0500 Subject: [PATCH 2/5] fix: use completion flag for legacy migration instead of existing == nil MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the `existing == nil` gate added in the previous commit with a UserDefaults boolean flag (`codexbar.legacySecretsMigrationCompleted`). The `existing == nil` gate had a blind spot: if the app crashed after configStore.save() but before clearLegacyStores(), the config would exist on the next launch (`existing != nil`) so migration would be skipped and orphaned Keychain items would never be cleaned up. The flag approach is safe for all cases: - Fresh install: flag absent → migration runs → items found → clear → set flag - Normal re-launch: flag present → skip (0 Keychain calls) - Crash-interrupted first migration: flag absent → migration re-runs → items still present → clear → set flag (data already in config via setIfEmpty) - No legacy data ever existed: flag absent → migration runs → finds nothing → set flag → never scans again Co-Authored-By: Claude Sonnet 4.6 --- .../Config/CodexBarConfigMigrator.swift | 29 ++++++++++++++----- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/Sources/CodexBar/Config/CodexBarConfigMigrator.swift b/Sources/CodexBar/Config/CodexBarConfigMigrator.swift index 3f7cc7cb9..b4105782e 100644 --- a/Sources/CodexBar/Config/CodexBarConfigMigrator.swift +++ b/Sources/CodexBar/Config/CodexBarConfigMigrator.swift @@ -20,6 +20,12 @@ struct CodexBarConfigMigrator { let tokenAccountStore: any ProviderTokenAccountStoring } + // Persisted once clearLegacyStores completes (or migration finds no legacy data). Guards against + // re-running ~28 SecItemCopyMatching calls on every launch (macOS 26.4 fault per call, issue #805). + // Using a flag rather than `existing == nil` so that a crash-interrupted first migration + // (config saved, clearLegacyStores not yet called) can finish cleanup on the next launch. + private static let legacyMigrationCompletedKey = "codexbar.legacySecretsMigrationCompleted" + private struct MigrationState { var didUpdate = false var sawLegacySecrets = false @@ -36,16 +42,18 @@ struct CodexBarConfigMigrator { var config = (existing ?? CodexBarConfig.makeDefault()).normalized() var state = MigrationState() - // applyLegacyCookieSources reads only UserDefaults, so it is cheap and runs unconditionally - // to pick up any newly-added cookie-source keys on every launch. + // applyLegacyCookieSources reads only UserDefaults — cheap, runs unconditionally so + // newly-added cookie-source keys are picked up on every launch. self.applyLegacyCookieSources(userDefaults: userDefaults, config: &config, state: &state) - // The heavy Keychain + file migrations only need to run once: on the very first launch before a - // config file exists. After that the config is the source of truth, and legacy stores have been - // cleared. Running them on every launch caused ~28 unnecessary SecItemCopyMatching calls on the - // main thread, which macOS 26.4 Performance Diagnostics flags as faults (issue #805). - if existing == nil { - self.applyLegacyOrderAndToggles(userDefaults: userDefaults, config: &config, state: &state) + let migrationCompleted = userDefaults.bool(forKey: Self.legacyMigrationCompletedKey) + if !migrationCompleted { + // Run once: migrate Keychain/file secrets then clear them. Using a completion flag rather + // than `existing == nil` ensures a crash between config-save and clearLegacyStores can + // finish cleanup on the next launch without re-doing the (already-saved) data migration. + if existing == nil { + self.applyLegacyOrderAndToggles(userDefaults: userDefaults, config: &config, state: &state) + } self.migrateLegacySecrets(userDefaults: userDefaults, stores: stores, config: &config, state: &state) self.migrateLegacyAccounts(stores: stores, config: &config, state: &state) } @@ -60,6 +68,11 @@ struct CodexBarConfigMigrator { if state.sawLegacySecrets || state.sawLegacyAccounts { self.clearLegacyStores(stores: stores, sawAccounts: state.sawLegacyAccounts, log: log) + userDefaults.set(true, forKey: Self.legacyMigrationCompletedKey) + } else if !migrationCompleted { + // Migration ran but found nothing — no legacy data ever existed; mark complete so we + // never pay the Keychain scan cost again. + userDefaults.set(true, forKey: Self.legacyMigrationCompletedKey) } return config.normalized() From 86e286d8b38e140eb4a48a8515edd787fc3e33d2 Mon Sep 17 00:00:00 2001 From: Willy Date: Tue, 5 May 2026 21:26:03 -0500 Subject: [PATCH 3/5] docs: add changelog entry for #805 Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d4c65efc..0897f168f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,7 @@ - Codex: prefer session turn-context model metadata when calculating local cost history so GPT-5.4 sessions are not bucketed as GPT-5 (#620). Thanks @betive37! - Codex: stop falling back from app-server RPC to bare CLI TUI during automatic usage refreshes, preventing unexpected OpenAI auth browser tabs. - Menu/keychain: block delayed test-time menu mutations after teardown and enforce no-UI keychain reads more reliably (#381). Thanks @artuskg! +- Menu bar: fix invisible status item icon on macOS 26.4 by removing remaining RenderBox-triggering SwiftUI compositing modifiers from `UsageProgressBar` (rewritten as a single Canvas) and eliminating ~28 redundant Keychain reads on every launch after the first-run migration (#805). ## 0.23 — 2026-04-26 From 1e08828f39ca86739b845e73e6895325aff9791f Mon Sep 17 00:00:00 2001 From: Willy Date: Tue, 5 May 2026 21:41:58 -0500 Subject: [PATCH 4/5] fix: only mark migration complete when clearLegacyStores succeeds clearLegacyStores catches keychain write failures internally and returns normally, so the completion flag was being set even on partial cleanup. A transient keychain error would permanently suppress retry on next launch. - Make clearLegacyStores return Bool (true = all deletes succeeded) - Gate legacyMigrationCompletedKey on that return value - Remove comment block above the key constant (SwiftFormat docComments lint) Co-Authored-By: Claude Sonnet 4.6 --- .../Config/CodexBarConfigMigrator.swift | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/Sources/CodexBar/Config/CodexBarConfigMigrator.swift b/Sources/CodexBar/Config/CodexBarConfigMigrator.swift index b4105782e..36f303a3a 100644 --- a/Sources/CodexBar/Config/CodexBarConfigMigrator.swift +++ b/Sources/CodexBar/Config/CodexBarConfigMigrator.swift @@ -20,10 +20,6 @@ struct CodexBarConfigMigrator { let tokenAccountStore: any ProviderTokenAccountStoring } - // Persisted once clearLegacyStores completes (or migration finds no legacy data). Guards against - // re-running ~28 SecItemCopyMatching calls on every launch (macOS 26.4 fault per call, issue #805). - // Using a flag rather than `existing == nil` so that a crash-interrupted first migration - // (config saved, clearLegacyStores not yet called) can finish cleanup on the next launch. private static let legacyMigrationCompletedKey = "codexbar.legacySecretsMigrationCompleted" private struct MigrationState { @@ -67,11 +63,11 @@ struct CodexBarConfigMigrator { } if state.sawLegacySecrets || state.sawLegacyAccounts { - self.clearLegacyStores(stores: stores, sawAccounts: state.sawLegacyAccounts, log: log) - userDefaults.set(true, forKey: Self.legacyMigrationCompletedKey) + let cleared = self.clearLegacyStores(stores: stores, sawAccounts: state.sawLegacyAccounts, log: log) + if cleared { + userDefaults.set(true, forKey: Self.legacyMigrationCompletedKey) + } } else if !migrationCompleted { - // Migration ran but found nothing — no legacy data ever existed; mark complete so we - // never pay the Keychain scan cost again. userDefaults.set(true, forKey: Self.legacyMigrationCompletedKey) } @@ -293,11 +289,13 @@ struct CodexBarConfigMigrator { return false } + @discardableResult private static func clearLegacyStores( stores: LegacyStores, sawAccounts: Bool, - log: CodexBarLogger) + log: CodexBarLogger) -> Bool { + var success = true do { try stores.zaiTokenStore.storeToken(nil) try stores.syntheticTokenStore.storeToken(nil) @@ -315,6 +313,7 @@ struct CodexBarConfigMigrator { try stores.ampCookieStore.storeCookieHeader(nil) } catch { log.error("Failed to clear legacy secrets: \(error)") + success = false } if sawAccounts { @@ -323,6 +322,8 @@ struct CodexBarConfigMigrator { try? FileManager.default.removeItem(at: legacyURL) } } + + return success } private static func applyProviderOrder(_ raw: [String], config: CodexBarConfig) -> CodexBarConfig { From 1160719b0595f46c1814bec675934166f2e3f054 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 6 May 2026 06:54:29 +0100 Subject: [PATCH 5/5] test: cover legacy migration completion flag --- CHANGELOG.md | 2 +- .../CodexBarConfigMigratorTests.swift | 153 ++++++++++++++++++ 2 files changed, 154 insertions(+), 1 deletion(-) create mode 100644 Tests/CodexBarTests/CodexBarConfigMigratorTests.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 0897f168f..f7daafe7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,7 +39,7 @@ - Codex: prefer session turn-context model metadata when calculating local cost history so GPT-5.4 sessions are not bucketed as GPT-5 (#620). Thanks @betive37! - Codex: stop falling back from app-server RPC to bare CLI TUI during automatic usage refreshes, preventing unexpected OpenAI auth browser tabs. - Menu/keychain: block delayed test-time menu mutations after teardown and enforce no-UI keychain reads more reliably (#381). Thanks @artuskg! -- Menu bar: fix invisible status item icon on macOS 26.4 by removing remaining RenderBox-triggering SwiftUI compositing modifiers from `UsageProgressBar` (rewritten as a single Canvas) and eliminating ~28 redundant Keychain reads on every launch after the first-run migration (#805). +- Menu bar: fix invisible status item icon on macOS 26.4 by removing remaining RenderBox-triggering SwiftUI compositing modifiers from `UsageProgressBar` (rewritten as a single Canvas) and eliminating ~28 redundant Keychain reads on every launch after the first-run migration (#805). Thanks @willytop8! ## 0.23 — 2026-04-26 diff --git a/Tests/CodexBarTests/CodexBarConfigMigratorTests.swift b/Tests/CodexBarTests/CodexBarConfigMigratorTests.swift new file mode 100644 index 000000000..03ffc9a48 --- /dev/null +++ b/Tests/CodexBarTests/CodexBarConfigMigratorTests.swift @@ -0,0 +1,153 @@ +import CodexBarCore +import Foundation +import Testing +@testable import CodexBar + +@Suite(.serialized) +struct CodexBarConfigMigratorTests { + @Test + func `legacy secret migration completion flag skips repeated scans`() throws { + let suite = "CodexBarConfigMigratorTests-skip-\(UUID().uuidString)" + let defaults = try #require(UserDefaults(suiteName: suite)) + defaults.removePersistentDomain(forName: suite) + defer { defaults.removePersistentDomain(forName: suite) } + + let secrets = CountingLegacySecretStore() + let accountStore = CountingTokenAccountStore() + let stores = Self.legacyStores(secrets: secrets, accountStore: accountStore) + let configStore = testConfigStore(suiteName: suite) + + _ = CodexBarConfigMigrator.loadOrMigrate(configStore: configStore, userDefaults: defaults, stores: stores) + + let firstSecretLoads = secrets.loadCount + let firstAccountLoads = accountStore.loadCount + #expect(firstSecretLoads > 0) + #expect(firstAccountLoads == 1) + #expect(defaults.bool(forKey: Self.legacyMigrationCompletedKey) == true) + + _ = CodexBarConfigMigrator.loadOrMigrate(configStore: configStore, userDefaults: defaults, stores: stores) + + #expect(secrets.loadCount == firstSecretLoads) + #expect(accountStore.loadCount == firstAccountLoads) + } + + @Test + func `legacy migration completion waits for successful cleanup`() throws { + let suite = "CodexBarConfigMigratorTests-cleanup-failure-\(UUID().uuidString)" + let defaults = try #require(UserDefaults(suiteName: suite)) + defaults.removePersistentDomain(forName: suite) + defer { defaults.removePersistentDomain(forName: suite) } + + let secrets = CountingLegacySecretStore(token: "legacy-token", throwOnStore: true) + let accountStore = CountingTokenAccountStore() + let stores = Self.legacyStores(secrets: secrets, accountStore: accountStore) + let configStore = testConfigStore(suiteName: suite) + + _ = CodexBarConfigMigrator.loadOrMigrate(configStore: configStore, userDefaults: defaults, stores: stores) + + let firstSecretLoads = secrets.loadCount + #expect(firstSecretLoads > 0) + #expect(secrets.clearAttempts > 0) + #expect(defaults.bool(forKey: Self.legacyMigrationCompletedKey) == false) + + secrets.throwOnStore = false + _ = CodexBarConfigMigrator.loadOrMigrate(configStore: configStore, userDefaults: defaults, stores: stores) + + #expect(secrets.loadCount > firstSecretLoads) + #expect(defaults.bool(forKey: Self.legacyMigrationCompletedKey) == true) + } + + private static let legacyMigrationCompletedKey = "codexbar.legacySecretsMigrationCompleted" + + private static func legacyStores( + secrets: CountingLegacySecretStore, + accountStore: CountingTokenAccountStore) -> CodexBarConfigMigrator.LegacyStores + { + CodexBarConfigMigrator.LegacyStores( + zaiTokenStore: secrets, + syntheticTokenStore: secrets, + codexCookieStore: secrets, + claudeCookieStore: secrets, + cursorCookieStore: secrets, + opencodeCookieStore: secrets, + factoryCookieStore: secrets, + minimaxCookieStore: secrets, + minimaxAPITokenStore: secrets, + kimiTokenStore: secrets, + kimiK2TokenStore: secrets, + augmentCookieStore: secrets, + ampCookieStore: secrets, + copilotTokenStore: secrets, + tokenAccountStore: accountStore) + } +} + +private final class CountingLegacySecretStore: ZaiTokenStoring, SyntheticTokenStoring, CookieHeaderStoring, + MiniMaxCookieStoring, MiniMaxAPITokenStoring, KimiTokenStoring, KimiK2TokenStoring, CopilotTokenStoring, + @unchecked Sendable +{ + private let lock = NSLock() + private var token: String? + var throwOnStore: Bool + private(set) var loadCount = 0 + private(set) var clearAttempts = 0 + + init(token: String? = nil, throwOnStore: Bool = false) { + self.token = token + self.throwOnStore = throwOnStore + } + + func loadToken() throws -> String? { + self.lock.lock() + defer { self.lock.unlock() } + self.loadCount += 1 + return self.token + } + + func storeToken(_ token: String?) throws { + try self.store(token) + } + + func loadCookieHeader() throws -> String? { + self.lock.lock() + defer { self.lock.unlock() } + self.loadCount += 1 + return self.token + } + + func storeCookieHeader(_ header: String?) throws { + try self.store(header) + } + + private func store(_ value: String?) throws { + self.lock.lock() + defer { self.lock.unlock() } + self.clearAttempts += value == nil ? 1 : 0 + if self.throwOnStore { + throw TestStoreError.storeFailed + } + self.token = value + } +} + +private final class CountingTokenAccountStore: ProviderTokenAccountStoring, @unchecked Sendable { + private let lock = NSLock() + private(set) var loadCount = 0 + + func loadAccounts() throws -> [UsageProvider: ProviderTokenAccountData] { + self.lock.lock() + defer { self.lock.unlock() } + self.loadCount += 1 + return [:] + } + + func storeAccounts(_: [UsageProvider: ProviderTokenAccountData]) throws {} + + func ensureFileExists() throws -> URL { + FileManager.default.temporaryDirectory.appendingPathComponent("codexbar-empty-accounts.json") + } +} + +private enum TestStoreError: Error { + case storeFailed +}