275 lines
10 KiB
Swift
275 lines
10 KiB
Swift
import CodexBarCore
|
|
import Foundation
|
|
import Testing
|
|
@testable import CodexBar
|
|
@testable import CodexBarCLI
|
|
|
|
struct ClawRouterUsageFetcherTests {
|
|
@Test
|
|
func `parses monthly budget and provider agnostic usage`() throws {
|
|
let parsed = try ClawRouterUsageFetcher._parseSnapshotForTesting(
|
|
Data(Self.budgetedResponse.utf8),
|
|
updatedAt: Date(timeIntervalSince1970: 1))
|
|
|
|
#expect(parsed.budgetLimitUSD == 25)
|
|
#expect(parsed.budgetSpentUSD == 0.006)
|
|
#expect(parsed.budgetRemainingUSD == 24.994)
|
|
#expect(parsed.requestCount == 6)
|
|
#expect(parsed.totalTokens == 54191)
|
|
#expect(parsed.providers.map(\.provider) == ["openai", "anthropic"])
|
|
|
|
let snapshot = parsed.toUsageSnapshot()
|
|
#expect(snapshot.identity?.providerID == .clawrouter)
|
|
#expect(snapshot.primary?.usedPercent == 0.024)
|
|
#expect(snapshot.secondary == nil)
|
|
#expect(snapshot.providerCost?.used == 0.006)
|
|
#expect(snapshot.providerCost?.limit == 25)
|
|
#expect(snapshot.clawRouterUsage?.providers.map(\.provider) == ["openai", "anthropic"])
|
|
#expect(snapshot.dataConfidence == .exact)
|
|
|
|
let reset = try #require(snapshot.primary?.resetsAt)
|
|
let expected = try #require(DateComponents(
|
|
calendar: Calendar(identifier: .gregorian),
|
|
timeZone: TimeZone(secondsFromGMT: 0),
|
|
year: 2026,
|
|
month: 8,
|
|
day: 1).date)
|
|
#expect(reset == expected)
|
|
}
|
|
|
|
@Test
|
|
func `supports unmetered policies and arbitrary providers`() throws {
|
|
let parsed = try ClawRouterUsageFetcher._parseSnapshotForTesting(
|
|
Data(Self.unmeteredResponse.utf8),
|
|
updatedAt: Date(timeIntervalSince1970: 1))
|
|
let snapshot = parsed.toUsageSnapshot()
|
|
|
|
#expect(!parsed.budgetConfigured)
|
|
#expect(parsed.providers.map(\.provider) == ["replicate", "tavily"])
|
|
#expect(snapshot.primary == nil)
|
|
#expect(snapshot.identity?.loginMethod == "Unmetered")
|
|
#expect(snapshot.providerCost?.used == 1.25)
|
|
#expect(snapshot.providerCost?.limit == 0)
|
|
}
|
|
|
|
@Test
|
|
func `usage URL accepts root and versioned base URLs`() throws {
|
|
#expect(
|
|
try ClawRouterUsageFetcher._usageURLForTesting(
|
|
baseURL: #require(URL(string: "https://router.example.com"))).absoluteString ==
|
|
"https://router.example.com/v1/usage")
|
|
#expect(
|
|
try ClawRouterUsageFetcher._usageURLForTesting(
|
|
baseURL: #require(URL(string: "https://router.example.com/v1"))).absoluteString ==
|
|
"https://router.example.com/v1/usage")
|
|
}
|
|
|
|
@Test
|
|
func `fetch sends bearer key and maps authorization failure`() async throws {
|
|
let transport = ProviderHTTPTransportStub { request in
|
|
#expect(request.url?.absoluteString == "https://router.example.com/v1/usage")
|
|
#expect(request.httpMethod == "GET")
|
|
#expect(request.value(forHTTPHeaderField: "Authorization") == "Bearer smoke-key")
|
|
let response = try #require(HTTPURLResponse(
|
|
url: request.url!,
|
|
statusCode: 401,
|
|
httpVersion: nil,
|
|
headerFields: nil))
|
|
return (Data(), response)
|
|
}
|
|
|
|
await #expect(throws: ClawRouterUsageError.invalidCredentials) {
|
|
_ = try await ClawRouterUsageFetcher.fetchUsage(
|
|
apiKey: "smoke-key",
|
|
baseURL: #require(URL(string: "https://router.example.com")),
|
|
transport: transport)
|
|
}
|
|
}
|
|
|
|
@Test
|
|
func `config projects API key and optional base URL`() {
|
|
let config = ProviderConfig(
|
|
id: .clawrouter,
|
|
apiKey: "router-token",
|
|
enterpriseHost: "https://router.example.com")
|
|
let environment = ProviderConfigEnvironment.applyProviderConfigOverrides(
|
|
base: [:],
|
|
provider: .clawrouter,
|
|
config: config)
|
|
|
|
#expect(environment[ClawRouterSettingsReader.apiKeyEnvironmentKey] == "router-token")
|
|
#expect(environment[ClawRouterSettingsReader.baseURLEnvironmentKey] == "https://router.example.com")
|
|
#expect(ProviderTokenResolver.clawRouterToken(environment: environment) == "router-token")
|
|
}
|
|
|
|
@Test
|
|
func `endpoint override is HTTPS only`() throws {
|
|
let key = ClawRouterSettingsReader.baseURLEnvironmentKey
|
|
try ClawRouterSettingsReader.validateEndpointOverride(environment: [key: "router.example.com/v1"])
|
|
#expect(ClawRouterSettingsReader.baseURL(environment: [key: "router.example.com/v1"]).absoluteString ==
|
|
"https://router.example.com/v1")
|
|
#expect(throws: ClawRouterSettingsError.invalidEndpointOverride(key)) {
|
|
try ClawRouterSettingsReader.validateEndpointOverride(environment: [key: "http://router.example.com"])
|
|
}
|
|
}
|
|
|
|
@Test
|
|
@MainActor
|
|
func `descriptor and settings are registered`() throws {
|
|
let descriptor = ProviderDescriptorRegistry.descriptor(for: .clawrouter)
|
|
#expect(descriptor.metadata.displayName == "ClawRouter")
|
|
#expect(descriptor.cli.aliases.contains("claw-router"))
|
|
|
|
let implementation = try #require(ProviderImplementationRegistry.implementation(for: .clawrouter))
|
|
#expect(implementation.id == .clawrouter)
|
|
}
|
|
|
|
@Test
|
|
func `usage snapshot preserves ClawRouter detail when cached`() throws {
|
|
let parsed = try ClawRouterUsageFetcher._parseSnapshotForTesting(
|
|
Data(Self.budgetedResponse.utf8),
|
|
updatedAt: Date(timeIntervalSince1970: 1))
|
|
let encoded = try JSONEncoder().encode(parsed.toUsageSnapshot())
|
|
let decoded = try JSONDecoder().decode(UsageSnapshot.self, from: encoded)
|
|
|
|
#expect(decoded.clawRouterUsage == parsed)
|
|
#expect(decoded.identity?.providerID == .clawrouter)
|
|
}
|
|
|
|
@Test
|
|
func `text CLI renders budgeted spend and routed usage`() throws {
|
|
let parsed = try ClawRouterUsageFetcher._parseSnapshotForTesting(
|
|
Data(Self.budgetedResponse.utf8),
|
|
updatedAt: Date(timeIntervalSince1970: 1))
|
|
|
|
let output = Self.renderText(parsed.toUsageSnapshot())
|
|
|
|
#expect(output.contains("Spend: $0.01 / $25.00"))
|
|
#expect(output.contains("Usage: 6 requests · 54K tokens"))
|
|
#expect(output.contains("Results: 5 succeeded · 1 failed"))
|
|
#expect(output.contains("Routed providers: openai: 4 · anthropic: 2"))
|
|
}
|
|
|
|
@Test
|
|
func `text CLI renders unmetered and zero spend without a zero limit`() throws {
|
|
let unmetered = try ClawRouterUsageFetcher._parseSnapshotForTesting(
|
|
Data(Self.unmeteredResponse.utf8),
|
|
updatedAt: Date(timeIntervalSince1970: 1))
|
|
let zeroSpend = try ClawRouterUsageFetcher._parseSnapshotForTesting(
|
|
Data(Self.unmeteredResponse.replacingOccurrences(of: "1250000", with: "0").utf8),
|
|
updatedAt: Date(timeIntervalSince1970: 1))
|
|
|
|
let unmeteredOutput = Self.renderText(unmetered.toUsageSnapshot())
|
|
let zeroSpendOutput = Self.renderText(zeroSpend.toUsageSnapshot())
|
|
|
|
#expect(unmeteredOutput.contains("Spend: $1.25"))
|
|
#expect(unmeteredOutput.contains("Usage: 3 requests · 0 tokens"))
|
|
#expect(!unmeteredOutput.contains(" / 0.0"))
|
|
#expect(zeroSpendOutput.contains("Spend: $0.00"))
|
|
#expect(zeroSpendOutput.contains("Usage: 3 requests · 0 tokens"))
|
|
#expect(!zeroSpendOutput.contains(" / 0.0"))
|
|
}
|
|
|
|
private static func renderText(_ snapshot: UsageSnapshot) -> String {
|
|
CLIRenderer.renderText(
|
|
provider: .clawrouter,
|
|
snapshot: snapshot,
|
|
credits: nil,
|
|
context: RenderContext(
|
|
header: "ClawRouter (api)",
|
|
status: nil,
|
|
useColor: false,
|
|
resetStyle: .countdown))
|
|
}
|
|
|
|
private static let budgetedResponse = """
|
|
{
|
|
"policyId": "openclaw-smoke",
|
|
"budget": {
|
|
"configured": true,
|
|
"ledger": "durable_object",
|
|
"windowKey": "openclaw/openclaw-smoke/2026-07",
|
|
"limitMicros": 25000000,
|
|
"spentMicros": 6000,
|
|
"remainingMicros": 24994000
|
|
},
|
|
"usage": {
|
|
"ledger": "ready",
|
|
"summary": {
|
|
"requestCount": 6,
|
|
"successCount": 5,
|
|
"errorCount": 1,
|
|
"inputTokens": 50000,
|
|
"outputTokens": 4191,
|
|
"totalTokens": 54191,
|
|
"actualCostMicros": 6000
|
|
},
|
|
"providers": [
|
|
{
|
|
"provider": "anthropic",
|
|
"requestCount": 2,
|
|
"successCount": 2,
|
|
"errorCount": 0,
|
|
"totalTokens": 12191,
|
|
"actualCostMicros": 2000
|
|
},
|
|
{
|
|
"provider": "openai",
|
|
"requestCount": 4,
|
|
"successCount": 3,
|
|
"errorCount": 1,
|
|
"totalTokens": 42000,
|
|
"actualCostMicros": 4000
|
|
}
|
|
],
|
|
"events": []
|
|
}
|
|
}
|
|
"""
|
|
|
|
private static let unmeteredResponse = """
|
|
{
|
|
"policyId": "any-provider-policy",
|
|
"budget": {
|
|
"configured": false,
|
|
"ledger": "unmetered",
|
|
"windowKey": null,
|
|
"limitMicros": null,
|
|
"spentMicros": null,
|
|
"remainingMicros": null
|
|
},
|
|
"usage": {
|
|
"ledger": "ready",
|
|
"summary": {
|
|
"requestCount": 3,
|
|
"successCount": 3,
|
|
"errorCount": 0,
|
|
"inputTokens": 0,
|
|
"outputTokens": 0,
|
|
"totalTokens": 0,
|
|
"actualCostMicros": 1250000
|
|
},
|
|
"providers": [
|
|
{
|
|
"provider": "tavily",
|
|
"requestCount": 2,
|
|
"successCount": 2,
|
|
"errorCount": 0,
|
|
"totalTokens": 0,
|
|
"actualCostMicros": 250000
|
|
},
|
|
{
|
|
"provider": "replicate",
|
|
"requestCount": 1,
|
|
"successCount": 1,
|
|
"errorCount": 0,
|
|
"totalTokens": 0,
|
|
"actualCostMicros": 1000000
|
|
}
|
|
],
|
|
"events": []
|
|
}
|
|
}
|
|
"""
|
|
}
|