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

849 lines
36 KiB
Swift

import CodexBarCore
import Foundation
import Testing
#if canImport(Darwin)
import Darwin
#else
import Glibc
#endif
@Suite(.serialized)
struct GeminiStatusProbeAPITests {
@Test
func `missing credentials throws not logged in`() async throws {
let env = try GeminiTestEnvironment()
defer { env.cleanup() }
let probe = GeminiStatusProbe(timeout: 1, homeDirectory: env.homeURL.path)
await Self.expectError(.notLoggedIn) {
_ = try await probe.fetch()
}
}
@Test(arguments: ["gemini-api-key", "api-key"])
func `rejects api key auth types`(authType: String) async throws {
let env = try GeminiTestEnvironment()
defer { env.cleanup() }
try env.writeSettings(authType: authType)
let probe = GeminiStatusProbe(timeout: 1, homeDirectory: env.homeURL.path)
await Self.expectError(.unsupportedAuthType("API key")) {
_ = try await probe.fetch()
}
}
@Test
func `rejects vertex auth type`() async throws {
let env = try GeminiTestEnvironment()
defer { env.cleanup() }
try env.writeSettings(authType: "vertex-ai")
let probe = GeminiStatusProbe(timeout: 1, homeDirectory: env.homeURL.path)
await Self.expectError(.unsupportedAuthType("Vertex AI")) {
_ = try await probe.fetch()
}
}
@Test
func `refreshes expired token and updates stored credentials`() async throws {
let env = try GeminiTestEnvironment()
defer { env.cleanup() }
try env.writeCredentials(
accessToken: "old-token",
refreshToken: "refresh-token",
expiry: Date().addingTimeInterval(-3600),
idToken: GeminiAPITestHelpers.makeIDToken(email: "user@example.com"))
let binURL = try env.writeFakeGeminiCLI()
let previousValue = ProcessInfo.processInfo.environment["GEMINI_CLI_PATH"]
setenv("GEMINI_CLI_PATH", binURL.path, 1)
defer {
if let previousValue {
setenv("GEMINI_CLI_PATH", previousValue, 1)
} else {
unsetenv("GEMINI_CLI_PATH")
}
}
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":
// Fail the refresh if the client_id did not come from the test stub.
// This guards against the probe accidentally extracting OAuth creds
// from an unrelated Gemini install on the developer's machine.
let body = request.httpBody.flatMap { String(data: $0, encoding: .utf8) } ?? ""
guard body.contains("client_id=test-client-id") else {
return GeminiAPITestHelpers.response(url: url.absoluteString, status: 400, body: Data())
}
let json = GeminiAPITestHelpers.jsonData([
"access_token": "new-token",
"expires_in": 3600,
"id_token": GeminiAPITestHelpers.makeIDToken(email: "user@example.com"),
])
return GeminiAPITestHelpers.response(url: url.absoluteString, status: 200, body: json)
case "cloudresourcemanager.googleapis.com":
let json = GeminiAPITestHelpers.jsonData(["projects": []])
return GeminiAPITestHelpers.response(url: url.absoluteString, status: 200, body: json)
case "cloudcode-pa.googleapis.com":
if url.path == "/v1internal:loadCodeAssist" {
let auth = request.value(forHTTPHeaderField: "Authorization")
if auth != "Bearer new-token" {
return GeminiAPITestHelpers.response(url: url.absoluteString, status: 401, body: Data())
}
return GeminiAPITestHelpers.response(
url: url.absoluteString,
status: 200,
body: GeminiAPITestHelpers.loadCodeAssistStandardTierResponse())
}
if url.path != "/v1internal:retrieveUserQuota" {
return GeminiAPITestHelpers.response(url: url.absoluteString, status: 404, body: Data())
}
let auth = request.value(forHTTPHeaderField: "Authorization")
if auth != "Bearer new-token" {
return GeminiAPITestHelpers.response(url: url.absoluteString, status: 401, body: Data())
}
return GeminiAPITestHelpers.response(
url: url.absoluteString,
status: 200,
body: GeminiAPITestHelpers.sampleQuotaResponse())
default:
return GeminiAPITestHelpers.response(url: url.absoluteString, status: 404, body: Data())
}
}
let probe = GeminiStatusProbe(timeout: 2, homeDirectory: env.homeURL.path, dataLoader: dataLoader)
let snapshot = try await probe.fetch()
#expect(snapshot.accountPlan == "Paid")
let updated = try env.readCredentials()
#expect(updated["access_token"] as? String == "new-token")
}
@Test
func `refreshes when stored Gemini credentials only have refresh token`() async throws {
let env = try GeminiTestEnvironment()
defer { env.cleanup() }
try env.writeCredentials(
accessToken: nil,
refreshToken: "refresh-token",
expiry: Date().addingTimeInterval(3600),
idToken: nil)
let binURL = try env.writeFakeGeminiCLI()
let previousValue = ProcessInfo.processInfo.environment["GEMINI_CLI_PATH"]
setenv("GEMINI_CLI_PATH", binURL.path, 1)
defer {
if let previousValue {
setenv("GEMINI_CLI_PATH", previousValue, 1)
} else {
unsetenv("GEMINI_CLI_PATH")
}
}
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 = request.httpBody.flatMap { String(data: $0, encoding: .utf8) } ?? ""
guard body.contains("client_id=test-client-id") else {
return GeminiAPITestHelpers.response(url: url.absoluteString, status: 400, body: Data())
}
let json = GeminiAPITestHelpers.jsonData([
"access_token": "new-token",
"expires_in": 3600,
"id_token": GeminiAPITestHelpers.makeIDToken(email: "user@example.com"),
])
return GeminiAPITestHelpers.response(url: url.absoluteString, status: 200, body: json)
case "cloudresourcemanager.googleapis.com":
return GeminiAPITestHelpers.response(
url: url.absoluteString,
status: 200,
body: GeminiAPITestHelpers.jsonData(["projects": []]))
case "cloudcode-pa.googleapis.com":
if url.path == "/v1internal:loadCodeAssist" {
return GeminiAPITestHelpers.response(
url: url.absoluteString,
status: 200,
body: GeminiAPITestHelpers.loadCodeAssistStandardTierResponse())
}
let auth = request.value(forHTTPHeaderField: "Authorization")
guard auth == "Bearer new-token" else {
return GeminiAPITestHelpers.response(url: url.absoluteString, status: 401, body: Data())
}
return GeminiAPITestHelpers.response(
url: url.absoluteString,
status: 200,
body: GeminiAPITestHelpers.sampleQuotaResponse())
default:
return GeminiAPITestHelpers.response(url: url.absoluteString, status: 404, body: Data())
}
}
let probe = GeminiStatusProbe(timeout: 2, homeDirectory: env.homeURL.path, dataLoader: dataLoader)
let snapshot = try await probe.fetch()
#expect(snapshot.accountEmail == "user@example.com")
let updated = try env.readCredentials()
#expect(updated["access_token"] as? String == "new-token")
}
@Test
func `refreshes expired token with nix share layout`() async throws {
let env = try GeminiTestEnvironment()
defer { env.cleanup() }
try env.writeCredentials(
accessToken: "old-token",
refreshToken: "refresh-token",
expiry: Date().addingTimeInterval(-3600),
idToken: GeminiAPITestHelpers.makeIDToken(email: "user@example.com"))
let binURL = try env.writeFakeGeminiCLI(layout: .nixShare)
let previousValue = ProcessInfo.processInfo.environment["GEMINI_CLI_PATH"]
setenv("GEMINI_CLI_PATH", binURL.path, 1)
defer {
if let previousValue {
setenv("GEMINI_CLI_PATH", previousValue, 1)
} else {
unsetenv("GEMINI_CLI_PATH")
}
}
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":
// Fail the refresh if the client_id did not come from the test stub.
// This guards against the probe accidentally extracting OAuth creds
// from an unrelated Gemini install on the developer's machine.
let body = request.httpBody.flatMap { String(data: $0, encoding: .utf8) } ?? ""
guard body.contains("client_id=test-client-id") else {
return GeminiAPITestHelpers.response(url: url.absoluteString, status: 400, body: Data())
}
let json = GeminiAPITestHelpers.jsonData([
"access_token": "new-token",
"expires_in": 3600,
"id_token": GeminiAPITestHelpers.makeIDToken(email: "user@example.com"),
])
return GeminiAPITestHelpers.response(url: url.absoluteString, status: 200, body: json)
case "cloudresourcemanager.googleapis.com":
let json = GeminiAPITestHelpers.jsonData(["projects": []])
return GeminiAPITestHelpers.response(url: url.absoluteString, status: 200, body: json)
case "cloudcode-pa.googleapis.com":
if url.path == "/v1internal:loadCodeAssist" {
let auth = request.value(forHTTPHeaderField: "Authorization")
if auth != "Bearer new-token" {
return GeminiAPITestHelpers.response(url: url.absoluteString, status: 401, body: Data())
}
return GeminiAPITestHelpers.response(
url: url.absoluteString,
status: 200,
body: GeminiAPITestHelpers.loadCodeAssistStandardTierResponse())
}
if url.path != "/v1internal:retrieveUserQuota" {
return GeminiAPITestHelpers.response(url: url.absoluteString, status: 404, body: Data())
}
let auth = request.value(forHTTPHeaderField: "Authorization")
if auth != "Bearer new-token" {
return GeminiAPITestHelpers.response(url: url.absoluteString, status: 401, body: Data())
}
return GeminiAPITestHelpers.response(
url: url.absoluteString,
status: 200,
body: GeminiAPITestHelpers.sampleQuotaResponse())
default:
return GeminiAPITestHelpers.response(url: url.absoluteString, status: 404, body: Data())
}
}
let probe = GeminiStatusProbe(timeout: 2, homeDirectory: env.homeURL.path, dataLoader: dataLoader)
let snapshot = try await probe.fetch()
#expect(snapshot.accountPlan == "Paid")
}
@Test
func `refreshes expired token with fnm bundle layout when fnm keeps stdout open`() async throws {
let env = try GeminiTestEnvironment()
defer { env.cleanup() }
let childPIDFile = env.homeURL.appendingPathComponent("fnm-child.pid")
defer {
if let text = try? String(contentsOf: childPIDFile, encoding: .utf8),
let childPID = pid_t(text.trimmingCharacters(in: .whitespacesAndNewlines))
{
_ = kill(childPID, SIGKILL)
}
}
try env.writeCredentials(
accessToken: "old-token",
refreshToken: "refresh-token",
expiry: Date().addingTimeInterval(-3600),
idToken: GeminiAPITestHelpers.makeIDToken(email: "user@example.com"))
let binURL = try env.writeFakeGeminiCLI(layout: .fnmBundle)
// Match the real fnm layout: package root is inside the same multishell
// dir as the bin symlink target, under lib/node_modules/@google/gemini-cli.
let multishellRoot = binURL.deletingLastPathComponent().deletingLastPathComponent()
let packageJSONPath = multishellRoot
.appendingPathComponent("lib")
.appendingPathComponent("node_modules")
.appendingPathComponent("@google")
.appendingPathComponent("gemini-cli")
.appendingPathComponent("package.json")
let npmRoot = packageJSONPath
.deletingLastPathComponent()
.deletingLastPathComponent()
.deletingLastPathComponent()
.path
_ = try env.writeFakeFnm(
npmRoot: npmRoot,
geminiPackageJSONPath: packageJSONPath.path,
holdNpmRootStdoutOpen: true)
let previousPath = ProcessInfo.processInfo.environment["PATH"]
let previousPIDFile = ProcessInfo.processInfo.environment["CODEXBAR_TEST_CHILD_PID_FILE"]
let fakeBinDir = env.homeURL.appendingPathComponent("bin").path
let pathValue = if let previousPath, !previousPath.isEmpty {
"\(fakeBinDir):\(binURL.deletingLastPathComponent().path):\(previousPath)"
} else {
"\(fakeBinDir):\(binURL.deletingLastPathComponent().path)"
}
setenv("PATH", pathValue, 1)
setenv("CODEXBAR_TEST_CHILD_PID_FILE", childPIDFile.path, 1)
let previousGeminiPath = ProcessInfo.processInfo.environment["GEMINI_CLI_PATH"]
setenv("GEMINI_CLI_PATH", binURL.path, 1)
defer {
if let previousPath {
setenv("PATH", previousPath, 1)
} else {
unsetenv("PATH")
}
if let previousPIDFile {
setenv("CODEXBAR_TEST_CHILD_PID_FILE", previousPIDFile, 1)
} else {
unsetenv("CODEXBAR_TEST_CHILD_PID_FILE")
}
if let previousGeminiPath {
setenv("GEMINI_CLI_PATH", previousGeminiPath, 1)
} else {
unsetenv("GEMINI_CLI_PATH")
}
}
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":
// Fail the refresh if the client_id did not come from the test stub.
// This guards against the probe accidentally extracting OAuth creds
// from an unrelated Gemini install on the developer's machine.
let body = request.httpBody.flatMap { String(data: $0, encoding: .utf8) } ?? ""
guard body.contains("client_id=test-client-id") else {
return GeminiAPITestHelpers.response(url: url.absoluteString, status: 400, body: Data())
}
let json = GeminiAPITestHelpers.jsonData([
"access_token": "new-token",
"expires_in": 3600,
"id_token": GeminiAPITestHelpers.makeIDToken(email: "user@example.com"),
])
return GeminiAPITestHelpers.response(url: url.absoluteString, status: 200, body: json)
case "cloudresourcemanager.googleapis.com":
let json = GeminiAPITestHelpers.jsonData(["projects": []])
return GeminiAPITestHelpers.response(url: url.absoluteString, status: 200, body: json)
case "cloudcode-pa.googleapis.com":
if url.path == "/v1internal:loadCodeAssist" {
let auth = request.value(forHTTPHeaderField: "Authorization")
if auth != "Bearer new-token" {
return GeminiAPITestHelpers.response(url: url.absoluteString, status: 401, body: Data())
}
return GeminiAPITestHelpers.response(
url: url.absoluteString,
status: 200,
body: GeminiAPITestHelpers.loadCodeAssistStandardTierResponse())
}
if url.path != "/v1internal:retrieveUserQuota" {
return GeminiAPITestHelpers.response(url: url.absoluteString, status: 404, body: Data())
}
let auth = request.value(forHTTPHeaderField: "Authorization")
if auth != "Bearer new-token" {
return GeminiAPITestHelpers.response(url: url.absoluteString, status: 401, body: Data())
}
return GeminiAPITestHelpers.response(
url: url.absoluteString,
status: 200,
body: GeminiAPITestHelpers.sampleQuotaResponse())
default:
return GeminiAPITestHelpers.response(url: url.absoluteString, status: 404, body: Data())
}
}
let probe = GeminiStatusProbe(timeout: 2, homeDirectory: env.homeURL.path, dataLoader: dataLoader)
let snapshot = try await probe.fetch()
#expect(snapshot.accountPlan == "Paid")
let childPIDText = try String(contentsOf: childPIDFile, encoding: .utf8)
let childPID = try #require(pid_t(childPIDText.trimmingCharacters(in: .whitespacesAndNewlines)))
#expect(kill(childPID, 0) == 0, "package discovery should return while the stdout-holding child is alive")
let updated = try env.readCredentials()
#expect(updated["access_token"] as? String == "new-token")
}
@Test
func `fnm helper timeout hard stops a process that ignores SIGTERM`() throws {
let env = try GeminiTestEnvironment()
defer { env.cleanup() }
let pidFile = env.homeURL.appendingPathComponent("fnm-timeout.pid")
let helper = env.homeURL.appendingPathComponent("fnm-timeout")
try """
#!/bin/sh
printf '%s\\n' "$$" > "$1"
trap '' TERM
while true; do sleep 1; done
""".write(to: helper, atomically: true, encoding: .utf8)
try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: helper.path)
let clock = ContinuousClock()
let start = clock.now
let result = GeminiStatusProbe.runProcess(
executable: helper.path,
arguments: [pidFile.path],
environment: [:],
timeout: 5)
let elapsed = start.duration(to: clock.now)
let text = try String(contentsOf: pidFile, encoding: .utf8)
let processID = try #require(pid_t(text.trimmingCharacters(in: .whitespacesAndNewlines)))
defer { _ = kill(processID, SIGKILL) }
#expect(result == nil)
#expect(kill(processID, 0) == -1)
#expect(elapsed < .seconds(7.5), "Ignored SIGTERM should escalate to SIGKILL, took \(elapsed)")
}
@Test
func `fnm helper completed no-output failure returns before deadline`() throws {
let env = try GeminiTestEnvironment()
defer { env.cleanup() }
let helper = env.homeURL.appendingPathComponent("fnm-failure")
try """
#!/bin/sh
exit 23
""".write(to: helper, atomically: true, encoding: .utf8)
try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: helper.path)
let clock = ContinuousClock()
let start = clock.now
let result = GeminiStatusProbe.runProcess(
executable: helper.path,
arguments: [],
environment: [:],
timeout: 10)
#expect(result == nil)
#expect(start.duration(to: clock.now) < .seconds(5))
}
@Test
func `fnm helper successful output returns first line`() throws {
let env = try GeminiTestEnvironment()
defer { env.cleanup() }
let helper = env.homeURL.appendingPathComponent("fnm-success")
try """
#!/bin/sh
sleep 0.05
printf '%s\n' '/tmp/gemini-package'
printf '%s\n' 'ignored trailing output'
""".write(to: helper, atomically: true, encoding: .utf8)
try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: helper.path)
let result = GeminiStatusProbe.runProcess(
executable: helper.path,
arguments: [],
environment: [:],
timeout: 2)
#expect(result == "/tmp/gemini-package")
}
@Test
func `refreshes expired token with homebrew bundle layout`() async throws {
let env = try GeminiTestEnvironment()
defer { env.cleanup() }
try env.writeCredentials(
accessToken: "old-token",
refreshToken: "refresh-token",
expiry: Date().addingTimeInterval(-3600),
idToken: GeminiAPITestHelpers.makeIDToken(email: "user@example.com"))
let binURL = try env.writeFakeGeminiCLI(layout: .homebrewBundle)
let previousGeminiPath = ProcessInfo.processInfo.environment["GEMINI_CLI_PATH"]
setenv("GEMINI_CLI_PATH", binURL.path, 1)
defer {
if let previousGeminiPath {
setenv("GEMINI_CLI_PATH", previousGeminiPath, 1)
} else {
unsetenv("GEMINI_CLI_PATH")
}
}
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 = request.httpBody.flatMap { String(data: $0, encoding: .utf8) } ?? ""
guard body.contains("client_id=test-client-id") else {
return GeminiAPITestHelpers.response(url: url.absoluteString, status: 400, body: Data())
}
let json = GeminiAPITestHelpers.jsonData([
"access_token": "new-token",
"expires_in": 3600,
"id_token": GeminiAPITestHelpers.makeIDToken(email: "user@example.com"),
])
return GeminiAPITestHelpers.response(url: url.absoluteString, status: 200, body: json)
case "cloudresourcemanager.googleapis.com":
return GeminiAPITestHelpers.response(
url: url.absoluteString,
status: 200,
body: GeminiAPITestHelpers.jsonData(["projects": []]))
case "cloudcode-pa.googleapis.com":
guard request.value(forHTTPHeaderField: "Authorization") == "Bearer new-token" else {
return GeminiAPITestHelpers.response(url: url.absoluteString, status: 401, body: Data())
}
if url.path == "/v1internal:loadCodeAssist" {
return GeminiAPITestHelpers.response(
url: url.absoluteString,
status: 200,
body: GeminiAPITestHelpers.loadCodeAssistStandardTierResponse())
}
if url.path == "/v1internal:retrieveUserQuota" {
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 probe = GeminiStatusProbe(timeout: 2, homeDirectory: env.homeURL.path, dataLoader: dataLoader)
let snapshot = try await probe.fetch()
#expect(snapshot.accountPlan == "Paid")
let updated = try env.readCredentials()
#expect(updated["access_token"] as? String == "new-token")
}
@Test
func `uses code assist project for quota`() async throws {
let env = try GeminiTestEnvironment()
defer { env.cleanup() }
try env.writeCredentials(
accessToken: "token",
refreshToken: nil,
expiry: Date().addingTimeInterval(3600),
idToken: nil)
final class ProjectCapture: @unchecked Sendable {
private let lock = NSLock()
private var value: String?
func set(_ newValue: String?) {
self.lock.lock()
self.value = newValue
self.lock.unlock()
}
func get() -> String? {
self.lock.lock()
defer { self.lock.unlock() }
return self.value
}
}
let projectId = "managed-project-123"
let seenProject = ProjectCapture()
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: "free-tier",
projectId: projectId))
}
if url.path == "/v1internal:retrieveUserQuota" {
if let body = request.httpBody,
let json = try? JSONSerialization.jsonObject(with: body) as? [String: Any]
{
seenProject.set(json["project"] as? String)
}
return GeminiAPITestHelpers.response(
url: url.absoluteString,
status: 200,
body: GeminiAPITestHelpers.sampleQuotaResponse())
}
return GeminiAPITestHelpers.response(url: url.absoluteString, status: 404, body: Data())
case "cloudresourcemanager.googleapis.com":
return GeminiAPITestHelpers.response(url: url.absoluteString, status: 500, body: Data())
default:
return GeminiAPITestHelpers.response(url: url.absoluteString, status: 404, body: Data())
}
}
let probe = GeminiStatusProbe(timeout: 1, homeDirectory: env.homeURL.path, dataLoader: dataLoader)
_ = try await probe.fetch()
#expect(seenProject.get() == projectId)
}
@Test
func `falls back to curl loader when URL session times out`() async throws {
let calls = LoaderCalls()
let url = try #require(URL(string: "https://cloudcode-pa.googleapis.com/v1internal:retrieveUserQuota"))
let request = URLRequest(url: url)
let body = Data("{\"ok\":true}".utf8)
let loader = GeminiStatusProbe.dataLoaderWithCurlFallback(
primary: { _ in
calls.incrementPrimary()
throw URLError(.timedOut)
},
fallback: { request in
calls.incrementFallback()
let (response, data) = GeminiAPITestHelpers.response(
url: request.url!.absoluteString,
status: 200,
body: body)
return (data, response)
})
let (loadedBody, loadedResponse) = try await loader(request)
let counts = calls.counts()
#expect(loadedBody == body)
#expect((loadedResponse as? HTTPURLResponse)?.statusCode == 200)
#expect(counts.primary == 1)
#expect(counts.fallback == 1)
}
@Test
func `does not fall back to curl loader for non-timeout errors`() async throws {
let calls = LoaderCalls()
let url = try #require(URL(string: "https://cloudcode-pa.googleapis.com/v1internal:retrieveUserQuota"))
let request = URLRequest(url: url)
let loader = GeminiStatusProbe.dataLoaderWithCurlFallback(
primary: { _ in
calls.incrementPrimary()
throw URLError(.cannotFindHost)
},
fallback: { request in
calls.incrementFallback()
let (response, data) = GeminiAPITestHelpers.response(
url: request.url!.absoluteString,
status: 200,
body: Data())
return (data, response)
})
do {
_ = try await loader(request)
Issue.record("Expected non-timeout URLSession error")
} catch let error as URLError {
#expect(error.code == .cannotFindHost)
}
let counts = calls.counts()
#expect(counts.primary == 1)
#expect(counts.fallback == 0)
}
@Test
func `reports api errors`() async throws {
let env = try GeminiTestEnvironment()
defer { env.cleanup() }
try env.writeCredentials(
accessToken: "token",
refreshToken: nil,
expiry: Date().addingTimeInterval(3600),
idToken: nil)
let dataLoader = GeminiAPITestHelpers.dataLoader { request in
guard let url = request.url, let host = url.host else {
throw URLError(.badURL)
}
switch host {
case "cloudresourcemanager.googleapis.com":
return GeminiAPITestHelpers.response(
url: url.absoluteString,
status: 200,
body: GeminiAPITestHelpers.jsonData(["projects": []]))
case "cloudcode-pa.googleapis.com":
if url.path != "/v1internal:retrieveUserQuota" {
return GeminiAPITestHelpers.response(url: url.absoluteString, status: 404, body: Data())
}
return GeminiAPITestHelpers.response(url: url.absoluteString, status: 500, body: Data())
default:
return GeminiAPITestHelpers.response(url: url.absoluteString, status: 404, body: Data())
}
}
let probe = GeminiStatusProbe(timeout: 1, homeDirectory: env.homeURL.path, dataLoader: dataLoader)
await Self.expectError(.apiError("HTTP 500")) {
_ = try await probe.fetch()
}
}
@Test
func `reports not logged in when access token missing`() async throws {
let env = try GeminiTestEnvironment()
defer { env.cleanup() }
try env.writeCredentials(
accessToken: "",
refreshToken: nil,
expiry: Date().addingTimeInterval(3600),
idToken: nil)
let probe = GeminiStatusProbe(timeout: 1, homeDirectory: env.homeURL.path)
await Self.expectError(.notLoggedIn) {
_ = try await probe.fetch()
}
}
@Test
func `reports not logged in on401`() async throws {
let env = try GeminiTestEnvironment()
defer { env.cleanup() }
try env.writeCredentials(
accessToken: "token",
refreshToken: nil,
expiry: Date().addingTimeInterval(3600),
idToken: nil)
let dataLoader = GeminiAPITestHelpers.dataLoader { request in
guard let url = request.url, let host = url.host else {
throw URLError(.badURL)
}
switch host {
case "cloudresourcemanager.googleapis.com":
return GeminiAPITestHelpers.response(
url: url.absoluteString,
status: 200,
body: GeminiAPITestHelpers.jsonData(["projects": []]))
case "cloudcode-pa.googleapis.com":
if url.path != "/v1internal:retrieveUserQuota" {
return GeminiAPITestHelpers.response(url: url.absoluteString, status: 404, body: Data())
}
return GeminiAPITestHelpers.response(url: url.absoluteString, status: 401, body: Data())
default:
return GeminiAPITestHelpers.response(url: url.absoluteString, status: 404, body: Data())
}
}
let probe = GeminiStatusProbe(timeout: 1, homeDirectory: env.homeURL.path, dataLoader: dataLoader)
await Self.expectError(.notLoggedIn) {
_ = try await probe.fetch()
}
}
@Test
func `reports parse errors for invalid payload`() async throws {
let env = try GeminiTestEnvironment()
defer { env.cleanup() }
try env.writeCredentials(
accessToken: "token",
refreshToken: nil,
expiry: Date().addingTimeInterval(3600),
idToken: nil)
let dataLoader = GeminiAPITestHelpers.dataLoader { request in
guard let url = request.url, let host = url.host else {
throw URLError(.badURL)
}
switch host {
case "cloudresourcemanager.googleapis.com":
return GeminiAPITestHelpers.response(
url: url.absoluteString,
status: 200,
body: GeminiAPITestHelpers.jsonData(["projects": []]))
case "cloudcode-pa.googleapis.com":
if url.path != "/v1internal:retrieveUserQuota" {
return GeminiAPITestHelpers.response(url: url.absoluteString, status: 404, body: Data())
}
return GeminiAPITestHelpers.response(
url: url.absoluteString,
status: 200,
body: GeminiAPITestHelpers.jsonData(["buckets": []]))
default:
return GeminiAPITestHelpers.response(url: url.absoluteString, status: 404, body: Data())
}
}
let probe = GeminiStatusProbe(timeout: 1, homeDirectory: env.homeURL.path, dataLoader: dataLoader)
do {
_ = try await probe.fetch()
#expect(Bool(false))
} catch {
let cast = error as? GeminiStatusProbeError
#expect(cast?.errorDescription?.contains("Could not parse Gemini usage") == true)
}
}
private static func expectError(
_ expected: GeminiStatusProbeError,
operation: () async throws -> Void) async
{
do {
try await operation()
#expect(Bool(false))
} catch {
#expect(error as? GeminiStatusProbeError == expected)
}
}
private final class LoaderCalls: @unchecked Sendable {
private let lock = NSLock()
private var primaryCount = 0
private var fallbackCount = 0
func incrementPrimary() {
self.lock.lock()
self.primaryCount += 1
self.lock.unlock()
}
func incrementFallback() {
self.lock.lock()
self.fallbackCount += 1
self.lock.unlock()
}
func counts() -> (primary: Int, fallback: Int) {
self.lock.lock()
defer { self.lock.unlock() }
return (self.primaryCount, self.fallbackCount)
}
}
}