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

904 lines
35 KiB
Swift

import Foundation
import Testing
@testable import CodexBarCore
private func antigravityBlockingSleep(_ interval: TimeInterval) {
Thread.sleep(forTimeInterval: interval)
}
private final class AntigravityCLICounter: @unchecked Sendable {
private let lock = NSLock()
private var count = 0
@discardableResult
func increment() -> Int {
self.lock.lock()
self.count += 1
let value = self.count
self.lock.unlock()
return value
}
var value: Int {
self.lock.lock()
let value = self.count
self.lock.unlock()
return value
}
}
private final class AntigravityCLIPortRecorder: @unchecked Sendable {
private let lock = NSLock()
private var ports: [[Int]] = []
func append(_ value: [Int]) {
self.lock.lock()
self.ports.append(value)
self.lock.unlock()
}
func snapshot() -> [[Int]] {
self.lock.lock()
let value = self.ports
self.lock.unlock()
return value
}
}
private final class AntigravityCLITimeoutRecorder: @unchecked Sendable {
private let lock = NSLock()
private var timeouts: [TimeInterval] = []
func append(_ value: TimeInterval) {
self.lock.lock()
self.timeouts.append(value)
self.lock.unlock()
}
func snapshot() -> [TimeInterval] {
self.lock.lock()
let value = self.timeouts
self.lock.unlock()
return value
}
}
private final class AntigravityCLITestClock: @unchecked Sendable {
private let lock = NSLock()
private var date: Date
init(date: Date) {
self.date = date
}
func now() -> Date {
self.lock.lock()
let value = self.date
self.date = self.date.addingTimeInterval(1)
self.lock.unlock()
return value
}
}
private final class AntigravityCLIOutputSequence: @unchecked Sendable {
private let lock = NSLock()
private var values: [Data]
init(_ values: [Data]) {
self.values = values
}
func next() -> Data {
self.lock.lock()
let value = self.values.isEmpty ? Data() : self.values.removeFirst()
self.lock.unlock()
return value
}
}
struct AntigravityCLIHTTPSFetchStrategyTests {
@Test
func `local strategy falls back to cli HTTPS in cli source mode`() {
let strategy = AntigravityStatusFetchStrategy()
let context = self.makeFetchContext(sourceMode: .cli)
#expect(strategy.shouldFallback(on: AntigravityStatusProbeError.notRunning, context: context))
}
@Test
func `local strategy falls back to cli HTTPS in auto source mode`() {
let strategy = AntigravityStatusFetchStrategy()
let context = self.makeFetchContext(sourceMode: .auto)
#expect(strategy.shouldFallback(on: AntigravityStatusProbeError.notRunning, context: context))
}
@Test
func `local strategy does not fallback for unrelated source modes`() {
let strategy = AntigravityStatusFetchStrategy()
#expect(!strategy.shouldFallback(
on: AntigravityStatusProbeError.notRunning,
context: self.makeFetchContext(sourceMode: .oauth)))
#expect(!strategy.shouldFallback(
on: AntigravityStatusProbeError.notRunning,
context: self.makeFetchContext(sourceMode: .web)))
#expect(!strategy.shouldFallback(
on: AntigravityStatusProbeError.notRunning,
context: self.makeFetchContext(sourceMode: .api)))
}
@Test
func `strategy pipeline includes cli HTTPS fallback in cli and auto modes`() async {
let descriptor = ProviderDescriptorRegistry.descriptor(for: .antigravity)
let cliStrategies = await descriptor.fetchPlan.pipeline.resolveStrategies(
self.makeFetchContext(sourceMode: .cli))
#expect(cliStrategies.map(\.id) == [
"antigravity.app-local",
"antigravity.cli-https",
"antigravity.ide-local",
])
let autoStrategies = await descriptor.fetchPlan.pipeline.resolveStrategies(
self.makeFetchContext(sourceMode: .auto))
#expect(autoStrategies.map(\.id) == [
"antigravity.app-local",
"antigravity.cli-https",
"antigravity.ide-local",
])
}
@Test
func `strategy pipeline keeps source mode authoritative with selected token account`() async {
let descriptor = ProviderDescriptorRegistry.descriptor(for: .antigravity)
let accountID = UUID()
let autoStrategies = await descriptor.fetchPlan.pipeline.resolveStrategies(
self.makeFetchContext(sourceMode: .auto, selectedTokenAccountID: accountID))
let cliStrategies = await descriptor.fetchPlan.pipeline.resolveStrategies(
self.makeFetchContext(sourceMode: .cli, selectedTokenAccountID: accountID))
let oauthStrategies = await descriptor.fetchPlan.pipeline.resolveStrategies(
self.makeFetchContext(sourceMode: .oauth, selectedTokenAccountID: accountID))
#expect(autoStrategies.map(\.id) == [
"antigravity.app-local",
"antigravity.cli-https",
"antigravity.ide-local",
"antigravity.oauth",
])
#expect(cliStrategies.map(\.id) == [
"antigravity.app-local",
"antigravity.cli-https",
"antigravity.ide-local",
])
#expect(oauthStrategies.map(\.id) == ["antigravity.oauth"])
}
@Test
func `auto strategy pipeline includes oauth when credentials are injected`() async {
let descriptor = ProviderDescriptorRegistry.descriptor(for: .antigravity)
let autoStrategies = await descriptor.fetchPlan.pipeline.resolveStrategies(
self.makeFetchContext(
sourceMode: .auto,
env: self.accountEnv(email: "selected@example.com")))
#expect(autoStrategies.map(\.id) == [
"antigravity.app-local",
"antigravity.cli-https",
"antigravity.ide-local",
"antigravity.oauth",
])
}
@Test
func `auto strategy pipeline preserves oauth fallback for shared credentials file`() async throws {
let root = FileManager.default.temporaryDirectory
.appendingPathComponent("antigravity-shared-auto-\(UUID().uuidString)", isDirectory: true)
try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
defer { try? FileManager.default.removeItem(at: root) }
let store = AntigravityOAuthCredentialsStore(
fileURL: AntigravityOAuthCredentialsStore.defaultURL(home: root))
try store.save(AntigravityOAuthCredentials(
accessToken: "access",
refreshToken: "refresh",
expiryDate: Date().addingTimeInterval(3600),
email: "legacy@example.com"))
let descriptor = ProviderDescriptorRegistry.descriptor(for: .antigravity)
let autoStrategies = await descriptor.fetchPlan.pipeline.resolveStrategies(
self.makeFetchContext(sourceMode: .auto, env: ["HOME": root.path]))
#expect(autoStrategies.map(\.id) == [
"antigravity.app-local",
"antigravity.cli-https",
"antigravity.ide-local",
"antigravity.oauth",
])
}
// MARK: - Selected-account guard
@Test
func `account guard ignores fetches without a selected account`() throws {
let usage = self.makeUsage(accountEmail: "ambient@example.com")
let context = self.makeFetchContext(
sourceMode: .auto,
env: self.accountEnv(email: "selected@example.com"))
try AntigravitySelectedAccountGuard.validate(usage, context: context)
}
@Test
func `account guard accepts matching ambient snapshot in auto mode`() throws {
let usage = self.makeUsage(accountEmail: "Selected@Example.com")
let context = self.makeFetchContext(
sourceMode: .auto,
selectedTokenAccountID: UUID(),
env: self.accountEnv(email: "selected@example.com"))
try AntigravitySelectedAccountGuard.validate(usage, context: context)
}
@Test
func `account guard rejects mismatched ambient snapshot in auto mode`() {
let usage = self.makeUsage(accountEmail: "ambient@example.com")
let context = self.makeFetchContext(
sourceMode: .auto,
selectedTokenAccountID: UUID(),
env: self.accountEnv(email: "selected@example.com"))
#expect(throws: AntigravityStatusProbeError.accountMismatch(
expected: "selected@example.com",
found: "ambient@example.com"))
{
try AntigravitySelectedAccountGuard.validate(usage, context: context)
}
}
@Test
func `account guard rejects snapshot without an identity email`() {
let usage = self.makeUsage(accountEmail: nil)
let context = self.makeFetchContext(
sourceMode: .auto,
selectedTokenAccountID: UUID(),
env: self.accountEnv(email: "selected@example.com"))
#expect(throws: AntigravityStatusProbeError.accountMismatch(
expected: "selected@example.com",
found: nil))
{
try AntigravitySelectedAccountGuard.validate(usage, context: context)
}
}
@Test
func `account guard rejects when selected account email cannot be resolved`() {
let usage = self.makeUsage(accountEmail: "ambient@example.com")
let context = self.makeFetchContext(
sourceMode: .auto,
selectedTokenAccountID: UUID())
#expect(throws: AntigravityStatusProbeError.accountMismatch(
expected: nil,
found: "ambient@example.com"))
{
try AntigravitySelectedAccountGuard.validate(usage, context: context)
}
}
@Test
func `account guard leaves explicit cli source mode authoritative`() throws {
let usage = self.makeUsage(accountEmail: "ambient@example.com")
let context = self.makeFetchContext(
sourceMode: .cli,
selectedTokenAccountID: UUID(),
env: self.accountEnv(email: "selected@example.com"))
try AntigravitySelectedAccountGuard.validate(usage, context: context)
}
@Test
func `selected account email resolves from id_token when email field missing`() {
let idToken = Self.makeIDToken(email: "jwt@example.com")
let context = self.makeFetchContext(
sourceMode: .auto,
selectedTokenAccountID: UUID(),
env: self.accountEnv(email: nil, idToken: idToken))
#expect(AntigravitySelectedAccountGuard.selectedAccountEmail(context: context) == "jwt@example.com")
}
@Test
func `selected account email prefers id_token over stored email field`() {
let idToken = Self.makeIDToken(email: "jwt@example.com")
let context = self.makeFetchContext(
sourceMode: .auto,
selectedTokenAccountID: UUID(),
env: self.accountEnv(email: "stored@example.com", idToken: idToken))
#expect(AntigravitySelectedAccountGuard.selectedAccountEmail(context: context) == "jwt@example.com")
}
@Test
func `cli HTTPS resets session only for one-shot CLI runtime`() {
// One-shot CLI invocation: reset after fetch.
#expect(AntigravityCLIHTTPSFetchStrategy.shouldResetSessionAfterFetch(self.makeFetchContext(runtime: .cli)))
// App runtime keeps the warm session.
#expect(!AntigravityCLIHTTPSFetchStrategy.shouldResetSessionAfterFetch(self.makeFetchContext(runtime: .app)))
// Long-lived CLI host (codexbar serve) keeps the warm session even at .cli runtime.
#expect(!AntigravityCLIHTTPSFetchStrategy.shouldResetSessionAfterFetch(
self.makeFetchContext(runtime: .cli, persistsCLISessions: true)))
}
@Test
func `cli HTTPS reports public source as cli`() {
#expect(AntigravityCLIHTTPSFetchStrategy.sourceLabel == "cli")
}
@Test
func `cli local strategy availability requires binary`() async throws {
let binaryURL = FileManager.default.temporaryDirectory
.appendingPathComponent("codexbar-antigravity-\(UUID().uuidString)")
try Data("#!/bin/sh\n".utf8).write(to: binaryURL)
try FileManager.default.setAttributes(
[.posixPermissions: 0o755],
ofItemAtPath: binaryURL.path)
defer { try? FileManager.default.removeItem(at: binaryURL) }
let strategy = AntigravityCLIHTTPSFetchStrategy()
let context = self.makeFetchContext(env: ["ANTIGRAVITY_CLI_PATH": binaryURL.path])
let isAvailable = await strategy.isAvailable(context)
#expect(isAvailable)
}
@Test
func `cli local endpoints remain HTTPS only on macOS`() {
#expect(
AntigravityStatusProbe.cliEndpoints(ports: [55624]) == [
AntigravityStatusProbe.AntigravityConnectionEndpoint(
scheme: "https",
port: 55624,
csrfToken: "",
source: .cliHTTPS),
])
}
@Test
func `cli HTTPS falls back to command model configs when quota summary and user status fail`() async throws {
let endpoints = [
AntigravityStatusProbe.AntigravityConnectionEndpoint(
scheme: "https",
port: 50080,
csrfToken: "",
source: .cliHTTPS),
]
let attempts = AntigravityCLICounter()
let snapshot = try await AntigravityStatusProbe.fetchSnapshot(
context: AntigravityStatusProbe.RequestContext(
endpoints: endpoints,
timeout: 1,
deadline: Date().addingTimeInterval(2)),
send: { payload, _, _ in
let attempt = attempts.increment()
if attempt == 1 {
#expect(payload.path == "/exa.language_server_pb.LanguageServerService/RetrieveUserQuotaSummary")
throw AntigravityStatusProbeError.apiError("quota summary unavailable")
}
if attempt == 2 {
#expect(payload.path == "/exa.language_server_pb.LanguageServerService/GetUserStatus")
throw AntigravityStatusProbeError.apiError("user status unavailable")
}
#expect(payload.path == "/exa.language_server_pb.LanguageServerService/GetCommandModelConfigs")
return Data("""
{
"clientModelConfigs": [
{
"label": "Claude Sonnet",
"modelOrAlias": { "model": "claude-sonnet" },
"quotaInfo": { "remainingFraction": 0.5 }
}
]
}
""".utf8)
})
#expect(snapshot.modelQuotas.first?.label == "Claude Sonnet")
#expect(attempts.value == 3)
}
@Test
func `cli HTTPS waits for user status after ports appear`() async throws {
let fetchAttempts = AntigravityCLICounter()
let drainAttempts = AntigravityCLICounter()
let fetchedPorts = AntigravityCLIPortRecorder()
let snapshot = try await AntigravityCLIHTTPSFetchStrategy.waitForSnapshot(
pid: 123,
deadline: Date().addingTimeInterval(5),
dependencies: AntigravityCLIHTTPSFetchStrategy.SnapshotWaitDependencies(
pollIntervalNanoseconds: 0,
listeningPorts: { _, _ in [50080, 50081] },
drainOutput: {
drainAttempts.increment()
return Data()
},
fetchSnapshot: { ports in
fetchedPorts.append(ports)
if fetchAttempts.increment() == 1 {
throw AntigravityStatusProbeError.apiError("HTTP 500: GetCascadeModelConfigData() is nil")
}
return AntigravityStatusSnapshot(
modelQuotas: [
AntigravityModelQuota(
label: "Claude Opus 4.6 (Thinking)",
modelId: "claude-opus-4.6-thinking",
remainingFraction: 1,
resetTime: nil,
resetDescription: nil),
],
accountEmail: "user@example.com",
accountPlan: "Pro",
source: .local)
}))
#expect(snapshot.accountEmail == "user@example.com")
#expect(fetchAttempts.value == 2)
#expect(fetchedPorts.snapshot() == [[50080, 50081], [50080, 50081]])
#expect(drainAttempts.value == 4)
}
@Test
func `cli HTTPS retries empty quota snapshots until usage is parseable`() async throws {
let fetchAttempts = AntigravityCLICounter()
let snapshot = try await AntigravityCLIHTTPSFetchStrategy.waitForSnapshot(
pid: 123,
deadline: Date().addingTimeInterval(5),
dependencies: AntigravityCLIHTTPSFetchStrategy.SnapshotWaitDependencies(
pollIntervalNanoseconds: 0,
listeningPorts: { _, _ in [50080] },
drainOutput: { Data() },
fetchSnapshot: { _ in
if fetchAttempts.increment() == 1 {
return AntigravityStatusSnapshot(
modelQuotas: [],
accountEmail: nil,
accountPlan: nil,
source: .local)
}
return AntigravityStatusSnapshot(
modelQuotas: [
AntigravityModelQuota(
label: "Claude Sonnet",
modelId: "claude-sonnet",
remainingFraction: 0.5,
resetTime: nil,
resetDescription: nil),
],
accountEmail: "user@example.com",
accountPlan: "Pro",
source: .local)
}))
#expect(fetchAttempts.value == 2)
#expect(snapshot.modelQuotas.first?.modelId == "claude-sonnet")
}
@Test
func `cli HTTPS drains output before ports appear`() async throws {
let portPolls = AntigravityCLICounter()
let drainAttempts = AntigravityCLICounter()
let snapshot = try await AntigravityCLIHTTPSFetchStrategy.waitForSnapshot(
pid: 123,
deadline: Date().addingTimeInterval(5),
dependencies: AntigravityCLIHTTPSFetchStrategy.SnapshotWaitDependencies(
pollIntervalNanoseconds: 0,
listeningPorts: { _, _ in
portPolls.increment() == 1 ? [] : [50080]
},
drainOutput: {
drainAttempts.increment()
return Data()
},
fetchSnapshot: { _ in
AntigravityStatusSnapshot(
modelQuotas: [
AntigravityModelQuota(
label: "Claude Sonnet",
modelId: "claude-sonnet",
remainingFraction: 1,
resetTime: nil,
resetDescription: nil),
],
accountEmail: "user@example.com",
accountPlan: "Pro",
source: .local)
}))
#expect(snapshot.accountEmail == "user@example.com")
#expect(portPolls.value == 2)
#expect(drainAttempts.value == 3)
}
@Test
func `cli HTTPS stops before probing when signed out prompt spans output chunks`() async {
let output = AntigravityCLIOutputSequence([
Data("Welcome. You are currently ".utf8),
Data("Welcome. You are currently not signed in.\nSelect login method:".utf8),
])
let portPolls = AntigravityCLICounter()
do {
_ = try await AntigravityCLIHTTPSFetchStrategy.waitForSnapshot(
pid: 123,
deadline: Date().addingTimeInterval(2),
dependencies: AntigravityCLIHTTPSFetchStrategy.SnapshotWaitDependencies(
pollIntervalNanoseconds: 0,
listeningPorts: { _, _ in
portPolls.increment()
return []
},
drainOutput: {
output.next()
},
fetchSnapshot: { _ in
Issue.record("Signed-out helper should not fetch a snapshot")
return AntigravityStatusSnapshot(
modelQuotas: [],
accountEmail: nil,
accountPlan: nil,
source: .local)
}))
Issue.record("Expected authentication failure")
} catch AntigravityStatusProbeError.authenticationRequired {
#expect(portPolls.value == 1)
} catch {
Issue.record("Expected authenticationRequired, got \(error)")
}
}
@Test
func `cli HTTPS allows transient automatic sign in banner`() async throws {
let output = AntigravityCLIOutputSequence([
Data("Welcome. You are currently not signed in.\nSigning in...".utf8),
Data("user@example.com\nGemini 3.1 Pro (High)".utf8),
])
let snapshot = try await AntigravityCLIHTTPSFetchStrategy.waitForSnapshot(
pid: 123,
deadline: Date().addingTimeInterval(2),
dependencies: AntigravityCLIHTTPSFetchStrategy.SnapshotWaitDependencies(
pollIntervalNanoseconds: 0,
listeningPorts: { _, _ in [50080] },
drainOutput: {
output.next()
},
fetchSnapshot: { _ in
AntigravityStatusSnapshot(
modelQuotas: [
AntigravityModelQuota(
label: "Claude Sonnet",
modelId: "claude-sonnet",
remainingFraction: 1,
resetTime: nil,
resetDescription: nil),
],
accountEmail: "user@example.com",
accountPlan: "Pro",
source: .local)
}))
#expect(snapshot.accountEmail == "user@example.com")
}
@Test
func `cli HTTPS rechecks signed out prompt after snapshot readiness`() async {
let output = AntigravityCLIOutputSequence([
Data(),
Data("You are currently not signed in.\nSelect login method:".utf8),
])
do {
_ = try await AntigravityCLIHTTPSFetchStrategy.waitForSnapshot(
pid: 123,
deadline: Date().addingTimeInterval(2),
dependencies: AntigravityCLIHTTPSFetchStrategy.SnapshotWaitDependencies(
pollIntervalNanoseconds: 0,
listeningPorts: { _, _ in [50080] },
drainOutput: {
output.next()
},
fetchSnapshot: { _ in
AntigravityStatusSnapshot(
modelQuotas: [
AntigravityModelQuota(
label: "Claude Sonnet",
modelId: "claude-sonnet",
remainingFraction: 1,
resetTime: nil,
resetDescription: nil),
],
accountEmail: "user@example.com",
accountPlan: "Pro",
source: .local)
}))
Issue.record("Expected authentication failure")
} catch AntigravityStatusProbeError.authenticationRequired {
// Expected: the late prompt wins over the apparently ready API.
} catch {
Issue.record("Expected authenticationRequired, got \(error)")
}
}
@Test
func `cli HTTPS treats empty lsof exit as ports not ready`() async throws {
let portPolls = AntigravityCLICounter()
let snapshot = try await AntigravityCLIHTTPSFetchStrategy.waitForSnapshot(
pid: 123,
deadline: Date().addingTimeInterval(5),
dependencies: AntigravityCLIHTTPSFetchStrategy.SnapshotWaitDependencies(
pollIntervalNanoseconds: 0,
listeningPorts: { _, _ in
if portPolls.increment() == 1 {
throw SubprocessRunnerError.nonZeroExit(code: 1, stderr: "")
}
return [50080]
},
drainOutput: { Data() },
fetchSnapshot: { _ in
AntigravityStatusSnapshot(
modelQuotas: [
AntigravityModelQuota(
label: "Claude Sonnet",
modelId: "claude-sonnet",
remainingFraction: 0.5,
resetTime: nil,
resetDescription: nil),
],
accountEmail: "user@example.com",
accountPlan: "Pro",
source: .local)
}))
#expect(snapshot.accountEmail == "user@example.com")
#expect(portPolls.value == 2)
}
@Test
func `parsed requests recompute timeout from shared deadline between endpoints`() async throws {
let timeoutRecorder = AntigravityCLITimeoutRecorder()
let attempts = AntigravityCLICounter()
let endpoints = [
AntigravityStatusProbe.AntigravityConnectionEndpoint(
scheme: "https",
port: 50080,
csrfToken: "",
source: .cliHTTPS),
AntigravityStatusProbe.AntigravityConnectionEndpoint(
scheme: "https",
port: 50081,
csrfToken: "",
source: .cliHTTPS),
]
let result = try await AntigravityStatusProbe.makeParsedRequest(
payload: AntigravityStatusProbe.RequestPayload(path: "/status", body: [:]),
context: AntigravityStatusProbe.RequestContext(
endpoints: endpoints,
timeout: 10,
deadline: Date().addingTimeInterval(10)),
send: { _, _, timeout in
timeoutRecorder.append(timeout)
if attempts.increment() == 1 {
antigravityBlockingSleep(0.1)
throw AntigravityStatusProbeError.apiError("first endpoint failed")
}
return Data("ok".utf8)
},
parse: { data in
guard let value = String(bytes: data, encoding: .utf8) else {
throw AntigravityStatusProbeError.apiError("invalid test data")
}
return value
})
let timeouts = timeoutRecorder.snapshot()
#expect(result == "ok")
#expect(timeouts.count == 2)
#expect(timeouts.allSatisfy { $0 < 10 })
#expect((timeouts.last ?? 10) < (timeouts.first ?? 0))
}
@Test
func `parsed request reports timeout when shared deadline is already expired`() async {
do {
_ = try await AntigravityStatusProbe.makeParsedRequest(
payload: AntigravityStatusProbe.RequestPayload(path: "/status", body: [:]),
context: AntigravityStatusProbe.RequestContext(
endpoints: [
AntigravityStatusProbe.AntigravityConnectionEndpoint(
scheme: "https",
port: 50080,
csrfToken: "",
source: .cliHTTPS),
],
timeout: 10,
deadline: Date().addingTimeInterval(-1)),
send: { _, _, _ in
Issue.record("Expired deadline should not send a request")
return Data()
},
parse: { _ in "ok" })
Issue.record("Expected timeout")
} catch AntigravityStatusProbeError.timedOut {
} catch {
Issue.record("Expected timedOut, got \(error)")
}
}
@Test
func `cli HTTPS reports last readiness error when ports never become usable`() async {
let fetchAttempts = AntigravityCLICounter()
let start = Date(timeIntervalSinceReferenceDate: 0)
let clock = AntigravityCLITestClock(date: start)
do {
_ = try await AntigravityCLIHTTPSFetchStrategy.waitForSnapshot(
pid: 123,
deadline: start.addingTimeInterval(5),
dependencies: AntigravityCLIHTTPSFetchStrategy.SnapshotWaitDependencies(
pollIntervalNanoseconds: 0,
listeningPorts: { _, _ in [50080] },
drainOutput: { Data() },
fetchSnapshot: { _ in
let attempt = fetchAttempts.increment()
throw AntigravityStatusProbeError.apiError("HTTP 500: warming attempt \(attempt)")
},
now: { clock.now() }))
Issue.record("Expected readiness polling to throw")
} catch let AntigravityStatusProbeError.apiError(message) {
#expect(fetchAttempts.value == 2)
#expect(message == "HTTP 500: warming attempt 2")
} catch {
Issue.record("Expected apiError, got \(error)")
}
}
@Test
func `cli HTTPS preserves non transient port detection errors`() async {
do {
_ = try await AntigravityCLIHTTPSFetchStrategy.waitForSnapshot(
pid: 123,
deadline: Date().addingTimeInterval(2),
dependencies: AntigravityCLIHTTPSFetchStrategy.SnapshotWaitDependencies(
pollIntervalNanoseconds: 0,
listeningPorts: { _, _ in
throw AntigravityStatusProbeError.portDetectionFailed("lsof not available")
},
drainOutput: { Data() },
fetchSnapshot: { _ in
Issue.record("Port detection failure should not fetch a snapshot")
return AntigravityStatusSnapshot(
modelQuotas: [],
accountEmail: nil,
accountPlan: nil,
source: .local)
}))
Issue.record("Expected port detection failure")
} catch let AntigravityStatusProbeError.portDetectionFailed(message) {
#expect(message == "lsof not available")
} catch {
Issue.record("Expected portDetectionFailed, got \(error)")
}
}
@Test
func `cli HTTPS endpoint does not require CSRF token`() {
let endpoint = AntigravityStatusProbe.AntigravityConnectionEndpoint(
scheme: "https",
port: 55624,
csrfToken: "ignored-by-cli",
source: .cliHTTPS)
#expect(!endpoint.requiresCSRFToken)
}
@Test
func `languageServer endpoint requires CSRF token`() {
let endpoint = AntigravityStatusProbe.AntigravityConnectionEndpoint(
scheme: "https",
port: 64440,
csrfToken: "",
source: .languageServer)
#expect(endpoint.requiresCSRFToken)
}
@Test
func `extensionServer endpoint requires CSRF token`() {
let endpoint = AntigravityStatusProbe.AntigravityConnectionEndpoint(
scheme: "http",
port: 64432,
csrfToken: "",
source: .extensionServer)
#expect(endpoint.requiresCSRFToken)
}
private func makeFetchContext(
runtime: ProviderRuntime = .app,
sourceMode: ProviderSourceMode = .auto,
selectedTokenAccountID: UUID? = nil,
persistsCLISessions: Bool = false,
env: [String: String] = [:]) -> ProviderFetchContext
{
var effectiveEnv = env
effectiveEnv["HOME"] = effectiveEnv["HOME"] ??
FileManager.default.temporaryDirectory
.appendingPathComponent("codexbar-antigravity-empty-home-\(UUID().uuidString)", isDirectory: true)
.path
return ProviderFetchContext(
runtime: runtime,
sourceMode: sourceMode,
includeCredits: false,
webTimeout: 1,
webDebugDumpHTML: false,
verbose: false,
env: effectiveEnv,
settings: nil,
fetcher: UsageFetcher(environment: effectiveEnv),
claudeFetcher: StubClaudeFetcher(),
browserDetection: BrowserDetection(cacheTTL: 0),
selectedTokenAccountID: selectedTokenAccountID,
persistsCLISessions: persistsCLISessions)
}
private func makeUsage(accountEmail: String?) -> UsageSnapshot {
UsageSnapshot(
primary: nil,
secondary: nil,
updatedAt: Date(),
identity: ProviderIdentitySnapshot(
providerID: .antigravity,
accountEmail: accountEmail,
accountOrganization: nil,
loginMethod: nil))
}
private func accountEnv(email: String?, idToken: String? = nil) -> [String: String] {
let credentials = AntigravityOAuthCredentials(
accessToken: "access",
refreshToken: "refresh",
expiryDate: Date().addingTimeInterval(3600),
idToken: idToken,
email: email)
guard let value = try? AntigravityOAuthCredentialsStore.tokenAccountValue(for: credentials) else {
return [:]
}
return [AntigravityOAuthCredentialsStore.environmentCredentialsKey: value]
}
private static func makeIDToken(email: String) -> String {
let payload = Data("{\"email\":\"\(email)\"}".utf8)
let encodedPayload = payload.base64EncodedString()
.replacingOccurrences(of: "+", with: "-")
.replacingOccurrences(of: "/", with: "_")
.replacingOccurrences(of: "=", with: "")
return "header.\(encodedPayload).signature"
}
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
}
}
}