diff --git a/Scripts/package_app.sh b/Scripts/package_app.sh
index 4a8a6de02..9e62c83dc 100755
--- a/Scripts/package_app.sh
+++ b/Scripts/package_app.sh
@@ -134,9 +134,10 @@ if [[ "$SIGNING_MODE" == "adhoc" ]]; then
AUTO_CHECKS=false
fi
WIDGET_BUNDLE_ID="${BUNDLE_ID}.widget"
-APP_GROUP_ID="group.com.steipete.codexbar"
+APP_TEAM_ID="${APP_TEAM_ID:-Y5PE65HELJ}"
+APP_GROUP_ID="${APP_TEAM_ID}.com.steipete.codexbar"
if [[ "$BUNDLE_ID" == *".debug"* ]]; then
- APP_GROUP_ID="group.com.steipete.codexbar.debug"
+ APP_GROUP_ID="${APP_TEAM_ID}.com.steipete.codexbar.debug"
fi
ENTITLEMENTS_DIR="$ROOT/.build/entitlements"
APP_ENTITLEMENTS="${ENTITLEMENTS_DIR}/CodexBar.entitlements"
@@ -197,6 +198,7 @@ cat > "$APP/Contents/Info.plist" <SUEnableAutomaticChecks<${AUTO_CHECKS}/>
CodexBuildTimestamp${BUILD_TIMESTAMP}
CodexGitCommit${GIT_COMMIT}
+ CodexBarTeamID${APP_TEAM_ID}
PLIST
@@ -292,6 +294,7 @@ if [[ -n "$(resolve_binary_path "CodexBarWidget" "${ARCH_LIST[0]}")" ]]; then
CFBundleShortVersionString${MARKETING_VERSION}
CFBundleVersion${BUILD_NUMBER}
LSMinimumSystemVersion14.0
+ CodexBarTeamID${APP_TEAM_ID}
NSExtension
NSExtensionPointIdentifiercom.apple.widgetkit-extension
diff --git a/Sources/CodexBar/SettingsStore.swift b/Sources/CodexBar/SettingsStore.swift
index 68ba707aa..d16439317 100644
--- a/Sources/CodexBar/SettingsStore.swift
+++ b/Sources/CodexBar/SettingsStore.swift
@@ -63,7 +63,7 @@ enum MenuBarMetricPreference: String, CaseIterable, Identifiable {
@MainActor
@Observable
final class SettingsStore {
- static let sharedDefaults = UserDefaults(suiteName: "group.com.steipete.codexbar")
+ static let sharedDefaults = AppGroupSupport.sharedDefaults()
static let mergedOverviewProviderLimit = 3
static let isRunningTests: Bool = {
let env = ProcessInfo.processInfo.environment
@@ -124,6 +124,20 @@ final class SettingsStore {
copilotTokenStore: any CopilotTokenStoring = KeychainCopilotTokenStore(),
tokenAccountStore: any ProviderTokenAccountStoring = FileTokenAccountStore())
{
+ let appGroupID = AppGroupSupport.currentGroupID()
+ let appGroupMigration = AppGroupSupport.migrateLegacyDataIfNeeded(standardDefaults: userDefaults)
+ let sharedDefaultsAvailable = Self.sharedDefaults != nil
+ if !Self.isRunningTests {
+ CodexBarLog.logger(LogCategories.settings).info(
+ "App group resolved",
+ metadata: [
+ "groupID": appGroupID,
+ "sharedDefaultsAvailable": sharedDefaultsAvailable ? "1" : "0",
+ "migrationStatus": appGroupMigration.status.rawValue,
+ "migratedSnapshot": appGroupMigration.copiedSnapshot ? "1" : "0",
+ ])
+ }
+
let hasStoredOpenAIWebAccessPreference = userDefaults.object(forKey: "openAIWebAccessEnabled") != nil
let hadExistingConfig = (try? configStore.load()) != nil
let legacyStores = CodexBarConfigMigrator.LegacyStores(
diff --git a/Sources/CodexBar/StatusItemController.swift b/Sources/CodexBar/StatusItemController.swift
index c818e13cc..fd00cf187 100644
--- a/Sources/CodexBar/StatusItemController.swift
+++ b/Sources/CodexBar/StatusItemController.swift
@@ -235,8 +235,8 @@ final class StatusItemController: NSObject, NSMenuDelegate, StatusItemControllin
// Status items for individual providers are now created lazily in updateVisibility()
super.init()
self.wireBindings()
- self.updateIcons()
self.updateVisibility()
+ self.updateIcons()
NotificationCenter.default.addObserver(
self,
selector: #selector(self.handleDebugReplayNotification(_:)),
diff --git a/Sources/CodexBarCore/AppGroupSupport.swift b/Sources/CodexBarCore/AppGroupSupport.swift
new file mode 100644
index 000000000..fa2237668
--- /dev/null
+++ b/Sources/CodexBarCore/AppGroupSupport.swift
@@ -0,0 +1,210 @@
+import Foundation
+#if os(macOS)
+import Security
+#endif
+
+public enum AppGroupSupport {
+ public static let defaultTeamID = "Y5PE65HELJ"
+ public static let teamIDInfoKey = "CodexBarTeamID"
+ public static let legacyReleaseGroupID = "group.com.steipete.codexbar"
+ public static let legacyDebugGroupID = "group.com.steipete.codexbar.debug"
+ public static let widgetSnapshotFilename = "widget-snapshot.json"
+ public static let migrationVersion = 1
+ public static let migrationVersionKey = "appGroupMigrationVersion"
+
+ public struct MigrationResult: Sendable {
+ public enum Status: String, Sendable {
+ case alreadyCompleted
+ case targetUnavailable
+ case noChangesNeeded
+ case migrated
+ }
+
+ public let status: Status
+ public let copiedSnapshot: Bool
+
+ public init(status: Status, copiedSnapshot: Bool = false) {
+ self.status = status
+ self.copiedSnapshot = copiedSnapshot
+ }
+ }
+
+ public static func currentGroupID(for bundleID: String? = Bundle.main.bundleIdentifier) -> String {
+ self.currentGroupID(teamID: self.resolvedTeamID(), bundleID: bundleID)
+ }
+
+ static func currentGroupID(teamID: String, bundleID: String?) -> String {
+ let base = "\(teamID).com.steipete.codexbar"
+ return self.isDebugBundleID(bundleID) ? "\(base).debug" : base
+ }
+
+ public static func resolvedTeamID(bundle: Bundle = .main) -> String {
+ self.resolvedTeamID(
+ infoDictionaryOverride: bundle.infoDictionary,
+ bundleURLOverride: bundle.bundleURL)
+ }
+
+ static func resolvedTeamID(
+ infoDictionaryOverride: [String: Any]?,
+ bundleURLOverride: URL?) -> String
+ {
+ if let teamID = self.codeSignatureTeamID(bundleURL: bundleURLOverride) {
+ return teamID
+ }
+ if let teamID = infoDictionaryOverride?[self.teamIDInfoKey] as? String,
+ !teamID.isEmpty
+ {
+ return teamID
+ }
+ return self.defaultTeamID
+ }
+
+ public static func legacyGroupID(for bundleID: String? = Bundle.main.bundleIdentifier) -> String {
+ self.isDebugBundleID(bundleID) ? self.legacyDebugGroupID : self.legacyReleaseGroupID
+ }
+
+ public static func sharedDefaults(
+ bundleID: String? = Bundle.main.bundleIdentifier,
+ fileManager: FileManager = .default)
+ -> UserDefaults?
+ {
+ guard self.currentContainerURL(bundleID: bundleID, fileManager: fileManager) != nil else { return nil }
+ return UserDefaults(suiteName: self.currentGroupID(for: bundleID))
+ }
+
+ public static func currentContainerURL(
+ bundleID: String? = Bundle.main.bundleIdentifier,
+ fileManager: FileManager = .default)
+ -> URL?
+ {
+ #if os(macOS)
+ fileManager.containerURL(forSecurityApplicationGroupIdentifier: self.currentGroupID(for: bundleID))
+ #else
+ nil
+ #endif
+ }
+
+ public static func snapshotURL(
+ bundleID: String? = Bundle.main.bundleIdentifier,
+ fileManager: FileManager = .default,
+ homeDirectory: URL = FileManager.default.homeDirectoryForCurrentUser)
+ -> URL
+ {
+ if let container = self.currentContainerURL(bundleID: bundleID, fileManager: fileManager) {
+ return container.appendingPathComponent(self.widgetSnapshotFilename, isDirectory: false)
+ }
+
+ let directory = self.localFallbackDirectory(fileManager: fileManager, homeDirectory: homeDirectory)
+ return directory.appendingPathComponent(self.widgetSnapshotFilename, isDirectory: false)
+ }
+
+ public static func localFallbackDirectory(
+ fileManager: FileManager = .default,
+ homeDirectory _: URL = FileManager.default.homeDirectoryForCurrentUser)
+ -> URL
+ {
+ let base = fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask).first
+ ?? fileManager.temporaryDirectory
+ let directory = base.appendingPathComponent("CodexBar", isDirectory: true)
+ try? fileManager.createDirectory(at: directory, withIntermediateDirectories: true)
+ return directory
+ }
+
+ public static func legacyContainerCandidateURL(
+ bundleID: String? = Bundle.main.bundleIdentifier,
+ homeDirectory: URL = FileManager.default.homeDirectoryForCurrentUser)
+ -> URL
+ {
+ homeDirectory
+ .appendingPathComponent("Library", isDirectory: true)
+ .appendingPathComponent("Group Containers", isDirectory: true)
+ .appendingPathComponent(self.legacyGroupID(for: bundleID), isDirectory: true)
+ }
+
+ public static func migrateLegacyDataIfNeeded(
+ bundleID: String? = Bundle.main.bundleIdentifier,
+ standardDefaults: UserDefaults = .standard,
+ fileManager: FileManager = .default,
+ homeDirectory: URL = FileManager.default.homeDirectoryForCurrentUser,
+ currentDefaultsOverride: UserDefaults? = nil,
+ currentSnapshotURLOverride: URL? = nil,
+ legacySnapshotURLOverride: URL? = nil)
+ -> MigrationResult
+ {
+ if standardDefaults.integer(forKey: self.migrationVersionKey) >= self.migrationVersion {
+ return MigrationResult(status: .alreadyCompleted)
+ }
+
+ guard currentDefaultsOverride ?? self.sharedDefaults(bundleID: bundleID, fileManager: fileManager) != nil else {
+ return MigrationResult(status: .targetUnavailable)
+ }
+
+ let currentSnapshotURL = currentSnapshotURLOverride
+ ?? self.currentContainerURL(bundleID: bundleID, fileManager: fileManager)?
+ .appendingPathComponent(self.widgetSnapshotFilename, isDirectory: false)
+ let legacySnapshotURL = legacySnapshotURLOverride
+ ?? self.legacyContainerCandidateURL(bundleID: bundleID, homeDirectory: homeDirectory)
+ .appendingPathComponent(self.widgetSnapshotFilename, isDirectory: false)
+
+ let copiedSnapshot = {
+ guard let currentSnapshotURL else { return false }
+ guard !fileManager.fileExists(atPath: currentSnapshotURL.path),
+ fileManager.fileExists(atPath: legacySnapshotURL.path)
+ else {
+ return false
+ }
+ do {
+ try fileManager.createDirectory(
+ at: currentSnapshotURL.deletingLastPathComponent(),
+ withIntermediateDirectories: true)
+ try fileManager.copyItem(at: legacySnapshotURL, to: currentSnapshotURL)
+ return true
+ } catch {
+ return false
+ }
+ }()
+
+ let result = if copiedSnapshot {
+ MigrationResult(status: .migrated, copiedSnapshot: true)
+ } else {
+ MigrationResult(status: .noChangesNeeded)
+ }
+
+ standardDefaults.set(self.migrationVersion, forKey: self.migrationVersionKey)
+ return result
+ }
+
+ private static func isDebugBundleID(_ bundleID: String?) -> Bool {
+ guard let bundleID, !bundleID.isEmpty else { return false }
+ return bundleID.contains(".debug")
+ }
+
+ private static func codeSignatureTeamID(bundleURL: URL?) -> String? {
+ #if os(macOS)
+ guard let bundleURL else { return nil }
+
+ var staticCode: SecStaticCode?
+ guard SecStaticCodeCreateWithPath(bundleURL as CFURL, SecCSFlags(), &staticCode) == errSecSuccess,
+ let code = staticCode
+ else {
+ return nil
+ }
+
+ var infoCF: CFDictionary?
+ guard SecCodeCopySigningInformation(
+ code,
+ SecCSFlags(rawValue: kSecCSSigningInformation),
+ &infoCF) == errSecSuccess,
+ let info = infoCF as? [String: Any],
+ let teamID = info[kSecCodeInfoTeamIdentifier as String] as? String,
+ !teamID.isEmpty
+ else {
+ return nil
+ }
+ return teamID
+ #else
+ _ = bundleURL
+ return nil
+ #endif
+ }
+}
diff --git a/Sources/CodexBarCore/KeychainAccessGate.swift b/Sources/CodexBarCore/KeychainAccessGate.swift
index 548a4add4..86451a75d 100644
--- a/Sources/CodexBarCore/KeychainAccessGate.swift
+++ b/Sources/CodexBarCore/KeychainAccessGate.swift
@@ -5,7 +5,6 @@ import SweetCookieKit
public enum KeychainAccessGate {
private static let flagKey = "debugDisableKeychainAccess"
- private static let appGroupID = "group.com.steipete.codexbar"
@TaskLocal private static var taskOverrideValue: Bool?
private nonisolated(unsafe) static var overrideValue: Bool?
@@ -19,9 +18,7 @@ public enum KeychainAccessGate {
#endif
if let overrideValue { return overrideValue }
if UserDefaults.standard.bool(forKey: Self.flagKey) { return true }
- if let shared = UserDefaults(suiteName: Self.appGroupID),
- shared.bool(forKey: Self.flagKey)
- {
+ if let shared = AppGroupSupport.sharedDefaults(), shared.bool(forKey: Self.flagKey) {
return true
}
return false
diff --git a/Sources/CodexBarCore/WidgetSnapshot.swift b/Sources/CodexBarCore/WidgetSnapshot.swift
index 0dc371f02..d87c4dcca 100644
--- a/Sources/CodexBarCore/WidgetSnapshot.swift
+++ b/Sources/CodexBarCore/WidgetSnapshot.swift
@@ -114,17 +114,16 @@ public struct WidgetSnapshot: Codable, Sendable {
}
public enum WidgetSnapshotStore {
- public static let appGroupID = "group.com.steipete.codexbar"
- private static let filename = "widget-snapshot.json"
+ private static let filename = AppGroupSupport.widgetSnapshotFilename
public static func load(bundleID: String? = Bundle.main.bundleIdentifier) -> WidgetSnapshot? {
- guard let url = self.snapshotURL(bundleID: bundleID) else { return nil }
+ let url = self.snapshotURL(bundleID: bundleID)
guard let data = try? Data(contentsOf: url) else { return nil }
return try? self.decoder.decode(WidgetSnapshot.self, from: data)
}
public static func save(_ snapshot: WidgetSnapshot, bundleID: String? = Bundle.main.bundleIdentifier) {
- guard let url = self.snapshotURL(bundleID: bundleID) else { return }
+ let url = self.snapshotURL(bundleID: bundleID)
do {
let data = try self.encoder.encode(snapshot)
try data.write(to: url, options: [.atomic])
@@ -133,32 +132,12 @@ public enum WidgetSnapshotStore {
}
}
- private static func snapshotURL(bundleID: String?) -> URL? {
- let fm = FileManager.default
- let groupID = self.groupID(for: bundleID)
- #if os(macOS)
- if let groupID, let container = fm.containerURL(forSecurityApplicationGroupIdentifier: groupID) {
- return container.appendingPathComponent(self.filename, isDirectory: false)
- }
- #endif
-
- let base = fm.urls(for: .applicationSupportDirectory, in: .userDomainMask).first
- ?? fm.temporaryDirectory
- let dir = base.appendingPathComponent("CodexBar", isDirectory: true)
- try? fm.createDirectory(at: dir, withIntermediateDirectories: true)
- return dir.appendingPathComponent(self.filename, isDirectory: false)
+ private static func snapshotURL(bundleID: String?) -> URL {
+ AppGroupSupport.snapshotURL(bundleID: bundleID)
}
public static func appGroupID(for bundleID: String?) -> String? {
- self.groupID(for: bundleID)
- }
-
- private static func groupID(for bundleID: String?) -> String? {
- guard let bundleID, !bundleID.isEmpty else { return self.appGroupID }
- if bundleID.contains(".debug") {
- return "group.com.steipete.codexbar.debug"
- }
- return self.appGroupID
+ AppGroupSupport.currentGroupID(for: bundleID)
}
private static var encoder: JSONEncoder {
@@ -178,7 +157,7 @@ public enum WidgetSelectionStore {
private static let selectedProviderKey = "widgetSelectedProvider"
public static func loadSelectedProvider(bundleID: String? = Bundle.main.bundleIdentifier) -> UsageProvider? {
- guard let defaults = self.sharedDefaults(bundleID: bundleID) else { return nil }
+ let defaults = self.sharedDefaults(bundleID: bundleID)
guard let raw = defaults.string(forKey: self.selectedProviderKey) else { return nil }
return UsageProvider(rawValue: raw)
}
@@ -187,12 +166,11 @@ public enum WidgetSelectionStore {
_ provider: UsageProvider,
bundleID: String? = Bundle.main.bundleIdentifier)
{
- guard let defaults = self.sharedDefaults(bundleID: bundleID) else { return }
+ let defaults = self.sharedDefaults(bundleID: bundleID)
defaults.set(provider.rawValue, forKey: self.selectedProviderKey)
}
- private static func sharedDefaults(bundleID: String?) -> UserDefaults? {
- guard let groupID = WidgetSnapshotStore.appGroupID(for: bundleID) else { return nil }
- return UserDefaults(suiteName: groupID)
+ private static func sharedDefaults(bundleID: String?) -> UserDefaults {
+ AppGroupSupport.sharedDefaults(bundleID: bundleID) ?? .standard
}
}
diff --git a/Sources/CodexBarWidget/CodexBarWidgetProvider.swift b/Sources/CodexBarWidget/CodexBarWidgetProvider.swift
index 7e4a7ddb0..e100f91b9 100644
--- a/Sources/CodexBarWidget/CodexBarWidgetProvider.swift
+++ b/Sources/CodexBarWidget/CodexBarWidgetProvider.swift
@@ -183,7 +183,7 @@ struct CodexBarTimelineProvider: AppIntentTimelineProvider {
in context: Context) async -> Timeline
{
let provider = configuration.provider.provider
- let snapshot = WidgetSnapshotStore.load() ?? WidgetPreviewData.snapshot()
+ let snapshot = WidgetSnapshotStore.load() ?? WidgetPreviewData.emptySnapshot()
let entry = CodexBarWidgetEntry(date: Date(), provider: provider, snapshot: snapshot)
let refresh = Date().addingTimeInterval(30 * 60)
return Timeline(entries: [entry], policy: .after(refresh))
@@ -212,7 +212,7 @@ struct CodexBarSwitcherTimelineProvider: TimelineProvider {
}
private func makeEntry() -> CodexBarSwitcherEntry {
- let snapshot = WidgetSnapshotStore.load() ?? WidgetPreviewData.snapshot()
+ let snapshot = WidgetSnapshotStore.load() ?? WidgetPreviewData.emptySnapshot()
let providers = self.availableProviders(from: snapshot)
let stored = WidgetSelectionStore.loadSelectedProvider()
let selected = providers.first { $0 == stored } ?? providers.first ?? .codex
@@ -261,7 +261,7 @@ struct CodexBarCompactTimelineProvider: AppIntentTimelineProvider {
in context: Context) async -> Timeline
{
let provider = configuration.provider.provider
- let snapshot = WidgetSnapshotStore.load() ?? WidgetPreviewData.snapshot()
+ let snapshot = WidgetSnapshotStore.load() ?? WidgetPreviewData.emptySnapshot()
let entry = CodexBarCompactEntry(
date: Date(),
provider: provider,
@@ -273,6 +273,10 @@ struct CodexBarCompactTimelineProvider: AppIntentTimelineProvider {
}
enum WidgetPreviewData {
+ static func emptySnapshot() -> WidgetSnapshot {
+ WidgetSnapshot(entries: [], enabledProviders: [], generatedAt: Date())
+ }
+
static func snapshot() -> WidgetSnapshot {
let primary = RateWindow(usedPercent: 35, windowMinutes: nil, resetsAt: nil, resetDescription: "Resets in 4h")
let secondary = RateWindow(usedPercent: 60, windowMinutes: nil, resetsAt: nil, resetDescription: "Resets in 3d")
diff --git a/Tests/CodexBarTests/AppGroupSupportTests.swift b/Tests/CodexBarTests/AppGroupSupportTests.swift
new file mode 100644
index 000000000..249f109c2
--- /dev/null
+++ b/Tests/CodexBarTests/AppGroupSupportTests.swift
@@ -0,0 +1,88 @@
+import Foundation
+import Testing
+@testable import CodexBarCore
+
+struct AppGroupSupportTests {
+ @Test
+ func `app group identifiers use resolved team-prefixed release and debug variants`() {
+ #expect(
+ AppGroupSupport.currentGroupID(teamID: "Y5PE65HELJ", bundleID: "com.steipete.codexbar")
+ == "Y5PE65HELJ.com.steipete.codexbar")
+ #expect(
+ AppGroupSupport.currentGroupID(teamID: "ABCDE12345", bundleID: "com.steipete.codexbar.debug")
+ == "ABCDE12345.com.steipete.codexbar.debug")
+ #expect(
+ AppGroupSupport.legacyGroupID(for: "com.steipete.codexbar")
+ == "group.com.steipete.codexbar")
+ #expect(
+ AppGroupSupport.legacyGroupID(for: "com.steipete.codexbar.debug")
+ == "group.com.steipete.codexbar.debug")
+ }
+
+ @Test
+ func `resolved team id falls back to plist and then default`() {
+ #expect(
+ AppGroupSupport.resolvedTeamID(
+ infoDictionaryOverride: [AppGroupSupport.teamIDInfoKey: "ABCDE12345"],
+ bundleURLOverride: nil) == "ABCDE12345")
+ #expect(
+ AppGroupSupport.resolvedTeamID(
+ infoDictionaryOverride: nil,
+ bundleURLOverride: nil) == AppGroupSupport.defaultTeamID)
+ }
+
+ @Test
+ func `legacy migration copies snapshot once`() throws {
+ let fileManager = FileManager.default
+ let root = fileManager.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true)
+ try fileManager.createDirectory(at: root, withIntermediateDirectories: true)
+ defer { try? fileManager.removeItem(at: root) }
+
+ let standardSuite = "AppGroupSupportTests-standard-\(UUID().uuidString)"
+ let currentSuite = "AppGroupSupportTests-current-\(UUID().uuidString)"
+ let legacySuite = "AppGroupSupportTests-legacy-\(UUID().uuidString)"
+
+ let standardDefaults = try #require(UserDefaults(suiteName: standardSuite))
+ let currentDefaults = try #require(UserDefaults(suiteName: currentSuite))
+ let legacyDefaults = try #require(UserDefaults(suiteName: legacySuite))
+ standardDefaults.removePersistentDomain(forName: standardSuite)
+ currentDefaults.removePersistentDomain(forName: currentSuite)
+ legacyDefaults.removePersistentDomain(forName: legacySuite)
+
+ legacyDefaults.set(true, forKey: "debugDisableKeychainAccess")
+ legacyDefaults.set(UsageProvider.cursor.rawValue, forKey: "widgetSelectedProvider")
+
+ let legacySnapshotURL = root.appendingPathComponent(
+ "legacy/widget-snapshot.json",
+ isDirectory: false)
+ try fileManager.createDirectory(
+ at: legacySnapshotURL.deletingLastPathComponent(),
+ withIntermediateDirectories: true)
+ try Data("legacy-snapshot".utf8).write(to: legacySnapshotURL)
+
+ let currentSnapshotURL = root.appendingPathComponent("current/widget-snapshot.json", isDirectory: false)
+ let result = AppGroupSupport.migrateLegacyDataIfNeeded(
+ bundleID: "com.steipete.codexbar",
+ standardDefaults: standardDefaults,
+ currentDefaultsOverride: currentDefaults,
+ currentSnapshotURLOverride: currentSnapshotURL,
+ legacySnapshotURLOverride: legacySnapshotURL)
+
+ #expect(result.status == .migrated)
+ #expect(result.copiedSnapshot)
+ #expect(!currentDefaults.bool(forKey: "debugDisableKeychainAccess"))
+ #expect(currentDefaults.string(forKey: "widgetSelectedProvider") == nil)
+ #expect(fileManager.fileExists(atPath: currentSnapshotURL.path))
+ #expect(
+ standardDefaults.integer(forKey: AppGroupSupport.migrationVersionKey)
+ == AppGroupSupport.migrationVersion)
+
+ let secondResult = AppGroupSupport.migrateLegacyDataIfNeeded(
+ bundleID: "com.steipete.codexbar",
+ standardDefaults: standardDefaults,
+ currentDefaultsOverride: currentDefaults,
+ currentSnapshotURLOverride: currentSnapshotURL,
+ legacySnapshotURLOverride: legacySnapshotURL)
+ #expect(secondResult.status == .alreadyCompleted)
+ }
+}
diff --git a/Tests/CodexBarTests/CodexBarWidgetProviderTests.swift b/Tests/CodexBarTests/CodexBarWidgetProviderTests.swift
index 71c6461de..56c376811 100644
--- a/Tests/CodexBarTests/CodexBarWidgetProviderTests.swift
+++ b/Tests/CodexBarTests/CodexBarWidgetProviderTests.swift
@@ -16,6 +16,13 @@ struct CodexBarWidgetProviderTests {
#expect(ProviderChoice.opencodego.provider == .opencodego)
}
+ @Test
+ func `supported providers fall back to codex when snapshot is empty`() {
+ let snapshot = WidgetSnapshot(entries: [], enabledProviders: [], generatedAt: Date())
+
+ #expect(CodexBarSwitcherTimelineProvider.supportedProviders(from: snapshot) == [.codex])
+ }
+
@Test
func `supported providers keep alibaba when it is the only enabled provider`() {
let now = Date(timeIntervalSince1970: 1_700_000_000)