chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
import Foundation
|
||||
import Testing
|
||||
@testable import CodexBarCore
|
||||
|
||||
#if os(Linux)
|
||||
struct AntigravityCLIStrategyLinuxTests {
|
||||
@Test
|
||||
func `cli local strategy is available with HTTP fallback`() 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 context = ProviderFetchContext(
|
||||
runtime: .cli,
|
||||
sourceMode: .cli,
|
||||
includeCredits: false,
|
||||
webTimeout: 1,
|
||||
webDebugDumpHTML: false,
|
||||
verbose: false,
|
||||
env: ["ANTIGRAVITY_CLI_PATH": binaryURL.path],
|
||||
settings: nil,
|
||||
fetcher: UsageFetcher(environment: [:]),
|
||||
claudeFetcher: StubClaudeFetcher(),
|
||||
browserDetection: BrowserDetection(cacheTTL: 0))
|
||||
let isAvailable = await AntigravityCLIHTTPSFetchStrategy().isAvailable(context)
|
||||
|
||||
#expect(isAvailable)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `cli local endpoints include Linux HTTP fallback`() {
|
||||
#expect(
|
||||
AntigravityStatusProbe.cliEndpoints(ports: [55624]) == [
|
||||
AntigravityStatusProbe.AntigravityConnectionEndpoint(
|
||||
scheme: "https",
|
||||
port: 55624,
|
||||
csrfToken: "",
|
||||
source: .cliHTTPS),
|
||||
AntigravityStatusProbe.AntigravityConnectionEndpoint(
|
||||
scheme: "http",
|
||||
port: 55624,
|
||||
csrfToken: "",
|
||||
source: .cliHTTPS),
|
||||
])
|
||||
}
|
||||
|
||||
@Test
|
||||
func `language server endpoints include Linux HTTP fallback`() {
|
||||
#expect(
|
||||
AntigravityStatusProbe.connectionCandidates(
|
||||
listeningPorts: [64440],
|
||||
languageServerCSRFToken: "language-token",
|
||||
extensionServerPort: nil,
|
||||
extensionServerCSRFToken: nil) == [
|
||||
AntigravityStatusProbe.AntigravityConnectionEndpoint(
|
||||
scheme: "https",
|
||||
port: 64440,
|
||||
csrfToken: "language-token",
|
||||
source: .languageServer),
|
||||
AntigravityStatusProbe.AntigravityConnectionEndpoint(
|
||||
scheme: "http",
|
||||
port: 64440,
|
||||
csrfToken: "language-token",
|
||||
source: .languageServer),
|
||||
])
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,72 @@
|
||||
#if canImport(Glibc) || canImport(Musl)
|
||||
import Foundation
|
||||
#if canImport(Glibc)
|
||||
import Glibc
|
||||
#elseif canImport(Musl)
|
||||
import Musl
|
||||
#endif
|
||||
import Testing
|
||||
@testable import CodexBarCore
|
||||
|
||||
struct AntigravityProcessLauncherLinuxTests {
|
||||
@Test
|
||||
func `pty launcher uses home and closes unrelated descriptors`() throws {
|
||||
let tempDirectory = FileManager.default.temporaryDirectory
|
||||
.appendingPathComponent("antigravity-spawn-\(UUID().uuidString)", isDirectory: true)
|
||||
try FileManager.default.createDirectory(at: tempDirectory, withIntermediateDirectories: true)
|
||||
defer { try? FileManager.default.removeItem(at: tempDirectory) }
|
||||
|
||||
let inheritedSourceFD = open("/dev/null", O_RDONLY)
|
||||
guard inheritedSourceFD >= 0 else {
|
||||
Issue.record("Failed to open descriptor fixture")
|
||||
return
|
||||
}
|
||||
defer { close(inheritedSourceFD) }
|
||||
let inheritedFD = fcntl(inheritedSourceFD, F_DUPFD, 200)
|
||||
guard inheritedFD >= 200 else {
|
||||
Issue.record("Failed to duplicate descriptor fixture")
|
||||
return
|
||||
}
|
||||
defer { close(inheritedFD) }
|
||||
|
||||
let outputURL = tempDirectory.appendingPathComponent("result.txt")
|
||||
let scriptURL = tempDirectory.appendingPathComponent("probe.sh")
|
||||
let script = """
|
||||
#!/bin/sh
|
||||
pwd > \(outputURL.path)
|
||||
if [ -e /proc/self/fd/\(inheritedFD) ]; then
|
||||
echo inherited >> \(outputURL.path)
|
||||
else
|
||||
echo closed >> \(outputURL.path)
|
||||
fi
|
||||
"""
|
||||
// Direct writes close the executable before spawn; atomic replacement can race with exec on overlay
|
||||
// filesystems.
|
||||
try Data(script.utf8).write(to: scriptURL)
|
||||
#expect(chmod(scriptURL.path, 0o700) == 0)
|
||||
|
||||
let handle = try AntigravityPTYProcessLauncher().launch(binary: scriptURL.path)
|
||||
defer {
|
||||
handle.killRoot()
|
||||
handle.terminateTree(signal: SIGKILL, knownDescendants: [])
|
||||
handle.closePTY()
|
||||
}
|
||||
|
||||
for _ in 0..<200 {
|
||||
if FileManager.default.fileExists(atPath: outputURL.path),
|
||||
let output = try? String(contentsOf: outputURL, encoding: .utf8)
|
||||
{
|
||||
let lines = output
|
||||
.split(separator: "\n")
|
||||
.map(String.init)
|
||||
if lines.count >= 2, output.hasSuffix("\n") { break }
|
||||
}
|
||||
Thread.sleep(forTimeInterval: 0.01)
|
||||
}
|
||||
let lines = try String(contentsOf: outputURL, encoding: .utf8)
|
||||
.split(separator: "\n")
|
||||
.map(String.init)
|
||||
#expect(lines == [NSHomeDirectory(), "closed"])
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,71 @@
|
||||
import CodexBarCore
|
||||
import Foundation
|
||||
#if canImport(FoundationNetworking)
|
||||
import FoundationNetworking
|
||||
#endif
|
||||
import Testing
|
||||
|
||||
@Suite
|
||||
struct AzureEndpointOverrideSecurityTests {
|
||||
@Test
|
||||
func azureOpenAIEndpointOverrideMustBeHTTPSOrBareHost() throws {
|
||||
let httpsURL = try #require(AzureOpenAISettingsReader.endpointURL(from: "https://proxy.example.com/base"))
|
||||
#expect(httpsURL.absoluteString == "https://proxy.example.com/base")
|
||||
|
||||
let bareURL = try #require(AzureOpenAISettingsReader.endpointURL(from: "resource.openai.azure.com"))
|
||||
#expect(bareURL.absoluteString == "https://resource.openai.azure.com")
|
||||
|
||||
let hostPortURL = try #require(AzureOpenAISettingsReader.endpointURL(from: "localhost:8443/openai"))
|
||||
#expect(hostPortURL.absoluteString == "https://localhost:8443/openai")
|
||||
|
||||
let trimmedURL = try #require(AzureOpenAISettingsReader.endpointURL(from: " https://trimmed.example.com/base "))
|
||||
#expect(trimmedURL.absoluteString == "https://trimmed.example.com/base")
|
||||
|
||||
#expect(AzureOpenAISettingsReader.endpointURL(from: "http://attacker.test") == nil)
|
||||
#expect(AzureOpenAISettingsReader.endpointURL(from: "https://user:pass@proxy.example.com") == nil)
|
||||
#expect(AzureOpenAISettingsReader.endpointURL(from: "https://proxy.example.com%2f.attacker.test") == nil)
|
||||
|
||||
#expect(throws: AzureOpenAISettingsError.invalidEndpointOverride(
|
||||
AzureOpenAISettingsReader.endpointEnvironmentKey))
|
||||
{
|
||||
try AzureOpenAISettingsReader.validateEndpointOverrides(environment: [
|
||||
AzureOpenAISettingsReader.endpointEnvironmentKey: "http://attacker.test",
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func azureOpenAIHTTPOverrideIsRejectedBeforeAPIKeyRequest() async throws {
|
||||
let endpoint = try #require(URL(string: "http://127.0.0.1:31337"))
|
||||
let transport = CapturingTransport { request in
|
||||
Issue.record("Azure OpenAI should reject insecure endpoint overrides before sending api-key headers")
|
||||
#expect(request.value(forHTTPHeaderField: "api-key") == nil)
|
||||
throw CapturingTransportError.unexpectedRequest
|
||||
}
|
||||
|
||||
do {
|
||||
_ = try await AzureOpenAIUsageFetcher.fetchUsage(
|
||||
apiKey: "AZURE_CANARY_KEY",
|
||||
endpoint: endpoint,
|
||||
deploymentName: "canary-deployment",
|
||||
transport: transport,
|
||||
updatedAt: Date(timeIntervalSince1970: 1_800_000_000))
|
||||
Issue.record("Expected AzureOpenAIUsageError.invalidEndpointOverride")
|
||||
} catch {
|
||||
#expect(error as? AzureOpenAIUsageError == .invalidEndpointOverride(
|
||||
AzureOpenAISettingsReader.endpointEnvironmentKey))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private enum CapturingTransportError: Error {
|
||||
case unexpectedRequest
|
||||
}
|
||||
|
||||
private struct CapturingTransport: ProviderHTTPTransport {
|
||||
let handler: @Sendable (URLRequest) async throws -> (Data, URLResponse)
|
||||
|
||||
func data(for request: URLRequest) async throws -> (Data, URLResponse) {
|
||||
try await self.handler(request)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,615 @@
|
||||
import CodexBarCore
|
||||
import Foundation
|
||||
import Testing
|
||||
@testable import CodexBarCLI
|
||||
|
||||
struct CLICardsRendererTests {
|
||||
@Test
|
||||
func `computes column count from terminal width`() {
|
||||
#expect(CLICardsRenderer.columnCount(terminalWidth: 80) == 2)
|
||||
#expect(CLICardsRenderer.columnCount(terminalWidth: 120) == 3)
|
||||
#expect(CLICardsRenderer.columnCount(terminalWidth: 160) == 4)
|
||||
#expect(CLICardsRenderer.columnCount(terminalWidth: 30) == 1)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `renders single codex card without color`() {
|
||||
let identity = ProviderIdentitySnapshot(
|
||||
providerID: .codex,
|
||||
accountEmail: "user@example.com",
|
||||
accountOrganization: nil,
|
||||
loginMethod: "pro")
|
||||
let snapshot = UsageSnapshot(
|
||||
primary: .init(usedPercent: 12, windowMinutes: 300, resetsAt: nil, resetDescription: "today at 3:00 PM"),
|
||||
secondary: .init(usedPercent: 25, windowMinutes: 10080, resetsAt: nil, resetDescription: "Fri at 9:00 AM"),
|
||||
tertiary: nil,
|
||||
updatedAt: Date(timeIntervalSince1970: 0),
|
||||
identity: identity)
|
||||
let card = CLICardsRenderer.makeCard(CLICardBuildInput(
|
||||
provider: .codex,
|
||||
snapshot: snapshot,
|
||||
credits: CreditsSnapshot(remaining: 42, events: [], updatedAt: Date()),
|
||||
source: "oauth",
|
||||
status: nil,
|
||||
notes: [],
|
||||
useColor: false,
|
||||
resetStyle: .absolute,
|
||||
weeklyWorkDays: nil,
|
||||
now: Date()))
|
||||
|
||||
let output = CLICardsRenderer.render(cards: [card], failures: [], terminalWidth: 80, useColor: false)
|
||||
|
||||
#expect(output.contains("Codex"))
|
||||
#expect(output.contains("[oauth]"))
|
||||
#expect(output.contains("PLAN Pro 20x"))
|
||||
#expect(output.contains("Session"))
|
||||
#expect(output.contains("88% left"))
|
||||
#expect(output.contains("[ "))
|
||||
#expect(output.contains("━"))
|
||||
#expect(output.contains("Credits:"))
|
||||
#expect(output.contains("42 left"))
|
||||
#expect(output.contains("@ user@example.com"))
|
||||
#expect(output.contains("╰"))
|
||||
}
|
||||
|
||||
@Test
|
||||
func `card includes account line`() {
|
||||
let identity = ProviderIdentitySnapshot(
|
||||
providerID: .codex,
|
||||
accountEmail: "user@example.com",
|
||||
accountOrganization: nil,
|
||||
loginMethod: "pro")
|
||||
let snapshot = UsageSnapshot(
|
||||
primary: .init(usedPercent: 12, windowMinutes: 300, resetsAt: nil, resetDescription: nil),
|
||||
secondary: nil,
|
||||
tertiary: nil,
|
||||
updatedAt: Date(timeIntervalSince1970: 0),
|
||||
identity: identity)
|
||||
let card = CLICardsRenderer.makeCard(CLICardBuildInput(
|
||||
provider: .codex,
|
||||
snapshot: snapshot,
|
||||
credits: nil,
|
||||
source: "cli",
|
||||
status: nil,
|
||||
notes: [],
|
||||
useColor: false,
|
||||
resetStyle: .absolute,
|
||||
weeklyWorkDays: nil,
|
||||
now: Date()))
|
||||
|
||||
let lines = CLICardsRenderer.renderCard(card, width: 48, useColor: false)
|
||||
let joined = lines.joined(separator: "\n")
|
||||
|
||||
#expect(joined.contains("@ user@example.com"))
|
||||
#expect(joined.contains("Session"))
|
||||
#expect(!joined.contains("Plan: Pro 20x"))
|
||||
}
|
||||
|
||||
@Test
|
||||
func `renders two card grid at fixed width`() {
|
||||
let codex = CLICardModel(
|
||||
provider: .codex,
|
||||
title: "Codex",
|
||||
sourceLabel: "oauth",
|
||||
planBadge: "Pro",
|
||||
accountLine: nil,
|
||||
infoLines: [],
|
||||
metrics: [CLICardMetric(label: "Session", remainingPercent: 88, resetText: nil)],
|
||||
extraLines: [],
|
||||
statusLine: nil)
|
||||
let claude = CLICardModel(
|
||||
provider: .claude,
|
||||
title: "Claude",
|
||||
sourceLabel: "web",
|
||||
planBadge: "Max",
|
||||
accountLine: nil,
|
||||
infoLines: [],
|
||||
metrics: [CLICardMetric(label: "Session", remainingPercent: 50, resetText: nil)],
|
||||
extraLines: [],
|
||||
statusLine: nil)
|
||||
|
||||
let output = CLICardsRenderer.render(cards: [codex, claude], failures: [], terminalWidth: 120, useColor: false)
|
||||
|
||||
#expect(output.contains("Codex"))
|
||||
#expect(output.contains("Claude"))
|
||||
#expect(output.contains("88% left"))
|
||||
#expect(output.contains("50% left"))
|
||||
#expect(output.components(separatedBy: "╰").count >= 3)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `renders failure footer without cards`() {
|
||||
let failures = [
|
||||
CLICardFailure(provider: .cursor, accountLabel: nil, message: "not configured"),
|
||||
]
|
||||
let output = CLICardsRenderer.render(cards: [], failures: failures, terminalWidth: 80, useColor: false)
|
||||
|
||||
#expect(output.contains("Failed providers:"))
|
||||
#expect(output.contains("Cursor: not configured"))
|
||||
}
|
||||
|
||||
@Test
|
||||
func `appends failure footer after successful cards`() {
|
||||
let card = CLICardModel(
|
||||
provider: .codex,
|
||||
title: "Codex",
|
||||
sourceLabel: "oauth",
|
||||
planBadge: nil,
|
||||
accountLine: nil,
|
||||
infoLines: [],
|
||||
metrics: [CLICardMetric(label: "Session", remainingPercent: 88, resetText: nil)],
|
||||
extraLines: [],
|
||||
statusLine: nil)
|
||||
let failures = [
|
||||
CLICardFailure(provider: .grok, accountLabel: nil, message: "timeout"),
|
||||
]
|
||||
|
||||
let output = CLICardsRenderer.render(cards: [card], failures: failures, terminalWidth: 80, useColor: false)
|
||||
|
||||
#expect(output.contains("88% left"))
|
||||
#expect(output.contains("Failed providers:"))
|
||||
#expect(output.contains("Grok: timeout"))
|
||||
}
|
||||
|
||||
@Test
|
||||
func `brief mode renders usage table`() {
|
||||
let card = CLICardModel(
|
||||
provider: .claude,
|
||||
title: "Claude",
|
||||
sourceLabel: "web",
|
||||
planBadge: "Max",
|
||||
accountLine: nil,
|
||||
infoLines: [],
|
||||
metrics: [CLICardMetric(label: "Session", remainingPercent: 2, resetText: "⏳ Resets in 1h 49m")],
|
||||
extraLines: [],
|
||||
statusLine: nil)
|
||||
let rows = CLICardsBriefRenderer.makeRows(cards: [card])
|
||||
let output = CLICardsBriefRenderer.render(
|
||||
rows: rows,
|
||||
failures: [],
|
||||
terminalWidth: 80,
|
||||
useColor: false,
|
||||
now: Date(timeIntervalSince1970: 0))
|
||||
|
||||
#expect(output.contains("codexbar • AI Usage & Limits"))
|
||||
#expect(output.contains("Provider"))
|
||||
#expect(output.contains("Claude"))
|
||||
#expect(output.contains("web"))
|
||||
#expect(output.contains("Max"))
|
||||
#expect(output.contains("98%"))
|
||||
#expect(output.contains("█"))
|
||||
#expect(output.contains("1h 49m"))
|
||||
#expect(output.contains("⚠ Warnings:"))
|
||||
let tableLine = output.split(separator: "\n").first { $0.hasPrefix("┌") } ?? ""
|
||||
#expect(tableLine.count >= 50)
|
||||
#expect(tableLine.count <= 72)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `synthetic quota lanes do not replace real brief usage`() {
|
||||
let snapshot = UsageSnapshot(
|
||||
primary: .init(
|
||||
usedPercent: 0,
|
||||
windowMinutes: 300,
|
||||
resetsAt: nil,
|
||||
resetDescription: nil,
|
||||
isSyntheticPlaceholder: true),
|
||||
secondary: .init(
|
||||
usedPercent: 20,
|
||||
windowMinutes: 10080,
|
||||
resetsAt: nil,
|
||||
resetDescription: nil),
|
||||
tertiary: nil,
|
||||
updatedAt: Date(timeIntervalSince1970: 0))
|
||||
let card = CLICardsRenderer.makeCard(CLICardBuildInput(
|
||||
provider: .claude,
|
||||
snapshot: snapshot,
|
||||
credits: nil,
|
||||
source: "oauth",
|
||||
status: nil,
|
||||
notes: [],
|
||||
useColor: false,
|
||||
resetStyle: .countdown,
|
||||
weeklyWorkDays: nil,
|
||||
now: Date(timeIntervalSince1970: 0)))
|
||||
let rows = CLICardsBriefRenderer.makeRows(cards: [card])
|
||||
|
||||
#expect(card.metrics.map(\.label) == ["Weekly"])
|
||||
#expect(rows.first?.usedPercent == 20)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `brief reset summary wraps to terminal width`() {
|
||||
let now = Date(timeIntervalSince1970: 1_700_000_000)
|
||||
let card = CLICardModel(
|
||||
provider: .alibabatokenplan,
|
||||
title: "Alibaba Token Plan",
|
||||
sourceLabel: "web",
|
||||
planBadge: "International",
|
||||
accountLine: nil,
|
||||
infoLines: [],
|
||||
metrics: [CLICardMetric(
|
||||
label: "Monthly budget",
|
||||
remainingPercent: 50,
|
||||
resetText: "⏳ Resets July 30 at 11:59 PM",
|
||||
resetAt: now.addingTimeInterval(3600))],
|
||||
extraLines: [],
|
||||
statusLine: nil)
|
||||
|
||||
let output = CLICardsBriefRenderer.render(
|
||||
rows: CLICardsBriefRenderer.makeRows(cards: [card]),
|
||||
failures: [],
|
||||
terminalWidth: 40,
|
||||
useColor: false,
|
||||
now: now)
|
||||
let lines = output.split(separator: "\n", omittingEmptySubsequences: false)
|
||||
|
||||
#expect(output.contains("Next reset: Alibaba Token Plan"))
|
||||
#expect(lines.allSatisfy { $0.count <= 40 })
|
||||
}
|
||||
|
||||
@Test
|
||||
func `detail backed quota descriptions are not rendered as resets`() {
|
||||
let snapshot = UsageSnapshot(
|
||||
primary: .init(
|
||||
usedPercent: 25,
|
||||
windowMinutes: nil,
|
||||
resetsAt: nil,
|
||||
resetDescription: "25/100 credits"),
|
||||
secondary: nil,
|
||||
tertiary: nil,
|
||||
updatedAt: Date(timeIntervalSince1970: 0))
|
||||
let card = CLICardsRenderer.makeCard(CLICardBuildInput(
|
||||
provider: .kilo,
|
||||
snapshot: snapshot,
|
||||
credits: nil,
|
||||
source: "api",
|
||||
status: nil,
|
||||
notes: [],
|
||||
useColor: false,
|
||||
resetStyle: .countdown,
|
||||
weeklyWorkDays: nil,
|
||||
now: Date(timeIntervalSince1970: 0)))
|
||||
|
||||
#expect(card.metrics.first?.resetText == nil)
|
||||
#expect(card.metrics.first?.detailText == "25/100 credits")
|
||||
|
||||
let output = CLICardsBriefRenderer.render(
|
||||
rows: CLICardsBriefRenderer.makeRows(cards: [card]),
|
||||
failures: [],
|
||||
terminalWidth: 80,
|
||||
useColor: false,
|
||||
now: Date(timeIntervalSince1970: 0))
|
||||
#expect(!output.contains("Next reset"))
|
||||
#expect(!output.contains("Reset 25/100 credits"))
|
||||
}
|
||||
|
||||
@Test
|
||||
func `card metrics honor reset display style`() {
|
||||
let now = Date(timeIntervalSince1970: 1_700_000_000)
|
||||
let snapshot = UsageSnapshot(
|
||||
primary: .init(
|
||||
usedPercent: 25,
|
||||
windowMinutes: 300,
|
||||
resetsAt: now.addingTimeInterval(3600),
|
||||
resetDescription: nil),
|
||||
secondary: nil,
|
||||
tertiary: nil,
|
||||
updatedAt: now)
|
||||
let countdown = CLICardsRenderer.makeCard(CLICardBuildInput(
|
||||
provider: .codex,
|
||||
snapshot: snapshot,
|
||||
credits: nil,
|
||||
source: "oauth",
|
||||
status: nil,
|
||||
notes: [],
|
||||
useColor: false,
|
||||
resetStyle: .countdown,
|
||||
weeklyWorkDays: nil,
|
||||
now: now))
|
||||
let absolute = CLICardsRenderer.makeCard(CLICardBuildInput(
|
||||
provider: .codex,
|
||||
snapshot: snapshot,
|
||||
credits: nil,
|
||||
source: "oauth",
|
||||
status: nil,
|
||||
notes: [],
|
||||
useColor: false,
|
||||
resetStyle: .absolute,
|
||||
weeklyWorkDays: nil,
|
||||
now: now))
|
||||
|
||||
#expect(countdown.metrics.first?.resetText != absolute.metrics.first?.resetText)
|
||||
#expect(countdown.metrics.first?.resetText?.contains("in 1h") == true)
|
||||
#expect(absolute.metrics.first?.resetAt == now.addingTimeInterval(3600))
|
||||
}
|
||||
|
||||
@Test
|
||||
func `long detail rows stay within card width`() {
|
||||
let card = CLICardModel(
|
||||
provider: .clawrouter,
|
||||
title: "ClawRouter",
|
||||
sourceLabel: "api",
|
||||
planBadge: nil,
|
||||
accountLine: nil,
|
||||
infoLines: ["Workspace: " + String(repeating: "long-name-", count: 12)],
|
||||
metrics: [],
|
||||
extraLines: [],
|
||||
statusLine: nil)
|
||||
|
||||
let lines = CLICardsRenderer.renderCard(card, width: 38, useColor: true, enhanced: true)
|
||||
#expect(lines.allSatisfy { TextParsing.stripANSICodes($0).count == 38 })
|
||||
}
|
||||
|
||||
@Test
|
||||
func `brief warnings name the actual quota metric`() {
|
||||
let card = CLICardModel(
|
||||
provider: .openrouter,
|
||||
title: "OpenRouter",
|
||||
sourceLabel: "api",
|
||||
planBadge: nil,
|
||||
accountLine: nil,
|
||||
infoLines: [],
|
||||
metrics: [CLICardMetric(label: "Spend", remainingPercent: 10, resetText: nil)],
|
||||
extraLines: [],
|
||||
statusLine: nil)
|
||||
|
||||
let output = CLICardsBriefRenderer.render(
|
||||
rows: CLICardsBriefRenderer.makeRows(cards: [card]),
|
||||
failures: [],
|
||||
terminalWidth: 80,
|
||||
useColor: false,
|
||||
now: Date(timeIntervalSince1970: 0))
|
||||
|
||||
#expect(output.contains("OpenRouter Spend: 90% used"))
|
||||
#expect(!output.contains("session limit"))
|
||||
}
|
||||
|
||||
@Test
|
||||
func `brief rows preserve account identity`() {
|
||||
let cards = [
|
||||
CLICardModel(
|
||||
provider: .codex,
|
||||
title: "Codex",
|
||||
sourceLabel: "oauth",
|
||||
planBadge: "Pro",
|
||||
accountLine: "one@x.dev",
|
||||
infoLines: [],
|
||||
metrics: [CLICardMetric(label: "Session", remainingPercent: 80, resetText: nil)],
|
||||
extraLines: [],
|
||||
statusLine: nil),
|
||||
CLICardModel(
|
||||
provider: .codex,
|
||||
title: "Codex",
|
||||
sourceLabel: "oauth",
|
||||
planBadge: "Pro",
|
||||
accountLine: "two@x.dev",
|
||||
infoLines: [],
|
||||
metrics: [CLICardMetric(label: "Session", remainingPercent: 60, resetText: nil)],
|
||||
extraLines: [],
|
||||
statusLine: nil),
|
||||
]
|
||||
|
||||
let output = CLICardsBriefRenderer.render(
|
||||
rows: CLICardsBriefRenderer.makeRows(cards: cards),
|
||||
failures: [],
|
||||
terminalWidth: 80,
|
||||
useColor: false,
|
||||
now: Date(timeIntervalSince1970: 0))
|
||||
|
||||
#expect(output.contains("one@x.dev"))
|
||||
#expect(output.contains("two@x.dev"))
|
||||
}
|
||||
|
||||
@Test
|
||||
func `brief warnings wrap to terminal width`() {
|
||||
let cards = ["OpenRouter", "Antigravity", "CommandCode"].map { title in
|
||||
CLICardModel(
|
||||
provider: .openrouter,
|
||||
title: title,
|
||||
sourceLabel: "api",
|
||||
planBadge: nil,
|
||||
accountLine: nil,
|
||||
infoLines: [],
|
||||
metrics: [CLICardMetric(label: "Monthly budget", remainingPercent: 5, resetText: nil)],
|
||||
extraLines: [],
|
||||
statusLine: nil)
|
||||
}
|
||||
|
||||
let output = CLICardsBriefRenderer.render(
|
||||
rows: CLICardsBriefRenderer.makeRows(cards: cards),
|
||||
failures: [],
|
||||
terminalWidth: 40,
|
||||
useColor: false,
|
||||
now: Date(timeIntervalSince1970: 0))
|
||||
let warningLines = output.split(separator: "\n").filter {
|
||||
$0.contains("Warnings:") || $0.contains("% used")
|
||||
}
|
||||
|
||||
#expect(warningLines.count > 1)
|
||||
#expect(warningLines.allSatisfy { $0.count <= 40 })
|
||||
}
|
||||
|
||||
@Test
|
||||
func `brief summary ignores unparseable reset labels and fits narrow terminals`() {
|
||||
let now = Date(timeIntervalSince1970: 1_700_000_000)
|
||||
let rows = CLICardsBriefRenderer.makeRows(cards: [
|
||||
CLICardModel(
|
||||
provider: .kilo,
|
||||
title: "Kilo",
|
||||
sourceLabel: "api",
|
||||
planBadge: nil,
|
||||
accountLine: nil,
|
||||
infoLines: [],
|
||||
metrics: [CLICardMetric(label: "Credits", remainingPercent: 75, resetText: "Reset Unlimited")],
|
||||
extraLines: [],
|
||||
statusLine: nil),
|
||||
CLICardModel(
|
||||
provider: .codex,
|
||||
title: "Codex",
|
||||
sourceLabel: "oauth",
|
||||
planBadge: "Pro",
|
||||
accountLine: nil,
|
||||
infoLines: [],
|
||||
metrics: [CLICardMetric(
|
||||
label: "Session",
|
||||
remainingPercent: 50,
|
||||
resetText: "⏳ Resets in 5h",
|
||||
resetAt: now.addingTimeInterval(5 * 3600))],
|
||||
extraLines: [],
|
||||
statusLine: nil),
|
||||
])
|
||||
|
||||
let output = CLICardsBriefRenderer.render(
|
||||
rows: rows,
|
||||
failures: [],
|
||||
terminalWidth: 40,
|
||||
useColor: false,
|
||||
now: now)
|
||||
let lines = output.split(separator: "\n", omittingEmptySubsequences: false)
|
||||
|
||||
#expect(output.contains("Next reset: Codex in 5h"))
|
||||
#expect(!output.contains("Next reset: Kilo"))
|
||||
#expect(lines.allSatisfy { $0.count <= 40 })
|
||||
}
|
||||
|
||||
@Test
|
||||
func `enhanced brief mode fills bars from used percentage`() {
|
||||
let rows = CLICardsBriefRenderer.makeRows(cards: [
|
||||
CLICardModel(
|
||||
provider: .codex,
|
||||
title: "Unused",
|
||||
sourceLabel: "oauth",
|
||||
planBadge: nil,
|
||||
accountLine: nil,
|
||||
infoLines: [],
|
||||
metrics: [CLICardMetric(label: "Session", remainingPercent: 100, resetText: nil)],
|
||||
extraLines: [],
|
||||
statusLine: nil),
|
||||
CLICardModel(
|
||||
provider: .openrouter,
|
||||
title: "Exhausted",
|
||||
sourceLabel: "api",
|
||||
planBadge: nil,
|
||||
accountLine: nil,
|
||||
infoLines: [],
|
||||
metrics: [CLICardMetric(label: "Session", remainingPercent: 0, resetText: nil)],
|
||||
extraLines: [],
|
||||
statusLine: nil),
|
||||
])
|
||||
let output = CLICardsBriefRenderer.render(
|
||||
rows: rows,
|
||||
failures: [],
|
||||
terminalWidth: 80,
|
||||
useColor: true,
|
||||
enhanced: true,
|
||||
now: Date(timeIntervalSince1970: 0))
|
||||
let plainLines = TextParsing.stripANSICodes(output).split(separator: "\n")
|
||||
let unusedLine = String(plainLines.first { $0.contains("Unused") } ?? "")
|
||||
let exhaustedLine = String(plainLines.first { $0.contains("Exhausted") } ?? "")
|
||||
|
||||
#expect(unusedLine.contains("0%"))
|
||||
#expect(unusedLine.filter { $0 == "█" }.isEmpty)
|
||||
#expect(exhaustedLine.contains("100%"))
|
||||
#expect(exhaustedLine.filter { $0 == "░" }.isEmpty)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `standard brief mode fills bars from used percentage`() {
|
||||
let rows = CLICardsBriefRenderer.makeRows(cards: [
|
||||
CLICardModel(
|
||||
provider: .codex,
|
||||
title: "Unused",
|
||||
sourceLabel: "oauth",
|
||||
planBadge: nil,
|
||||
accountLine: nil,
|
||||
infoLines: [],
|
||||
metrics: [CLICardMetric(label: "Session", remainingPercent: 100, resetText: nil)],
|
||||
extraLines: [],
|
||||
statusLine: nil),
|
||||
CLICardModel(
|
||||
provider: .openrouter,
|
||||
title: "Exhausted",
|
||||
sourceLabel: "api",
|
||||
planBadge: nil,
|
||||
accountLine: nil,
|
||||
infoLines: [],
|
||||
metrics: [CLICardMetric(label: "Session", remainingPercent: 0, resetText: nil)],
|
||||
extraLines: [],
|
||||
statusLine: nil),
|
||||
])
|
||||
let output = CLICardsBriefRenderer.render(
|
||||
rows: rows,
|
||||
failures: [],
|
||||
terminalWidth: 80,
|
||||
useColor: false,
|
||||
enhanced: false,
|
||||
now: Date(timeIntervalSince1970: 0))
|
||||
let plainLines = output.split(separator: "\n")
|
||||
let unusedLine = String(plainLines.first { $0.contains("Unused") } ?? "")
|
||||
let exhaustedLine = String(plainLines.first { $0.contains("Exhausted") } ?? "")
|
||||
|
||||
#expect(unusedLine.contains("0%"))
|
||||
#expect(unusedLine.filter { $0 == "█" }.isEmpty)
|
||||
#expect(exhaustedLine.contains("100%"))
|
||||
#expect(exhaustedLine.filter { $0 == "░" }.isEmpty)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `standard card grid shows empty remaining bar at exhaustion`() {
|
||||
let card = CLICardModel(
|
||||
provider: .openrouter,
|
||||
title: "Exhausted",
|
||||
sourceLabel: "api",
|
||||
planBadge: nil,
|
||||
accountLine: nil,
|
||||
infoLines: [],
|
||||
metrics: [CLICardMetric(label: "Session", remainingPercent: 0, resetText: nil)],
|
||||
extraLines: [],
|
||||
statusLine: nil)
|
||||
let lines = CLICardsRenderer.renderCard(card, width: 48, useColor: false, enhanced: false)
|
||||
let barLine = String(lines.first { $0.contains("[ ") && $0.contains("]") } ?? "")
|
||||
#expect(barLine.filter { $0 == "━" }.isEmpty)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `enhanced card grid shows empty remaining bar at exhaustion`() {
|
||||
let card = CLICardModel(
|
||||
provider: .openrouter,
|
||||
title: "Exhausted",
|
||||
sourceLabel: "api",
|
||||
planBadge: nil,
|
||||
accountLine: nil,
|
||||
infoLines: [],
|
||||
metrics: [CLICardMetric(label: "Session", remainingPercent: 0, resetText: nil)],
|
||||
extraLines: [],
|
||||
statusLine: nil)
|
||||
let lines = CLICardsRenderer.renderCard(card, width: 48, useColor: true, enhanced: true)
|
||||
let plainBarLine = TextParsing.stripANSICodes(
|
||||
String(lines.first { $0.contains("[ ") && $0.contains("]") } ?? ""))
|
||||
#expect(plainBarLine.filter { !$0.isWhitespace && $0 != "│" && $0 != "[" && $0 != "]" }.isEmpty)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `enhanced mode uses truecolor gradient bars`() {
|
||||
let card = CLICardModel(
|
||||
provider: .codex,
|
||||
title: "Codex",
|
||||
sourceLabel: "oauth",
|
||||
planBadge: "Pro",
|
||||
accountLine: nil,
|
||||
infoLines: [],
|
||||
metrics: [CLICardMetric(label: "Session", remainingPercent: 50, resetText: nil)],
|
||||
extraLines: [],
|
||||
statusLine: nil)
|
||||
let output = CLICardsRenderer.render(
|
||||
cards: [card],
|
||||
failures: [],
|
||||
terminalWidth: 80,
|
||||
useColor: true,
|
||||
enhanced: true)
|
||||
#expect(output.contains("38;2;"))
|
||||
#expect(output.contains("48;2;"))
|
||||
#expect(output.contains("[ "))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import Foundation
|
||||
import Testing
|
||||
@testable import CodexBarCLI
|
||||
|
||||
struct CLITerminalCapabilitiesTests {
|
||||
@Test
|
||||
func `detects kitty graphics backend`() {
|
||||
let env = ["KITTY_WINDOW_ID": "1", "TERM": "xterm-kitty"]
|
||||
#expect(CLITerminalCapabilities.detect(environment: env) == .kittyGraphics)
|
||||
#expect(CLITerminalCapabilities.supportsEnhancedCards(useColor: true, environment: env))
|
||||
}
|
||||
|
||||
@Test
|
||||
func `detects ghostty backend`() {
|
||||
let env = ["GHOSTTY_RESOURCES_DIR": "/usr/share/ghostty", "TERM": "xterm-ghostty"]
|
||||
#expect(CLITerminalCapabilities.detect(environment: env) == .kittyGraphics)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `detects truecolor without graphics env`() {
|
||||
let env = ["COLORTERM": "truecolor", "TERM": "alacritty"]
|
||||
#expect(CLITerminalCapabilities.detect(environment: env) == .truecolor)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `respects forced enhanced env override`() {
|
||||
let env = ["TERM": "dumb", "CODEXBAR_CARDS_ENHANCED": "1"]
|
||||
#expect(CLITerminalCapabilities.supportsEnhancedCards(useColor: true, environment: env))
|
||||
}
|
||||
|
||||
@Test
|
||||
func `defaults cards to standard on plain ansi terminals`() {
|
||||
let env = ["TERM": "xterm-256color"]
|
||||
#expect(!CLITerminalCapabilities.supportsEnhancedCards(useColor: true, environment: env))
|
||||
#expect(!CLITerminalCapabilities.supportsEnhancedCards(useColor: false, environment: env))
|
||||
}
|
||||
|
||||
@Test
|
||||
func `defaults cards to enhanced on truecolor terminals`() {
|
||||
let env = ["TERM": "xterm-256color", "COLORTERM": "truecolor"]
|
||||
#expect(CLITerminalCapabilities.supportsEnhancedCards(useColor: true, environment: env))
|
||||
}
|
||||
|
||||
@Test
|
||||
func `respects forced enhanced opt out`() {
|
||||
let env = ["TERM": "xterm-256color", "CODEXBAR_CARDS_ENHANCED": "0"]
|
||||
#expect(!CLITerminalCapabilities.supportsEnhancedCards(useColor: true, environment: env))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import Foundation
|
||||
import Testing
|
||||
@testable import CodexBarCore
|
||||
|
||||
struct ClaudeOAuthCredentialHistoryLinuxTests {
|
||||
@Test
|
||||
func historyOwnerIdentifierUsesSwiftCrypto() throws {
|
||||
let first = ClaudeOAuthCredentials(
|
||||
accessToken: "access-token-a",
|
||||
refreshToken: "refresh-token-a",
|
||||
expiresAt: Date(timeIntervalSinceNow: 3600),
|
||||
scopes: ["user:profile"],
|
||||
rateLimitTier: nil)
|
||||
let second = ClaudeOAuthCredentials(
|
||||
accessToken: "access-token-b",
|
||||
refreshToken: "refresh-token-b",
|
||||
expiresAt: Date(timeIntervalSinceNow: 3600),
|
||||
scopes: ["user:profile"],
|
||||
rateLimitTier: nil)
|
||||
|
||||
let firstIdentifier = try #require(first.historyOwnerIdentifier)
|
||||
let secondIdentifier = try #require(second.historyOwnerIdentifier)
|
||||
#expect(firstIdentifier.count == 64)
|
||||
#expect(firstIdentifier != secondIdentifier)
|
||||
#expect(firstIdentifier.allSatisfy { $0.isHexDigit })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
import Foundation
|
||||
import Testing
|
||||
@testable import CodexBarCore
|
||||
|
||||
@Suite(.serialized)
|
||||
struct ClaudeOAuthDelegatedRefreshLinuxTests {
|
||||
private actor Counter {
|
||||
private var value = 0
|
||||
|
||||
func increment() {
|
||||
self.value += 1
|
||||
}
|
||||
|
||||
func current() -> Int {
|
||||
self.value
|
||||
}
|
||||
}
|
||||
|
||||
private actor VersionDetectionCapture {
|
||||
private var value: Bool?
|
||||
|
||||
func record(_ value: Bool) {
|
||||
self.value = value
|
||||
}
|
||||
|
||||
func current() -> Bool? {
|
||||
self.value
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func cliOAuthSkipsVersionDetectionWhileAppPreservesIt() async throws {
|
||||
#expect(try await self.detectsClaudeVersion(runtime: .cli) == false)
|
||||
#expect(try await self.detectsClaudeVersion(runtime: .app) == true)
|
||||
}
|
||||
|
||||
@Test
|
||||
func cliOAuthDoesNotDelegateRefreshEvenForUserAction() async {
|
||||
let result = await self.runDelegatedRefresh(
|
||||
runtime: .cli,
|
||||
interaction: .userInitiated,
|
||||
promptMode: .always)
|
||||
|
||||
#expect(result.attempts == 0)
|
||||
#expect(result.message.contains("CodexBar CLI does not launch Claude"))
|
||||
}
|
||||
|
||||
@Test
|
||||
func appOAuthPreservesUserInitiatedDelegatedRefresh() async {
|
||||
let result = await self.runDelegatedRefresh(
|
||||
runtime: .app,
|
||||
interaction: .userInitiated,
|
||||
promptMode: .onlyOnUserAction)
|
||||
|
||||
#expect(result.attempts == 1)
|
||||
#expect(result.message.contains("still unavailable after delegated Claude CLI refresh"))
|
||||
}
|
||||
|
||||
@Test
|
||||
func appOAuthBackgroundRespectsPlatformKeychainPromptPolicy() async {
|
||||
let result = await self.runDelegatedRefresh(
|
||||
runtime: .app,
|
||||
interaction: .background,
|
||||
promptMode: .onlyOnUserAction)
|
||||
|
||||
#expect(result.attempts == 0)
|
||||
#expect(result.message.contains("background repair is suppressed"))
|
||||
}
|
||||
|
||||
private func runDelegatedRefresh(
|
||||
runtime: ProviderRuntime,
|
||||
interaction: ProviderInteraction,
|
||||
promptMode: ClaudeOAuthKeychainPromptMode) async -> (attempts: Int, message: String)
|
||||
{
|
||||
let counter = Counter()
|
||||
let fetcher = ClaudeUsageFetcher(
|
||||
browserDetection: BrowserDetection(cacheTTL: 0),
|
||||
environment: [:],
|
||||
runtime: runtime,
|
||||
dataSource: .oauth)
|
||||
let credentialsOverride: @Sendable (
|
||||
[String: String],
|
||||
Bool,
|
||||
Bool) async throws -> ClaudeOAuthCredentials = { _, _, _ in
|
||||
throw ClaudeOAuthCredentialsError.refreshDelegatedToClaudeCLI
|
||||
}
|
||||
let delegatedOverride: @Sendable (
|
||||
Date,
|
||||
TimeInterval,
|
||||
[String: String]) async -> ClaudeOAuthDelegatedRefreshCoordinator.Outcome = { _, _, _ in
|
||||
await counter.increment()
|
||||
return .attemptedSucceeded
|
||||
}
|
||||
|
||||
do {
|
||||
_ = try await ClaudeOAuthKeychainPromptPreference.withTaskOverrideForTesting(promptMode) {
|
||||
try await ProviderInteractionContext.$current.withValue(interaction) {
|
||||
try await ClaudeUsageFetcher.$loadOAuthCredentialsOverride
|
||||
.withValue(credentialsOverride) {
|
||||
try await ClaudeUsageFetcher.$delegatedRefreshAttemptOverride
|
||||
.withValue(delegatedOverride) {
|
||||
try await fetcher.loadLatestUsage(model: "sonnet")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Issue.record("Expected delegated-refresh path to fail with mocked stale credentials")
|
||||
return (await counter.current(), "")
|
||||
} catch let error as ClaudeUsageError {
|
||||
guard case let .oauthFailed(message) = error else {
|
||||
Issue.record("Expected ClaudeUsageError.oauthFailed, got \(error)")
|
||||
return (await counter.current(), "")
|
||||
}
|
||||
return (await counter.current(), message)
|
||||
} catch {
|
||||
Issue.record("Expected ClaudeUsageError, got \(error)")
|
||||
return (await counter.current(), "")
|
||||
}
|
||||
}
|
||||
|
||||
private func detectsClaudeVersion(runtime: ProviderRuntime) async throws -> Bool {
|
||||
let capture = VersionDetectionCapture()
|
||||
let fetcher = ClaudeUsageFetcher(
|
||||
browserDetection: BrowserDetection(cacheTTL: 0),
|
||||
environment: [:],
|
||||
runtime: runtime,
|
||||
dataSource: .oauth)
|
||||
let credentialsOverride: @Sendable (
|
||||
[String: String],
|
||||
Bool,
|
||||
Bool) async throws -> ClaudeOAuthCredentials = { _, _, _ in
|
||||
ClaudeOAuthCredentials(
|
||||
accessToken: "access-token",
|
||||
refreshToken: nil,
|
||||
expiresAt: Date(timeIntervalSinceNow: 3600),
|
||||
scopes: ["user:profile"],
|
||||
rateLimitTier: nil)
|
||||
}
|
||||
let fetchOverride: @Sendable (String, Bool) async throws -> OAuthUsageResponse = {
|
||||
_, detectClaudeVersion in
|
||||
await capture.record(detectClaudeVersion)
|
||||
return try Self.makeOAuthUsageResponse()
|
||||
}
|
||||
|
||||
_ = try await ClaudeUsageFetcher.$loadOAuthCredentialsOverride.withValue(credentialsOverride) {
|
||||
try await ClaudeUsageFetcher.$fetchOAuthUsageOverride.withValue(fetchOverride) {
|
||||
try await fetcher.loadLatestUsage(model: "sonnet")
|
||||
}
|
||||
}
|
||||
|
||||
guard let value = await capture.current() else {
|
||||
Issue.record("Expected OAuth fetch to report its version-detection policy")
|
||||
return false
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
private static func makeOAuthUsageResponse() throws -> OAuthUsageResponse {
|
||||
let json = """
|
||||
{
|
||||
"five_hour": { "utilization": 7, "resets_at": "2025-12-23T16:00:00.000Z" },
|
||||
"seven_day": { "utilization": 21, "resets_at": "2025-12-29T23:00:00.000Z" }
|
||||
}
|
||||
"""
|
||||
return try ClaudeOAuthUsageFetcher._decodeUsageResponseForTesting(Data(json.utf8))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import Logging
|
||||
import Testing
|
||||
@testable import CodexBarCore
|
||||
|
||||
@Suite(.serialized)
|
||||
struct CodexBarLoggingPerformanceTests {
|
||||
@Test
|
||||
func `filtered log messages are not evaluated`() {
|
||||
let probe = LogEvaluationProbe()
|
||||
let logger = CodexBarLogger(minimumLevel: .info) { _, message, _ in
|
||||
probe.loggedMessages.append(message)
|
||||
}
|
||||
|
||||
logger.debug(probe.expensiveMessage())
|
||||
|
||||
#expect(probe.evaluations == 0)
|
||||
#expect(probe.loggedMessages.isEmpty)
|
||||
|
||||
logger.info(probe.expensiveMessage())
|
||||
|
||||
#expect(probe.evaluations == 1)
|
||||
#expect(probe.loggedMessages == ["evaluated"])
|
||||
}
|
||||
|
||||
@Test
|
||||
func `disabled file logging does not format metadata`() {
|
||||
let sink = FileLogSink()
|
||||
var handler = FileLogHandler(label: "test", sink: sink)
|
||||
let probe = LogEvaluationProbe()
|
||||
handler[metadataKey: "expensive"] = .stringConvertible(ExpensiveMetadataValue {
|
||||
probe.evaluations += 1
|
||||
})
|
||||
|
||||
handler.log(event: LogEvent(
|
||||
level: .info,
|
||||
message: "hello",
|
||||
metadata: nil,
|
||||
source: "test",
|
||||
file: #filePath,
|
||||
function: #function,
|
||||
line: #line))
|
||||
|
||||
#expect(probe.evaluations == 0)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `redactor leaves ordinary log lines unchanged`() {
|
||||
let line = "CodexBar starting version=1.2.3 build=456"
|
||||
|
||||
#expect(LogRedactor.redact(line) == line)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `redactor still redacts sensitive log lines`() {
|
||||
let line = "Authorization: Bearer secret-token\nContact: user@example.com"
|
||||
let redacted = LogRedactor.redact(line)
|
||||
|
||||
#expect(redacted.contains("secret-token") == false)
|
||||
#expect(redacted.contains("user@example.com") == false)
|
||||
#expect(redacted.contains("Authorization: <redacted>"))
|
||||
#expect(redacted.contains("<redacted-email>"))
|
||||
}
|
||||
}
|
||||
|
||||
private final class LogEvaluationProbe: @unchecked Sendable {
|
||||
var evaluations = 0
|
||||
var loggedMessages: [String] = []
|
||||
|
||||
func expensiveMessage() -> String {
|
||||
self.evaluations += 1
|
||||
return "evaluated"
|
||||
}
|
||||
}
|
||||
|
||||
private struct ExpensiveMetadataValue: CustomStringConvertible, Sendable {
|
||||
let onRender: @Sendable () -> Void
|
||||
|
||||
var description: String {
|
||||
self.onRender()
|
||||
return "rendered"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import Foundation
|
||||
import Testing
|
||||
@testable import CodexBarCore
|
||||
|
||||
@Suite
|
||||
struct CodexOAuthCredentialsStoreLinuxTests {
|
||||
@Test
|
||||
func saveKeepsAuthJSONPrivate() throws {
|
||||
#if os(macOS) || os(Linux)
|
||||
let codexHome = FileManager.default.temporaryDirectory
|
||||
.appendingPathComponent("codex-oauth-permissions-\(UUID().uuidString)", isDirectory: true)
|
||||
defer { try? FileManager.default.removeItem(at: codexHome) }
|
||||
|
||||
let credentials = CodexOAuthCredentials(
|
||||
accessToken: "access-token",
|
||||
refreshToken: "refresh-token",
|
||||
idToken: "id-token",
|
||||
accountId: "account-123",
|
||||
lastRefresh: Date())
|
||||
|
||||
try CodexOAuthCredentialsStore.save(credentials, env: ["CODEX_HOME": codexHome.path])
|
||||
|
||||
let authURL = codexHome.appendingPathComponent("auth.json")
|
||||
let attributes = try FileManager.default.attributesOfItem(atPath: authURL.path)
|
||||
let permissions = try #require(attributes[.posixPermissions] as? NSNumber)
|
||||
#expect(permissions.intValue & 0o777 == 0o600)
|
||||
#else
|
||||
#expect(Bool(true))
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import CodexBarCore
|
||||
import Foundation
|
||||
import Testing
|
||||
|
||||
@Suite
|
||||
struct CostUsageScanExecutorLinuxTests {
|
||||
@Test
|
||||
func returnsWorkValue() async throws {
|
||||
let value = try await CostUsageScanExecutor.run { _ in 42 }
|
||||
#expect(value == 42)
|
||||
}
|
||||
|
||||
@Test
|
||||
func cancelledTaskThrowsCancellationError() async {
|
||||
let task = Task {
|
||||
try await CostUsageScanExecutor.run { checkCancellation in
|
||||
while true {
|
||||
try checkCancellation()
|
||||
Thread.sleep(forTimeInterval: 0.005)
|
||||
}
|
||||
}
|
||||
}
|
||||
task.cancel()
|
||||
|
||||
await #expect(throws: CancellationError.self) {
|
||||
try await task.value
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
#if os(Linux)
|
||||
import Foundation
|
||||
import Testing
|
||||
@testable import CodexBarCLI
|
||||
@testable import CodexBarCore
|
||||
|
||||
struct CursorLinuxTests {
|
||||
@Test
|
||||
func `Cursor database path honors absolute XDG config home`() {
|
||||
let path = CursorAppAuthStore.resolveDefaultDBPath(
|
||||
home: "/home/test",
|
||||
environment: ["XDG_CONFIG_HOME": "/custom/config"])
|
||||
#expect(path == "/custom/config/Cursor/User/globalStorage/state.vscdb")
|
||||
}
|
||||
|
||||
@Test
|
||||
func `Cursor database path falls back to dot config`() {
|
||||
let path = CursorAppAuthStore.resolveDefaultDBPath(
|
||||
home: "/home/test",
|
||||
environment: [:])
|
||||
#expect(path == "/home/test/.config/Cursor/User/globalStorage/state.vscdb")
|
||||
}
|
||||
|
||||
@Test
|
||||
func `Cursor database path rejects relative XDG config home`() {
|
||||
let path = CursorAppAuthStore.resolveDefaultDBPath(
|
||||
home: "/home/test",
|
||||
environment: ["XDG_CONFIG_HOME": "relative/config"])
|
||||
#expect(path == "/home/test/.config/Cursor/User/globalStorage/state.vscdb")
|
||||
}
|
||||
|
||||
@Test
|
||||
func `Cursor automatic source does not require macOS web support`() {
|
||||
#expect(!CodexBarCLI.sourceModeRequiresWebSupport(
|
||||
.auto,
|
||||
provider: .cursor,
|
||||
settings: ProviderSettingsSnapshot.make(
|
||||
cursor: .init(cookieSource: .auto, manualCookieHeader: nil))))
|
||||
}
|
||||
|
||||
@Test
|
||||
func `Cursor descriptor accepts explicit web source`() {
|
||||
#expect(CursorProviderDescriptor.descriptor.fetchPlan.sourceModes.contains(.web))
|
||||
}
|
||||
|
||||
@Test
|
||||
func `Cursor manual cookie does not require macOS web support`() {
|
||||
#expect(!CodexBarCLI.sourceModeRequiresWebSupport(
|
||||
.web,
|
||||
provider: .cursor,
|
||||
settings: ProviderSettingsSnapshot.make(
|
||||
cursor: .init(
|
||||
cookieSource: .manual,
|
||||
manualCookieHeader: "WorkosCursorSessionToken=test"))))
|
||||
}
|
||||
|
||||
@Test
|
||||
func `disabled Cursor web source still requires macOS web support`() {
|
||||
#expect(CodexBarCLI.sourceModeRequiresWebSupport(
|
||||
.web,
|
||||
provider: .cursor,
|
||||
settings: ProviderSettingsSnapshot.make(
|
||||
cursor: .init(cookieSource: .off, manualCookieHeader: nil))))
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,190 @@
|
||||
import CodexBarCore
|
||||
import Foundation
|
||||
import Testing
|
||||
|
||||
/// Tests for the regex-based JetBrains XML parser used on Linux.
|
||||
/// These tests verify that the non-libxml2 implementation correctly parses
|
||||
/// JetBrains AI Assistant quota files.
|
||||
@Suite
|
||||
struct JetBrainsParserLinuxTests {
|
||||
@Test
|
||||
func parsesQuotaXMLWithBothOptions() throws {
|
||||
let quotaInfo = [
|
||||
"{ "type": "Available",",
|
||||
" "current": "7478.3",",
|
||||
" "maximum": "1000000",",
|
||||
" "until": "2026-11-09T21:00:00Z",",
|
||||
" "tariffQuota": {",
|
||||
" "current": "7478.3",",
|
||||
" "maximum": "1000000",",
|
||||
" "available": "992521.7"",
|
||||
" } }",
|
||||
].joined()
|
||||
let nextRefill = [
|
||||
"{ "type": "Known",",
|
||||
" "next": "2026-01-16T14:00:54.939Z",",
|
||||
" "tariff": {",
|
||||
" "amount": "1000000",",
|
||||
" "duration": "PT720H"",
|
||||
" } }",
|
||||
].joined()
|
||||
|
||||
let xml = """
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<application>
|
||||
<component name="AIAssistantQuotaManager2">
|
||||
<option name="quotaInfo" value="\(quotaInfo)" />
|
||||
<option name="nextRefill" value="\(nextRefill)" />
|
||||
</component>
|
||||
</application>
|
||||
"""
|
||||
|
||||
let data = Data(xml.utf8)
|
||||
let snapshot = try JetBrainsStatusProbe.parseXMLData(data, detectedIDE: nil)
|
||||
|
||||
#expect(snapshot.quotaInfo.type == "Available")
|
||||
#expect(snapshot.quotaInfo.used == 7478.3)
|
||||
#expect(snapshot.quotaInfo.maximum == 1_000_000)
|
||||
#expect(snapshot.quotaInfo.available == 992_521.7)
|
||||
#expect(snapshot.refillInfo?.type == "Known")
|
||||
#expect(snapshot.refillInfo?.amount == 1_000_000)
|
||||
}
|
||||
|
||||
@Test
|
||||
func parsesQuotaXMLWithOnlyQuotaInfo() throws {
|
||||
let quotaInfo = "{"type":"free","current":"5000","maximum":"100000"}"
|
||||
|
||||
let xml = """
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<application>
|
||||
<component name="AIAssistantQuotaManager2">
|
||||
<option name="quotaInfo" value="\(quotaInfo)" />
|
||||
</component>
|
||||
</application>
|
||||
"""
|
||||
|
||||
let data = Data(xml.utf8)
|
||||
let snapshot = try JetBrainsStatusProbe.parseXMLData(data, detectedIDE: nil)
|
||||
|
||||
#expect(snapshot.quotaInfo.type == "free")
|
||||
#expect(snapshot.quotaInfo.used == 5000)
|
||||
#expect(snapshot.quotaInfo.maximum == 100_000)
|
||||
#expect(snapshot.refillInfo == nil)
|
||||
}
|
||||
|
||||
@Test
|
||||
func parsesXMLWithReversedAttributeOrder() throws {
|
||||
// Test that parser handles value before name (different attribute order)
|
||||
let quotaInfo = "{"type":"paid","current":"1000","maximum":"50000"}"
|
||||
|
||||
let xml = """
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<application>
|
||||
<component name="AIAssistantQuotaManager2">
|
||||
<option value="\(quotaInfo)" name="quotaInfo" />
|
||||
</component>
|
||||
</application>
|
||||
"""
|
||||
|
||||
let data = Data(xml.utf8)
|
||||
let snapshot = try JetBrainsStatusProbe.parseXMLData(data, detectedIDE: nil)
|
||||
|
||||
#expect(snapshot.quotaInfo.type == "paid")
|
||||
#expect(snapshot.quotaInfo.used == 1000)
|
||||
#expect(snapshot.quotaInfo.maximum == 50000)
|
||||
}
|
||||
|
||||
@Test
|
||||
func handlesHTMLEntities() throws {
|
||||
let quotaInfo = [
|
||||
"{"type":"test"",
|
||||
","current":"0"",
|
||||
","maximum":"50000"}",
|
||||
].joined()
|
||||
|
||||
let xml = """
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<application>
|
||||
<component name="AIAssistantQuotaManager2">
|
||||
<option name="quotaInfo" value="\(quotaInfo)" />
|
||||
</component>
|
||||
</application>
|
||||
"""
|
||||
|
||||
let data = Data(xml.utf8)
|
||||
let snapshot = try JetBrainsStatusProbe.parseXMLData(data, detectedIDE: nil)
|
||||
|
||||
#expect(snapshot.quotaInfo.type == "test")
|
||||
#expect(snapshot.quotaInfo.maximum == 50000)
|
||||
}
|
||||
|
||||
@Test
|
||||
func throwsOnMissingQuotaInfo() throws {
|
||||
let xml = """
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<application>
|
||||
<component name="AIAssistantQuotaManager2">
|
||||
</component>
|
||||
</application>
|
||||
"""
|
||||
|
||||
let data = Data(xml.utf8)
|
||||
#expect(throws: JetBrainsStatusProbeError.noQuotaInfo) {
|
||||
_ = try JetBrainsStatusProbe.parseXMLData(data, detectedIDE: nil)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func throwsOnMissingComponent() throws {
|
||||
let xml = """
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<application>
|
||||
<component name="SomeOtherComponent">
|
||||
<option name="quotaInfo" value="{}" />
|
||||
</component>
|
||||
</application>
|
||||
"""
|
||||
|
||||
let data = Data(xml.utf8)
|
||||
#expect(throws: JetBrainsStatusProbeError.noQuotaInfo) {
|
||||
_ = try JetBrainsStatusProbe.parseXMLData(data, detectedIDE: nil)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func throwsOnEmptyQuotaInfo() throws {
|
||||
let xml = """
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<application>
|
||||
<component name="AIAssistantQuotaManager2">
|
||||
<option name="quotaInfo" value="" />
|
||||
</component>
|
||||
</application>
|
||||
"""
|
||||
|
||||
let data = Data(xml.utf8)
|
||||
#expect(throws: JetBrainsStatusProbeError.noQuotaInfo) {
|
||||
_ = try JetBrainsStatusProbe.parseXMLData(data, detectedIDE: nil)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func parsesXMLWithSingleQuotes() throws {
|
||||
let quotaInfo = "{"type":"single","current":"100","maximum":"10000"}"
|
||||
|
||||
let xml = """
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<application>
|
||||
<component name='AIAssistantQuotaManager2'>
|
||||
<option name='quotaInfo' value='\(quotaInfo)' />
|
||||
</component>
|
||||
</application>
|
||||
"""
|
||||
|
||||
let data = Data(xml.utf8)
|
||||
let snapshot = try JetBrainsStatusProbe.parseXMLData(data, detectedIDE: nil)
|
||||
|
||||
#expect(snapshot.quotaInfo.type == "single")
|
||||
#expect(snapshot.quotaInfo.maximum == 10000)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import CodexBarCore
|
||||
import Foundation
|
||||
import Testing
|
||||
|
||||
/// Cross-platform tests for the OpenAI dashboard text parser.
|
||||
///
|
||||
/// The dashboard renders reset countdowns like "Resets Wednesday at 3pm". The parser
|
||||
/// converts a textual weekday into the next occurrence of that weekday so it can be
|
||||
/// formatted into a concrete `Date`. These tests pin down full-weekday-name coverage
|
||||
/// because the underlying regex previously missed "Wednesday" and "Saturday" (their
|
||||
/// abbreviations were not long enough to combine with the optional "day" suffix).
|
||||
@Suite
|
||||
struct OpenAIDashboardParserLinuxTests {
|
||||
private static func fixedNow() -> Date {
|
||||
var calendar = Calendar(identifier: .gregorian)
|
||||
calendar.timeZone = TimeZone(secondsFromGMT: 0)!
|
||||
// 2026-05-21 is a Thursday in the Gregorian calendar; using a fixed anchor keeps
|
||||
// the test deterministic regardless of when it executes.
|
||||
return calendar.date(from: DateComponents(year: 2026, month: 5, day: 21))!
|
||||
}
|
||||
|
||||
private static func body(forWeekday weekday: String) -> String {
|
||||
"""
|
||||
5h limit
|
||||
50% remaining
|
||||
Resets \(weekday)
|
||||
"""
|
||||
}
|
||||
|
||||
@Test
|
||||
func parsesResetLineForEveryFullWeekdayName() {
|
||||
let now = Self.fixedNow()
|
||||
for weekday in ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"] {
|
||||
let result = OpenAIDashboardParser.parseRateLimits(
|
||||
bodyText: Self.body(forWeekday: weekday),
|
||||
now: now)
|
||||
#expect(
|
||||
result.primary?.resetsAt != nil,
|
||||
"Expected a parsed resetsAt for weekday \(weekday)")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func parsesResetLineForLowercaseWednesday() {
|
||||
let now = Self.fixedNow()
|
||||
let result = OpenAIDashboardParser.parseRateLimits(
|
||||
bodyText: Self.body(forWeekday: "wednesday"),
|
||||
now: now)
|
||||
#expect(result.primary?.resetsAt != nil)
|
||||
}
|
||||
|
||||
@Test
|
||||
func parsesResetLineForLowercaseSaturday() {
|
||||
let now = Self.fixedNow()
|
||||
let result = OpenAIDashboardParser.parseRateLimits(
|
||||
bodyText: Self.body(forWeekday: "saturday"),
|
||||
now: now)
|
||||
#expect(result.primary?.resetsAt != nil)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import CodexBarCore
|
||||
import Foundation
|
||||
import Testing
|
||||
@testable import CodexBarCLI
|
||||
|
||||
#if canImport(SQLite3)
|
||||
import SQLite3
|
||||
#elseif canImport(CSQLite3)
|
||||
import CSQLite3
|
||||
#endif
|
||||
|
||||
#if canImport(SQLite3) || canImport(CSQLite3)
|
||||
@Suite
|
||||
struct OpenCodeGoLinuxTests {
|
||||
@Test
|
||||
func autoSourceDoesNotRequireWebSupport() {
|
||||
#expect(!CodexBarCLI.sourceModeRequiresWebSupport(.auto, provider: .opencodego))
|
||||
#expect(CodexBarCLI.sourceModeRequiresWebSupport(.web, provider: .opencodego))
|
||||
}
|
||||
|
||||
@Test
|
||||
func commandCodeManualCookieDoesNotRequireMacOSWebSupport() {
|
||||
let settings = ProviderSettingsSnapshot.make(
|
||||
commandcode: .init(cookieSource: .manual, manualCookieHeader: "session=manual"))
|
||||
|
||||
#expect(!CodexBarCLI.sourceModeRequiresWebSupport(
|
||||
.auto,
|
||||
provider: .commandcode,
|
||||
settings: settings))
|
||||
#expect(!CodexBarCLI.sourceModeRequiresWebSupport(
|
||||
.web,
|
||||
provider: .commandcode,
|
||||
settings: settings))
|
||||
}
|
||||
|
||||
@Test
|
||||
func localReaderLoadsOpenCodeDatabase() throws {
|
||||
let root = FileManager.default.temporaryDirectory
|
||||
.appendingPathComponent("OpenCodeGoLinuxTests-\(UUID().uuidString)", isDirectory: true)
|
||||
defer { try? FileManager.default.removeItem(at: root) }
|
||||
try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
|
||||
|
||||
let databaseURL = root.appendingPathComponent("opencode.db")
|
||||
let authURL = root.appendingPathComponent("auth.json")
|
||||
let now = Date(timeIntervalSince1970: 1_800_000_000)
|
||||
let createdMs = Int64((now.timeIntervalSince1970 - 60) * 1000)
|
||||
try Self.createDatabase(at: databaseURL, createdMs: createdMs)
|
||||
|
||||
let snapshot = try OpenCodeGoLocalUsageReader(authURL: authURL, databaseURL: databaseURL).fetch(now: now)
|
||||
|
||||
#expect(snapshot.rollingUsagePercent == 50)
|
||||
#expect(snapshot.weeklyUsagePercent == 20)
|
||||
#expect(snapshot.monthlyUsagePercent == 10)
|
||||
}
|
||||
|
||||
private static func createDatabase(at url: URL, createdMs: Int64) throws {
|
||||
var database: OpaquePointer?
|
||||
guard sqlite3_open(url.path, &database) == SQLITE_OK else {
|
||||
sqlite3_close(database)
|
||||
throw OpenCodeGoLocalUsageError.sqliteFailed("open failed")
|
||||
}
|
||||
defer { sqlite3_close(database) }
|
||||
|
||||
let data = "{\"time\":{\"created\":\(createdMs)},\"cost\":6,\"providerID\":\"opencode-go\",\"role\":\"assistant\"}"
|
||||
let sql = """
|
||||
CREATE TABLE message (id TEXT PRIMARY KEY, time_created INTEGER NOT NULL, data TEXT NOT NULL);
|
||||
INSERT INTO message (id, time_created, data) VALUES ('message-1', \(createdMs), '\(data)');
|
||||
"""
|
||||
guard sqlite3_exec(database, sql, nil, nil, nil) == SQLITE_OK else {
|
||||
throw OpenCodeGoLocalUsageError.sqliteFailed("fixture creation failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,257 @@
|
||||
import Foundation
|
||||
import Testing
|
||||
@testable import CodexBarCLI
|
||||
@testable import CodexBarCore
|
||||
|
||||
@Suite
|
||||
struct PlatformGatingTests {
|
||||
@Test
|
||||
func `shell probe requests a detached Linux session`() {
|
||||
#if os(Linux)
|
||||
#expect(ShellCommandLocator.test_shellSpawnFlags == 0x80)
|
||||
#else
|
||||
#expect(Bool(true))
|
||||
#endif
|
||||
}
|
||||
|
||||
@Test
|
||||
func ampAutoSource_doesNotRequireWebSupport() {
|
||||
#expect(!CodexBarCLI.sourceModeRequiresWebSupport(.auto, provider: .amp))
|
||||
}
|
||||
|
||||
@Test
|
||||
func claudeAutoSource_allowsPlannerToFallBackToCLI() {
|
||||
#expect(!CodexBarCLI.sourceModeRequiresWebSupport(.auto, provider: .claude))
|
||||
#expect(CodexBarCLI.sourceModeRequiresWebSupport(.web, provider: .claude))
|
||||
}
|
||||
|
||||
@Test
|
||||
func claudeAutoPipeline_skipsUnsupportedWebAndUsesCLI() async throws {
|
||||
#if os(Linux)
|
||||
let binaryURL = try Self.makeClaudeCLI(loggedIn: true)
|
||||
defer { try? FileManager.default.removeItem(at: binaryURL) }
|
||||
let context = self.makeClaudeAutoContext(env: ["CLAUDE_CLI_PATH": binaryURL.path])
|
||||
let cliFetchOverride: ClaudeStatusProbe.FetchOverride = { _, _, _ in
|
||||
Self.makeClaudeStatus()
|
||||
}
|
||||
let outcome = await ClaudeStatusProbe.withFetchOverrideForTesting(cliFetchOverride) {
|
||||
await ClaudeProviderDescriptor.makeDescriptor().fetchPlan.fetchOutcome(
|
||||
context: context,
|
||||
provider: .claude)
|
||||
}
|
||||
let result = try outcome.result.get()
|
||||
|
||||
#expect(result.strategyID == "claude.cli")
|
||||
#expect(outcome.attempts.map(\.strategyID) == ["claude.web", "claude.cli"])
|
||||
#expect(outcome.attempts.map(\.wasAvailable) == [false, true])
|
||||
#else
|
||||
#expect(Bool(true))
|
||||
#endif
|
||||
}
|
||||
|
||||
@Test
|
||||
func claudeAutoPipeline_withoutCLIReportsNoAvailableStrategy() async {
|
||||
#if os(Linux)
|
||||
let context = self.makeClaudeAutoContext()
|
||||
let outcome = await ClaudeCLIResolver.withResolvedBinaryPathOverrideForTesting(
|
||||
"/definitely/missing/claude")
|
||||
{
|
||||
await ClaudeProviderDescriptor.makeDescriptor().fetchPlan.fetchOutcome(
|
||||
context: context,
|
||||
provider: .claude)
|
||||
}
|
||||
|
||||
switch outcome.result {
|
||||
case .success:
|
||||
Issue.record("Expected Claude auto without a CLI to report no available strategy")
|
||||
case let .failure(error):
|
||||
guard let fetchError = error as? ProviderFetchError else {
|
||||
Issue.record("Expected ProviderFetchError, got \(error)")
|
||||
return
|
||||
}
|
||||
switch fetchError {
|
||||
case let .noAvailableStrategy(provider):
|
||||
#expect(provider == .claude)
|
||||
}
|
||||
}
|
||||
#expect(outcome.attempts.map(\.strategyID) == ["claude.web", "claude.cli"])
|
||||
#expect(outcome.attempts.map(\.wasAvailable) == [false, false])
|
||||
#else
|
||||
#expect(Bool(true))
|
||||
#endif
|
||||
}
|
||||
|
||||
@Test(arguments: [ProviderSourceMode.auto, .cli])
|
||||
func `Claude CLI runtime skips logged out interactive fallback`(sourceMode: ProviderSourceMode) async throws {
|
||||
#if os(Linux)
|
||||
let invocationLog = FileManager.default.temporaryDirectory
|
||||
.appendingPathComponent("claude-cli-runtime-invocations-\(UUID().uuidString).log")
|
||||
let binaryURL = try Self.makeClaudeCLI(loggedIn: false, invocationLog: invocationLog)
|
||||
defer {
|
||||
try? FileManager.default.removeItem(at: binaryURL)
|
||||
try? FileManager.default.removeItem(at: invocationLog)
|
||||
}
|
||||
let context = self.makeClaudeContext(
|
||||
sourceMode: sourceMode,
|
||||
env: ["CLAUDE_CLI_PATH": binaryURL.path])
|
||||
let cliFetchOverride: ClaudeStatusProbe.FetchOverride = { _, _, _ in
|
||||
Issue.record("Logged-out Claude CLI reached the interactive usage probe")
|
||||
return Self.makeClaudeStatus()
|
||||
}
|
||||
|
||||
let outcome = await ClaudeStatusProbe.withFetchOverrideForTesting(cliFetchOverride) {
|
||||
await ClaudeProviderDescriptor.makeDescriptor().fetchPlan.fetchOutcome(
|
||||
context: context,
|
||||
provider: .claude)
|
||||
}
|
||||
|
||||
switch outcome.result {
|
||||
case .success:
|
||||
Issue.record("Expected logged-out Claude CLI to report no available strategy")
|
||||
case let .failure(error):
|
||||
guard let fetchError = error as? ProviderFetchError else {
|
||||
Issue.record("Expected ProviderFetchError, got \(error)")
|
||||
return
|
||||
}
|
||||
switch fetchError {
|
||||
case let .noAvailableStrategy(provider):
|
||||
#expect(provider == .claude)
|
||||
}
|
||||
}
|
||||
let expectedStrategyIDs = sourceMode == .auto ? ["claude.web", "claude.cli"] : ["claude.cli"]
|
||||
#expect(outcome.attempts.map(\.strategyID) == expectedStrategyIDs)
|
||||
#expect(outcome.attempts.allSatisfy { !$0.wasAvailable })
|
||||
#expect(try String(contentsOf: invocationLog, encoding: .utf8) == "auth status --json\n")
|
||||
#else
|
||||
#expect(Bool(true))
|
||||
#endif
|
||||
}
|
||||
|
||||
@Test
|
||||
func claudeOAuthUsageDoesNotDetectCLIVersion() {
|
||||
#expect(!CodexBarCLI.shouldDetectVersion(
|
||||
provider: .claude,
|
||||
result: self.makeResult(kind: .oauth)))
|
||||
#expect(CodexBarCLI.shouldDetectVersion(
|
||||
provider: .claude,
|
||||
result: self.makeResult(kind: .cli)))
|
||||
#expect(CodexBarCLI.shouldDetectVersion(
|
||||
provider: .codex,
|
||||
result: self.makeResult(kind: .oauth)))
|
||||
}
|
||||
|
||||
@Test
|
||||
func claudeWebFetcher_isNotSupportedOnLinux() async {
|
||||
#if os(Linux)
|
||||
let error = await #expect(throws: ClaudeWebAPIFetcher.FetchError.self) {
|
||||
_ = try await ClaudeWebAPIFetcher.fetchUsage()
|
||||
}
|
||||
let isExpectedError = error.map { thrown in
|
||||
if case .notSupportedOnThisPlatform = thrown { return true }
|
||||
return false
|
||||
} ?? false
|
||||
#expect(isExpectedError)
|
||||
#else
|
||||
#expect(Bool(true))
|
||||
#endif
|
||||
}
|
||||
|
||||
@Test
|
||||
func claudeWebFetcher_hasSessionKey_isFalseOnLinux() {
|
||||
#if os(Linux)
|
||||
#expect(ClaudeWebAPIFetcher.hasSessionKey(cookieHeader: nil) == false)
|
||||
#else
|
||||
#expect(Bool(true))
|
||||
#endif
|
||||
}
|
||||
|
||||
@Test
|
||||
func claudeWebFetcher_sessionKeyInfo_throwsOnLinux() {
|
||||
#if os(Linux)
|
||||
let error = #expect(throws: ClaudeWebAPIFetcher.FetchError.self) {
|
||||
_ = try ClaudeWebAPIFetcher.sessionKeyInfo()
|
||||
}
|
||||
let isExpectedError = error.map { thrown in
|
||||
if case .notSupportedOnThisPlatform = thrown { return true }
|
||||
return false
|
||||
} ?? false
|
||||
#expect(isExpectedError)
|
||||
#else
|
||||
#expect(Bool(true))
|
||||
#endif
|
||||
}
|
||||
private func makeClaudeAutoContext(env: [String: String] = [:]) -> ProviderFetchContext {
|
||||
self.makeClaudeContext(sourceMode: .auto, env: env)
|
||||
}
|
||||
|
||||
private func makeClaudeContext(
|
||||
sourceMode: ProviderSourceMode,
|
||||
env: [String: String] = [:]) -> ProviderFetchContext
|
||||
{
|
||||
let browserDetection = BrowserDetection(cacheTTL: 0)
|
||||
let usageDataSource: ClaudeUsageDataSource = sourceMode == .cli ? .cli : .auto
|
||||
return ProviderFetchContext(
|
||||
runtime: .cli,
|
||||
sourceMode: sourceMode,
|
||||
includeCredits: false,
|
||||
webTimeout: 1,
|
||||
webDebugDumpHTML: false,
|
||||
verbose: false,
|
||||
env: env,
|
||||
settings: ProviderSettingsSnapshot.make(claude: .init(
|
||||
usageDataSource: usageDataSource,
|
||||
webExtrasEnabled: false,
|
||||
cookieSource: .auto,
|
||||
manualCookieHeader: nil)),
|
||||
fetcher: UsageFetcher(environment: env),
|
||||
claudeFetcher: ClaudeUsageFetcher(browserDetection: browserDetection),
|
||||
browserDetection: browserDetection)
|
||||
}
|
||||
|
||||
private static func makeClaudeCLI(loggedIn: Bool, invocationLog: URL? = nil) throws -> URL {
|
||||
if let invocationLog {
|
||||
try Data().write(to: invocationLog)
|
||||
}
|
||||
let binaryURL = FileManager.default.temporaryDirectory
|
||||
.appendingPathComponent("claude-cli-runtime-\(UUID().uuidString)")
|
||||
let recordInvocation = invocationLog.map { "printf '%s\\n' \"$*\" >> '\($0.path)'" } ?? ""
|
||||
let loggedInJSON = loggedIn ? "true" : "false"
|
||||
let script = """
|
||||
#!/bin/sh
|
||||
\(recordInvocation)
|
||||
if [ "$1" = "auth" ] && [ "$2" = "status" ]; then
|
||||
printf '%s\\n' '{"loggedIn":\(loggedInJSON)}'
|
||||
fi
|
||||
"""
|
||||
try Data(script.utf8).write(to: binaryURL)
|
||||
try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: binaryURL.path)
|
||||
return binaryURL
|
||||
}
|
||||
|
||||
private static func makeClaudeStatus() -> ClaudeStatusSnapshot {
|
||||
ClaudeStatusSnapshot(
|
||||
sessionPercentLeft: 80,
|
||||
weeklyPercentLeft: nil,
|
||||
opusPercentLeft: nil,
|
||||
accountEmail: nil,
|
||||
accountOrganization: nil,
|
||||
loginMethod: nil,
|
||||
primaryResetDescription: nil,
|
||||
secondaryResetDescription: nil,
|
||||
opusResetDescription: nil,
|
||||
rawText: "stub")
|
||||
}
|
||||
|
||||
private func makeResult(kind: ProviderFetchKind) -> ProviderFetchResult {
|
||||
ProviderFetchResult(
|
||||
usage: UsageSnapshot(
|
||||
primary: nil,
|
||||
secondary: nil,
|
||||
updatedAt: Date(timeIntervalSince1970: 0)),
|
||||
credits: nil,
|
||||
dashboard: nil,
|
||||
sourceLabel: "test",
|
||||
strategyID: "test",
|
||||
strategyKind: kind)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import Foundation
|
||||
import Testing
|
||||
@testable import CodexBarCore
|
||||
|
||||
/// Tests for the `/proc/<pid>/net/tcp` listening-port parser used on Linux as a
|
||||
/// fallback for Antigravity CLI port detection when `lsof` is unavailable.
|
||||
struct ProcNetTCPListeningPortParserLinuxTests {
|
||||
/// Two loopback LISTEN sockets (inodes 111111, 222222) and one established
|
||||
/// connection (inode 333333, st 01) that must be ignored.
|
||||
private static let sample = """
|
||||
sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode
|
||||
0: 0100007F:1F90 00000000:0000 0A 00000000:00000000 00:00000000 00000000 1000 0 111111 1 0000000000000000 100 0 0 10 0
|
||||
1: 0100007F:C000 00000000:0000 0A 00000000:00000000 00:00000000 00000000 1000 0 222222 1 0000000000000000 100 0 0 10 0
|
||||
2: 0100007F:1F91 0100007F:E1F0 01 00000000:00000000 00:00000000 00000000 1000 0 333333 1 0000000000000000 100 0 0 10 0
|
||||
"""
|
||||
|
||||
@Test
|
||||
func `returns listening ports for owned socket inodes`() {
|
||||
let ports = ProcNetTCPListeningPortParser.listeningPorts(
|
||||
Self.sample, socketInodes: ["111111", "222222"])
|
||||
#expect(ports == [8080, 49152])
|
||||
}
|
||||
|
||||
@Test
|
||||
func `parses tcp6 and deduplicates ports across tables`() {
|
||||
let tcp6 = """
|
||||
sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode
|
||||
0: 00000000000000000000000000000000:C000 00000000000000000000000000000000:0000 0A 00000000:00000000 00:00000000 00000000 1000 0 222222
|
||||
"""
|
||||
let tcpPorts = ProcNetTCPListeningPortParser.listeningPorts(
|
||||
Self.sample, socketInodes: ["222222"])
|
||||
let tcp6Ports = ProcNetTCPListeningPortParser.listeningPorts(
|
||||
tcp6, socketInodes: ["222222"])
|
||||
#expect(tcpPorts.union(tcp6Ports) == [49152])
|
||||
}
|
||||
|
||||
@Test
|
||||
func `ignores malformed and out of range ports`() {
|
||||
let malformed = """
|
||||
sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode
|
||||
0: 0100007F:NOTHEX 00000000:0000 0A 00000000:00000000 00:00000000 00000000 1000 0 111111
|
||||
1: 0100007F:10000 00000000:0000 0A 00000000:00000000 00:00000000 00000000 1000 0 111111
|
||||
2: missing-columns
|
||||
"""
|
||||
#expect(ProcNetTCPListeningPortParser.listeningPorts(
|
||||
malformed, socketInodes: ["111111"]).isEmpty)
|
||||
#expect(ProcNetTCPListeningPortParser.listeningPorts(
|
||||
"header only", socketInodes: ["111111"]).isEmpty)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `accepts a headerless proc row`() {
|
||||
let row = Self.sample.split(separator: "\n")[1]
|
||||
#expect(ProcNetTCPListeningPortParser.listeningPorts(
|
||||
String(row), socketInodes: ["111111"]) == [8080])
|
||||
}
|
||||
|
||||
@Test
|
||||
func `ignores listening sockets owned by other processes`() {
|
||||
let ports = ProcNetTCPListeningPortParser.listeningPorts(
|
||||
Self.sample, socketInodes: ["999999"])
|
||||
#expect(ports.isEmpty)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `ignores non listening sockets`() {
|
||||
// inode 333333 is an established (st 01) socket, not LISTEN.
|
||||
let ports = ProcNetTCPListeningPortParser.listeningPorts(
|
||||
Self.sample, socketInodes: ["333333"])
|
||||
#expect(ports.isEmpty)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `parses socket inode from FD symlink destination`() {
|
||||
#expect(ProcNetTCPListeningPortParser.socketInode(fromLink: "socket:[12345]") == "12345")
|
||||
#expect(ProcNetTCPListeningPortParser.socketInode(fromLink: "/dev/pts/0") == nil)
|
||||
#expect(ProcNetTCPListeningPortParser.socketInode(fromLink: "anon_inode:[eventpoll]") == nil)
|
||||
#expect(ProcNetTCPListeningPortParser.socketInode(fromLink: "socket:[]") == nil)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `reads process scoped TCP tables`() throws {
|
||||
let fileManager = FileManager.default
|
||||
let procRoot = fileManager.temporaryDirectory
|
||||
.appendingPathComponent("codexbar-proc-\(UUID().uuidString)")
|
||||
let processRoot = procRoot.appendingPathComponent("42")
|
||||
let fdDirectory = processRoot.appendingPathComponent("fd")
|
||||
let netDirectory = processRoot.appendingPathComponent("net")
|
||||
let callerNetDirectory = procRoot.appendingPathComponent("net")
|
||||
try fileManager.createDirectory(at: fdDirectory, withIntermediateDirectories: true)
|
||||
try fileManager.createDirectory(at: netDirectory, withIntermediateDirectories: true)
|
||||
try fileManager.createDirectory(at: callerNetDirectory, withIntermediateDirectories: true)
|
||||
defer { try? fileManager.removeItem(at: procRoot) }
|
||||
|
||||
try fileManager.createSymbolicLink(
|
||||
atPath: fdDirectory.appendingPathComponent("7").path,
|
||||
withDestinationPath: "socket:[111111]")
|
||||
try Self.sample.write(
|
||||
to: netDirectory.appendingPathComponent("tcp"),
|
||||
atomically: true,
|
||||
encoding: .utf8)
|
||||
try Self.sample.replacingOccurrences(of: ":1F90", with: ":C001").write(
|
||||
to: callerNetDirectory.appendingPathComponent("tcp"),
|
||||
atomically: true,
|
||||
encoding: .utf8)
|
||||
|
||||
#expect(AntigravityStatusProbe.procListeningPorts(
|
||||
pid: 42,
|
||||
procRoot: procRoot.path) == [8080])
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
@testable import CodexBarCore
|
||||
import Foundation
|
||||
#if canImport(FoundationNetworking)
|
||||
import FoundationNetworking
|
||||
#endif
|
||||
import Testing
|
||||
|
||||
@Suite
|
||||
struct ProviderEndpointOverrideSecurityLinuxTests {
|
||||
@Test
|
||||
func mimoInvalidEndpointOverrideDoesNotFallbackToLocalCache() {
|
||||
#expect(MiMoWebFetchStrategy.shouldFallbackToLocal(
|
||||
error: MiMoSettingsError.invalidEndpointOverride(MiMoSettingsReader.apiURLKey)) == false)
|
||||
#expect(MiMoWebFetchStrategy.shouldFallbackToLocal(error: MiMoSettingsError.missingCookie()) == true)
|
||||
#expect(MiMoWebFetchStrategy.shouldFallbackToLocal(error: MiMoSettingsError.invalidCookie) == true)
|
||||
}
|
||||
|
||||
@Test
|
||||
func deepgramRejectsInsecureOverrideBeforeSendingToken() async {
|
||||
let transport = FailingTransport()
|
||||
do {
|
||||
_ = try await DeepgramUsageFetcher.fetchUsage(
|
||||
apiKey: "dg-test-token",
|
||||
environment: [DeepgramUsageFetcher.apiURLKey: "http://attacker.test/v1"],
|
||||
transport: transport)
|
||||
Issue.record("Expected DeepgramUsageError.invalidEndpointOverride")
|
||||
} catch DeepgramUsageError.invalidEndpointOverride(DeepgramUsageFetcher.apiURLKey) {
|
||||
// Expected.
|
||||
} catch {
|
||||
Issue.record("Expected DeepgramUsageError.invalidEndpointOverride, got \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func zaiRejectsInsecureQuotaOverrideBeforeSendingToken() async {
|
||||
do {
|
||||
_ = try await ZaiUsageFetcher.fetchUsage(
|
||||
apiKey: "zai-test-token",
|
||||
environment: [ZaiSettingsReader.quotaURLKey: "http://attacker.test/quota"])
|
||||
Issue.record("Expected ZaiSettingsError.invalidEndpointOverride")
|
||||
} catch ZaiSettingsError.invalidEndpointOverride(ZaiSettingsReader.quotaURLKey) {
|
||||
// Expected.
|
||||
} catch {
|
||||
Issue.record("Expected ZaiSettingsError.invalidEndpointOverride, got \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func zaiRejectsInsecureAPIHostOverrideWhenQuotaURLIsAbsent() {
|
||||
#expect(throws: ZaiSettingsError.invalidEndpointOverride(ZaiSettingsReader.apiHostKey)) {
|
||||
try ZaiSettingsReader.validateEndpointOverrides(
|
||||
environment: [ZaiSettingsReader.apiHostKey: "http://attacker.test"])
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func zaiQuotaResolutionIgnoresInvalidLowerPriorityAPIHost() throws {
|
||||
let environment = [
|
||||
ZaiSettingsReader.quotaURLKey: "https://zai-proxy.test/quota",
|
||||
ZaiSettingsReader.apiHostKey: "http://attacker.test",
|
||||
]
|
||||
|
||||
try ZaiSettingsReader.validateQuotaEndpointOverride(environment: environment)
|
||||
#expect(ZaiUsageFetcher.resolveQuotaURL(region: .global, environment: environment).absoluteString ==
|
||||
"https://zai-proxy.test/quota")
|
||||
}
|
||||
|
||||
@Test
|
||||
func zaiCombinedFetchRejectsInvalidAPIHostBeforeQuotaRequest() async {
|
||||
let environment = [
|
||||
ZaiSettingsReader.quotaURLKey: "https://127.0.0.1:31337/quota",
|
||||
ZaiSettingsReader.apiHostKey: "http://127.0.0.1:31337",
|
||||
]
|
||||
|
||||
await #expect(throws: ZaiSettingsError.invalidEndpointOverride(ZaiSettingsReader.apiHostKey)) {
|
||||
try await ZaiUsageFetcher.fetchUsageWithModelUsage(
|
||||
apiKey: "ZAI_CANARY_KEY",
|
||||
environment: environment)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func zaiModelUsageRejectsInsecureAPIHostOverride() async {
|
||||
do {
|
||||
_ = try await ZaiUsageFetcher.fetchModelUsage(
|
||||
apiKey: "zai-test-token",
|
||||
environment: [ZaiSettingsReader.apiHostKey: "http://attacker.test"])
|
||||
Issue.record("Expected ZaiSettingsError.invalidEndpointOverride")
|
||||
} catch ZaiSettingsError.invalidEndpointOverride(ZaiSettingsReader.apiHostKey) {
|
||||
// Expected.
|
||||
} catch {
|
||||
Issue.record("Expected ZaiSettingsError.invalidEndpointOverride, got \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func mimoRejectsInsecureOverrideBeforeSendingCookie() async {
|
||||
let transport = FailingTransport()
|
||||
do {
|
||||
_ = try await MiMoUsageFetcher.fetchUsage(
|
||||
cookieHeader: "api-platform_serviceToken=session-token; userId=user-1",
|
||||
environment: [MiMoSettingsReader.apiURLKey: "http://attacker.test/api/v1"],
|
||||
session: transport)
|
||||
Issue.record("Expected MiMoSettingsError.invalidEndpointOverride")
|
||||
} catch MiMoSettingsError.invalidEndpointOverride(MiMoSettingsReader.apiURLKey) {
|
||||
// Expected.
|
||||
} catch {
|
||||
Issue.record("Expected MiMoSettingsError.invalidEndpointOverride, got \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func affectedProviderOverridesAcceptHTTPSAndBareHosts() throws {
|
||||
try DeepgramUsageFetcher.validateEndpointOverrides(environment: [
|
||||
DeepgramUsageFetcher.apiURLKey: "deepgram-proxy.test/v1",
|
||||
])
|
||||
try ZaiSettingsReader
|
||||
.validateEndpointOverrides(environment: [ZaiSettingsReader.quotaURLKey: "https://zai-proxy.test/quota"])
|
||||
try ZaiSettingsReader.validateEndpointOverrides(environment: [ZaiSettingsReader.apiHostKey: "localhost:9443"])
|
||||
try MiMoSettingsReader
|
||||
.validateEndpointOverrides(environment: [MiMoSettingsReader.apiURLKey: "mimo-proxy.test/api/v1"])
|
||||
|
||||
#expect(ZaiSettingsReader.quotaURL(environment: [ZaiSettingsReader.quotaURLKey: "zai-proxy.test/quota"])?
|
||||
.absoluteString == "https://zai-proxy.test/quota")
|
||||
#expect(MiMoSettingsReader.apiURL(environment: [MiMoSettingsReader.apiURLKey: "mimo-proxy.test/api/v1"])
|
||||
.absoluteString == "https://mimo-proxy.test/api/v1")
|
||||
}
|
||||
}
|
||||
|
||||
private struct FailingTransport: ProviderHTTPTransport {
|
||||
func data(for request: URLRequest) async throws -> (Data, URLResponse) {
|
||||
Issue
|
||||
.record(
|
||||
"Endpoint override validation should fail before any request is sent to \(request.url?.absoluteString ?? "<nil>")")
|
||||
throw URLError(.badURL)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import CodexBarCore
|
||||
import Foundation
|
||||
import Testing
|
||||
|
||||
/// Regression tests for a copy-pasted quote-unwrap helper that traps on length-1 input.
|
||||
///
|
||||
/// 32 provider settings readers (plus `CodexBarConfig` and `CLIConfigCommand`) share a
|
||||
/// `cleaned(_:)` helper of the form:
|
||||
///
|
||||
/// if (value.hasPrefix("\"") && value.hasSuffix("\"")) ||
|
||||
/// (value.hasPrefix("'") && value.hasSuffix("'"))
|
||||
/// {
|
||||
/// value.removeFirst()
|
||||
/// value.removeLast()
|
||||
/// }
|
||||
///
|
||||
/// For a value of length 1 (the single character `"` or `'`), both `hasPrefix` and
|
||||
/// `hasSuffix` return true, `removeFirst()` empties the string, and `removeLast()` then
|
||||
/// traps with "Can't remove last element from empty collection." This is reachable from
|
||||
/// a misconfigured env var (e.g. `ALIBABA_TOKEN_PLAN_COOKIE='"'`) and from quoted JSON
|
||||
/// values in `~/.codexbar/config.json`, both of which are user-controllable.
|
||||
///
|
||||
/// These tests exercise two representative public readers — Alibaba Token Plan (the
|
||||
/// newest addition in #1098) and the Ollama API key reader (added in #1087) — by
|
||||
/// passing the trap-inducing single-quote inputs and asserting the readers return nil
|
||||
/// instead of crashing. The patch swaps `removeFirst()/removeLast()` for
|
||||
/// `String(value.dropFirst().dropLast())`, which is empty-safe.
|
||||
@Suite
|
||||
struct SettingsReaderQuoteUnwrapTrapTests {
|
||||
@Test
|
||||
func alibabaTokenPlanCookieHeader_returnsNilForLoneDoubleQuoteValue() {
|
||||
let env = [AlibabaTokenPlanSettingsReader.cookieHeaderKey: "\""]
|
||||
#expect(AlibabaTokenPlanSettingsReader.cookieHeader(environment: env) == nil)
|
||||
}
|
||||
|
||||
@Test
|
||||
func alibabaTokenPlanCookieHeader_returnsNilForLoneApostropheValue() {
|
||||
let env = [AlibabaTokenPlanSettingsReader.cookieHeaderKey: "'"]
|
||||
#expect(AlibabaTokenPlanSettingsReader.cookieHeader(environment: env) == nil)
|
||||
}
|
||||
|
||||
@Test
|
||||
func alibabaTokenPlanCookieHeader_unwrapsProperlyDoubleQuotedValue() {
|
||||
let env = [AlibabaTokenPlanSettingsReader.cookieHeaderKey: "\"abc=def\""]
|
||||
#expect(AlibabaTokenPlanSettingsReader.cookieHeader(environment: env) == "abc=def")
|
||||
}
|
||||
|
||||
@Test
|
||||
func alibabaTokenPlanCookieHeader_unwrapsProperlySingleQuotedValue() {
|
||||
let env = [AlibabaTokenPlanSettingsReader.cookieHeaderKey: "'abc=def'"]
|
||||
#expect(AlibabaTokenPlanSettingsReader.cookieHeader(environment: env) == "abc=def")
|
||||
}
|
||||
|
||||
@Test
|
||||
func ollamaAPIKey_returnsNilForLoneDoubleQuoteValue() {
|
||||
for key in OllamaAPISettingsReader.apiKeyEnvironmentKeys {
|
||||
let env = [key: "\""]
|
||||
#expect(OllamaAPISettingsReader.apiKey(environment: env) == nil)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func ollamaAPIKey_returnsNilForLoneApostropheValue() {
|
||||
for key in OllamaAPISettingsReader.apiKeyEnvironmentKeys {
|
||||
let env = [key: "'"]
|
||||
#expect(OllamaAPISettingsReader.apiKey(environment: env) == nil)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func ollamaAPIKey_unwrapsProperlyQuotedValue() {
|
||||
let env = [OllamaAPISettingsReader.apiKeyEnvironmentKeys[0]: "\"sk-token\""]
|
||||
#expect(OllamaAPISettingsReader.apiKey(environment: env) == "sk-token")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import Foundation
|
||||
import Testing
|
||||
@testable import CodexBarCore
|
||||
|
||||
#if os(Linux)
|
||||
@Suite(.serialized)
|
||||
struct ShellCommandSessionLinuxTests {
|
||||
@Test
|
||||
func `shell probe launches as a detached session leader`() throws {
|
||||
let output = ShellCommandLocator.test_runShellCommand(
|
||||
shell: "/bin/sh",
|
||||
arguments: ["-c", "printf '%s ' \"$$\"; ps -o sid= -p \"$$\""],
|
||||
timeout: 5)
|
||||
let text = try #require(output.flatMap { String(data: $0, encoding: .utf8) })
|
||||
let identifiers = text.split(whereSeparator: \.isWhitespace).compactMap { Int32($0) }
|
||||
|
||||
#expect(identifiers.count == 2)
|
||||
guard identifiers.count == 2 else { return }
|
||||
#expect(identifiers[0] == identifiers[1])
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,15 @@
|
||||
import CodexBarCore
|
||||
import Testing
|
||||
|
||||
@Suite(.serialized)
|
||||
struct UsageFormatterLinuxTests {
|
||||
@Test
|
||||
func `rate-window formatting uses the standalone English fallback`() {
|
||||
UsageFormatter.clearLocalizationProvider()
|
||||
UsageFormatter.clearLocaleProvider()
|
||||
|
||||
#expect(UsageFormatter.usageLine(remaining: 25, used: 75, showUsed: false) == "25% left")
|
||||
#expect(UsageFormatter.usageLine(remaining: 25, used: 75, showUsed: true) == "75% used")
|
||||
#expect(UsageFormatter.usageLine(remaining: 0.75, used: 99.25, showUsed: false) == "<1% left")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,447 @@
|
||||
import CodexBarCore
|
||||
import Foundation
|
||||
#if canImport(FoundationNetworking)
|
||||
import FoundationNetworking
|
||||
#endif
|
||||
import Testing
|
||||
@testable import CodexBarCLI
|
||||
|
||||
/// Fixtures below were captured verbatim from a locally running Wayfinder gateway
|
||||
/// (`wayfinder-router serve`, two-tier priced config) after routing real traffic.
|
||||
struct WayfinderProviderLinuxTests {
|
||||
@Test
|
||||
func `assembles a snapshot from live gateway payloads`() throws {
|
||||
let snapshot = try Self.makeSnapshot()
|
||||
|
||||
#expect(snapshot.gatewayStatus == "ok")
|
||||
#expect(!snapshot.offline)
|
||||
#expect(!snapshot.dryRun)
|
||||
#expect(snapshot.missingKeys.isEmpty)
|
||||
#expect(snapshot.modelCount == 2)
|
||||
#expect(snapshot.requests == 14)
|
||||
#expect(snapshot.tokens == 1028)
|
||||
#expect(snapshot.priced)
|
||||
#expect(snapshot.saved == 0.005694)
|
||||
#expect(snapshot.savedPct == 61.5)
|
||||
#expect(snapshot.routes.map(\.name) == ["local", "cloud"])
|
||||
#expect(snapshot.routes.first { $0.name == "local" }?.requests == 10)
|
||||
#expect(snapshot.routes.first { $0.name == "cloud" }?.requests == 4)
|
||||
#expect(snapshot.statusLabel == "Local gateway")
|
||||
#expect(snapshot.gatewaySummary == "ok · 2 models")
|
||||
#expect(snapshot.displayLines == [
|
||||
"Gateway: ok · 2 models",
|
||||
"Routed: local: 10 · cloud: 4",
|
||||
"Saved: <$0.01 · 61.5% vs highest-cost route",
|
||||
"Avg decision: 0.1 ms",
|
||||
])
|
||||
|
||||
let avgMs = try #require(snapshot.avgDecisionMs)
|
||||
#expect(abs(avgMs - 0.0804) < 0.001)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `maps the snapshot onto the shared usage snapshot`() throws {
|
||||
let usage = try Self.makeSnapshot().toUsageSnapshot()
|
||||
|
||||
#expect(usage.primary == nil)
|
||||
#expect(usage.secondary == nil)
|
||||
#expect(usage.providerCost == nil)
|
||||
#expect(usage.identity?.providerID == .wayfinder)
|
||||
#expect(usage.identity?.accountEmail == nil)
|
||||
#expect(usage.identity?.accountOrganization == "2 models · local gateway")
|
||||
#expect(usage.identity?.loginMethod == "Local gateway")
|
||||
#expect(usage.dataConfidence == .exact)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `degraded health reports the missing key count`() throws {
|
||||
let snapshot = try Self.makeSnapshot(healthData: Self.healthDegraded)
|
||||
#expect(snapshot.gatewayStatus == "degraded")
|
||||
#expect(snapshot.missingKeys == ["cloud"])
|
||||
#expect(snapshot.statusLabel == "Degraded — 1 key missing")
|
||||
}
|
||||
|
||||
@Test
|
||||
func `empty savings suppress the routed and saved summaries`() throws {
|
||||
let snapshot = try Self.makeSnapshot(savingsData: Self.savingsZeros)
|
||||
#expect(snapshot.requests == 0)
|
||||
#expect(snapshot.routedSummary == nil)
|
||||
#expect(snapshot.savedSummary == nil)
|
||||
#expect(snapshot.toUsageSnapshot().providerCost == nil)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `unpriced savings never render dollars`() throws {
|
||||
let snapshot = try Self.makeSnapshot(savingsData: Self.savingsUnpriced)
|
||||
#expect(!snapshot.priced)
|
||||
#expect(snapshot.savedSummary == "40% vs highest-cost route")
|
||||
#expect(snapshot.toUsageSnapshot().providerCost == nil)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `sub-cent priced savings render below one cent`() throws {
|
||||
let snapshot = try Self.makeSnapshot()
|
||||
#expect(snapshot.routedSummary == "local: 10 · cloud: 4")
|
||||
#expect(snapshot.savedSummary == "<$0.01 · 61.5% vs highest-cost route")
|
||||
#expect(snapshot.avgDecisionSummary == "0.1 ms")
|
||||
}
|
||||
|
||||
@Test
|
||||
func `metrics parsing is best effort`() throws {
|
||||
#expect(WayfinderUsageFetcher._averageDecisionMillisecondsForTesting("") == nil)
|
||||
#expect(WayfinderUsageFetcher._averageDecisionMillisecondsForTesting("garbage\nlines\n") == nil)
|
||||
#expect(WayfinderUsageFetcher._averageDecisionMillisecondsForTesting(
|
||||
"wayfinder_router_decision_latency_seconds_sum 1.5\n") == nil)
|
||||
#expect(WayfinderUsageFetcher._averageDecisionMillisecondsForTesting(
|
||||
"wayfinder_router_decision_latency_seconds_sum 1.5\n" +
|
||||
"wayfinder_router_decision_latency_seconds_count 0\n") == nil)
|
||||
|
||||
let labeled = WayfinderUsageFetcher._averageDecisionMillisecondsForTesting(
|
||||
"wayfinder_router_decision_latency_seconds_sum{route=\"all\"} 2.0\n" +
|
||||
"wayfinder_router_decision_latency_seconds_count{route=\"all\"} 4\n")
|
||||
#expect(labeled == 500)
|
||||
|
||||
let snapshot = try Self.makeSnapshot(metricsText: nil)
|
||||
#expect(snapshot.avgDecisionMs == nil)
|
||||
#expect(snapshot.avgDecisionSummary == nil)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `endpoint URLs preserve prefixes and trailing slashes`() throws {
|
||||
func endpoint(_ base: String, _ path: String) throws -> String {
|
||||
try WayfinderUsageFetcher._endpointURLForTesting(
|
||||
baseURL: #require(URL(string: base)),
|
||||
path: path).absoluteString
|
||||
}
|
||||
#expect(try endpoint("http://127.0.0.1:8088", "healthz") == "http://127.0.0.1:8088/healthz")
|
||||
#expect(try endpoint("http://127.0.0.1:8088/", "healthz") == "http://127.0.0.1:8088/healthz")
|
||||
#expect(try endpoint("https://wayfinder.example.com/wf", "v1/savings") ==
|
||||
"https://wayfinder.example.com/wf/v1/savings")
|
||||
}
|
||||
|
||||
@Test
|
||||
func `gateway URL override allows loopback HTTP and rejects remote HTTP`() throws {
|
||||
let key = WayfinderSettingsReader.baseURLEnvironmentKey
|
||||
|
||||
try WayfinderSettingsReader.validateEndpointOverride(environment: [key: "http://127.0.0.1:9090"])
|
||||
try WayfinderSettingsReader.validateEndpointOverride(environment: [key: "http://localhost:8088"])
|
||||
try WayfinderSettingsReader.validateEndpointOverride(environment: [key: "https://wayfinder.example.com"])
|
||||
#expect(WayfinderSettingsReader.baseURL(environment: [key: "http://127.0.0.1:9090"]).absoluteString ==
|
||||
"http://127.0.0.1:9090")
|
||||
|
||||
#expect(throws: WayfinderSettingsError.invalidEndpointOverride(key)) {
|
||||
try WayfinderSettingsReader.validateEndpointOverride(environment: [key: "http://192.168.1.5:8088"])
|
||||
}
|
||||
#expect(throws: WayfinderSettingsError.invalidEndpointOverride(key)) {
|
||||
try WayfinderSettingsReader.validateEndpointOverride(environment: [key: "http://user@127.0.0.1:8088"])
|
||||
}
|
||||
#expect(WayfinderSettingsReader.baseURL(environment: [key: "http://attacker.test"]) ==
|
||||
WayfinderSettingsReader.defaultBaseURL)
|
||||
#expect(WayfinderSettingsReader.baseURL(environment: [:]) == WayfinderSettingsReader.defaultBaseURL)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `dashboard URL follows the configured gateway and preserves its prefix`() {
|
||||
let key = WayfinderSettingsReader.baseURLEnvironmentKey
|
||||
|
||||
#expect(WayfinderSettingsReader.dashboardURL(environment: [:]).absoluteString ==
|
||||
"http://127.0.0.1:8088/router")
|
||||
#expect(WayfinderSettingsReader.dashboardURL(
|
||||
environment: [key: "http://localhost:9191/wayfinder/"]).absoluteString ==
|
||||
"http://localhost:9191/wayfinder/router")
|
||||
}
|
||||
|
||||
@Test
|
||||
func `config projects the gateway URL into the fetch environment`() {
|
||||
let config = ProviderConfig(id: .wayfinder, enterpriseHost: "http://localhost:9099")
|
||||
let environment = ProviderConfigEnvironment.applyProviderConfigOverrides(
|
||||
base: [:],
|
||||
provider: .wayfinder,
|
||||
config: config)
|
||||
|
||||
#expect(environment[WayfinderSettingsReader.baseURLEnvironmentKey] == "http://localhost:9099")
|
||||
#expect(WayfinderSettingsReader.baseURL(environment: environment).absoluteString == "http://localhost:9099")
|
||||
}
|
||||
|
||||
@Test
|
||||
func `descriptor is registered`() {
|
||||
let descriptor = ProviderDescriptorRegistry.descriptor(for: .wayfinder)
|
||||
#expect(descriptor.metadata.displayName == "Wayfinder")
|
||||
#expect(descriptor.metadata.cliName == "wayfinder")
|
||||
#expect(descriptor.cli.aliases.contains("wayfinder-router"))
|
||||
#expect(!descriptor.metadata.defaultEnabled)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `usage snapshot preserves Wayfinder detail when cached`() throws {
|
||||
let snapshot = try Self.makeSnapshot()
|
||||
let encoded = try JSONEncoder().encode(snapshot.toUsageSnapshot())
|
||||
let decoded = try JSONDecoder().decode(UsageSnapshot.self, from: encoded)
|
||||
|
||||
#expect(decoded.wayfinderUsage == snapshot)
|
||||
#expect(decoded.identity?.providerID == .wayfinder)
|
||||
}
|
||||
|
||||
@Test
|
||||
func `fetch polls only the documented read-only endpoints`() async throws {
|
||||
let log = RequestLog()
|
||||
let transport = ProviderHTTPTransportHandler { request in
|
||||
let url = try #require(request.url)
|
||||
log.append(url)
|
||||
#expect(request.httpMethod == "GET")
|
||||
let body: Data = switch url.path {
|
||||
case "/healthz": Self.healthOK
|
||||
case "/router/models": Self.models
|
||||
case "/v1/savings": Self.savings30d
|
||||
case "/metrics": Data(Self.metricsText.utf8)
|
||||
default: Data()
|
||||
}
|
||||
let response = try #require(HTTPURLResponse(
|
||||
url: url,
|
||||
statusCode: 200,
|
||||
httpVersion: nil,
|
||||
headerFields: nil))
|
||||
return (body, response)
|
||||
}
|
||||
|
||||
let snapshot = try await WayfinderUsageFetcher.fetchUsage(
|
||||
baseURL: #require(URL(string: "http://127.0.0.1:8088")),
|
||||
transport: transport)
|
||||
|
||||
#expect(snapshot.requests == 14)
|
||||
#expect(log.paths() == ["/healthz", "/router/models", "/v1/savings", "/metrics"])
|
||||
#expect(log.queries().contains("period=30d"))
|
||||
}
|
||||
|
||||
@Test
|
||||
func `fetch maps HTTP failures to actionable errors`() async throws {
|
||||
let failing = ProviderHTTPTransportHandler { _ in
|
||||
throw URLError(.cannotConnectToHost)
|
||||
}
|
||||
await #expect(throws: WayfinderUsageError.gatewayUnreachable) {
|
||||
_ = try await WayfinderUsageFetcher.fetchUsage(
|
||||
baseURL: #require(URL(string: "http://127.0.0.1:8088")),
|
||||
transport: failing)
|
||||
}
|
||||
|
||||
let serverError = ProviderHTTPTransportHandler { request in
|
||||
let url = try #require(request.url)
|
||||
let response = try #require(HTTPURLResponse(
|
||||
url: url,
|
||||
statusCode: 500,
|
||||
httpVersion: nil,
|
||||
headerFields: nil))
|
||||
return (Data(), response)
|
||||
}
|
||||
await #expect(throws: WayfinderUsageError.apiError(500)) {
|
||||
_ = try await WayfinderUsageFetcher.fetchUsage(
|
||||
baseURL: #require(URL(string: "http://127.0.0.1:8088")),
|
||||
transport: serverError)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func `required request cancellation remains cancellation`() async throws {
|
||||
for error in [CancellationError() as any Error, URLError(.cancelled) as any Error] {
|
||||
let cancelling = ProviderHTTPTransportHandler { _ in throw error }
|
||||
await #expect(throws: CancellationError.self) {
|
||||
_ = try await WayfinderUsageFetcher.fetchUsage(
|
||||
baseURL: #require(URL(string: "http://127.0.0.1:8088")),
|
||||
transport: cancelling)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func `optional metrics cancellation remains cancellation`() async throws {
|
||||
let cancelling = ProviderHTTPTransportHandler { request in
|
||||
let url = try #require(request.url)
|
||||
if url.path == "/metrics" {
|
||||
throw CancellationError()
|
||||
}
|
||||
let body: Data = switch url.path {
|
||||
case "/healthz": Self.healthOK
|
||||
case "/router/models": Self.models
|
||||
case "/v1/savings": Self.savings30d
|
||||
default: Data()
|
||||
}
|
||||
let response = try #require(HTTPURLResponse(
|
||||
url: url,
|
||||
statusCode: 200,
|
||||
httpVersion: nil,
|
||||
headerFields: nil))
|
||||
return (body, response)
|
||||
}
|
||||
|
||||
await #expect(throws: CancellationError.self) {
|
||||
_ = try await WayfinderUsageFetcher.fetchUsage(
|
||||
baseURL: #require(URL(string: "http://127.0.0.1:8088")),
|
||||
transport: cancelling)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func `fetch rejects responses from a different origin`() async throws {
|
||||
let redirecting = ProviderHTTPTransportHandler { _ in
|
||||
let elsewhere = try #require(URL(string: "http://attacker.test/healthz"))
|
||||
let response = try #require(HTTPURLResponse(
|
||||
url: elsewhere,
|
||||
statusCode: 200,
|
||||
httpVersion: nil,
|
||||
headerFields: nil))
|
||||
return (Self.healthOK, response)
|
||||
}
|
||||
await #expect(throws: WayfinderUsageError.unexpectedRedirect) {
|
||||
_ = try await WayfinderUsageFetcher.fetchUsage(
|
||||
baseURL: #require(URL(string: "http://127.0.0.1:8088")),
|
||||
transport: redirecting)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
func `text CLI renders gateway health routed split savings and latency`() throws {
|
||||
let output = try CLIRenderer.renderText(
|
||||
provider: .wayfinder,
|
||||
snapshot: Self.makeSnapshot().toUsageSnapshot(),
|
||||
credits: nil,
|
||||
context: RenderContext(
|
||||
header: "Wayfinder (api)",
|
||||
status: nil,
|
||||
useColor: false,
|
||||
resetStyle: .countdown))
|
||||
|
||||
#expect(output.contains("Gateway: ok · 2 models"))
|
||||
#expect(output.contains("Routed: local: 10 · cloud: 4"))
|
||||
#expect(output.contains("Saved: <$0.01 · 61.5% vs highest-cost route"))
|
||||
#expect(output.contains("Avg decision: 0.1 ms"))
|
||||
#expect(!output.contains("Cost:"))
|
||||
}
|
||||
|
||||
@Test
|
||||
func `routed summary reflects request counts regardless of configured model order`() throws {
|
||||
// The heavier-traffic route ("primary-tier") is configured SECOND in /router/models,
|
||||
// and the lighter one ("secondary-tier") FIRST — proving nothing in the summary is
|
||||
// derived from array position (the gateway's config order is not a semantic signal).
|
||||
let reorderedModels = Data("""
|
||||
{"models":[{"name":"secondary-tier","endpoint":"http://127.0.0.1:9102/v1",\
|
||||
"model":"stand-in-large","api_key_env":"RIG_CLOUD_KEY","key_ok":true},\
|
||||
{"name":"primary-tier","endpoint":"http://127.0.0.1:9101/v1","model":"stand-in-small",\
|
||||
"api_key_env":null,"key_ok":true}],"dry_run":false}
|
||||
""".utf8)
|
||||
let snapshot = try Self.makeSnapshot(modelsData: reorderedModels)
|
||||
|
||||
#expect(snapshot.routedSummary == "local: 10 · cloud: 4")
|
||||
#expect(snapshot.routes.first?.name == "local")
|
||||
}
|
||||
|
||||
@Test
|
||||
func `routed summary uses the gateway's own route names, not a hardcoded local or cloud label`() throws {
|
||||
// Route names are whatever the user named their endpoints in the Wayfinder config —
|
||||
// there is no "local"/"cloud" semantic anywhere in the gateway's JSON.
|
||||
let customNamedSavings = Data("""
|
||||
{"period_days":30,"unit":"usd","priced":true,"requests":14,"estimated_requests":0,\
|
||||
"tokens":1028,"realized":0.003558,"baseline":0.009252,"saved":0.005694,"saved_pct":61.5,\
|
||||
"by_route":{"groq-8b":{"requests":10,"realized":0.000264,"baseline":0.005958,\
|
||||
"saved":0.005694,"tokens":662},"openai-o1":{"requests":4,"realized":0.003294,\
|
||||
"baseline":0.003294,"saved":0.0,"tokens":366}},"by_key":{},\
|
||||
"price_table_version":"a3db80fd9a78"}
|
||||
""".utf8)
|
||||
let snapshot = try Self.makeSnapshot(savingsData: customNamedSavings)
|
||||
let summary = try #require(snapshot.routedSummary)
|
||||
|
||||
#expect(summary == "groq-8b: 10 · openai-o1: 4")
|
||||
#expect(!summary.contains("local"))
|
||||
#expect(!summary.contains("cloud"))
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private static func makeSnapshot(
|
||||
healthData: Data = Self.healthOK,
|
||||
modelsData: Data = Self.models,
|
||||
savingsData: Data = Self.savings30d,
|
||||
metricsText: String? = Self.metricsText) throws -> WayfinderUsageSnapshot
|
||||
{
|
||||
try WayfinderUsageFetcher._makeSnapshotForTesting(
|
||||
healthData: healthData,
|
||||
modelsData: modelsData,
|
||||
savingsData: savingsData,
|
||||
metricsText: metricsText,
|
||||
updatedAt: Date(timeIntervalSince1970: 1))
|
||||
}
|
||||
|
||||
private final class RequestLog: @unchecked Sendable {
|
||||
private let lock = NSLock()
|
||||
private var urls: [URL] = []
|
||||
|
||||
func append(_ url: URL) {
|
||||
self.lock.lock()
|
||||
defer { self.lock.unlock() }
|
||||
self.urls.append(url)
|
||||
}
|
||||
|
||||
func paths() -> [String] {
|
||||
self.lock.lock()
|
||||
defer { self.lock.unlock() }
|
||||
return self.urls.map(\.path)
|
||||
}
|
||||
|
||||
func queries() -> [String] {
|
||||
self.lock.lock()
|
||||
defer { self.lock.unlock() }
|
||||
return self.urls.compactMap(\.query)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Fixtures (captured from a live gateway)
|
||||
|
||||
private static let healthOK = Data("""
|
||||
{"status":"ok","models":["cloud","local"],"offline":false}
|
||||
""".utf8)
|
||||
|
||||
private static let healthDegraded = Data("""
|
||||
{"status":"degraded","models":["cloud","local"],"offline":false,"missing_keys":["cloud"]}
|
||||
""".utf8)
|
||||
|
||||
private static let models = Data("""
|
||||
{"models":[{"name":"local","endpoint":"http://127.0.0.1:9101/v1","model":"stand-in-small",\
|
||||
"api_key_env":null,"key_ok":true},{"name":"cloud","endpoint":"http://127.0.0.1:9102/v1",\
|
||||
"model":"stand-in-large","api_key_env":"RIG_CLOUD_KEY","key_ok":true}],"dry_run":false}
|
||||
""".utf8)
|
||||
|
||||
private static let savings30d = Data("""
|
||||
{"period_days":30,"unit":"usd","priced":true,"requests":14,"estimated_requests":0,\
|
||||
"tokens":1028,"realized":0.003558,"baseline":0.009252,"saved":0.005694,"saved_pct":61.5,\
|
||||
"by_route":{"cloud":{"requests":4,"realized":0.003294,"baseline":0.003294,"saved":0.0,\
|
||||
"tokens":366},"local":{"requests":10,"realized":0.000264,"baseline":0.005958,\
|
||||
"saved":0.005694,"tokens":662}},"by_key":{},"price_table_version":"a3db80fd9a78"}
|
||||
""".utf8)
|
||||
|
||||
private static let savingsZeros = Data("""
|
||||
{"period_days":30,"unit":"usd","priced":true,"requests":0,"estimated_requests":0,\
|
||||
"tokens":0,"realized":0.0,"baseline":0.0,"saved":0.0,"saved_pct":0.0,"by_route":{},\
|
||||
"by_key":{},"price_table_version":"a3db80fd9a78"}
|
||||
""".utf8)
|
||||
|
||||
private static let savingsUnpriced = Data("""
|
||||
{"period_days":30,"unit":"relative","priced":false,"requests":5,"estimated_requests":0,\
|
||||
"tokens":420,"realized":1.8,"baseline":3.0,"saved":1.2,"saved_pct":40.0,\
|
||||
"by_route":{"local":{"requests":4,"realized":0.8,"baseline":2.0,"saved":1.2,"tokens":320},\
|
||||
"cloud":{"requests":1,"realized":1.0,"baseline":1.0,"saved":0.0,"tokens":100}},\
|
||||
"by_key":{},"price_table_version":"a3db80fd9a78"}
|
||||
""".utf8)
|
||||
|
||||
private static let metricsText = """
|
||||
# HELP wayfinder_router_requests_total Routed requests by model and mode.
|
||||
# TYPE wayfinder_router_requests_total counter
|
||||
wayfinder_router_requests_total{model="local",mode="scored"} 10
|
||||
wayfinder_router_requests_total{model="cloud",mode="scored"} 4
|
||||
# HELP wayfinder_router_decision_latency_seconds Time to score a prompt and pick a model (no model call).
|
||||
# TYPE wayfinder_router_decision_latency_seconds histogram
|
||||
wayfinder_router_decision_latency_seconds_bucket{le="0.0001"} 13
|
||||
wayfinder_router_decision_latency_seconds_bucket{le="0.00025"} 14
|
||||
wayfinder_router_decision_latency_seconds_bucket{le="+Inf"} 14
|
||||
wayfinder_router_decision_latency_seconds_sum 0.00112602
|
||||
wayfinder_router_decision_latency_seconds_count 14
|
||||
"""
|
||||
}
|
||||
Reference in New Issue
Block a user