Files
2026-07-13 12:22:33 +08:00

790 lines
25 KiB
Swift

import Foundation
import Testing
@testable import CodexBarCore
struct KiloUsageFetcherTests {
private struct StubClaudeFetcher: ClaudeUsageFetching {
func loadLatestUsage(model _: String) async throws -> ClaudeUsageSnapshot {
throw ClaudeUsageError.parseFailed("stub")
}
func debugRawProbe(model _: String) async -> String {
"stub"
}
func detectVersion() -> String? {
nil
}
}
private func makeContext(
env: [String: String] = [:],
sourceMode: ProviderSourceMode = .api) -> ProviderFetchContext
{
let browserDetection = BrowserDetection(cacheTTL: 0)
return ProviderFetchContext(
runtime: .cli,
sourceMode: sourceMode,
includeCredits: false,
webTimeout: 1,
webDebugDumpHTML: false,
verbose: false,
env: env,
settings: nil,
fetcher: UsageFetcher(environment: env),
claudeFetcher: StubClaudeFetcher(),
browserDetection: browserDetection)
}
@Test
func `batch URL uses authenticated TRPC batch format`() throws {
let baseURL = try #require(URL(string: "https://kilo.example/trpc"))
let url = try KiloUsageFetcher._buildBatchURLForTesting(baseURL: baseURL)
#expect(url.path.contains("user.getCreditBlocks,kiloPass.getState,user.getAutoTopUpPaymentMethod"))
let components = try #require(URLComponents(url: url, resolvingAgainstBaseURL: false))
let batch = components.queryItems?.first(where: { $0.name == "batch" })?.value
let inputValue = components.queryItems?.first(where: { $0.name == "input" })?.value
#expect(batch == "1")
let requiredInput = try #require(inputValue)
let inputData = Data(requiredInput.utf8)
let inputObject = try #require(try JSONSerialization.jsonObject(with: inputData) as? [String: Any])
let first = try #require(inputObject["0"] as? [String: Any])
let second = try #require(inputObject["1"] as? [String: Any])
let third = try #require(inputObject["2"] as? [String: Any])
#expect(inputObject.keys.contains("0"))
#expect(inputObject.keys.contains("1"))
#expect(inputObject.keys.contains("2"))
#expect(first["json"] is NSNull)
#expect(second["json"] is NSNull)
#expect(third["json"] is NSNull)
}
@Test
func `parse snapshot maps business fields and identity`() throws {
let json = """
[
{
"result": {
"data": {
"json": {
"blocks": [
{
"usedCredits": 25,
"totalCredits": 100,
"remainingCredits": 75
}
]
}
}
}
},
{
"result": {
"data": {
"json": {
"plan": {
"name": "Kilo Pass Pro"
}
}
}
}
},
{
"result": {
"data": {
"json": {
"enabled": true,
"paymentMethod": "visa"
}
}
}
}
]
"""
let parsed = try KiloUsageFetcher._parseSnapshotForTesting(Data(json.utf8))
let snapshot = parsed.toUsageSnapshot()
#expect(snapshot.primary?.usedPercent == 25)
#expect(snapshot.identity?.providerID == .kilo)
#expect(snapshot.loginMethod(for: .kilo)?.contains("Kilo Pass Pro") == true)
#expect(snapshot.loginMethod(for: .kilo)?.contains("Auto top-up") == true)
}
@Test
func `parse snapshot maps kilo pass window from subscription state`() throws {
let json = """
[
{
"result": {
"data": {
"creditBlocks": [
{
"id": "cb-1",
"effective_date": "2026-02-01T00:00:00Z",
"expiry_date": null,
"balance_mUsd": 19000000,
"amount_mUsd": 19000000,
"is_free": false
}
],
"totalBalance_mUsd": 19000000,
"autoTopUpEnabled": false
}
}
},
{
"result": {
"data": {
"subscription": {
"tier": "tier_19",
"currentPeriodUsageUsd": 0,
"currentPeriodBaseCreditsUsd": 19.0,
"currentPeriodBonusCreditsUsd": 9.5,
"nextBillingAt": "2026-03-28T04:00:00.000Z"
}
}
}
},
{
"result": {
"data": {
"enabled": false,
"amountCents": 5000,
"paymentMethod": null
}
}
}
]
"""
let parsed = try KiloUsageFetcher._parseSnapshotForTesting(Data(json.utf8))
let snapshot = parsed.toUsageSnapshot()
#expect(snapshot.primary?.usedPercent == 0)
#expect(snapshot.secondary?.usedPercent == 0)
#expect(snapshot.secondary?.resetsAt != nil)
#expect(snapshot.secondary?.resetDescription == "$0.00 / $19.00 (+ $9.50 bonus)")
#expect(snapshot.loginMethod(for: .kilo) == "Starter · Auto top-up: off")
}
@Test
func `parse snapshot maps known tier names and defaults to kilo pass`() throws {
let proTierJSON = """
[
{ "result": { "data": { "creditBlocks": [], "totalBalance_mUsd": 0, "autoTopUpEnabled": false } } },
{ "result": { "data": { "subscription": { "tier": "tier_49" } } } },
{ "result": { "data": { "enabled": false, "paymentMethod": null } } }
]
"""
let proTierSnapshot = try KiloUsageFetcher._parseSnapshotForTesting(Data(proTierJSON.utf8)).toUsageSnapshot()
#expect(proTierSnapshot.loginMethod(for: .kilo) == "Pro · Auto top-up: off")
let noTierJSON = """
[
{ "result": { "data": { "creditBlocks": [], "totalBalance_mUsd": 0, "autoTopUpEnabled": false } } },
{ "result": { "data": { "subscription": {
"currentPeriodUsageUsd": 1.0,
"currentPeriodBaseCreditsUsd": 19.0
} } } },
{ "result": { "data": { "enabled": false, "paymentMethod": null } } }
]
"""
let noTierSnapshot = try KiloUsageFetcher._parseSnapshotForTesting(Data(noTierJSON.utf8)).toUsageSnapshot()
#expect(noTierSnapshot.loginMethod(for: .kilo) == "Kilo Pass · Auto top-up: off")
}
@Test
func `parse snapshot uses auto top up amount when enabled without payment method`() throws {
let json = """
[
{ "result": { "data": { "creditBlocks": [], "totalBalance_mUsd": 0, "autoTopUpEnabled": true } } },
{ "result": { "data": { "subscription": null } } },
{ "result": { "data": { "enabled": true, "amountCents": 5000, "paymentMethod": null } } }
]
"""
let snapshot = try KiloUsageFetcher._parseSnapshotForTesting(Data(json.utf8)).toUsageSnapshot()
#expect(snapshot.loginMethod(for: .kilo) == "Auto top-up: $50")
}
@Test
func `parse snapshot fallback pass fields use micro dollar scale`() throws {
let json = """
[
{
"result": {
"data": {
"json": {
"blocks": [
{
"usedCredits": 0,
"totalCredits": 19,
"remainingCredits": 19
}
]
}
}
}
},
{
"result": {
"data": {
"json": {
"planName": "Starter",
"amount_mUsd": 28500000,
"used_mUsd": 3500000,
"bonus_mUsd": 9500000,
"nextRenewalAt": "2026-03-28T04:00:00.000Z"
}
}
}
},
{
"result": {
"data": {
"json": {
"enabled": false,
"paymentMethod": null
}
}
}
}
]
"""
let snapshot = try KiloUsageFetcher._parseSnapshotForTesting(Data(json.utf8)).toUsageSnapshot()
#expect(snapshot.secondary?.resetDescription == "$3.50 / $19.00 (+ $9.50 bonus)")
#expect(snapshot.loginMethod(for: .kilo) == "Starter · Auto top-up: off")
}
@Test
func `parse snapshot treats empty and null business fields as no data success`() throws {
let json = """
[
{
"result": {
"data": {
"json": {
"blocks": []
}
}
}
},
{
"result": {
"data": {
"json": {
"plan": {
"name": null
}
}
}
}
},
{
"result": {
"data": {
"json": {
"enabled": null,
"paymentMethod": null
}
}
}
}
]
"""
let parsed = try KiloUsageFetcher._parseSnapshotForTesting(Data(json.utf8))
let snapshot = parsed.toUsageSnapshot()
#expect(snapshot.primary == nil)
#expect(snapshot.identity?.providerID == .kilo)
#expect(snapshot.loginMethod(for: .kilo) == nil)
}
@Test
func `parse snapshot keeps sparse indexed object routing by procedure index`() throws {
let json = """
{
"0": {
"result": {
"data": {
"json": {
"creditsUsed": 10,
"creditsRemaining": 90
}
}
}
},
"2": {
"result": {
"data": {
"json": {
"planName": "wrong-route",
"enabled": true,
"method": "visa"
}
}
}
}
}
"""
let parsed = try KiloUsageFetcher._parseSnapshotForTesting(Data(json.utf8))
let snapshot = parsed.toUsageSnapshot()
#expect(snapshot.primary?.usedPercent == 10)
#expect(snapshot.loginMethod(for: .kilo) == "Auto top-up: visa")
}
@Test
func `parse snapshot uses top level credits used fallback`() throws {
let json = """
[
{
"result": {
"data": {
"json": {
"creditsUsed": 40,
"creditsRemaining": 60
}
}
}
}
]
"""
let parsed = try KiloUsageFetcher._parseSnapshotForTesting(Data(json.utf8))
let snapshot = parsed.toUsageSnapshot()
#expect(snapshot.primary?.usedPercent == 40)
#expect(snapshot.primary?.resetDescription == "40/100 credits")
}
@Test
func `parse snapshot keeps zero total visible when activity exists`() throws {
let json = """
[
{
"result": {
"data": {
"json": {
"creditsUsed": 0,
"creditsRemaining": 0
}
}
}
},
{
"result": {
"data": {
"json": {
"planName": "Kilo Pass Pro"
}
}
}
},
{
"result": {
"data": {
"json": {
"enabled": true,
"paymentMethod": "visa"
}
}
}
}
]
"""
let parsed = try KiloUsageFetcher._parseSnapshotForTesting(Data(json.utf8))
let snapshot = parsed.toUsageSnapshot()
#expect(snapshot.primary?.remainingPercent == 0)
#expect(snapshot.primary?.usedPercent == 100)
#expect(snapshot.primary?.resetDescription == "0/0 credits")
#expect(snapshot.loginMethod(for: .kilo)?.contains("Auto top-up: visa") == true)
}
@Test
func `parse snapshot treats zero balance without credit blocks as visible zero total`() throws {
let json = """
[
{
"result": {
"data": {
"creditBlocks": [],
"totalBalance_mUsd": 0,
"isFirstPurchase": true,
"autoTopUpEnabled": false
}
}
},
{
"result": {
"data": {
"subscription": null
}
}
},
{
"result": {
"data": {
"enabled": false,
"amountCents": 5000,
"paymentMethod": null
}
}
}
]
"""
let parsed = try KiloUsageFetcher._parseSnapshotForTesting(Data(json.utf8))
let snapshot = parsed.toUsageSnapshot()
#expect(snapshot.primary?.usedPercent == 100)
#expect(snapshot.primary?.remainingPercent == 0)
#expect(snapshot.primary?.resetDescription == "0/0 credits")
#expect(snapshot.loginMethod(for: .kilo) == "Auto top-up: off")
}
@Test
func `parse snapshot degrades optional auto top up TRPC error`() throws {
let json = """
[
{
"result": {
"data": {
"json": {
"creditsUsed": 10,
"creditsRemaining": 90
}
}
}
},
{
"result": {
"data": {
"json": {
"planName": "Starter"
}
}
}
},
{
"error": {
"json": {
"message": "Internal server error",
"data": {
"code": "INTERNAL_SERVER_ERROR"
}
}
}
}
]
"""
let parsed = try KiloUsageFetcher._parseSnapshotForTesting(Data(json.utf8))
let snapshot = parsed.toUsageSnapshot()
#expect(snapshot.primary?.usedPercent == 10)
#expect(snapshot.loginMethod(for: .kilo) == "Starter")
}
@Test
func `parse snapshot keeps required procedure TRPC error fatal`() {
let json = """
[
{
"result": {
"data": {
"json": {
"creditsUsed": 10,
"creditsRemaining": 90
}
}
}
},
{
"error": {
"json": {
"message": "Unauthorized",
"data": {
"code": "UNAUTHORIZED"
}
}
}
}
]
"""
#expect {
_ = try KiloUsageFetcher._parseSnapshotForTesting(Data(json.utf8))
} throws: { error in
guard let kiloError = error as? KiloUsageError else { return false }
guard case .unauthorized = kiloError else { return false }
return true
}
}
@Test
func `parse snapshot maps unauthorized TRPC error`() {
let json = """
[
{
"error": {
"json": {
"message": "Unauthorized",
"data": {
"code": "UNAUTHORIZED"
}
}
}
}
]
"""
#expect {
_ = try KiloUsageFetcher._parseSnapshotForTesting(Data(json.utf8))
} throws: { error in
guard let kiloError = error as? KiloUsageError else { return false }
guard case .unauthorized = kiloError else { return false }
return true
}
}
@Test
func `parse snapshot maps invalid JSON to parse error`() {
#expect {
_ = try KiloUsageFetcher._parseSnapshotForTesting(Data("not-json".utf8))
} throws: { error in
guard let kiloError = error as? KiloUsageError else { return false }
guard case .parseFailed = kiloError else { return false }
return true
}
}
@Test
func `status error mapping covers auth and server failures`() {
#expect(KiloUsageFetcher._statusErrorForTesting(401) == .unauthorized)
#expect(KiloUsageFetcher._statusErrorForTesting(403) == .unauthorized)
#expect(KiloUsageFetcher._statusErrorForTesting(404) == .endpointNotFound)
guard let serviceError = KiloUsageFetcher._statusErrorForTesting(503) else {
Issue.record("Expected service unavailable mapping")
return
}
guard case let .serviceUnavailable(statusCode) = serviceError else {
Issue.record("Expected service unavailable mapping")
return
}
#expect(statusCode == 503)
}
@Test
func `fetch usage without credentials fails fast`() async {
await #expect(throws: KiloUsageError.missingCredentials) {
_ = try await KiloUsageFetcher.fetchUsage(apiKey: " ", environment: [:])
}
}
@Test
func `descriptor fetch outcome without credentials returns actionable error`() async {
let descriptor = ProviderDescriptorRegistry.descriptor(for: .kilo)
let outcome = await descriptor.fetchOutcome(context: self.makeContext())
switch outcome.result {
case .success:
Issue.record("Expected missing credentials failure")
case let .failure(error):
#expect((error as? KiloUsageError) == .missingCredentials)
}
#expect(outcome.attempts.count == 1)
#expect(outcome.attempts.first?.strategyID == "kilo.api")
#expect(outcome.attempts.first?.wasAvailable == true)
}
@Test
func `descriptor API mode ignores CLI session fallback`() async throws {
let homeDirectory = try self.makeTemporaryHomeDirectory()
defer { try? FileManager.default.removeItem(at: homeDirectory) }
try self.writeKiloAuthFile(
homeDirectory: homeDirectory,
contents: #"{"kilo":{"access":"file-token"}}"#)
let descriptor = ProviderDescriptorRegistry.descriptor(for: .kilo)
let outcome = await descriptor.fetchOutcome(context: self.makeContext(
env: ["HOME": homeDirectory.path],
sourceMode: .api))
switch outcome.result {
case .success:
Issue.record("Expected missing API credentials failure")
case let .failure(error):
#expect((error as? KiloUsageError) == .missingCredentials)
}
#expect(outcome.attempts.count == 1)
#expect(outcome.attempts.first?.strategyID == "kilo.api")
}
@Test
func `descriptor CLI mode missing session returns actionable error`() async throws {
let homeDirectory = try self.makeTemporaryHomeDirectory()
defer { try? FileManager.default.removeItem(at: homeDirectory) }
let expectedPath = KiloSettingsReader.defaultAuthFileURL(homeDirectory: homeDirectory).path
let descriptor = ProviderDescriptorRegistry.descriptor(for: .kilo)
let outcome = await descriptor.fetchOutcome(context: self.makeContext(
env: ["HOME": homeDirectory.path],
sourceMode: .cli))
switch outcome.result {
case .success:
Issue.record("Expected missing CLI session failure")
case let .failure(error):
#expect((error as? KiloUsageError) == .cliSessionMissing(expectedPath))
}
#expect(outcome.attempts.count == 1)
#expect(outcome.attempts.first?.strategyID == "kilo.cli")
}
@Test
func `descriptor auto mode falls back from API to CLI`() async throws {
let homeDirectory = try self.makeTemporaryHomeDirectory()
defer { try? FileManager.default.removeItem(at: homeDirectory) }
let expectedPath = KiloSettingsReader.defaultAuthFileURL(homeDirectory: homeDirectory).path
let descriptor = ProviderDescriptorRegistry.descriptor(for: .kilo)
let outcome = await descriptor.fetchOutcome(context: self.makeContext(
env: ["HOME": homeDirectory.path],
sourceMode: .auto))
switch outcome.result {
case .success:
Issue.record("Expected missing CLI session failure after API fallback")
case let .failure(error):
#expect((error as? KiloUsageError) == .cliSessionMissing(expectedPath))
}
#expect(outcome.attempts.count == 2)
#expect(outcome.attempts.map(\.strategyID) == ["kilo.api", "kilo.cli"])
}
@Test
func `api strategy falls back on unauthorized only in auto mode`() {
let strategy = KiloAPIFetchStrategy()
#expect(strategy.shouldFallback(
on: KiloUsageError.unauthorized,
context: self.makeContext(sourceMode: .auto)))
#expect(!strategy.shouldFallback(
on: KiloUsageError.unauthorized,
context: self.makeContext(sourceMode: .api)))
}
@Test
func `api strategy falls back on missing credentials only in auto mode`() {
let strategy = KiloAPIFetchStrategy()
#expect(strategy.shouldFallback(
on: KiloUsageError.missingCredentials,
context: self.makeContext(sourceMode: .auto)))
#expect(!strategy.shouldFallback(
on: KiloUsageError.missingCredentials,
context: self.makeContext(sourceMode: .api)))
}
@Test
func `request builder adds org header for organization scope`() throws {
let baseURL = try #require(URL(string: "https://kilo.example/trpc"))
let request = try KiloUsageFetcher._buildRequestForTesting(
baseURL: baseURL,
apiKey: "test-token",
scope: .organization(id: "org_42", name: "Acme"))
#expect(request.value(forHTTPHeaderField: "X-KILOCODE-ORGANIZATIONID") == "org_42")
#expect(request.value(forHTTPHeaderField: "Authorization") == "Bearer test-token")
}
@Test
func `request builder omits org header for personal scope`() throws {
let baseURL = try #require(URL(string: "https://kilo.example/trpc"))
let request = try KiloUsageFetcher._buildRequestForTesting(
baseURL: baseURL,
apiKey: "test-token",
scope: .personal)
#expect(request.value(forHTTPHeaderField: "X-KILOCODE-ORGANIZATIONID") == nil)
#expect(request.value(forHTTPHeaderField: "Authorization") == "Bearer test-token")
}
@Test
func `parseOrganizations decodes tRPC array shape`() throws {
let json = #"""
[
{
"result": {
"data": {
"json": [
{ "id": "org_1", "name": "Alpha", "role": "owner" },
{ "id": "org_2", "name": "Beta", "role": "member" }
]
}
}
}
]
"""#
let orgs = try KiloUsageFetcher._parseOrganizationsForTesting(Data(json.utf8))
#expect(orgs.count == 2)
#expect(orgs[0].id == "org_1")
#expect(orgs[0].name == "Alpha")
#expect(orgs[0].role == "owner")
#expect(orgs[1].id == "org_2")
#expect(orgs[1].role == "member")
}
@Test
func `parseOrganizations decodes profile REST shape`() throws {
let json = #"""
{
"user": { "email": "test@example.com" },
"organizations": [
{ "id": "org_42", "name": "Gamma" }
]
}
"""#
let orgs = try KiloUsageFetcher._parseOrganizationsForTesting(Data(json.utf8))
#expect(orgs.count == 1)
#expect(orgs[0].id == "org_42")
#expect(orgs[0].role == nil)
}
@Test
func `parseOrganizations returns empty for no orgs`() throws {
let json = #"""
{ "user": { "email": "x@y" }, "organizations": [] }
"""#
let orgs = try KiloUsageFetcher._parseOrganizationsForTesting(Data(json.utf8))
#expect(orgs.isEmpty)
}
private func makeTemporaryHomeDirectory() throws -> URL {
let directory = FileManager.default.temporaryDirectory
.appendingPathComponent(UUID().uuidString, isDirectory: true)
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
return directory
}
private func writeKiloAuthFile(homeDirectory: URL, contents: String) throws {
let fileURL = KiloSettingsReader.defaultAuthFileURL(homeDirectory: homeDirectory)
try FileManager.default.createDirectory(
at: fileURL.deletingLastPathComponent(),
withIntermediateDirectories: true)
try contents.write(to: fileURL, atomically: true, encoding: .utf8)
}
}