Files
steipete--codexbar/Tests/CodexBarTests/AntigravityRemoteUsageFetcherTests.swift
2026-07-13 12:22:33 +08:00

1248 lines
52 KiB
Swift

import Foundation
import Testing
@testable import CodexBarCore
private actor AntigravityCredentialUpdateCapture {
private var captured: [AntigravityOAuthCredentials] = []
func append(_ credentials: AntigravityOAuthCredentials) {
self.captured.append(credentials)
}
func values() -> [AntigravityOAuthCredentials] {
self.captured
}
}
@Suite(.serialized)
// swiftlint:disable:next type_body_length
struct AntigravityRemoteUsageFetcherTests {
@Test
func `antigravity supports token accounts for quick account switching`() {
let support = TokenAccountSupportCatalog.support(for: .antigravity)
#expect(support?.title == "Google accounts")
#expect(support?.requiresManualCookieSource == false)
#expect(TokenAccountSupportCatalog.envOverride(
for: .antigravity,
token: "serialized-credentials")?[AntigravityOAuthCredentialsStore.environmentCredentialsKey] ==
"serialized-credentials")
}
@Test
func `oauth credentials round trip through token account value`() throws {
let credentials = AntigravityOAuthCredentials(
accessToken: "access-token",
refreshToken: "refresh-token",
expiryDate: Date(timeIntervalSince1970: 1_700_000_000),
idToken: GeminiAPITestHelpers.makeIDToken(email: "user@example.com"),
email: "user@example.com",
projectID: "project-123",
clientID: "client-id",
clientSecret: "client-secret")
let token = try AntigravityOAuthCredentialsStore.tokenAccountValue(for: credentials)
let decoded = try #require(AntigravityOAuthCredentialsStore.credentials(fromTokenAccountValue: token))
#expect(decoded == credentials)
}
@Test
func `remote fetch uses selected token account credentials before shared credentials`() async throws {
let env = try GeminiTestEnvironment()
defer { env.cleanup() }
try env.writeAntigravityCredentials(
accessToken: "shared-token",
refreshToken: nil,
expiry: Date().addingTimeInterval(3600),
idToken: GeminiAPITestHelpers.makeIDToken(email: "shared@example.com"),
email: "shared@example.com")
let selectedCredentials = AntigravityOAuthCredentials(
accessToken: "selected-token",
refreshToken: nil,
expiryDate: Date().addingTimeInterval(3600),
idToken: GeminiAPITestHelpers.makeIDToken(email: "selected@example.com"),
email: "selected@example.com",
projectID: nil,
clientID: nil,
clientSecret: nil)
let token = try AntigravityOAuthCredentialsStore.tokenAccountValue(for: selectedCredentials)
let dataLoader = GeminiAPITestHelpers.dataLoader { request in
guard let url = request.url, let host = url.host else {
throw URLError(.badURL)
}
#expect(request.value(forHTTPHeaderField: "Authorization") == "Bearer selected-token")
switch host {
case "cloudcode-pa.googleapis.com":
if url.path == "/v1internal:loadCodeAssist" {
return GeminiAPITestHelpers.response(
url: url.absoluteString,
status: 200,
body: GeminiAPITestHelpers.jsonData([
"currentTier": ["id": "free-tier", "name": "free"],
"cloudaicompanionProject": "managed-project-123",
]))
}
if url.path == "/v1internal:fetchAvailableModels" {
return GeminiAPITestHelpers.response(
url: url.absoluteString,
status: 200,
body: Self.availableModelsResponse())
}
return GeminiAPITestHelpers.response(url: url.absoluteString, status: 404, body: Data())
default:
return GeminiAPITestHelpers.response(url: url.absoluteString, status: 404, body: Data())
}
}
let fetcher = AntigravityRemoteUsageFetcher(
timeout: 1,
homeDirectory: env.homeURL.path,
environment: [AntigravityOAuthCredentialsStore.environmentCredentialsKey: token],
dataLoader: dataLoader)
let snapshot = try await fetcher.fetch()
#expect(snapshot.accountEmail == "selected@example.com")
}
@Test
func `remote fetch refreshes selected token account without mutating shared credentials`() async throws {
let env = try GeminiTestEnvironment()
defer { env.cleanup() }
try env.writeAntigravityCredentials(
accessToken: "shared-token",
refreshToken: "shared-refresh",
expiry: Date().addingTimeInterval(3600),
idToken: GeminiAPITestHelpers.makeIDToken(email: "shared@example.com"),
email: "shared@example.com",
clientID: "shared-client-id",
clientSecret: "shared-client-secret")
let selectedCredentials = AntigravityOAuthCredentials(
accessToken: "selected-old-token",
refreshToken: "selected-refresh",
expiryDate: Date().addingTimeInterval(-3600),
idToken: GeminiAPITestHelpers.makeIDToken(email: "selected-old@example.com"),
email: "selected-old@example.com",
projectID: nil,
clientID: "selected-client-id",
clientSecret: "selected-client-secret")
let token = try AntigravityOAuthCredentialsStore.tokenAccountValue(for: selectedCredentials)
let updateCapture = AntigravityCredentialUpdateCapture()
let dataLoader = GeminiAPITestHelpers.dataLoader { request in
guard let url = request.url, let host = url.host else {
throw URLError(.badURL)
}
switch host {
case "oauth2.googleapis.com":
let body = String(data: request.httpBody ?? Data(), encoding: .utf8) ?? ""
#expect(body.contains("client_id=selected-client-id"))
#expect(body.contains("refresh_token=selected-refresh"))
return GeminiAPITestHelpers.response(
url: url.absoluteString,
status: 200,
body: GeminiAPITestHelpers.jsonData([
"access_token": "selected-new-token",
"expires_in": 3600,
"id_token": GeminiAPITestHelpers.makeIDToken(email: "selected-new@example.com"),
]))
case "cloudcode-pa.googleapis.com":
#expect(request.value(forHTTPHeaderField: "Authorization") == "Bearer selected-new-token")
if url.path == "/v1internal:loadCodeAssist" {
return GeminiAPITestHelpers.response(
url: url.absoluteString,
status: 200,
body: GeminiAPITestHelpers.jsonData([
"currentTier": ["id": "free-tier", "name": "free"],
"cloudaicompanionProject": "selected-project-123",
]))
}
if url.path == "/v1internal:fetchAvailableModels" {
return GeminiAPITestHelpers.response(
url: url.absoluteString,
status: 200,
body: Self.availableModelsResponse())
}
return GeminiAPITestHelpers.response(url: url.absoluteString, status: 404, body: Data())
default:
return GeminiAPITestHelpers.response(url: url.absoluteString, status: 404, body: Data())
}
}
let fetcher = AntigravityRemoteUsageFetcher(
timeout: 1,
homeDirectory: env.homeURL.path,
environment: [AntigravityOAuthCredentialsStore.environmentCredentialsKey: token],
dataLoader: dataLoader,
credentialsUpdateHandler: { credentials in
await updateCapture.append(credentials)
})
let snapshot = try await fetcher.fetch()
let shared = try env.readAntigravityCredentials()
let updatedCredentials = await updateCapture.values()
#expect(snapshot.accountEmail == "selected-new@example.com")
#expect(shared["access_token"] as? String == "shared-token")
#expect(shared["email"] as? String == "shared@example.com")
#expect(updatedCredentials.last?.accessToken == "selected-new-token")
#expect(updatedCredentials.last?.projectID == "selected-project-123")
}
@Test
func `remote fetch ignores selected token account project id persistence failure`() async throws {
let env = try GeminiTestEnvironment()
defer { env.cleanup() }
let selectedCredentials = AntigravityOAuthCredentials(
accessToken: "selected-token",
refreshToken: nil,
expiryDate: Date().addingTimeInterval(3600),
idToken: GeminiAPITestHelpers.makeIDToken(email: "selected@example.com"),
email: "selected@example.com",
projectID: nil,
clientID: nil,
clientSecret: nil)
let token = try AntigravityOAuthCredentialsStore.tokenAccountValue(for: selectedCredentials)
let dataLoader = GeminiAPITestHelpers.dataLoader { request in
guard let url = request.url, let host = url.host else {
throw URLError(.badURL)
}
switch host {
case "cloudcode-pa.googleapis.com":
if url.path == "/v1internal:loadCodeAssist" {
return GeminiAPITestHelpers.response(
url: url.absoluteString,
status: 200,
body: GeminiAPITestHelpers.jsonData([
"currentTier": ["id": "free-tier", "name": "free"],
"cloudaicompanionProject": "selected-project-123",
]))
}
if url.path == "/v1internal:fetchAvailableModels" {
return GeminiAPITestHelpers.response(
url: url.absoluteString,
status: 200,
body: Self.availableModelsResponse())
}
return GeminiAPITestHelpers.response(url: url.absoluteString, status: 404, body: Data())
default:
return GeminiAPITestHelpers.response(url: url.absoluteString, status: 404, body: Data())
}
}
let fetcher = AntigravityRemoteUsageFetcher(
timeout: 1,
homeDirectory: env.homeURL.path,
environment: [AntigravityOAuthCredentialsStore.environmentCredentialsKey: token],
dataLoader: dataLoader,
credentialsUpdateHandler: { _ in
throw CocoaError(.fileWriteUnknown)
})
let snapshot = try await fetcher.fetch()
#expect(snapshot.accountEmail == "selected@example.com")
}
@Test
func `remote fetch rejects invalid selected token account`() async throws {
let env = try GeminiTestEnvironment()
defer { env.cleanup() }
try env.writeAntigravityCredentials(
accessToken: "shared-token",
refreshToken: nil,
expiry: Date().addingTimeInterval(3600),
idToken: GeminiAPITestHelpers.makeIDToken(email: "shared@example.com"),
email: "shared@example.com")
let fetcher = AntigravityRemoteUsageFetcher(
timeout: 1,
homeDirectory: env.homeURL.path,
environment: [AntigravityOAuthCredentialsStore.environmentCredentialsKey: "not-json"],
dataLoader: GeminiAPITestHelpers.dataLoader { _ in
throw URLError(.badServerResponse)
})
do {
_ = try await fetcher.fetch()
#expect(Bool(false), "Expected selected account decode failure")
} catch let error as AntigravityRemoteFetchError {
guard case let .parseFailed(message) = error else {
#expect(Bool(false), "Unexpected Antigravity error: \(error)")
return
}
#expect(message.contains("selected account"))
} catch {
#expect(Bool(false), "Unexpected error: \(error)")
}
}
@Test
func `remote fetch maps cloud code models into antigravity usage`() async throws {
let env = try GeminiTestEnvironment()
defer { env.cleanup() }
try env.writeAntigravityCredentials(
accessToken: "token",
refreshToken: nil,
expiry: Date().addingTimeInterval(3600),
idToken: GeminiAPITestHelpers.makeIDToken(email: "user@company.com", hostedDomain: "company.com"),
email: "user@company.com")
let dataLoader = GeminiAPITestHelpers.dataLoader { request in
guard let url = request.url, let host = url.host else {
throw URLError(.badURL)
}
switch host {
case "cloudcode-pa.googleapis.com":
if url.path == "/v1internal:loadCodeAssist" {
return GeminiAPITestHelpers.response(
url: url.absoluteString,
status: 200,
body: GeminiAPITestHelpers.jsonData([
"currentTier": ["id": "standard-tier", "name": "standard"],
"cloudaicompanionProject": "managed-project-123",
]))
}
if url.path == "/v1internal:fetchAvailableModels" {
let body = try #require(request.httpBody)
let json = try #require(JSONSerialization.jsonObject(with: body) as? [String: Any])
#expect(json["project"] as? String == "managed-project-123")
return GeminiAPITestHelpers.response(
url: url.absoluteString,
status: 200,
body: Self.availableModelsResponse())
}
return GeminiAPITestHelpers.response(url: url.absoluteString, status: 404, body: Data())
default:
return GeminiAPITestHelpers.response(url: url.absoluteString, status: 404, body: Data())
}
}
let fetcher = AntigravityRemoteUsageFetcher(
timeout: 1,
homeDirectory: env.homeURL.path,
dataLoader: dataLoader)
let snapshot = try await fetcher.fetch()
#expect(snapshot.accountEmail == "user@company.com")
#expect(snapshot.accountPlan == "Paid")
let usage = try snapshot.toUsageSnapshot()
#expect(usage.primary?.remainingPercent.rounded() == 20)
#expect(usage.secondary?.remainingPercent.rounded() == 50)
#expect(usage.tertiary == nil)
}
@Test
func `remote fetch verifies full model quotas with quota endpoint`() async throws {
let env = try GeminiTestEnvironment()
defer { env.cleanup() }
try env.writeAntigravityCredentials(
accessToken: "token",
refreshToken: nil,
expiry: Date().addingTimeInterval(3600),
idToken: GeminiAPITestHelpers.makeIDToken(email: "user@example.com"),
email: "user@example.com")
final class Counter: @unchecked Sendable {
private let lock = NSLock()
private var value = 0
func increment() {
self.lock.lock()
self.value += 1
self.lock.unlock()
}
func get() -> Int {
self.lock.lock()
defer { self.lock.unlock() }
return self.value
}
}
let quotaCalls = Counter()
let dataLoader = GeminiAPITestHelpers.dataLoader { request in
guard let url = request.url, let host = url.host else {
throw URLError(.badURL)
}
switch host {
case "cloudcode-pa.googleapis.com":
if url.path == "/v1internal:loadCodeAssist" {
return GeminiAPITestHelpers.response(
url: url.absoluteString,
status: 200,
body: GeminiAPITestHelpers.loadCodeAssistResponse(
tierId: "standard-tier",
projectId: "managed-project-123"))
}
if url.path == "/v1internal:fetchAvailableModels" {
return GeminiAPITestHelpers.response(
url: url.absoluteString,
status: 200,
body: GeminiAPITestHelpers.jsonData([
"models": [
"claude-sonnet-4": [
"displayName": "Claude Sonnet 4",
"quotaInfo": ["remainingFraction": 1],
],
"gemini-2.5-pro": [
"displayName": "Gemini 2.5 Pro",
"quotaInfo": ["remainingFraction": 1],
],
"gemini-2.5-flash": [
"displayName": "Gemini 2.5 Flash",
"quotaInfo": ["remainingFraction": 1],
],
],
]))
}
if url.path == "/v1internal:retrieveUserQuota" {
quotaCalls.increment()
return GeminiAPITestHelpers.response(
url: url.absoluteString,
status: 200,
body: GeminiAPITestHelpers.jsonData([
"buckets": [
[
"modelId": "claude-sonnet-4",
"resetTime": "2025-01-01T00:00:00Z",
],
[
"modelId": "gemini-2.5-pro",
"remainingFraction": 0.6,
"resetTime": "2025-01-01T00:00:00Z",
],
[
"modelId": "gemini-2.5-flash",
"remainingFraction": 0.9,
"resetTime": "2025-01-01T00:00:00Z",
],
],
]))
}
return GeminiAPITestHelpers.response(url: url.absoluteString, status: 404, body: Data())
default:
return GeminiAPITestHelpers.response(url: url.absoluteString, status: 404, body: Data())
}
}
let snapshot = try await AntigravityRemoteUsageFetcher(
timeout: 1,
homeDirectory: env.homeURL.path,
dataLoader: dataLoader)
.fetch()
let usage = try snapshot.toUsageSnapshot()
#expect(quotaCalls.get() == 1)
#expect(usage.primary?.remainingPercent == 60.0)
#expect(usage.secondary?.remainingPercent == 100.0)
#expect(usage.tertiary == nil)
}
@Test
func `remote fetch ignores full model availability when verification has no quota data`() async throws {
let env = try GeminiTestEnvironment()
defer { env.cleanup() }
try env.writeAntigravityCredentials(
accessToken: "token",
refreshToken: nil,
expiry: Date().addingTimeInterval(3600),
idToken: GeminiAPITestHelpers.makeIDToken(email: "user@example.com"),
email: "user@example.com")
let dataLoader = GeminiAPITestHelpers.dataLoader { request in
guard let url = request.url, let host = url.host else {
throw URLError(.badURL)
}
switch host {
case "cloudcode-pa.googleapis.com":
if url.path == "/v1internal:loadCodeAssist" {
return GeminiAPITestHelpers.response(
url: url.absoluteString,
status: 200,
body: GeminiAPITestHelpers.loadCodeAssistResponse(
tierId: "standard-tier",
projectId: "managed-project-123"))
}
if url.path == "/v1internal:fetchAvailableModels" {
return GeminiAPITestHelpers.response(
url: url.absoluteString,
status: 200,
body: GeminiAPITestHelpers.jsonData([
"models": [
"claude-sonnet-4": [
"displayName": "Claude Sonnet 4",
"quotaInfo": ["remainingFraction": 1],
],
"gemini-2.5-pro": [
"displayName": "Gemini 2.5 Pro",
"quotaInfo": ["remainingFraction": 1],
],
],
]))
}
if url.path == "/v1internal:retrieveUserQuota" {
return GeminiAPITestHelpers.response(
url: url.absoluteString,
status: 200,
body: GeminiAPITestHelpers.jsonData(["buckets": []]))
}
return GeminiAPITestHelpers.response(url: url.absoluteString, status: 404, body: Data())
default:
return GeminiAPITestHelpers.response(url: url.absoluteString, status: 404, body: Data())
}
}
let snapshot = try await AntigravityRemoteUsageFetcher(
timeout: 1,
homeDirectory: env.homeURL.path,
dataLoader: dataLoader)
.fetch()
#expect(snapshot.modelQuotas.isEmpty)
#expect(snapshot.accountEmail == "user@example.com")
}
@Test
func `remote fetch propagates quota verification server errors`() async throws {
let env = try GeminiTestEnvironment()
defer { env.cleanup() }
try env.writeAntigravityCredentials(
accessToken: "token",
refreshToken: nil,
expiry: Date().addingTimeInterval(3600),
idToken: GeminiAPITestHelpers.makeIDToken(email: "user@example.com"),
email: "user@example.com")
let dataLoader = GeminiAPITestHelpers.dataLoader { request in
guard let url = request.url, let host = url.host else {
throw URLError(.badURL)
}
switch host {
case "cloudcode-pa.googleapis.com":
if url.path == "/v1internal:loadCodeAssist" {
return GeminiAPITestHelpers.response(
url: url.absoluteString,
status: 200,
body: GeminiAPITestHelpers.loadCodeAssistResponse(
tierId: "standard-tier",
projectId: "managed-project-123"))
}
if url.path == "/v1internal:fetchAvailableModels" {
return GeminiAPITestHelpers.response(
url: url.absoluteString,
status: 200,
body: GeminiAPITestHelpers.jsonData([
"models": [
"claude-sonnet-4": [
"displayName": "Claude Sonnet 4",
"quotaInfo": ["remainingFraction": 1],
],
"gemini-2.5-pro": [
"displayName": "Gemini 2.5 Pro",
"quotaInfo": ["remainingFraction": 1],
],
],
]))
}
if url.path == "/v1internal:retrieveUserQuota" {
return GeminiAPITestHelpers.response(
url: url.absoluteString,
status: 503,
body: Data("temporary outage".utf8))
}
return GeminiAPITestHelpers.response(url: url.absoluteString, status: 404, body: Data())
default:
return GeminiAPITestHelpers.response(url: url.absoluteString, status: 404, body: Data())
}
}
do {
_ = try await AntigravityRemoteUsageFetcher(
timeout: 1,
homeDirectory: env.homeURL.path,
dataLoader: dataLoader)
.fetch()
Issue.record("Expected quota verification server error")
} catch let error as AntigravityRemoteFetchError {
guard case let .apiError(message) = error else {
Issue.record("Unexpected Antigravity error: \(error)")
return
}
#expect(message.contains("HTTP 503"))
} catch {
Issue.record("Unexpected error: \(error)")
}
}
@Test
func `remote fetch keeps full quotas when verified quota endpoint has fractions`() async throws {
let env = try GeminiTestEnvironment()
defer { env.cleanup() }
try env.writeAntigravityCredentials(
accessToken: "token",
refreshToken: nil,
expiry: Date().addingTimeInterval(3600),
idToken: GeminiAPITestHelpers.makeIDToken(email: "user@example.com"),
email: "user@example.com")
let dataLoader = GeminiAPITestHelpers.dataLoader { request in
guard let url = request.url, let host = url.host else {
throw URLError(.badURL)
}
switch host {
case "cloudcode-pa.googleapis.com":
if url.path == "/v1internal:loadCodeAssist" {
return GeminiAPITestHelpers.response(
url: url.absoluteString,
status: 200,
body: GeminiAPITestHelpers.loadCodeAssistResponse(
tierId: "standard-tier",
projectId: "managed-project-123"))
}
if url.path == "/v1internal:fetchAvailableModels" {
return GeminiAPITestHelpers.response(
url: url.absoluteString,
status: 200,
body: GeminiAPITestHelpers.jsonData([
"models": [
"claude-sonnet-4": [
"displayName": "Claude Sonnet 4",
"quotaInfo": ["remainingFraction": 1],
],
"gemini-2.5-pro": [
"displayName": "Gemini 2.5 Pro",
"quotaInfo": ["remainingFraction": 1],
],
],
]))
}
if url.path == "/v1internal:retrieveUserQuota" {
return GeminiAPITestHelpers.response(
url: url.absoluteString,
status: 200,
body: GeminiAPITestHelpers.jsonData([
"buckets": [
[
"modelId": "claude-sonnet-4",
"remainingFraction": 1,
"resetTime": "2025-01-01T00:00:00Z",
],
[
"modelId": "gemini-2.5-pro",
"remainingFraction": 1,
"resetTime": "2025-01-01T00:00:00Z",
],
],
]))
}
return GeminiAPITestHelpers.response(url: url.absoluteString, status: 404, body: Data())
default:
return GeminiAPITestHelpers.response(url: url.absoluteString, status: 404, body: Data())
}
}
let snapshot = try await AntigravityRemoteUsageFetcher(
timeout: 1,
homeDirectory: env.homeURL.path,
dataLoader: dataLoader)
.fetch()
let usage = try snapshot.toUsageSnapshot()
#expect(usage.primary?.remainingPercent == 100.0)
#expect(usage.secondary?.remainingPercent == 100.0)
#expect(usage.tertiary == nil)
}
@Test
func `remote fetch drops full quota rows absent from partial verification`() async throws {
let env = try GeminiTestEnvironment()
defer { env.cleanup() }
try env.writeAntigravityCredentials(
accessToken: "token",
refreshToken: nil,
expiry: Date().addingTimeInterval(3600),
idToken: GeminiAPITestHelpers.makeIDToken(email: "user@example.com"),
email: "user@example.com")
let dataLoader = GeminiAPITestHelpers.dataLoader { request in
guard let url = request.url else {
throw URLError(.badURL)
}
if url.path == "/v1internal:loadCodeAssist" {
return GeminiAPITestHelpers.response(
url: url.absoluteString,
status: 200,
body: GeminiAPITestHelpers.loadCodeAssistResponse(
tierId: "standard-tier",
projectId: "managed-project-123"))
}
if url.path == "/v1internal:fetchAvailableModels" {
return GeminiAPITestHelpers.response(
url: url.absoluteString,
status: 200,
body: GeminiAPITestHelpers.jsonData([
"models": [
"claude-sonnet-4": [
"displayName": "Claude Sonnet 4",
"quotaInfo": ["remainingFraction": 1],
],
"gemini-2.5-pro": [
"displayName": "Gemini 2.5 Pro",
"quotaInfo": ["remainingFraction": 1],
],
],
]))
}
if url.path == "/v1internal:retrieveUserQuota" {
return GeminiAPITestHelpers.response(
url: url.absoluteString,
status: 200,
body: GeminiAPITestHelpers.jsonData([
"buckets": [
[
"modelId": "gemini-2.5-pro",
"remainingFraction": 0.5,
],
],
]))
}
return GeminiAPITestHelpers.response(url: url.absoluteString, status: 404, body: Data())
}
let snapshot = try await AntigravityRemoteUsageFetcher(
timeout: 1,
homeDirectory: env.homeURL.path,
dataLoader: dataLoader)
.fetch()
#expect(snapshot.modelQuotas.map(\.modelId) == ["gemini-2.5-pro"])
#expect(snapshot.modelQuotas.map(\.remainingFraction) == [0.5])
}
@Test
func `remote fetch refreshes expired shared google token`() async throws {
let env = try GeminiTestEnvironment()
defer { env.cleanup() }
try env.writeAntigravityCredentials(
accessToken: "old-token",
refreshToken: "refresh-token",
expiry: Date().addingTimeInterval(-3600),
idToken: GeminiAPITestHelpers.makeIDToken(email: "stale@example.com"),
email: "stale@example.com",
clientID: "test-client-id",
clientSecret: "test-client-secret")
let dataLoader = GeminiAPITestHelpers.dataLoader { request in
guard let url = request.url, let host = url.host else {
throw URLError(.badURL)
}
switch host {
case "oauth2.googleapis.com":
return GeminiAPITestHelpers.response(
url: url.absoluteString,
status: 200,
body: GeminiAPITestHelpers.jsonData([
"access_token": "new-token",
"expires_in": 3600,
"id_token": GeminiAPITestHelpers.makeIDToken(email: "refreshed@example.com"),
]))
case "cloudcode-pa.googleapis.com":
let auth = request.value(forHTTPHeaderField: "Authorization")
#expect(auth == "Bearer new-token")
if url.path == "/v1internal:loadCodeAssist" {
return GeminiAPITestHelpers.response(
url: url.absoluteString,
status: 200,
body: GeminiAPITestHelpers.jsonData([
"currentTier": ["id": "standard-tier", "name": "standard"],
"cloudaicompanionProject": "managed-project-123",
]))
}
if url.path == "/v1internal:fetchAvailableModels" {
return GeminiAPITestHelpers.response(
url: url.absoluteString,
status: 200,
body: Self.availableModelsResponse())
}
return GeminiAPITestHelpers.response(url: url.absoluteString, status: 404, body: Data())
default:
return GeminiAPITestHelpers.response(url: url.absoluteString, status: 404, body: Data())
}
}
let fetcher = AntigravityRemoteUsageFetcher(
timeout: 2,
homeDirectory: env.homeURL.path,
dataLoader: dataLoader)
let snapshot = try await fetcher.fetch()
let updated = try env.readAntigravityCredentials()
#expect(updated["access_token"] as? String == "new-token")
#expect(snapshot.accountEmail == "refreshed@example.com")
}
@Test
func `remote fetch refreshes nearly expired shared google token`() async throws {
let env = try GeminiTestEnvironment()
defer { env.cleanup() }
try env.writeAntigravityCredentials(
accessToken: "old-token",
refreshToken: "refresh-token",
expiry: Date().addingTimeInterval(5),
idToken: GeminiAPITestHelpers.makeIDToken(email: "stale@example.com"),
email: "stale@example.com",
clientID: "test-client-id",
clientSecret: "test-client-secret")
let dataLoader = GeminiAPITestHelpers.dataLoader { request in
guard let url = request.url, let host = url.host else {
throw URLError(.badURL)
}
switch host {
case "oauth2.googleapis.com":
return GeminiAPITestHelpers.response(
url: url.absoluteString,
status: 200,
body: GeminiAPITestHelpers.jsonData([
"access_token": "new-token",
"expires_in": 3600,
"id_token": GeminiAPITestHelpers.makeIDToken(email: "refreshed@example.com"),
]))
case "cloudcode-pa.googleapis.com":
#expect(request.value(forHTTPHeaderField: "Authorization") == "Bearer new-token")
if url.path == "/v1internal:loadCodeAssist" {
return GeminiAPITestHelpers.response(
url: url.absoluteString,
status: 200,
body: GeminiAPITestHelpers.jsonData([
"currentTier": ["id": "standard-tier", "name": "standard"],
"cloudaicompanionProject": "managed-project-123",
]))
}
if url.path == "/v1internal:fetchAvailableModels" {
return GeminiAPITestHelpers.response(
url: url.absoluteString,
status: 200,
body: Self.availableModelsResponse())
}
return GeminiAPITestHelpers.response(url: url.absoluteString, status: 404, body: Data())
default:
return GeminiAPITestHelpers.response(url: url.absoluteString, status: 404, body: Data())
}
}
let fetcher = AntigravityRemoteUsageFetcher(
timeout: 2,
homeDirectory: env.homeURL.path,
dataLoader: dataLoader)
let snapshot = try await fetcher.fetch()
let updated = try env.readAntigravityCredentials()
#expect(updated["access_token"] as? String == "new-token")
#expect(snapshot.accountEmail == "refreshed@example.com")
}
@Test
func `remote refresh requires configured oauth client`() async throws {
let env = try GeminiTestEnvironment()
defer { env.cleanup() }
try env.writeAntigravityCredentials(
accessToken: "old-token",
refreshToken: "refresh-token",
expiry: Date().addingTimeInterval(-3600),
idToken: GeminiAPITestHelpers.makeIDToken(email: "user@example.com"),
email: "user@example.com")
let fetcher = AntigravityRemoteUsageFetcher(
timeout: 1,
homeDirectory: env.homeURL.path,
dataLoader: GeminiAPITestHelpers.dataLoader { _ in
throw URLError(.badServerResponse)
},
oauthClientResolver: { nil })
do {
_ = try await fetcher.fetch()
#expect(Bool(false), "Expected missing OAuth client configuration error")
} catch let error as AntigravityRemoteFetchError {
guard case let .apiError(message) = error else {
#expect(Bool(false), "Unexpected Antigravity error: \(error)")
return
}
#expect(message.contains("ANTIGRAVITY_OAUTH_CLIENT_ID"))
} catch {
#expect(Bool(false), "Unexpected error: \(error)")
}
}
@Test
func `remote fetch onboards project before fetching models`() async throws {
let env = try GeminiTestEnvironment()
defer { env.cleanup() }
try env.writeAntigravityCredentials(
accessToken: "token",
refreshToken: nil,
expiry: Date().addingTimeInterval(3600),
idToken: GeminiAPITestHelpers.makeIDToken(email: "user@example.com"),
email: "user@example.com")
final class Recorder: @unchecked Sendable {
private let lock = NSLock()
private var projects: [String] = []
func append(_ value: String) {
self.lock.lock()
self.projects.append(value)
self.lock.unlock()
}
func last() -> String? {
self.lock.lock()
defer { self.lock.unlock() }
return self.projects.last
}
}
let recorder = Recorder()
let dataLoader = GeminiAPITestHelpers.dataLoader { request in
guard let url = request.url, let host = url.host else {
throw URLError(.badURL)
}
switch host {
case "cloudcode-pa.googleapis.com":
if url.path == "/v1internal:loadCodeAssist" {
return GeminiAPITestHelpers.response(
url: url.absoluteString,
status: 200,
body: GeminiAPITestHelpers.jsonData([
"currentTier": ["id": "standard-tier", "name": "standard"],
"allowedTiers": [["id": "standard-tier", "isDefault": true]],
]))
}
if url.path == "/v1internal:onboardUser" {
return GeminiAPITestHelpers.response(
url: url.absoluteString,
status: 200,
body: GeminiAPITestHelpers.jsonData([
"response": [
"cloudaicompanionProject": [
"id": "onboarded-project-456",
],
],
]))
}
if url.path == "/v1internal:fetchAvailableModels" {
let body = try #require(request.httpBody)
let json = try #require(JSONSerialization.jsonObject(with: body) as? [String: Any])
if let project = json["project"] as? String {
recorder.append(project)
}
return GeminiAPITestHelpers.response(
url: url.absoluteString,
status: 200,
body: Self.availableModelsResponse())
}
return GeminiAPITestHelpers.response(url: url.absoluteString, status: 404, body: Data())
default:
return GeminiAPITestHelpers.response(url: url.absoluteString, status: 404, body: Data())
}
}
let fetcher = AntigravityRemoteUsageFetcher(
timeout: 1,
homeDirectory: env.homeURL.path,
dataLoader: dataLoader)
_ = try await fetcher.fetch()
#expect(recorder.last() == "onboarded-project-456")
}
@Test
func `remote fetch falls back to retrieve user quota when model endpoint is forbidden`() async throws {
let env = try GeminiTestEnvironment()
defer { env.cleanup() }
try env.writeAntigravityCredentials(
accessToken: "token",
refreshToken: nil,
expiry: Date().addingTimeInterval(3600),
idToken: GeminiAPITestHelpers.makeIDToken(email: "user@example.com"),
email: "user@example.com")
final class Counter: @unchecked Sendable {
private let lock = NSLock()
private var value = 0
func increment() {
self.lock.lock()
self.value += 1
self.lock.unlock()
}
func get() -> Int {
self.lock.lock()
defer { self.lock.unlock() }
return self.value
}
}
let quotaCalls = Counter()
let dataLoader = GeminiAPITestHelpers.dataLoader { request in
guard let url = request.url, let host = url.host else {
throw URLError(.badURL)
}
switch host {
case "cloudcode-pa.googleapis.com":
if url.path == "/v1internal:loadCodeAssist" {
return GeminiAPITestHelpers.response(
url: url.absoluteString,
status: 200,
body: GeminiAPITestHelpers.jsonData([
"currentTier": ["id": "standard-tier", "name": "standard"],
"cloudaicompanionProject": "managed-project-123",
]))
}
if url.path == "/v1internal:fetchAvailableModels" {
return GeminiAPITestHelpers.response(
url: url.absoluteString,
status: 403,
body: GeminiAPITestHelpers.jsonData([
"error": [
"code": 403,
"message": "The caller does not have permission",
"status": "PERMISSION_DENIED",
],
]))
}
if url.path == "/v1internal:retrieveUserQuota" {
quotaCalls.increment()
return GeminiAPITestHelpers.response(
url: url.absoluteString,
status: 200,
body: GeminiAPITestHelpers.sampleQuotaResponse())
}
return GeminiAPITestHelpers.response(url: url.absoluteString, status: 404, body: Data())
default:
return GeminiAPITestHelpers.response(url: url.absoluteString, status: 404, body: Data())
}
}
let fetcher = AntigravityRemoteUsageFetcher(
timeout: 1,
homeDirectory: env.homeURL.path,
dataLoader: dataLoader)
let snapshot = try await fetcher.fetch()
let usage = try snapshot.toUsageSnapshot()
#expect(quotaCalls.get() == 1)
#expect(usage.primary?.remainingPercent == 60.0)
#expect(usage.secondary == nil)
#expect(usage.tertiary == nil)
}
@Test
func `antigravity descriptor advertises oauth mode`() {
let descriptor = ProviderDescriptorRegistry.descriptor(for: .antigravity)
#expect(descriptor.fetchPlan.sourceModes == [.auto, .cli, .oauth])
}
@Test
func `remote fetch returns identity when both remote quota endpoints are forbidden`() async throws {
let env = try GeminiTestEnvironment()
defer { env.cleanup() }
try env.writeAntigravityCredentials(
accessToken: "token",
refreshToken: nil,
expiry: Date().addingTimeInterval(3600),
idToken: GeminiAPITestHelpers.makeIDToken(email: "user@example.com"),
email: "user@example.com")
let dataLoader = GeminiAPITestHelpers.dataLoader { request in
guard let url = request.url, let host = url.host else {
throw URLError(.badURL)
}
switch host {
case "cloudcode-pa.googleapis.com":
if url.path == "/v1internal:loadCodeAssist" {
return GeminiAPITestHelpers.response(
url: url.absoluteString,
status: 200,
body: GeminiAPITestHelpers.jsonData([
"currentTier": ["id": "standard-tier", "name": "standard"],
"cloudaicompanionProject": "managed-project-123",
]))
}
if url.path == "/v1internal:fetchAvailableModels" || url.path == "/v1internal:retrieveUserQuota" {
return GeminiAPITestHelpers.response(
url: url.absoluteString,
status: 403,
body: GeminiAPITestHelpers.jsonData([
"error": [
"code": 403,
"message": "The caller does not have permission",
"status": "PERMISSION_DENIED",
],
]))
}
return GeminiAPITestHelpers.response(url: url.absoluteString, status: 404, body: Data())
default:
return GeminiAPITestHelpers.response(url: url.absoluteString, status: 404, body: Data())
}
}
let snapshot = try await AntigravityRemoteUsageFetcher(
timeout: 1,
homeDirectory: env.homeURL.path,
dataLoader: dataLoader)
.fetch()
let usage = try AntigravityOAuthFetchStrategy.usageSnapshot(from: snapshot)
#expect(snapshot.modelQuotas.isEmpty)
#expect(snapshot.accountEmail == "user@example.com")
#expect(snapshot.accountPlan == "Paid")
#expect(usage.rateLimitsUnavailable(for: .antigravity))
}
@Test
func `remote fetch ignores gemini credentials when antigravity auth is missing`() async throws {
let env = try GeminiTestEnvironment()
defer { env.cleanup() }
try env.writeCredentials(
accessToken: "gemini-token",
refreshToken: nil,
expiry: Date().addingTimeInterval(3600),
idToken: GeminiAPITestHelpers.makeIDToken(email: "gemini@example.com"))
let fetcher = AntigravityRemoteUsageFetcher(
timeout: 1,
homeDirectory: env.homeURL.path,
dataLoader: GeminiAPITestHelpers.dataLoader { _ in
throw URLError(.badServerResponse)
})
await #expect(throws: AntigravityRemoteFetchError.notLoggedIn) {
try await fetcher.fetch()
}
}
@Test
func `remote fetch prefers stored project id from antigravity credentials`() async throws {
let env = try GeminiTestEnvironment()
defer { env.cleanup() }
try env.writeAntigravityCredentials(
accessToken: "token",
refreshToken: nil,
expiry: Date().addingTimeInterval(3600),
idToken: GeminiAPITestHelpers.makeIDToken(email: "user@example.com"),
email: "user@example.com",
projectID: "stored-project-789")
final class Recorder: @unchecked Sendable {
private let lock = NSLock()
private var projects: [String] = []
func append(_ value: String) {
self.lock.lock()
self.projects.append(value)
self.lock.unlock()
}
func last() -> String? {
self.lock.lock()
defer { self.lock.unlock() }
return self.projects.last
}
}
let recorder = Recorder()
let dataLoader = GeminiAPITestHelpers.dataLoader { request in
guard let url = request.url, let host = url.host else {
throw URLError(.badURL)
}
switch host {
case "cloudcode-pa.googleapis.com":
if url.path == "/v1internal:loadCodeAssist" {
return GeminiAPITestHelpers.response(
url: url.absoluteString,
status: 200,
body: GeminiAPITestHelpers.jsonData([
"currentTier": ["id": "standard-tier", "name": "standard"],
]))
}
if url.path == "/v1internal:fetchAvailableModels" {
let body = try #require(request.httpBody)
let json = try #require(JSONSerialization.jsonObject(with: body) as? [String: Any])
if let project = json["project"] as? String {
recorder.append(project)
}
return GeminiAPITestHelpers.response(
url: url.absoluteString,
status: 200,
body: Self.availableModelsResponse())
}
return GeminiAPITestHelpers.response(url: url.absoluteString, status: 404, body: Data())
default:
return GeminiAPITestHelpers.response(url: url.absoluteString, status: 404, body: Data())
}
}
_ = try await AntigravityRemoteUsageFetcher(
timeout: 1,
homeDirectory: env.homeURL.path,
dataLoader: dataLoader)
.fetch()
#expect(recorder.last() == "stored-project-789")
}
private static func availableModelsResponse() -> Data {
GeminiAPITestHelpers.jsonData([
"models": [
"claude-sonnet-4": [
"displayName": "Claude Sonnet 4",
"quotaInfo": [
"remainingFraction": 0.5,
"resetTime": "2025-01-01T00:00:00Z",
],
],
"gemini-3-pro-low": [
"displayName": "Gemini 3 Pro Low",
"quotaInfo": [
"remainingFraction": 0.8,
"resetTime": "2025-01-01T00:00:00Z",
],
],
"gemini-3-flash": [
"displayName": "Gemini 3 Flash",
"quotaInfo": [
"remainingFraction": 0.2,
"resetTime": "2025-01-01T00:00:00Z",
],
],
"gemini-3-flash-lite": [
"displayName": "Gemini 3 Flash Lite",
"quotaInfo": [
"remainingFraction": 0.7,
"resetTime": "2025-01-01T00:00:00Z",
],
],
],
])
}
}