import Foundation import Testing @testable import CodexBarCore struct CopilotUsageModelsTests { @Test func `decodes quota snapshots payload`() throws { let response = try Self.decodeFixture( """ { "copilot_plan": "free", "assigned_date": "2025-01-01", "quota_reset_date": "2025-02-01", "quota_snapshots": { "premium_interactions": { "entitlement": 500, "remaining": 450, "percent_remaining": 90, "quota_id": "premium_interactions" }, "chat": { "entitlement": 300, "remaining": 150, "percent_remaining": 50, "quota_id": "chat" } } } """) #expect(response.copilotPlan == "free") #expect(response.assignedDate == "2025-01-01") #expect(response.quotaResetDate == "2025-02-01") #expect(response.quotaSnapshots.premiumInteractions?.remaining == 450) #expect(response.quotaSnapshots.chat?.remaining == 150) } @Test func `decodes chat only quota snapshots payload`() throws { let response = try Self.decodeFixture( """ { "copilot_plan": "free", "quota_snapshots": { "chat": { "entitlement": 200, "remaining": 75, "percent_remaining": 37.5, "quota_id": "chat" } } } """) #expect(response.quotaSnapshots.premiumInteractions == nil) #expect(response.quotaSnapshots.chat?.quotaId == "chat") #expect(response.quotaSnapshots.chat?.entitlement == 200) #expect(response.quotaSnapshots.chat?.remaining == 75) } @Test func `preserves missing date fields as nil`() throws { let response = try Self.decodeFixture( """ { "copilot_plan": "free", "quota_snapshots": { "chat": { "entitlement": 200, "remaining": 75, "percent_remaining": 37.5, "quota_id": "chat" } } } """) #expect(response.assignedDate == nil) #expect(response.quotaResetDate == nil) } @Test func `preserves explicit empty date fields`() throws { let response = try Self.decodeFixture( """ { "copilot_plan": "free", "assigned_date": "", "quota_reset_date": "", "quota_snapshots": { "chat": { "entitlement": 200, "remaining": 75, "percent_remaining": 37.5, "quota_id": "chat" } } } """) #expect(response.assignedDate?.isEmpty == true) #expect(response.quotaResetDate?.isEmpty == true) } @Test func `decodes monthly and limited quota payload`() throws { let response = try Self.decodeFixture( """ { "copilot_plan": "free", "monthly_quotas": { "chat": "500", "completions": 300 }, "limited_user_quotas": { "chat": 125, "completions": "75" } } """) #expect(response.quotaSnapshots.premiumInteractions?.quotaId == "completions") #expect(response.quotaSnapshots.premiumInteractions?.entitlement == 300) #expect(response.quotaSnapshots.premiumInteractions?.remaining == 75) #expect(response.quotaSnapshots.premiumInteractions?.percentRemaining == 25) #expect(response.quotaSnapshots.chat?.quotaId == "chat") #expect(response.quotaSnapshots.chat?.entitlement == 500) #expect(response.quotaSnapshots.chat?.remaining == 125) #expect(response.quotaSnapshots.chat?.percentRemaining == 25) } @Test func `does not assume full quota when limited quotas are missing`() throws { let response = try Self.decodeFixture( """ { "copilot_plan": "free", "monthly_quotas": { "chat": 500, "completions": 300 } } """) #expect(response.quotaSnapshots.premiumInteractions == nil) #expect(response.quotaSnapshots.chat == nil) } @Test func `computes monthly fallback per quota only when limited value exists`() throws { let response = try Self.decodeFixture( """ { "copilot_plan": "free", "monthly_quotas": { "chat": 500, "completions": 300 }, "limited_user_quotas": { "completions": 60 } } """) #expect(response.quotaSnapshots.premiumInteractions?.quotaId == "completions") #expect(response.quotaSnapshots.premiumInteractions?.entitlement == 300) #expect(response.quotaSnapshots.premiumInteractions?.remaining == 60) #expect(response.quotaSnapshots.premiumInteractions?.percentRemaining == 20) #expect(response.quotaSnapshots.chat == nil) } @Test func `merges direct and monthly fallback lanes when direct is partial`() throws { let response = try Self.decodeFixture( """ { "copilot_plan": "free", "quota_snapshots": { "chat": { "entitlement": 200, "remaining": 75, "percent_remaining": 37.5, "quota_id": "chat" } }, "monthly_quotas": { "chat": 500, "completions": 300 }, "limited_user_quotas": { "chat": 125, "completions": 60 } } """) #expect(response.quotaSnapshots.chat?.quotaId == "chat") #expect(response.quotaSnapshots.chat?.entitlement == 200) #expect(response.quotaSnapshots.chat?.remaining == 75) #expect(response.quotaSnapshots.chat?.percentRemaining == 37.5) #expect(response.quotaSnapshots.premiumInteractions?.quotaId == "completions") #expect(response.quotaSnapshots.premiumInteractions?.entitlement == 300) #expect(response.quotaSnapshots.premiumInteractions?.remaining == 60) #expect(response.quotaSnapshots.premiumInteractions?.percentRemaining == 20) } @Test func `decodes unknown quota snapshot keys using fallback`() throws { let response = try Self.decodeFixture( """ { "copilot_plan": "free", "quota_snapshots": { "mystery_bucket": { "entitlement": 100, "remaining": 40, "percent_remaining": 40, "quota_id": "mystery_bucket" } } } """) #expect(response.quotaSnapshots.premiumInteractions == nil) #expect(response.quotaSnapshots.chat?.quotaId == "mystery_bucket") #expect(response.quotaSnapshots.chat?.entitlement == 100) #expect(response.quotaSnapshots.chat?.remaining == 40) #expect(response.quotaSnapshots.chat?.percentRemaining == 40) } @Test func `ignores placeholder known snapshot when selecting unknown key fallback`() throws { let response = try Self.decodeFixture( """ { "copilot_plan": "free", "quota_snapshots": { "premium_interactions": {}, "mystery_bucket": { "entitlement": 100, "remaining": 40, "percent_remaining": 40, "quota_id": "mystery_bucket" } } } """) #expect(response.quotaSnapshots.premiumInteractions == nil) #expect(response.quotaSnapshots.chat?.quotaId == "mystery_bucket") #expect(response.quotaSnapshots.chat?.hasPercentRemaining == true) } @Test func `derives percent remaining when missing but entitlement exists`() throws { let response = try Self.decodeFixture( """ { "copilot_plan": "free", "quota_snapshots": { "chat": { "entitlement": 120, "remaining": 30, "quota_id": "chat" } } } """) #expect(response.quotaSnapshots.chat?.hasPercentRemaining == true) #expect(response.quotaSnapshots.chat?.percentRemaining == 25) } @Test func `preserves over quota percent remaining`() throws { let response = try Self.decodeFixture( """ { "copilot_plan": "paid", "quota_snapshots": { "premium_interactions": { "entitlement": 500, "remaining": -75, "percent_remaining": -15, "quota_id": "premium_interactions" } } } """) let snapshot = try #require(response.quotaSnapshots.premiumInteractions) #expect(snapshot.percentRemaining == -15) #expect(snapshot.usedPercent == 115) #expect(snapshot.overQuotaUsedPercent == 115) let window = try #require(CopilotUsageFetcher.makeRateWindow(from: snapshot)) #expect(window.usedPercent == 115) #expect(window.resetDescription == "115% used") } @Test func `derives over quota percent from negative remaining`() throws { let response = try Self.decodeFixture( """ { "copilot_plan": "paid", "quota_snapshots": { "chat": { "entitlement": 500, "remaining": -75, "quota_id": "chat" } } } """) let snapshot = try #require(response.quotaSnapshots.chat) #expect(snapshot.hasPercentRemaining) #expect(snapshot.percentRemaining == -15) #expect(snapshot.usedPercent == 115) #expect(snapshot.overQuotaUsedPercent == 115) } @Test func `marks percent remaining as unavailable when underdetermined`() throws { let response = try Self.decodeFixture( """ { "copilot_plan": "free", "quota_snapshots": { "chat": { "remaining": 30, "quota_id": "chat" } } } """) #expect(response.quotaSnapshots.chat?.hasPercentRemaining == false) #expect(response.quotaSnapshots.chat?.percentRemaining == 0) } @Test func `marks percent remaining as unavailable when remaining is missing`() throws { let response = try Self.decodeFixture( """ { "copilot_plan": "free", "quota_snapshots": { "chat": { "entitlement": 120, "quota_id": "chat" } } } """) #expect(response.quotaSnapshots.chat?.hasPercentRemaining == false) #expect(response.quotaSnapshots.chat?.percentRemaining == 0) } @Test func `falls back to monthly when direct snapshot is missing remaining`() throws { let response = try Self.decodeFixture( """ { "copilot_plan": "free", "quota_snapshots": { "chat": { "entitlement": 120, "quota_id": "chat" } }, "monthly_quotas": { "chat": 400 }, "limited_user_quotas": { "chat": 100 } } """) #expect(response.quotaSnapshots.premiumInteractions == nil) #expect(response.quotaSnapshots.chat?.quotaId == "chat") #expect(response.quotaSnapshots.chat?.entitlement == 400) #expect(response.quotaSnapshots.chat?.remaining == 100) #expect(response.quotaSnapshots.chat?.percentRemaining == 25) #expect(response.quotaSnapshots.chat?.hasPercentRemaining == true) } @Test func `falls back to monthly when direct snapshots lack computable percent`() throws { let response = try Self.decodeFixture( """ { "copilot_plan": "free", "quota_snapshots": { "chat": { "remaining": 30, "quota_id": "chat" } }, "monthly_quotas": { "chat": 400 }, "limited_user_quotas": { "chat": 100 } } """) #expect(response.quotaSnapshots.premiumInteractions == nil) #expect(response.quotaSnapshots.chat?.quotaId == "chat") #expect(response.quotaSnapshots.chat?.entitlement == 400) #expect(response.quotaSnapshots.chat?.remaining == 100) #expect(response.quotaSnapshots.chat?.percentRemaining == 25) #expect(response.quotaSnapshots.chat?.hasPercentRemaining == true) } @Test func `skips monthly fallback when monthly denominator is zero`() throws { let response = try Self.decodeFixture( """ { "copilot_plan": "free", "monthly_quotas": { "chat": 0 }, "limited_user_quotas": { "chat": 0 } } """) #expect(response.quotaSnapshots.premiumInteractions == nil) #expect(response.quotaSnapshots.chat == nil) } @Test func `treats business token billing zero entitlement quotas as unavailable`() throws { // GitHub Copilot Business token-based billing reports every quota as // entitlement=0, remaining=0, percent_remaining=100. That previously rendered as a // misleading "0% used" (100 - 100). A zero-entitlement quota carries no usage signal, // so the snapshots must drop out instead of showing as usage. (#1258) let response = try Self.decodeFixture( """ { "copilot_plan": "business", "token_based_billing": true, "quota_snapshots": { "premium_interactions": { "entitlement": 0, "remaining": 0, "percent_remaining": 100, "quota_id": "premium_interactions" }, "chat": { "entitlement": 0, "remaining": 0, "percent_remaining": 100, "quota_id": "chat" }, "completions": { "entitlement": 0, "remaining": 0, "percent_remaining": 100, "quota_id": "completions" } } } """) #expect(response.tokenBasedBilling) #expect(response.quotaSnapshots.premiumInteractions == nil) #expect(response.quotaSnapshots.chat == nil) } @Test func `keeps unlimited chat fallback quota without percent remaining`() throws { let response = try Self.decodeFixture( """ { "copilot_plan": "individual", "quota_snapshots": { "premium_interactions": { "entitlement": 200, "remaining": 191, "percent_remaining": 95.5, "quota_id": "premium_interactions" }, "chat_messages": { "entitlement": 0, "remaining": 0, "quota_id": "chat_messages", "unlimited": true } } } """) #expect(response.quotaSnapshots.premiumInteractions?.quotaId == "premium_interactions") #expect(response.quotaSnapshots.chat?.quotaId == "chat_messages") #expect(response.quotaSnapshots.chat?.unlimited == true) #expect(response.quotaSnapshots.chat?.usedPercent == 0) } @Test func `unlimited quota overrides placeholder percent remaining`() throws { let response = try Self.decodeFixture( """ { "copilot_plan": "individual", "quota_snapshots": { "chat": { "entitlement": 0, "remaining": 0, "percent_remaining": 0, "quota_id": "chat", "unlimited": true } } } """) let chat = try #require(response.quotaSnapshots.chat) #expect(chat.percentRemaining == 100) #expect(chat.usedPercent == 0) #expect(!chat.isPlaceholder) } @Test func `flags zero entitlement snapshot as placeholder`() { let snapshot = CopilotUsageResponse.QuotaSnapshot( entitlement: 0, remaining: 0, percentRemaining: 100, quotaId: "chat") #expect(snapshot.isPlaceholder) } @Test func `keeps fully consumed quota with positive entitlement`() { // entitlement > 0 with remaining 0 is a real "100% used" window, not a placeholder. let snapshot = CopilotUsageResponse.QuotaSnapshot( entitlement: 500, remaining: 0, percentRemaining: 0, quotaId: "premium_interactions") #expect(!snapshot.isPlaceholder) #expect(snapshot.usedPercent == 100) } @Test func `keeps percent only quota snapshots available`() throws { let response = try Self.decodeFixture( """ { "copilot_plan": "business", "quota_snapshots": { "chat": { "percent_remaining": 40, "quota_id": "chat" } } } """) #expect(response.quotaSnapshots.chat?.percentRemaining == 40) #expect(response.quotaSnapshots.chat?.usedPercent == 60) #expect(response.quotaSnapshots.chat?.isPlaceholder == false) } private static func decodeFixture(_ fixture: String) throws -> CopilotUsageResponse { try JSONDecoder().decode(CopilotUsageResponse.self, from: Data(fixture.utf8)) } }