import CodexBarCore import Foundation import Testing @Suite(.serialized) struct CodexActiveSourceConfigTests { @Test func `legacy config without codex active source decodes to nil`() throws { let legacyJSON = """ { "version": 1, "providers": [ { "id": "codex" } ] } """ let decoded = try JSONDecoder().decode( CodexBarConfig.self, from: Data(legacyJSON.utf8)) #expect(decoded.providerConfig(for: .codex)?.codexActiveSource == nil) #expect(decoded.providerConfig(for: .codex)?.quotaWarnings == nil) } @Test func `provider config round trips quota warning overrides`() throws { let config = CodexBarConfig( providers: [ ProviderConfig( id: .codex, quotaWarnings: QuotaWarningConfig( session: QuotaWarningWindowConfig(thresholds: [10]), weekly: QuotaWarningWindowConfig(thresholds: [50, 20]))), ]) let data = try JSONEncoder().encode(config) let decoded = try JSONDecoder().decode(CodexBarConfig.self, from: data) let quotaWarnings = try #require(decoded.providerConfig(for: .codex)?.quotaWarnings) #expect(quotaWarnings.thresholds(for: .session, global: [80]) == [10]) #expect(quotaWarnings.thresholds(for: .weekly, global: [80]) == [50, 20]) } @Test func `quota warning window enabled defaults stay backward compatible`() throws { let legacyJSON = """ { "version": 1, "providers": [ { "id": "codex", "quotaWarnings": { "session": { "thresholds": [10] }, "weekly": { "enabled": false } } } ] } """ let decoded = try JSONDecoder().decode(CodexBarConfig.self, from: Data(legacyJSON.utf8)) let quotaWarnings = try #require(decoded.providerConfig(for: .codex)?.quotaWarnings) #expect(quotaWarnings.isEnabled(for: .session, global: false) == true) #expect(quotaWarnings.isEnabled(for: .weekly, global: true) == false) #expect(quotaWarnings.hasOverride(for: .session) == true) #expect(quotaWarnings.hasOverride(for: .weekly) == true) } @Test func `provider config encodes live system active source with expected schema`() throws { let config = CodexBarConfig( providers: [ ProviderConfig( id: .codex, codexActiveSource: .liveSystem), ]) let data = try JSONEncoder().encode(config) let object = try JSONSerialization.jsonObject(with: data) as? [String: Any] let providers = try #require(object?["providers"] as? [[String: Any]]) let provider = try #require(providers.first(where: { $0["id"] as? String == "codex" })) let activeSource = try #require(provider["codexActiveSource"] as? [String: Any]) #expect(activeSource.count == 1) #expect(activeSource["kind"] as? String == "liveSystem") #expect(activeSource["accountID"] == nil) } @Test func `provider config encodes managed account active source with expected schema`() throws { let accountID = UUID() let config = CodexBarConfig( providers: [ ProviderConfig( id: .codex, codexActiveSource: .managedAccount(id: accountID)), ]) let data = try JSONEncoder().encode(config) let object = try JSONSerialization.jsonObject(with: data) as? [String: Any] let providers = try #require(object?["providers"] as? [[String: Any]]) let provider = try #require(providers.first(where: { $0["id"] as? String == "codex" })) let activeSource = try #require(provider["codexActiveSource"] as? [String: Any]) #expect(activeSource.count == 2) #expect(activeSource["kind"] as? String == "managedAccount") #expect((activeSource["accountID"] as? String) == accountID.uuidString) } @Test func `provider config encodes profile home source in downgrade readable envelope`() throws { let config = CodexBarConfig( providers: [ ProviderConfig( id: .codex, codexActiveSource: .profileHome(path: "/Users/test/.codex-work"), codexProfileHomePaths: ["/Users/test/.codex-work"]), ]) let data = try JSONEncoder().encode(config) let object = try JSONSerialization.jsonObject(with: data) as? [String: Any] let providers = try #require(object?["providers"] as? [[String: Any]]) let provider = try #require(providers.first(where: { $0["id"] as? String == "codex" })) let activeSource = try #require(provider["codexActiveSource"] as? [String: Any]) #expect(activeSource.count == 2) #expect(activeSource["kind"] as? String == "liveSystem") #expect(activeSource["homePath"] as? String == "/Users/test/.codex-work") #expect(provider["codexProfileHomePaths"] as? [String] == ["/Users/test/.codex-work"]) let releasedConfig = try JSONDecoder().decode(ReleasedCodexBarConfig.self, from: data) #expect(releasedConfig.providers.first?.codexActiveSource == .liveSystem) } @Test func `provider config round trips live system active source`() throws { let config = CodexBarConfig( providers: [ ProviderConfig( id: .codex, codexActiveSource: .liveSystem), ]) let data = try JSONEncoder().encode(config) let decoded = try JSONDecoder().decode(CodexBarConfig.self, from: data) #expect(decoded.providerConfig(for: .codex)?.codexActiveSource == .liveSystem) } @Test func `provider config round trips managed account active source`() throws { let accountID = UUID() let config = CodexBarConfig( providers: [ ProviderConfig( id: .codex, codexActiveSource: .managedAccount(id: accountID)), ]) let data = try JSONEncoder().encode(config) let decoded = try JSONDecoder().decode(CodexBarConfig.self, from: data) #expect(decoded.providerConfig(for: .codex)?.codexActiveSource == .managedAccount(id: accountID)) } @Test func `provider config round trips profile home active source`() throws { let config = CodexBarConfig( providers: [ ProviderConfig( id: .codex, codexActiveSource: .profileHome(path: "/Users/test/.codex-work"), codexProfileHomePaths: ["/Users/test/.codex-work"]), ]) let data = try JSONEncoder().encode(config) let decoded = try JSONDecoder().decode(CodexBarConfig.self, from: data) let providerConfig = decoded.providerConfig(for: .codex) #expect(providerConfig?.codexActiveSource == .profileHome(path: "/Users/test/.codex-work")) #expect(providerConfig?.codexProfileHomePaths == ["/Users/test/.codex-work"]) } @Test func `profile home discriminator written by development builds still decodes`() throws { let data = Data(#"{"kind":"profileHome","homePath":"/Users/test/.codex-work"}"#.utf8) let decoded = try JSONDecoder().decode(CodexActiveSource.self, from: data) let canonicalData = try JSONEncoder().encode(decoded) let canonical = try #require(JSONSerialization.jsonObject(with: canonicalData) as? [String: Any]) #expect(decoded == .profileHome(path: "/Users/test/.codex-work")) #expect(canonical["kind"] as? String == "liveSystem") #expect(canonical["homePath"] as? String == "/Users/test/.codex-work") } @Test func `blank profile home sentinel falls back to live system`() throws { let data = Data(#"{"kind":"liveSystem","homePath":" "}"#.utf8) let decoded = try JSONDecoder().decode(CodexActiveSource.self, from: data) #expect(decoded == .liveSystem) } } private struct ReleasedCodexBarConfig: Decodable { let providers: [ReleasedProviderConfig] } private struct ReleasedProviderConfig: Decodable { let codexActiveSource: ReleasedCodexActiveSource? } private enum ReleasedCodexActiveSource: Decodable, Equatable { case liveSystem case managedAccount(id: UUID) private enum CodingKeys: String, CodingKey { case kind case accountID } private enum Kind: String, Decodable { case liveSystem case managedAccount } init(from decoder: any Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) switch try container.decode(Kind.self, forKey: .kind) { case .liveSystem: self = .liveSystem case .managedAccount: self = try .managedAccount(id: container.decode(UUID.self, forKey: .accountID)) } } }