227 lines · 8.2 KB · Swift
RawDownload
import Foundationimport Testing@testable import CodexBarCore struct ClaudeAdminAPIUsageTests {    private func makeContext(        apiKey: String = "sk-ant-admin-test",        sourceMode: ProviderSourceMode = .api) -> ProviderFetchContext    {        let browserDetection = BrowserDetection(cacheTTL: 0)        let env = [ClaudeAdminAPISettingsReader.adminAPIKeyEnvironmentKey: apiKey]        return ProviderFetchContext(            runtime: .app,            sourceMode: sourceMode,            includeCredits: false,            webTimeout: 1,            webDebugDumpHTML: false,            verbose: false,            env: env,            settings: nil,            fetcher: UsageFetcher(environment: env),            claudeFetcher: ClaudeUsageFetcher(browserDetection: browserDetection),            browserDetection: browserDetection)    }     @Test    func `prefers primary Anthropic admin key environment variable`() {        let token = ClaudeAdminAPISettingsReader.apiKey(environment: [            ClaudeAdminAPISettingsReader.alternateAdminAPIKeyEnvironmentKey: "sk-ant-admin-alt",            ClaudeAdminAPISettingsReader.adminAPIKeyEnvironmentKey: "sk-ant-admin-primary",        ])         #expect(token == "sk-ant-admin-primary")    }     @Test    func `routes Claude token account admin keys into admin api environment`() {        let env = TokenAccountSupportCatalog.envOverride(for: .claude, token: "Bearer sk-ant-admin-token")         #expect(env?[ClaudeAdminAPISettingsReader.adminAPIKeyEnvironmentKey] == "sk-ant-admin-token")    }     @Test    func `auto source uses configured admin api key`() async {        let descriptor = ProviderDescriptorRegistry.descriptor(for: .claude)        let context = self.makeContext(sourceMode: .auto)        let strategies = await descriptor.fetchPlan.pipeline.resolveStrategies(context)         #expect(strategies.map(\.id) == ["claude.admin-api"])    }     @Test    func `parses Anthropic admin cost and messages usage into daily summaries`() throws {        let now = Date(timeIntervalSince1970: 1_700_179_200)        let costs = """        {          "data": [            {              "starting_at": "2023-11-14T00:00:00Z",              "ending_at": "2023-11-15T00:00:00Z",              "results": [                {                  "currency": "USD",                  "amount": "12345.00",                  "description": "Claude Sonnet 4 Usage - Input Tokens",                  "cost_type": "tokens"                },                {                  "currency": "USD",                  "amount": "2500.00",                  "description": "Web Search Usage",                  "cost_type": "web_search"                }              ]            },            {              "starting_at": "2023-11-15T00:00:00Z",              "ending_at": "2023-11-16T00:00:00Z",              "results": [                {                  "currency": "USD",                  "amount": "5000",                  "description": "Claude Haiku Usage - Output Tokens",                  "cost_type": "tokens"                }              ]            }          ],          "has_more": false,          "next_page": null        }        """        let messages = """        {          "data": [            {              "starting_at": "2023-11-14T00:00:00Z",              "ending_at": "2023-11-15T00:00:00Z",              "results": [                {                  "uncached_input_tokens": 1500,                  "cache_creation": {                    "ephemeral_1h_input_tokens": 1000,                    "ephemeral_5m_input_tokens": 500                  },                  "cache_read_input_tokens": 200,                  "output_tokens": 500,                  "model": "claude-sonnet-4-20250514"                },                {                  "uncached_input_tokens": 100,                  "output_tokens": 50,                  "model": "claude-opus-4-20250514"                }              ]            },            {              "starting_at": "2023-11-15T00:00:00Z",              "ending_at": "2023-11-16T00:00:00Z",              "results": [                {                  "uncached_input_tokens": 200,                  "cache_read_input_tokens": 300,                  "output_tokens": 100,                  "model": "claude-sonnet-4-20250514"                }              ]            }          ],          "has_more": false,          "next_page": null        }        """         let snapshot = try ClaudeAdminAPIUsageFetcher._parseSnapshotForTesting(            costs: Data(costs.utf8),            messages: Data(messages.utf8),            now: now)         #expect(snapshot.daily.count == 2)        #expect(snapshot.daily[0].costUSD == 148.45)        #expect(snapshot.daily[0].inputTokens == 1600)        #expect(snapshot.daily[0].cacheCreationInputTokens == 1500)        #expect(snapshot.daily[0].cacheReadInputTokens == 200)        #expect(snapshot.daily[0].outputTokens == 550)        #expect(snapshot.daily[0].totalTokens == 3850)        #expect(snapshot.last30Days.costUSD == 198.45)        #expect(snapshot.last30Days.totalTokens == 4450)        #expect(snapshot.topModels.first?.name == "claude-sonnet-4-20250514")        #expect(snapshot.topModels.first?.totalTokens == 4300)    }     @Test    func `maps Anthropic admin usage to Claude usage snapshot`() {        let now = Date(timeIntervalSince1970: 1_700_179_200)        let apiUsage = ClaudeAdminAPIUsageSnapshot(            daily: [                ClaudeAdminAPIUsageSnapshot.DailyBucket(                    day: "2023-11-14",                    startTime: now,                    endTime: now.addingTimeInterval(86400),                    costUSD: 8.5,                    inputTokens: 1000,                    cacheCreationInputTokens: 400,                    cacheReadInputTokens: 300,                    outputTokens: 250,                    totalTokens: 1950,                    costItems: [],                    models: []),            ],            updatedAt: now)         let usage = apiUsage.toUsageSnapshot()         #expect(usage.primary == nil)        #expect(usage.providerCost?.used == 8.5)        #expect(usage.providerCost?.limit == 0)        #expect(usage.providerCost?.period == "Last 30 days")        #expect(usage.claudeAdminAPIUsage?.last30Days.totalTokens == 1950)        #expect(usage.identity?.providerID == .claude)        #expect(usage.identity?.loginMethod == "Admin API")    }     @Test    func `current day summary is zero when Claude admin history is stale`() throws {        let now = try Self.localNoon(year: 2023, month: 11, day: 17)        let bucketDay = try Self.localNoon(year: 2023, month: 11, day: 14)        let apiUsage = ClaudeAdminAPIUsageSnapshot(            daily: [                ClaudeAdminAPIUsageSnapshot.DailyBucket(                    day: "2023-11-14",                    startTime: bucketDay,                    endTime: bucketDay.addingTimeInterval(86400),                    costUSD: 8.5,                    inputTokens: 1000,                    cacheCreationInputTokens: 400,                    cacheReadInputTokens: 300,                    outputTokens: 250,                    totalTokens: 1950,                    costItems: [],                    models: []),            ],            updatedAt: now)         #expect(apiUsage.currentDay.costUSD == 0)        #expect(apiUsage.currentDay.totalTokens == 0)        #expect(apiUsage.latestDay.costUSD == 8.5)        #expect(apiUsage.latestDay.totalTokens == 1950)    }     @Test    func `fetch strategy reports admin api source label`() async throws {        let strategy = ClaudeAdminAPIFetchStrategy(usageFetcher: { apiKey in            #expect(apiKey == "sk-ant-admin-test")            return ClaudeAdminAPIUsageSnapshot(daily: [], updatedAt: Date(timeIntervalSince1970: 1_700_000_000))        })         let result = try await strategy.fetch(self.makeContext())         #expect(result.sourceLabel == "admin-api")        #expect(result.usage.identity?.loginMethod == "Admin API")    }     private static func localNoon(year: Int, month: Int, day: Int) throws -> Date {        try #require(Calendar.current.date(from: DateComponents(year: year, month: month, day: day, hour: 12)))    }}