From d0771a3481dc74158a58f2743fdecad85f378412 Mon Sep 17 00:00:00 2001 From: Robin Ebers Date: Tue, 25 Aug 2026 08:03:04 +0400 Subject: [PATCH] Deduplicate test fixtures and prune subsumed tests Second simplification pass after #1143, covering the suites that pass did not touch: consolidates duplicated fixtures (Cursor JWT/SQLite fakes, OpenCode stub, BlockingParser, sequence runtimes), parameterizes near-identical test clusters, and removes tests whose behavior is verified elsewhere or that restated the implementation. Co-Authored-By: Claude Fable 5 --- .../AntigravityLayoutTests.swift | 11 +- .../AntigravityProviderTests.swift | 75 +++---- .../ClaudeDesktopAuthStoreTests.swift | 188 ++++++------------ .../CursorOptionalEndpointTests.swift | 182 ++++++----------- .../OpenUsageTests/CursorProviderTests.swift | 51 +---- Tests/OpenUsageTests/CursorSpendTests.swift | 25 +-- .../CursorUsageSummaryTests.swift | 30 +-- Tests/OpenUsageTests/DevinProviderTests.swift | 50 ++--- .../OpenUsageTests/FailureBackoffTests.swift | 22 +- .../GrokCreditsConfigTests.swift | 51 ++--- .../JSONLScannerCancellationTests.swift | 21 +- .../JSONLScannerTestSupport.swift | 20 +- Tests/OpenUsageTests/LocalUsageAPITests.swift | 6 +- .../OpenUsageTests/MenuBarContentTests.swift | 16 +- .../OpenUsageTests/MetricFormatterTests.swift | 3 +- .../OpenCodeProviderTests.swift | 105 ++++------ .../OpenCodeUsageScannerTests.swift | 55 +++-- .../PanelHeightCoordinatorTests.swift | 8 - .../ProviderAccountsStoreTests.swift | 1 - .../OpenUsageTests/ReorderGeometryTests.swift | 25 +-- .../ShareCardRendererTests.swift | 33 +-- .../ShellEnvironmentSnapshotTests.swift | 14 +- Tests/OpenUsageTests/TestSupport.swift | 72 +++++-- .../OpenUsageTests/WidgetDataStoreTests.swift | 18 +- Tests/OpenUsageTests/ZAIProviderTests.swift | 6 +- 25 files changed, 364 insertions(+), 724 deletions(-) diff --git a/Tests/OpenUsageTests/AntigravityLayoutTests.swift b/Tests/OpenUsageTests/AntigravityLayoutTests.swift index 7cd7892cf..8a0f264c4 100644 --- a/Tests/OpenUsageTests/AntigravityLayoutTests.swift +++ b/Tests/OpenUsageTests/AntigravityLayoutTests.swift @@ -98,15 +98,8 @@ final class AntigravityLayoutTests: XCTestCase { XCTAssertEqual(store.pinnedMetricIDs, ["antigravity.geminiPro", "antigravity.geminiWeekly"]) } - func testSavedPinsKeyIsRespectedExactly() { - let defaults = makeDefaults("PinsPresent") - saveStored([PlacedWidget(descriptorID: "antigravity.geminiPro")], forKey: "layout", in: defaults) - defaults.set(["antigravity.claude"], forKey: "layout.menuBarPins") - - let store = LayoutStore(registry: .antigravityOnly, defaults: defaults, storageKey: "layout") - XCTAssertEqual(store.pinnedMetricIDs, ["antigravity.claude"], - "a user-saved pin set must not gain the new default pins") - } + // A saved pins key never gains new default pins — asserted above in + // testSavedGeminiFlashStateIsFilteredEverywhere (the exact-pin-set check). // MARK: - Fixtures diff --git a/Tests/OpenUsageTests/AntigravityProviderTests.swift b/Tests/OpenUsageTests/AntigravityProviderTests.swift index 5bb56fc9c..10904c3ca 100644 --- a/Tests/OpenUsageTests/AntigravityProviderTests.swift +++ b/Tests/OpenUsageTests/AntigravityProviderTests.swift @@ -290,6 +290,25 @@ final class AntigravityProviderTests: XCTestCase { // MARK: - Provider integration (Cloud Code path, no language server) + /// A provider on the Cloud Code path (no language server), signed in via a wrapped keychain + /// token. Pass `keychainExpiry: nil` for a signed-out provider (empty keychain). + @MainActor + private func makeCloudCodeProvider( + routing: RoutingHTTPClient, + keychainExpiry: String? = "2099-01-01T00:00:00Z" + ) -> AntigravityProvider { + let wrapped = keychainExpiry.map { expiry in + let inner = #"{"token":{"access_token":"ya29.kc","refresh_token":"1//r","expiry":"\#(expiry)"}}"# + return "go-keyring-base64:" + Data(inner.utf8).base64EncodedString() + } + return AntigravityProvider( + authStore: AntigravityAuthStore(keychain: FakeKeychain(wrapped), files: FakeFiles()), + usageClient: AntigravityUsageClient(lsHTTP: routing, http: routing), + discovery: LanguageServerDiscovery(processRunner: EmptyProcessRunner()), + dbUsageScanner: AntigravityDbUsageScanner(conversationsDirectory: { "/nonexistent-antigravity-tests" }) + ) + } + @MainActor func testRefreshUsesCloudCodeWhenNoLanguageServer() async { let modelsJSON = """ @@ -316,15 +335,7 @@ final class AntigravityProviderTests: XCTestCase { return HTTPResponse(statusCode: 404, headers: [:], body: Data()) } - let inner = #"{"token":{"access_token":"ya29.kc","refresh_token":"1//r","expiry":"2099-01-01T00:00:00Z"}}"# - let wrapped = "go-keyring-base64:" + Data(inner.utf8).base64EncodedString() - - let provider = AntigravityProvider( - authStore: AntigravityAuthStore(keychain: FakeKeychain(wrapped), files: FakeFiles()), - usageClient: AntigravityUsageClient(lsHTTP: routing, http: routing), - discovery: LanguageServerDiscovery(processRunner: EmptyProcessRunner()), - dbUsageScanner: AntigravityDbUsageScanner(conversationsDirectory: { "/nonexistent-antigravity-tests" }) - ) + let provider = makeCloudCodeProvider(routing: routing) let snapshot = await provider.refresh() XCTAssertEqual(snapshot.plan, "Pro") @@ -337,11 +348,7 @@ final class AntigravityProviderTests: XCTestCase { @MainActor func testRefreshErrorsWhenNothingAvailable() async { let routing = RoutingHTTPClient { _ in HTTPResponse(statusCode: 500, headers: [:], body: Data()) } - let provider = AntigravityProvider( - authStore: AntigravityAuthStore(keychain: FakeKeychain(nil), files: FakeFiles()), - usageClient: AntigravityUsageClient(lsHTTP: routing, http: routing), - discovery: LanguageServerDiscovery(processRunner: EmptyProcessRunner()) - ) + let provider = makeCloudCodeProvider(routing: routing, keychainExpiry: nil) let snapshot = await provider.refresh() XCTAssertTrue(snapshot.lines.contains { $0.isError }) XCTAssertEqual(snapshot.errorCategory, .notLoggedIn) @@ -352,13 +359,7 @@ final class AntigravityProviderTests: XCTestCase { // Valid keychain token, but every Cloud Code endpoint is down. A signed-in user should see a // transient failure (.network), not "not signed in" (.notLoggedIn). let routing = RoutingHTTPClient { _ in HTTPResponse(statusCode: 503, headers: [:], body: Data()) } - let inner = #"{"token":{"access_token":"ya29.kc","refresh_token":"1//r","expiry":"2099-01-01T00:00:00Z"}}"# - let wrapped = "go-keyring-base64:" + Data(inner.utf8).base64EncodedString() - let provider = AntigravityProvider( - authStore: AntigravityAuthStore(keychain: FakeKeychain(wrapped), files: FakeFiles()), - usageClient: AntigravityUsageClient(lsHTTP: routing, http: routing), - discovery: LanguageServerDiscovery(processRunner: EmptyProcessRunner()) - ) + let provider = makeCloudCodeProvider(routing: routing) let snapshot = await provider.refresh() XCTAssertTrue(snapshot.lines.contains { $0.isError }) XCTAssertEqual(snapshot.errorCategory, .network) @@ -374,13 +375,7 @@ final class AntigravityProviderTests: XCTestCase { } return HTTPResponse(statusCode: 503, headers: [:], body: Data()) } - let inner = #"{"token":{"access_token":"ya29.old","refresh_token":"1//dead","expiry":"2000-01-01T00:00:00Z"}}"# - let wrapped = "go-keyring-base64:" + Data(inner.utf8).base64EncodedString() - let provider = AntigravityProvider( - authStore: AntigravityAuthStore(keychain: FakeKeychain(wrapped), files: FakeFiles()), - usageClient: AntigravityUsageClient(lsHTTP: routing, http: routing), - discovery: LanguageServerDiscovery(processRunner: EmptyProcessRunner()) - ) + let provider = makeCloudCodeProvider(routing: routing, keychainExpiry: "2000-01-01T00:00:00Z") let snapshot = await provider.refresh() XCTAssertEqual(snapshot.errorCategory, .authExpired) } @@ -396,13 +391,7 @@ final class AntigravityProviderTests: XCTestCase { } return HTTPResponse(statusCode: 401, headers: [:], body: Data()) } - let inner = #"{"token":{"access_token":"ya29.kc","refresh_token":"1//r","expiry":"2099-01-01T00:00:00Z"}}"# - let wrapped = "go-keyring-base64:" + Data(inner.utf8).base64EncodedString() - let provider = AntigravityProvider( - authStore: AntigravityAuthStore(keychain: FakeKeychain(wrapped), files: FakeFiles()), - usageClient: AntigravityUsageClient(lsHTTP: routing, http: routing), - discovery: LanguageServerDiscovery(processRunner: EmptyProcessRunner()) - ) + let provider = makeCloudCodeProvider(routing: routing) let snapshot = await provider.refresh() XCTAssertEqual(snapshot.errorCategory, .network) } @@ -417,13 +406,7 @@ final class AntigravityProviderTests: XCTestCase { } return HTTPResponse(statusCode: 503, headers: [:], body: Data()) } - let inner = #"{"token":{"access_token":"ya29.old","refresh_token":"1//r","expiry":"2000-01-01T00:00:00Z"}}"# - let wrapped = "go-keyring-base64:" + Data(inner.utf8).base64EncodedString() - let provider = AntigravityProvider( - authStore: AntigravityAuthStore(keychain: FakeKeychain(wrapped), files: FakeFiles()), - usageClient: AntigravityUsageClient(lsHTTP: routing, http: routing), - discovery: LanguageServerDiscovery(processRunner: EmptyProcessRunner()) - ) + let provider = makeCloudCodeProvider(routing: routing, keychainExpiry: "2000-01-01T00:00:00Z") let snapshot = await provider.refresh() XCTAssertEqual(snapshot.errorCategory, .network) } @@ -443,13 +426,7 @@ final class AntigravityProviderTests: XCTestCase { } return HTTPResponse(statusCode: 503, headers: [:], body: Data()) } - let inner = #"{"token":{"access_token":"ya29.kc","refresh_token":"1//r","expiry":"2099-01-01T00:00:00Z"}}"# - let wrapped = "go-keyring-base64:" + Data(inner.utf8).base64EncodedString() - let provider = AntigravityProvider( - authStore: AntigravityAuthStore(keychain: FakeKeychain(wrapped), files: FakeFiles()), - usageClient: AntigravityUsageClient(lsHTTP: routing, http: routing), - discovery: LanguageServerDiscovery(processRunner: EmptyProcessRunner()) - ) + let provider = makeCloudCodeProvider(routing: routing) let snapshot = await provider.refresh() XCTAssertEqual(snapshot.errorCategory, .network) } diff --git a/Tests/OpenUsageTests/ClaudeDesktopAuthStoreTests.swift b/Tests/OpenUsageTests/ClaudeDesktopAuthStoreTests.swift index e663be237..25c3e17a8 100644 --- a/Tests/OpenUsageTests/ClaudeDesktopAuthStoreTests.swift +++ b/Tests/OpenUsageTests/ClaudeDesktopAuthStoreTests.swift @@ -134,16 +134,7 @@ final class ClaudeDesktopAuthStoreTests: XCTestCase { activeOrganization: organization, v2: [cacheKey(organization: organization): tokenEntry("desktop-token", expiresIn: 3_600)] ) - let now = now - let authStore = ClaudeAuthStore( - environment: FakeEnvironment(["CLAUDE_CONFIG_DIR": "/tmp/claude"]), - files: fixture.files, - keychain: FakeKeychain( - #"{"claudeAiOauth":{"accessToken":"cli-token","expiresAt":4102444800000,"scopes":["user:profile"]}}"# - ), - desktop: fixture.store, - now: { now } - ) + let authStore = makeAuthStore(fixture, keychainJSON: cliCredentials(token: "cli-token")) let load = authStore.loadCredentialSet() @@ -157,16 +148,7 @@ final class ClaudeDesktopAuthStoreTests: XCTestCase { activeOrganization: organization, v2: [cacheKey(organization: organization): tokenEntry("desktop-token", expiresIn: 3_600)] ) - let now = now - let authStore = ClaudeAuthStore( - environment: FakeEnvironment(["CLAUDE_CONFIG_DIR": "/tmp/claude"]), - files: fixture.files, - keychain: FakeKeychain( - #"{"claudeAiOauth":{"accessToken":" ","expiresAt":4102444800000,"scopes":["user:profile"]}}"# - ), - desktop: fixture.store, - now: { now } - ) + let authStore = makeAuthStore(fixture, keychainJSON: cliCredentials(token: " ")) let load = authStore.loadCredentialSet() @@ -182,22 +164,11 @@ final class ClaudeDesktopAuthStoreTests: XCTestCase { v2: [cacheKey(organization: organization): tokenEntry("desktop-token", expiresIn: 3_600)], requiresInteraction: true ) - let now = now let httpClient = FakeHTTPClient(response: HTTPResponse(statusCode: 200, headers: [:], body: Data())) - let provider = ClaudeProvider( - authStore: ClaudeAuthStore( - environment: FakeEnvironment(["CLAUDE_CONFIG_DIR": "/tmp/claude"]), - files: fixture.files, - keychain: FakeKeychain( - #"{"claudeAiOauth":{"accessToken":"inference-only-cli","expiresAt":4102444800000,"scopes":["user:inference"]}}"# - ), - desktop: fixture.store, - now: { now } - ), - usageClient: ClaudeUsageClient(httpClient: httpClient), - logUsageScanner: ClaudeLogFixture.scanner(home: nil), - now: { now }, - pricing: { TestPricing.bundled } + let provider = makeProvider( + fixture, + keychainJSON: cliCredentials(token: "inference-only-cli", scope: "user:inference"), + httpClient: httpClient ) let snapshot = await provider.refresh() @@ -215,14 +186,7 @@ final class ClaudeDesktopAuthStoreTests: XCTestCase { activeOrganization: organization, v2: [cacheKey(organization: organization): tokenEntry("desktop-token", expiresIn: 3_600)] ) - let now = now - let authStore = ClaudeAuthStore( - environment: FakeEnvironment(), - files: files, - keychain: keychain, - desktop: fixture.store, - now: { now } - ) + let authStore = makeAuthStore(fixture, environment: [:], files: files, keychain: keychain) let state = authStore.loadCredentialCandidates().first! XCTAssertFalse(try authStore.save(state, ifUnchanged: ClaudeCredentialGeneration([state]))) @@ -240,20 +204,7 @@ final class ClaudeDesktopAuthStoreTests: XCTestCase { XCTAssertTrue(request.url.absoluteString.hasSuffix("/api/oauth/usage")) return HTTPResponse(statusCode: 401, headers: [:], body: Data()) } - let now = now - let provider = ClaudeProvider( - authStore: ClaudeAuthStore( - environment: FakeEnvironment(), - files: fixture.files, - keychain: FakeKeychain(), - desktop: fixture.store, - now: { now } - ), - usageClient: ClaudeUsageClient(httpClient: httpClient), - logUsageScanner: ClaudeLogFixture.scanner(home: nil), - now: { now }, - pricing: { TestPricing.bundled } - ) + let provider = makeProvider(fixture, environment: [:], keychainJSON: nil, httpClient: httpClient) let snapshot = await ProviderRefreshContext.$isManual.withValue(true) { await provider.refresh() @@ -264,12 +215,13 @@ final class ClaudeDesktopAuthStoreTests: XCTestCase { } @MainActor - func testRevokedCLILoginFallsBackToDesktop() async throws { + func testRevokedCLILoginFallsBackToDesktopBeforeEnvironmentToken() async throws { + // The stored CLI login 401s (revoked); the desktop token must be the next candidate tried — + // even when a lower-priority environment token is also available. let fixture = try makeFixture( activeOrganization: organization, v2: [cacheKey(organization: organization): tokenEntry("desktop-token", expiresIn: 3_600)] ) - let now = now let httpClient = RoutingHTTPClient { request in let authorization = request.headers["Authorization"] ?? "" if authorization.contains("desktop-token") { @@ -281,20 +233,14 @@ final class ClaudeDesktopAuthStoreTests: XCTestCase { } return HTTPResponse(statusCode: 401, headers: [:], body: Data()) } - let provider = ClaudeProvider( - authStore: ClaudeAuthStore( - environment: FakeEnvironment(["CLAUDE_CONFIG_DIR": "/tmp/claude"]), - files: fixture.files, - keychain: FakeKeychain( - #"{"claudeAiOauth":{"accessToken":"revoked-cli","expiresAt":4102444800000,"scopes":["user:profile"]}}"# - ), - desktop: fixture.store, - now: { now } - ), - usageClient: ClaudeUsageClient(httpClient: httpClient), - logUsageScanner: ClaudeLogFixture.scanner(home: nil), - now: { now }, - pricing: { TestPricing.bundled } + let provider = makeProvider( + fixture, + environment: [ + "CLAUDE_CONFIG_DIR": "/tmp/claude", + "CLAUDE_CODE_OAUTH_TOKEN": "inference-only-env" + ], + keychainJSON: cliCredentials(token: "revoked-cli"), + httpClient: httpClient ) let snapshot = await ProviderRefreshContext.$isManual.withValue(true) { @@ -307,83 +253,61 @@ final class ClaudeDesktopAuthStoreTests: XCTestCase { } @MainActor - func testRevokedCLILoginTriesDesktopBeforeEnvironmentToken() async throws { + func testStaleDesktopDoesNotMaskRevokedCLIError() async throws { let fixture = try makeFixture( activeOrganization: organization, - v2: [cacheKey(organization: organization): tokenEntry("desktop-token", expiresIn: 3_600)] + v2: [cacheKey(organization: organization): tokenEntry("expired-desktop", expiresIn: -1)] ) - let now = now - let httpClient = RoutingHTTPClient { request in - let authorization = request.headers["Authorization"] ?? "" - if authorization.contains("desktop-token") { - return HTTPResponse( - statusCode: 200, - headers: [:], - body: Data(#"{"five_hour":{"utilization":25,"resets_at":"2099-01-01T00:00:00.000Z"}}"#.utf8) - ) - } - return HTTPResponse(statusCode: 401, headers: [:], body: Data()) + let httpClient = RoutingHTTPClient { _ in + HTTPResponse(statusCode: 401, headers: [:], body: Data()) } - let provider = ClaudeProvider( - authStore: ClaudeAuthStore( - environment: FakeEnvironment([ - "CLAUDE_CONFIG_DIR": "/tmp/claude", - "CLAUDE_CODE_OAUTH_TOKEN": "inference-only-env" - ]), - files: fixture.files, - keychain: FakeKeychain( - #"{"claudeAiOauth":{"accessToken":"revoked-cli","expiresAt":4102444800000,"scopes":["user:profile"]}}"# - ), - desktop: fixture.store, - now: { now } - ), - usageClient: ClaudeUsageClient(httpClient: httpClient), - logUsageScanner: ClaudeLogFixture.scanner(home: nil), - now: { now }, - pricing: { TestPricing.bundled } - ) + let provider = makeProvider(fixture, keychainJSON: cliCredentials(token: "revoked-cli"), httpClient: httpClient) let snapshot = await ProviderRefreshContext.$isManual.withValue(true) { await provider.refresh() } - XCTAssertNil(badge(snapshot.lines, "Error")) - XCTAssertEqual(httpClient.requests.count, 2) - XCTAssertTrue(httpClient.requests.last?.headers["Authorization"]?.contains("desktop-token") == true) + XCTAssertEqual(badge(snapshot.lines, "Error"), ClaudeAuthError.tokenExpired.localizedDescription) + XCTAssertEqual(httpClient.requests.count, 1) } - @MainActor - func testStaleDesktopDoesNotMaskRevokedCLIError() async throws { - let fixture = try makeFixture( - activeOrganization: organization, - v2: [cacheKey(organization: organization): tokenEntry("expired-desktop", expiresIn: -1)] + /// The stock CLI keychain payload: one far-future OAuth token with the given scope. + private func cliCredentials(token: String, scope: String = "user:profile") -> String { + #"{"claudeAiOauth":{"accessToken":"\#(token)","expiresAt":4102444800000,"scopes":["\#(scope)"]}}"# + } + + private func makeAuthStore( + _ fixture: DesktopFixture, + environment: [String: String] = ["CLAUDE_CONFIG_DIR": "/tmp/claude"], + keychainJSON: String? = nil, + files: FakeFiles? = nil, + keychain: (any KeychainAccessing)? = nil + ) -> ClaudeAuthStore { + let now = now + return ClaudeAuthStore( + environment: FakeEnvironment(environment), + files: files ?? fixture.files, + keychain: keychain ?? FakeKeychain(keychainJSON), + desktop: fixture.store, + now: { now } ) + } + + @MainActor + private func makeProvider( + _ fixture: DesktopFixture, + environment: [String: String] = ["CLAUDE_CONFIG_DIR": "/tmp/claude"], + keychainJSON: String?, + httpClient: any HTTPClient + ) -> ClaudeProvider { let now = now - let httpClient = RoutingHTTPClient { _ in - HTTPResponse(statusCode: 401, headers: [:], body: Data()) - } - let provider = ClaudeProvider( - authStore: ClaudeAuthStore( - environment: FakeEnvironment(["CLAUDE_CONFIG_DIR": "/tmp/claude"]), - files: fixture.files, - keychain: FakeKeychain( - #"{"claudeAiOauth":{"accessToken":"revoked-cli","expiresAt":4102444800000,"scopes":["user:profile"]}}"# - ), - desktop: fixture.store, - now: { now } - ), + return ClaudeProvider( + authStore: makeAuthStore(fixture, environment: environment, keychainJSON: keychainJSON), usageClient: ClaudeUsageClient(httpClient: httpClient), logUsageScanner: ClaudeLogFixture.scanner(home: nil), now: { now }, pricing: { TestPricing.bundled } ) - - let snapshot = await ProviderRefreshContext.$isManual.withValue(true) { - await provider.refresh() - } - - XCTAssertEqual(badge(snapshot.lines, "Error"), ClaudeAuthError.tokenExpired.localizedDescription) - XCTAssertEqual(httpClient.requests.count, 1) } private func makeFixture( diff --git a/Tests/OpenUsageTests/CursorOptionalEndpointTests.swift b/Tests/OpenUsageTests/CursorOptionalEndpointTests.swift index 41ff007d2..3573b3a87 100644 --- a/Tests/OpenUsageTests/CursorOptionalEndpointTests.swift +++ b/Tests/OpenUsageTests/CursorOptionalEndpointTests.swift @@ -4,7 +4,7 @@ import XCTest @MainActor final class CursorOptionalEndpointTests: XCTestCase { func testOptionalSchemaAndHTTPFailuresAreLoggedWithoutDiscardingPrimaryUsage() async throws { - let accessToken = makeCursorJWT(includeSubject: true) + let accessToken = makeCursorJWT() let provider = makeProvider(accessToken: accessToken) { request in switch request.url { case CursorUsageClient.usageURL: @@ -31,7 +31,7 @@ final class CursorOptionalEndpointTests: XCTestCase { } func testOptionalTransportAndSessionPreparationFailuresAreLogged() async throws { - let provider = makeProvider(accessToken: makeCursorJWT(includeSubject: false)) { request in + let provider = makeProvider(accessToken: makeCursorJWT(sub: nil)) { request in switch request.url { case CursorUsageClient.usageURL: return Self.primaryUsageResponse @@ -51,99 +51,73 @@ final class CursorOptionalEndpointTests: XCTestCase { XCTAssertTrue(logs.contains("optional prepaid-balance request could not be prepared from the current session"), logs) } - func testGrokBotRequestFailureDoesNotDiscardPrimaryUsage() async throws { - let provider = makeProvider { request in - switch request.url { - case CursorUsageClient.usageURL: - return Self.primaryUsageResponse - case CursorUsageClient.planURL: - return HTTPResponse(statusCode: 200, headers: [:], body: Data(#"{"planInfo":{"planName":"Ultra"}}"#.utf8)) - case CursorUsageClient.grokBotUsageURL: - return HTTPResponse(statusCode: 503, headers: [:], body: Data()) - default: - return HTTPResponse(statusCode: 404, headers: [:], body: Data()) - } + /// The optional Grok Bot meter: failures log, ineligible shapes stay silent, and no case may + /// discard the primary usage or emit a Grok Bot line. + func testGrokBotFailuresAndIneligibleShapesHideTheMeterWithoutDiscardingPrimaryUsage() async throws { + struct GrokBotCase { + var name: String + var response: HTTPResponse + var expectedLog: String? // nil → the invalid-usage warning must NOT appear } - - let (snapshot, logs) = try await captureLogs { await provider.refresh() } - - XCTAssertNil(snapshot.errorCategory) - XCTAssertEqual(progress(snapshot.lines, "Total usage")?.used, 20) - XCTAssertNil(snapshot.lines.first { $0.label == "Grok Bot usage" }) - XCTAssertTrue(logs.contains("optional Grok Bot usage request returned HTTP 503"), logs) - } - - func testInvalidGrokBotUsageIsLoggedWithoutDiscardingPrimaryUsage() async throws { - let provider = makeProvider { request in - switch request.url { - case CursorUsageClient.usageURL: - return Self.primaryUsageResponse - case CursorUsageClient.planURL: - return HTTPResponse(statusCode: 200, headers: [:], body: Data(#"{"planInfo":{"planName":"Ultra"}}"#.utf8)) - case CursorUsageClient.grokBotUsageURL: - return HTTPResponse( - statusCode: 200, - headers: [:], + let invalidUsageLog = "Grok Bot usage response contained invalid usage metadata" + let cases = [ + GrokBotCase( + name: "HTTP failure", + response: HTTPResponse(statusCode: 503, headers: [:], body: Data()), + expectedLog: "optional Grok Bot usage request returned HTTP 503" + ), + GrokBotCase( + name: "invalid usage metadata", + response: HTTPResponse( + statusCode: 200, headers: [:], body: Data(#"{"usagePercent":true,"hasNonZeroIncludedLimit":true}"#.utf8) - ) - default: - return HTTPResponse(statusCode: 404, headers: [:], body: Data()) - } - } - - let (snapshot, logs) = try await captureLogs { await provider.refresh() } - - XCTAssertNil(snapshot.errorCategory) - XCTAssertEqual(progress(snapshot.lines, "Total usage")?.used, 20) - XCTAssertNil(snapshot.lines.first { $0.label == "Grok Bot usage" }) - XCTAssertTrue(logs.contains("optional Grok Bot usage response contained invalid usage metadata"), logs) - } - - func testIneligibleGrokBotAccountDoesNotLogAnInvalidUsageError() async throws { - let provider = makeProvider { request in - switch request.url { - case CursorUsageClient.usageURL: - return Self.primaryUsageResponse - case CursorUsageClient.planURL: - return HTTPResponse(statusCode: 200, headers: [:], body: Data(#"{"planInfo":{"planName":"Pro"}}"#.utf8)) - case CursorUsageClient.grokBotUsageURL: - return HTTPResponse(statusCode: 200, headers: [:], body: Data(#"{"includedLimitZero":true}"#.utf8)) - default: - return HTTPResponse(statusCode: 404, headers: [:], body: Data()) + ), + expectedLog: "optional " + invalidUsageLog + ), + GrokBotCase( + name: "ineligible account", + response: HTTPResponse( + statusCode: 200, headers: [:], + body: Data(#"{"includedLimitZero":true}"#.utf8) + ), + expectedLog: nil + ), + GrokBotCase( + name: "zero usage without included allowance", + response: HTTPResponse( + statusCode: 200, headers: [:], + body: Data(#"{"usagePercent":0,"hasNonZeroIncludedLimit":false}"#.utf8) + ), + expectedLog: nil + ) + ] + + for grokBotCase in cases { + let response = grokBotCase.response + let provider = makeProvider { request in + switch request.url { + case CursorUsageClient.usageURL: + return Self.primaryUsageResponse + case CursorUsageClient.planURL: + return HTTPResponse(statusCode: 200, headers: [:], body: Data(#"{"planInfo":{"planName":"Pro"}}"#.utf8)) + case CursorUsageClient.grokBotUsageURL: + return response + default: + return HTTPResponse(statusCode: 404, headers: [:], body: Data()) + } } - } - - let (snapshot, logs) = try await captureLogs { await provider.refresh() } - XCTAssertNil(snapshot.errorCategory) - XCTAssertNil(snapshot.lines.first { $0.label == "Grok Bot usage" }) - XCTAssertFalse(logs.contains("Grok Bot usage response contained invalid usage metadata"), logs) - } + let (snapshot, logs) = try await captureLogs { await provider.refresh() } - func testGrokBotZeroUsageWithoutIncludedAllowanceIsHiddenWithoutWarning() async throws { - let provider = makeProvider { request in - switch request.url { - case CursorUsageClient.usageURL: - return Self.primaryUsageResponse - case CursorUsageClient.planURL: - return HTTPResponse(statusCode: 200, headers: [:], body: Data(#"{"planInfo":{"planName":"Pro"}}"#.utf8)) - case CursorUsageClient.grokBotUsageURL: - return HTTPResponse( - statusCode: 200, - headers: [:], - body: Data(#"{"usagePercent":0,"hasNonZeroIncludedLimit":false}"#.utf8) - ) - default: - return HTTPResponse(statusCode: 404, headers: [:], body: Data()) + XCTAssertNil(snapshot.errorCategory, grokBotCase.name) + XCTAssertEqual(progress(snapshot.lines, "Total usage")?.used, 20, grokBotCase.name) + XCTAssertNil(snapshot.lines.first { $0.label == "Grok Bot usage" }, grokBotCase.name) + if let expectedLog = grokBotCase.expectedLog { + XCTAssertTrue(logs.contains(expectedLog), "\(grokBotCase.name): \(logs)") + } else { + XCTAssertFalse(logs.contains(invalidUsageLog), "\(grokBotCase.name): \(logs)") } } - - let (snapshot, logs) = try await captureLogs { await provider.refresh() } - - XCTAssertNil(snapshot.errorCategory) - XCTAssertEqual(progress(snapshot.lines, "Total usage")?.used, 20) - XCTAssertNil(snapshot.lines.first { $0.label == "Grok Bot usage" }) - XCTAssertFalse(logs.contains("Grok Bot usage response contained invalid usage metadata"), logs) } func testInvalidPlanMetadataStillEnablesRequestBasedFallback() async throws { @@ -272,12 +246,12 @@ final class CursorOptionalEndpointTests: XCTestCase { } private func makeProvider( - accessToken: String = makeCursorJWT(includeSubject: true), + accessToken: String = makeCursorJWT(), handler: @escaping @Sendable (HTTPRequest) async throws -> HTTPResponse ) -> CursorProvider { CursorProvider( authStore: CursorAuthStore( - sqlite: OptionalCursorSQLite(values: [CursorAuthStore.accessTokenKey: accessToken]), + sqlite: KeyValueSQLite(values: [CursorAuthStore.accessTokenKey: accessToken]), keychain: FakeKeychain() ), usageClient: CursorUsageClient(http: RoutingHTTPClient(handler: handler)), @@ -318,30 +292,4 @@ final class CursorOptionalEndpointTests: XCTestCase { } } -private func makeCursorJWT(includeSubject: Bool) -> String { - let payload = includeSubject - ? #"{"exp":9999999999,"sub":"google-oauth2|user"}"# - : #"{"exp":9999999999}"# - let encoded = Data(payload.utf8).base64EncodedString() - .replacingOccurrences(of: "=", with: "") - .replacingOccurrences(of: "+", with: "-") - .replacingOccurrences(of: "/", with: "_") - return "a.\(encoded).c" -} - -private final class OptionalCursorSQLite: SQLiteAccessing, @unchecked Sendable { - private let values: [String: String] - - init(values: [String: String]) { - self.values = values - } - - func queryValue(path: String, sql: String) throws -> String? { - for (key, value) in values where sql.contains(key) { - return value - } - return nil - } - - func execute(path: String, sql: String) throws {} -} +// makeCursorJWT and KeyValueSQLite live in TestSupport.swift. diff --git a/Tests/OpenUsageTests/CursorProviderTests.swift b/Tests/OpenUsageTests/CursorProviderTests.swift index 3c7c14aaf..c9be0d702 100644 --- a/Tests/OpenUsageTests/CursorProviderTests.swift +++ b/Tests/OpenUsageTests/CursorProviderTests.swift @@ -5,7 +5,7 @@ final class CursorAuthStoreTests: XCTestCase { func testPrefersKeychainWhenSQLiteLooksFreeAndSubjectsDiffer() { let sqliteToken = makeCursorJWT(sub: "google-oauth2|sqlite-user") let keychainToken = makeCursorJWT(sub: "auth0|keychain-user") - let sqlite = FakeSQLite(values: [ + let sqlite = KeyValueSQLite(values: [ CursorAuthStore.accessTokenKey: sqliteToken, CursorAuthStore.refreshTokenKey: "sqlite-refresh", CursorAuthStore.membershipTypeKey: "free" @@ -24,7 +24,7 @@ final class CursorAuthStoreTests: XCTestCase { } func testPersistsSQLiteAccessToken() throws { - let sqlite = FakeSQLite() + let sqlite = KeyValueSQLite() let store = CursorAuthStore(sqlite: sqlite, keychain: FakeKeychain()) try store.saveAccessToken("fresh-token", source: .sqlite) @@ -305,7 +305,7 @@ final class CursorProviderTests: XCTestCase { } let provider = CursorProvider( authStore: CursorAuthStore( - sqlite: FakeSQLite(values: [CursorAuthStore.accessTokenKey: accessToken]), + sqlite: KeyValueSQLite(values: [CursorAuthStore.accessTokenKey: accessToken]), keychain: FakeKeychain() ), usageClient: CursorUsageClient(http: http), @@ -342,47 +342,4 @@ private func dollarValue(_ lines: [MetricLine], _ label: String) -> Double? { return values.first { $0.kind == .dollars }?.number } -private func makeCursorJWT(sub: String = "google-oauth2|user", exp: Double = 9_999_999_999) -> String { - let payload = #"{"sub":"\#(sub)","exp":\#(exp)}"# - let encoded = Data(payload.utf8).base64EncodedString() - .replacingOccurrences(of: "=", with: "") - .replacingOccurrences(of: "+", with: "-") - .replacingOccurrences(of: "/", with: "_") - return "a.\(encoded).c" -} - -private final class FakeSQLite: SQLiteAccessing, @unchecked Sendable { - var values: [String: String] - var writtenValues: [String: String] = [:] - - init(values: [String: String] = [:]) { - self.values = values - } - - func queryValue(path: String, sql: String) throws -> String? { - for (key, value) in values where sql.contains(key) { - return value - } - return nil - } - - func execute(path: String, sql: String) throws { - guard let key = sqlValue(after: "(key, value) VALUES ('", in: sql), - let value = sqlValue(after: "', '", in: sql) - else { - return - } - writtenValues[key] = value - } - - private func sqlValue(after marker: String, in sql: String) -> String? { - guard let start = sql.range(of: marker)?.upperBound, - let end = sql[start...].range(of: "'")?.lowerBound - else { - return nil - } - return String(sql[start.. String { - let payload = #"{"sub":"\#(sub)","exp":\#(exp)}"# - let encoded = Data(payload.utf8).base64EncodedString() - .replacingOccurrences(of: "=", with: "") - .replacingOccurrences(of: "+", with: "-") - .replacingOccurrences(of: "/", with: "_") - return "a.\(encoded).c" -} - -private final class FakeSQLite: SQLiteAccessing, @unchecked Sendable { - var values: [String: String] - init(values: [String: String] = [:]) { self.values = values } - func queryValue(path: String, sql: String) throws -> String? { - for (key, value) in values where sql.contains(key) { return value } - return nil - } - func execute(path: String, sql: String) throws {} -} - -// RoutingHTTPClient lives in TestSupport.swift (shared, records requests). +// makeCursorJWT, KeyValueSQLite, and RoutingHTTPClient live in TestSupport.swift. diff --git a/Tests/OpenUsageTests/CursorUsageSummaryTests.swift b/Tests/OpenUsageTests/CursorUsageSummaryTests.swift index 180ae485a..217402fca 100644 --- a/Tests/OpenUsageTests/CursorUsageSummaryTests.swift +++ b/Tests/OpenUsageTests/CursorUsageSummaryTests.swift @@ -158,7 +158,7 @@ final class CursorUsageSummaryMapperTests: XCTestCase { final class CursorEnterpriseProviderTests: XCTestCase { func testRefreshCombinesEnterpriseMetersAndStillAppendsUsageHistory() async throws { let now = try XCTUnwrap(OpenUsageISO8601.date(from: "2026-07-13T12:00:00.000Z")) - let accessToken = makeSummaryCursorJWT(sub: "google-oauth2|enterprise-user") + let accessToken = makeCursorJWT(sub: "google-oauth2|enterprise-user") let csv = """ Date,Model,Max Mode,Input (w/ Cache Write),Input (w/o Cache Write),Cache Read,Output Tokens,Cost 2026-07-13T10:00:00Z,composer-1,No,0,1000,0,100,Included @@ -221,7 +221,7 @@ final class CursorEnterpriseProviderTests: XCTestCase { } let provider = CursorProvider( authStore: CursorAuthStore( - sqlite: SummaryCursorSQLite(values: [CursorAuthStore.accessTokenKey: accessToken]), + sqlite: KeyValueSQLite(values: [CursorAuthStore.accessTokenKey: accessToken]), keychain: FakeKeychain() ), usageClient: CursorUsageClient(http: http), @@ -306,28 +306,4 @@ final class CursorEnterpriseProviderTests: XCTestCase { } } -private func makeSummaryCursorJWT(sub: String, exp: Double = 9_999_999_999) -> String { - let payload = #"{"sub":"\#(sub)","exp":\#(exp)}"# - let encoded = Data(payload.utf8).base64EncodedString() - .replacingOccurrences(of: "=", with: "") - .replacingOccurrences(of: "+", with: "-") - .replacingOccurrences(of: "/", with: "_") - return "a.\(encoded).c" -} - -private final class SummaryCursorSQLite: SQLiteAccessing, @unchecked Sendable { - private let values: [String: String] - - init(values: [String: String]) { - self.values = values - } - - func queryValue(path: String, sql: String) throws -> String? { - for (key, value) in values where sql.contains(key) { - return value - } - return nil - } - - func execute(path: String, sql: String) throws {} -} +// makeCursorJWT and KeyValueSQLite live in TestSupport.swift. diff --git a/Tests/OpenUsageTests/DevinProviderTests.swift b/Tests/OpenUsageTests/DevinProviderTests.swift index ee1753e30..a902e0dda 100644 --- a/Tests/OpenUsageTests/DevinProviderTests.swift +++ b/Tests/OpenUsageTests/DevinProviderTests.swift @@ -2,38 +2,27 @@ import XCTest @testable import OpenUsage final class DevinAuthStoreTests: XCTestCase { - func testParsesCredentialsTomlAndCleansServerURL() { - let store = DevinAuthStore( - files: FakeFiles([ - DevinAuthStore.credentialsPath: """ - windsurf_api_key = "devin-session-token$cli" - api_server_url = "https://server.codeium.test/" - """ - ]), - sqlite: FakeSQLite() - ) - - let auth = store.loadCredentialsFile() - - XCTAssertEqual(auth?.apiKey, "devin-session-token$cli") - XCTAssertEqual(auth?.apiServerUrl, "https://server.codeium.test") - } - - func testIgnoresPlaintextServerURL() { - let store = DevinAuthStore( - files: FakeFiles([ - DevinAuthStore.credentialsPath: """ - windsurf_api_key = "devin-session-token$cli" - api_server_url = "http://server.codeium.test" - """ - ]), - sqlite: FakeSQLite() - ) + func testParsesCredentialsTomlAndAcceptsOnlyHTTPSServerURL() { + func load(serverURL: String) -> DevinAuth? { + DevinAuthStore( + files: FakeFiles([ + DevinAuthStore.credentialsPath: """ + windsurf_api_key = "devin-session-token$cli" + api_server_url = "\(serverURL)" + """ + ]), + sqlite: FakeSQLite() + ).loadCredentialsFile() + } - let auth = store.loadCredentialsFile() + let https = load(serverURL: "https://server.codeium.test/") + XCTAssertEqual(https?.apiKey, "devin-session-token$cli") + XCTAssertEqual(https?.apiServerUrl, "https://server.codeium.test", "trailing slash cleaned") - XCTAssertEqual(auth?.apiKey, "devin-session-token$cli") - XCTAssertNil(auth?.apiServerUrl) + // A plaintext URL is dropped; the key still parses (proving the file parsed at all). + let http = load(serverURL: "http://server.codeium.test") + XCTAssertEqual(http?.apiKey, "devin-session-token$cli") + XCTAssertNil(http?.apiServerUrl) } func testReadsAppAuthFromSQLiteState() { @@ -90,7 +79,6 @@ final class DevinUsageMapperTests: XCTestCase { // The hidden daily quota fills the missing Weekly row and is still flipped from "remaining" // to "used": 30% remaining -> 70% used (not passed through raw as 30). XCTAssertEqual(progress(mapped.lines, "Weekly quota")?.used, 70) - XCTAssertEqual(try XCTUnwrap(dollars(mapped.lines, "Extra usage balance")), 964.22, accuracy: 0.0001) } func testThrowsQuotaUnavailableWhenNoDisplayableFieldsExist() { diff --git a/Tests/OpenUsageTests/FailureBackoffTests.swift b/Tests/OpenUsageTests/FailureBackoffTests.swift index f1a1941fb..8c2228201 100644 --- a/Tests/OpenUsageTests/FailureBackoffTests.swift +++ b/Tests/OpenUsageTests/FailureBackoffTests.swift @@ -145,24 +145,4 @@ final class FailureBackoffTests: XCTestCase { } } -/// Returns a different snapshot per call (then repeats the last), so a test can model a provider that -/// fails and later recovers — which `CountingProviderRuntime` (one fixed snapshot) can't express. -@MainActor -final class SequenceProviderRuntime: ProviderRuntime { - let provider: Provider - let widgetDescriptors: [WidgetDescriptor] - private let snapshots: [ProviderSnapshot] - private(set) var refreshCount = 0 - - init(provider: Provider, descriptors: [WidgetDescriptor], snapshots: [ProviderSnapshot]) { - self.provider = provider - self.widgetDescriptors = descriptors - self.snapshots = snapshots - } - - func refresh() async -> ProviderSnapshot { - let snapshot = snapshots[min(refreshCount, snapshots.count - 1)] - refreshCount += 1 - return snapshot - } -} +// SequenceProviderRuntime lives in TestSupport.swift. diff --git a/Tests/OpenUsageTests/GrokCreditsConfigTests.swift b/Tests/OpenUsageTests/GrokCreditsConfigTests.swift index dc864121b..69ef10cc6 100644 --- a/Tests/OpenUsageTests/GrokCreditsConfigTests.swift +++ b/Tests/OpenUsageTests/GrokCreditsConfigTests.swift @@ -14,37 +14,30 @@ final class GrokCreditsConfigDecoderTests: XCTestCase { XCTAssertEqual(config.periodDurationMs, 7 * 24 * 60 * 60 * 1000) } - func testAbsentPercentDecodesAsZero() throws { + func testAbsentZeroValuedFieldsDecodeAsZero() throws { // proto-JSON drops zero-valued fields: a fresh weekly period omits `creditUsagePercent` - // entirely. That's a genuine 0%, never a schema-change error. - let config = try GrokCreditsConfigDecoder.decode( + // and a disabled cap omits `onDemandCap`. Those are genuine zeros, never schema errors. + let noPercent = try GrokCreditsConfigDecoder.decode( responseBody: GrokCreditsFixtures.responseBody(percent: nil) ) - XCTAssertEqual(config.usedPercent, 0) - } + XCTAssertEqual(noPercent.usedPercent, 0) - func testAbsentOnDemandCapDecodesAsZero() throws { - let config = try GrokCreditsConfigDecoder.decode( + let noCap = try GrokCreditsConfigDecoder.decode( responseBody: GrokCreditsFixtures.responseBody(onDemandCap: nil) ) - XCTAssertEqual(config.onDemandCap, 0) - } - - func testRejectsNonNumericOnDemandCap() { - XCTAssertThrowsError( - try GrokCreditsConfigDecoder.decode(responseBody: GrokCreditsFixtures.responseBody(onDemandCap: "lots")) - ) { error in - XCTAssertEqual(error as? GrokUsageError, .invalidResponse) - } + XCTAssertEqual(noCap.onDemandCap, 0) } - func testRejectsNonNumericPercent() { - // A present but non-numeric percent is a schema change, not a 0 — clamping it to a - // believable "0% used" would hide the drift. - XCTAssertThrowsError( - try GrokCreditsConfigDecoder.decode(responseBody: GrokCreditsFixtures.responseBody(percent: "high")) - ) { error in - XCTAssertEqual(error as? GrokUsageError, .invalidResponse) + func testRejectsNonNumericFields() { + // A present but non-numeric value is a schema change, not a 0 — clamping it to a + // believable "0" would hide the drift. + for body in [ + GrokCreditsFixtures.responseBody(percent: "high"), + GrokCreditsFixtures.responseBody(onDemandCap: "lots") + ] { + XCTAssertThrowsError(try GrokCreditsConfigDecoder.decode(responseBody: body)) { error in + XCTAssertEqual(error as? GrokUsageError, .invalidResponse) + } } } @@ -109,16 +102,8 @@ final class GrokCreditsConfigMapperTests: XCTestCase { XCTAssertEqual(colorHex, "#22c55e") } - func testNonWeeklyPeriodMapsToNoWeeklyLine() throws { - // An account still on monthly-only billing has no weekly pool; the tile must read "No data" - // rather than mislabel a monthly percent as weekly. The badge still renders. - let mapped = try GrokUsageMapper.mapCreditsConfig(HTTPResponse( - statusCode: 200, headers: [:], - body: GrokCreditsFixtures.responseBody(periodType: "USAGE_PERIOD_TYPE_MONTHLY") - )) - XCTAssertNil(mapped.lines.first(where: { $0.label == "Weekly limit" })) - XCTAssertNotNil(mapped.lines.first(where: { $0.label == "Pay as you go" })) - } + // The non-weekly (monthly) period shape is covered end-to-end by + // GrokProviderTests.testNonWeeklyPeriodShowsNoWeeklyLineAndNoWarning. func testClampsOutOfRangePercent() throws { let mapped = try GrokUsageMapper.mapCreditsConfig(HTTPResponse( diff --git a/Tests/OpenUsageTests/JSONLScannerCancellationTests.swift b/Tests/OpenUsageTests/JSONLScannerCancellationTests.swift index 4935bfd0c..40849e488 100644 --- a/Tests/OpenUsageTests/JSONLScannerCancellationTests.swift +++ b/Tests/OpenUsageTests/JSONLScannerCancellationTests.swift @@ -57,7 +57,7 @@ final class JSONLScannerCancellationTests: XCTestCase { defer { try? FileManager.default.removeItem(at: home) } let file = try XCTUnwrap(JSONLScanning.jsonlFiles(under: home.appendingPathComponent("projects")).first) let incremental = IncrementalJSONLScanner() - let blockingParser = BlockingClaudeParser() + let blockingParser = BlockingParser(finish: { ClaudeLogUsageScanner.parseFile($0) }) let firstTask = Task { await incremental.items( from: [file], @@ -177,22 +177,3 @@ final class JSONLScannerCancellationTests: XCTestCase { } } -private final class BlockingClaudeParser: @unchecked Sendable { - private let lock = NSLock() - private var started = false - private let release = DispatchSemaphore(value: 0) - - var hasStarted: Bool { - lock.withLock { started } - } - - func parse(_ data: Data) -> [ClaudeLogUsageScanner.Entry]? { - lock.withLock { started = true } - release.wait() - return ClaudeLogUsageScanner.parseFile(data) - } - - func unblock() { - release.signal() - } -} diff --git a/Tests/OpenUsageTests/JSONLScannerTestSupport.swift b/Tests/OpenUsageTests/JSONLScannerTestSupport.swift index 0495691ae..3f4c6b15e 100644 --- a/Tests/OpenUsageTests/JSONLScannerTestSupport.swift +++ b/Tests/OpenUsageTests/JSONLScannerTestSupport.swift @@ -58,22 +58,36 @@ final class ParseCounter: @unchecked Sendable { } } -final class BlockingParser: @unchecked Sendable { +/// Blocks its first parse until `unblock()`, then delegates to `finish`. Generic over the scanner's +/// item type so the integer fixtures and the Claude-entry cancellation test share one blocker. +final class BlockingParser: @unchecked Sendable { private let lock = NSLock() private var started = false private let release = DispatchSemaphore(value: 0) + private let finish: @Sendable (Data) -> [Item]? + + init(finish: @escaping @Sendable (Data) -> [Item]?) { + self.finish = finish + } var hasStarted: Bool { lock.withLock { started } } - func parse(_ data: Data) -> [Int]? { + func parse(_ data: Data) -> [Item]? { lock.withLock { started = true } release.wait() - return String(data: data, encoding: .utf8).flatMap(Int.init).map { [$0] } + return finish(data) } func unblock() { release.signal() } } + +extension BlockingParser where Item == Int { + /// The one-integer-per-file shape the scanner fixtures write. + convenience init() { + self.init { String(data: $0, encoding: .utf8).flatMap(Int.init).map { [$0] } } + } +} diff --git a/Tests/OpenUsageTests/LocalUsageAPITests.swift b/Tests/OpenUsageTests/LocalUsageAPITests.swift index 74b86f436..cd19d363b 100644 --- a/Tests/OpenUsageTests/LocalUsageAPITests.swift +++ b/Tests/OpenUsageTests/LocalUsageAPITests.swift @@ -89,13 +89,11 @@ final class LocalUsageAPITests: XCTestCase { XCTAssertEqual(pending.status, 200) XCTAssertEqual(try XCTUnwrap(try json(pending.body) as? [Any]).count, 0) - // A token naming no known card and no family → 404 provider_not_found. + // A token naming no known card and no family → 404 provider_not_found. (The limits route's + // unknown-token 404 is covered in LocalLimitsAPITests.) let unknown = LocalUsageAPI.respond(method: "GET", path: "/v1/usage/nope", state: state) XCTAssertEqual(unknown.status, 404) XCTAssertEqual((try json(unknown.body) as? [String: Any])?["error"] as? String, "provider_not_found") - - let unknownLimits = LocalUsageAPI.respond(method: "GET", path: "/v1/limits/nope", state: state) - XCTAssertEqual(unknownLimits.status, 404) } func testFamilyTokenMatchesEveryCardOfTheFamily() throws { diff --git a/Tests/OpenUsageTests/MenuBarContentTests.swift b/Tests/OpenUsageTests/MenuBarContentTests.swift index 80f78df9d..c5e880292 100644 --- a/Tests/OpenUsageTests/MenuBarContentTests.swift +++ b/Tests/OpenUsageTests/MenuBarContentTests.swift @@ -107,20 +107,8 @@ final class MenuBarContentTests: XCTestCase { XCTAssertEqual(content.groups[0].metrics.map(\.value), ["67%", "$12K", "412", "$42"]) } - func testUnboundedNumbersAreCompacted() { - // Standard compact notation for big numbers; values shown in full drop their decimals. - let content = MenuBarContentBuilder.build( - groups: [group("a", - unbounded("a.big", "Big", 12923), // → $12.9K - unbounded("a.small", "Small", 129.81))], // → $130 (no decimals) - data: { $0.sample } - ) - - let big = content.groups[0].metrics[0].value - XCTAssertTrue(big.hasSuffix("K"), "expected compact thousands, got \(big)") - XCTAssertFalse(big.contains("923"), "expected the raw number to be compacted away, got \(big)") - XCTAssertEqual(content.groups[0].metrics[1].value, "$130") - } + // Compact-notation rules for tray values (abbreviation, decimal rounding) are pinned exactly in + // MetricFormatterTests — the strip only relays MetricFormatter output. // MARK: - Fixtures diff --git a/Tests/OpenUsageTests/MetricFormatterTests.swift b/Tests/OpenUsageTests/MetricFormatterTests.swift index 2096031fe..c2d901a3c 100644 --- a/Tests/OpenUsageTests/MetricFormatterTests.swift +++ b/Tests/OpenUsageTests/MetricFormatterTests.swift @@ -6,8 +6,9 @@ import XCTest /// behavior the old `WidgetData.format`, `MenuBarContent.compactValue`, and `formatTokens` each had. final class MetricFormatterTests: XCTestCase { func testDollarsAbbreviateAboveAThousandPerStyle() { - // Tray: whole dollars under $1k, abbreviated above. + // Tray: whole dollars under $1k (decimals round away), abbreviated above. XCTAssertEqual(MetricFormatter.number(42, kind: .dollars, style: .tray), "$42") + XCTAssertEqual(MetricFormatter.number(129.81, kind: .dollars, style: .tray), "$130") XCTAssertEqual(MetricFormatter.number(2059.07, kind: .dollars, style: .tray), "$2.1K") // Row: full cents under $1k, abbreviated with one decimal above (matching token counts). XCTAssertEqual(MetricFormatter.number(40.76, kind: .dollars, style: .row), "$40.76") diff --git a/Tests/OpenUsageTests/OpenCodeProviderTests.swift b/Tests/OpenUsageTests/OpenCodeProviderTests.swift index 37219e141..1b969bbb9 100644 --- a/Tests/OpenUsageTests/OpenCodeProviderTests.swift +++ b/Tests/OpenUsageTests/OpenCodeProviderTests.swift @@ -5,11 +5,6 @@ import XCTest /// usage API, and local spend tiles + trend, plus auth/empty paths. @MainActor final class OpenCodeProviderTests: XCTestCase { - private func d(_ iso: String) -> Date { OpenUsageISO8601.date(from: iso)! } - private func epochMs(_ iso: String) -> Int { Int(d(iso).timeIntervalSince1970 * 1000) } - private func row(_ iso: String, _ cost: String, _ tokens: Int, _ model: String, _ provider: String) -> String { - "[\(epochMs(iso)),\(cost),\(tokens),\"\(model)\",\"\(provider)\"]" - } private let authJSON = #"{"opencode-go":{"type":"api","key":"sk-test"}}"# private let now = OpenUsageISO8601.date(from: "2026-07-12T12:00:00.000Z")! @@ -55,47 +50,39 @@ final class OpenCodeProviderTests: XCTestCase { func testHasLocalCredentialsViaGoAuthKey() async { let provider = provider( files: FakeFiles(["/oc/auth.json": authJSON]), - scanner: OpenCodeUsageScanner(sqlite: StubSQLite(), databasePaths: { [] }) + scanner: OpenCodeUsageScanner(sqlite: OpenCodeFakeSQLite(), databasePaths: { [] }) ) let has = await provider.hasLocalCredentials() XCTAssertTrue(has) } - func testHasLocalCredentialsViaLocalUsage() async { - let db = "[" + row("2026-07-12T10:00:00.000Z", "1.0", 500, "gpt-5.5", "opencode") + "]" - let provider = provider( - files: FakeFiles(), - scanner: OpenCodeUsageScanner( - sqlite: StubSQLite(data: ["/oc/opencode.db": db]), - databasePaths: { ["/oc/opencode.db"] } - ) - ) - let has = await provider.hasLocalCredentials() - XCTAssertTrue(has) - } + func testHasLocalCredentialsViaLocalUsageButNotForEmptyDatabase() async { + func probe(db: String) async -> Bool { + await provider( + files: FakeFiles(), + scanner: OpenCodeUsageScanner( + sqlite: OpenCodeFakeSQLite(data: ["/oc/opencode.db": db]), + databasePaths: { ["/oc/opencode.db"] } + ) + ).hasLocalCredentials() + } - func testHasLocalCredentialsFalseWhenAbsent() async { - let provider = provider( - files: FakeFiles(), - scanner: OpenCodeUsageScanner( - sqlite: StubSQLite(data: ["/oc/opencode.db": "[]"]), - databasePaths: { ["/oc/opencode.db"] } - ) - ) - let has = await provider.hasLocalCredentials() - XCTAssertFalse(has) + let withUsage = await probe(db: "[" + openCodeRow("2026-07-12T10:00:00.000Z", "1.0", 500, "gpt-5.5", "opencode") + "]") + XCTAssertTrue(withUsage) + let empty = await probe(db: "[]") + XCTAssertFalse(empty) } func testRefreshProducesMetersTilesAndTrend() async { let db = "[" + [ - row("2026-07-12T11:00:00.000Z", "2.0", 1000, "glm-5.2", "opencode-go"), - row("2026-07-12T10:00:00.000Z", "1.0", 500, "gpt-5.5", "opencode") + openCodeRow("2026-07-12T11:00:00.000Z", "2.0", 1000, "glm-5.2", "opencode-go"), + openCodeRow("2026-07-12T10:00:00.000Z", "1.0", 500, "gpt-5.5", "opencode") ].joined(separator: ",") + "]" let http = FakeHTTPClient(response: HTTPResponse(statusCode: 200, headers: [:], body: usageJSON())) let snapshot = await provider( files: FakeFiles(["/oc/auth.json": authJSON]), scanner: OpenCodeUsageScanner( - sqlite: StubSQLite(data: ["/oc/opencode.db": db]), + sqlite: OpenCodeFakeSQLite(data: ["/oc/opencode.db": db]), databasePaths: { ["/oc/opencode.db"] } ), client: OpenCodeUsageClient(http: http) @@ -122,7 +109,7 @@ final class OpenCodeProviderTests: XCTestCase { func testRefreshNotLoggedInWhenNoKeyAndNoDatabase() async { let snapshot = await provider( files: FakeFiles(), - scanner: OpenCodeUsageScanner(sqlite: StubSQLite(), databasePaths: { [] }) + scanner: OpenCodeUsageScanner(sqlite: OpenCodeFakeSQLite(), databasePaths: { [] }) ).refresh() XCTAssertEqual(snapshot.errorCategory, .notLoggedIn) } @@ -130,16 +117,11 @@ final class OpenCodeProviderTests: XCTestCase { func testRefreshShowsAPIMetersWithGoKeyButNoDatabase() async { let snapshot = await provider( files: FakeFiles(["/oc/auth.json": authJSON]), - scanner: OpenCodeUsageScanner(sqlite: StubSQLite(), databasePaths: { [] }) + scanner: OpenCodeUsageScanner(sqlite: OpenCodeFakeSQLite(), databasePaths: { [] }) ).refresh() XCTAssertNil(snapshot.errorCategory) XCTAssertEqual(snapshot.plan, "Go") - guard case let .progress(_, used, limit, format, _, _, _)? = snapshot.line(label: "Session") else { - return XCTFail("expected a Session meter") - } - XCTAssertEqual(used, 12) - XCTAssertEqual(limit, 100) - XCTAssertEqual(format, .percent) + XCTAssertNotNil(snapshot.line(label: "Session")) XCTAssertNil(snapshot.line(label: "Today")) } @@ -147,7 +129,7 @@ final class OpenCodeProviderTests: XCTestCase { let snapshot = await provider( files: FakeFiles(["/oc/auth.json": authJSON]), scanner: OpenCodeUsageScanner( - sqlite: StubSQLite(failing: ["/oc/opencode.db"]), + sqlite: OpenCodeFakeSQLite(failing: ["/oc/opencode.db"]), databasePaths: { ["/oc/opencode.db"] } ) ).refresh() @@ -161,7 +143,7 @@ final class OpenCodeProviderTests: XCTestCase { let snapshot = await provider( files: FakeFiles(), scanner: OpenCodeUsageScanner( - sqlite: StubSQLite(failing: ["/oc/opencode.db"]), + sqlite: OpenCodeFakeSQLite(failing: ["/oc/opencode.db"]), databasePaths: { ["/oc/opencode.db"] } ) ).refresh() @@ -172,7 +154,7 @@ final class OpenCodeProviderTests: XCTestCase { func testRefreshSurfacesUnreadableAuthFileInsteadOfNotLoggedIn() async { let snapshot = await provider( files: UnreadableFiles(present: ["/oc/auth.json"]), - scanner: OpenCodeUsageScanner(sqlite: StubSQLite(), databasePaths: { [] }) + scanner: OpenCodeUsageScanner(sqlite: OpenCodeFakeSQLite(), databasePaths: { [] }) ).refresh() XCTAssertEqual(snapshot.errorCategory, .credentialAccess) } @@ -180,18 +162,18 @@ final class OpenCodeProviderTests: XCTestCase { func testHasLocalCredentialsTrueWhenAuthFileUnreadable() async { let provider = provider( files: UnreadableFiles(present: ["/oc/auth.json"]), - scanner: OpenCodeUsageScanner(sqlite: StubSQLite(), databasePaths: { [] }) + scanner: OpenCodeUsageScanner(sqlite: OpenCodeFakeSQLite(), databasePaths: { [] }) ) let has = await provider.hasLocalCredentials() XCTAssertTrue(has) } func testSpendTilesAreNotMarkedEstimated() async { - let db = "[" + row("2026-07-12T10:00:00.000Z", "1.0", 500, "gpt-5.5", "opencode") + "]" + let db = "[" + openCodeRow("2026-07-12T10:00:00.000Z", "1.0", 500, "gpt-5.5", "opencode") + "]" let snapshot = await provider( files: FakeFiles(), scanner: OpenCodeUsageScanner( - sqlite: StubSQLite(data: ["/oc/opencode.db": db]), + sqlite: OpenCodeFakeSQLite(data: ["/oc/opencode.db": db]), databasePaths: { ["/oc/opencode.db"] } ) ).refresh() @@ -206,7 +188,7 @@ final class OpenCodeProviderTests: XCTestCase { func testUnauthorizedKeyFailsLoudly() async { let snapshot = await provider( files: FakeFiles(["/oc/auth.json": authJSON]), - scanner: OpenCodeUsageScanner(sqlite: StubSQLite(), databasePaths: { [] }), + scanner: OpenCodeUsageScanner(sqlite: OpenCodeFakeSQLite(), databasePaths: { [] }), client: OpenCodeUsageClient(http: FakeHTTPClient(response: HTTPResponse( statusCode: 401, headers: [:], @@ -219,7 +201,7 @@ final class OpenCodeProviderTests: XCTestCase { func testEntitlementErrorWithoutLocalUsageIsNoGoSubscription() async { let snapshot = await provider( files: FakeFiles(["/oc/auth.json": authJSON]), - scanner: OpenCodeUsageScanner(sqlite: StubSQLite(), databasePaths: { [] }), + scanner: OpenCodeUsageScanner(sqlite: OpenCodeFakeSQLite(), databasePaths: { [] }), client: OpenCodeUsageClient(http: FakeHTTPClient(response: HTTPResponse( statusCode: 403, headers: [:], @@ -230,11 +212,11 @@ final class OpenCodeProviderTests: XCTestCase { } func testEntitlementErrorWithZenUsageShowsTilesWithoutGoMeters() async { - let db = "[" + row("2026-07-12T10:00:00.000Z", "1.0", 500, "gpt-5.5", "opencode") + "]" + let db = "[" + openCodeRow("2026-07-12T10:00:00.000Z", "1.0", 500, "gpt-5.5", "opencode") + "]" let snapshot = await provider( files: FakeFiles(["/oc/auth.json": authJSON]), scanner: OpenCodeUsageScanner( - sqlite: StubSQLite(data: ["/oc/opencode.db": db]), + sqlite: OpenCodeFakeSQLite(data: ["/oc/opencode.db": db]), databasePaths: { ["/oc/opencode.db"] } ), client: OpenCodeUsageClient(http: FakeHTTPClient(response: HTTPResponse( @@ -252,7 +234,7 @@ final class OpenCodeProviderTests: XCTestCase { func testGeneric403FailsLoudly() async { let snapshot = await provider( files: FakeFiles(["/oc/auth.json": authJSON]), - scanner: OpenCodeUsageScanner(sqlite: StubSQLite(), databasePaths: { [] }), + scanner: OpenCodeUsageScanner(sqlite: OpenCodeFakeSQLite(), databasePaths: { [] }), client: OpenCodeUsageClient(http: FakeHTTPClient(response: HTTPResponse( statusCode: 403, headers: [:], body: Data("denied".utf8) ))) @@ -263,7 +245,7 @@ final class OpenCodeProviderTests: XCTestCase { func testConnectionFailureFailsLoudly() async { let snapshot = await provider( files: FakeFiles(["/oc/auth.json": authJSON]), - scanner: OpenCodeUsageScanner(sqlite: StubSQLite(), databasePaths: { [] }), + scanner: OpenCodeUsageScanner(sqlite: OpenCodeFakeSQLite(), databasePaths: { [] }), client: OpenCodeUsageClient(http: ThrowingHTTPClient()) ).refresh() XCTAssertEqual(snapshot.errorCategory, .network) @@ -276,23 +258,4 @@ private final class ThrowingHTTPClient: HTTPClient, @unchecked Sendable { } } -private final class StubSQLite: SQLiteAccessing, @unchecked Sendable { - var data: [String: String] - var failing: Set - init(data: [String: String] = [:], failing: Set = []) { - self.data = data - self.failing = failing - } - - func queryValue(path: String, sql: String) throws -> String? { - if failing.contains(path) { throw SQLiteError.queryFailed("boom") } - if sql.contains("json_group_array") { return data[path] } - if sql.contains("SELECT 1") { - let payload = data[path] - return (payload != nil && payload != "[]" && !(payload ?? "").isEmpty) ? "1" : nil - } - return nil - } - - func execute(path: String, sql: String) throws {} -} +// openCodeRow and OpenCodeFakeSQLite live in OpenCodeUsageScannerTests.swift (shared fixtures). diff --git a/Tests/OpenUsageTests/OpenCodeUsageScannerTests.swift b/Tests/OpenUsageTests/OpenCodeUsageScannerTests.swift index cd8d866f2..83a625d1f 100644 --- a/Tests/OpenUsageTests/OpenCodeUsageScannerTests.swift +++ b/Tests/OpenUsageTests/OpenCodeUsageScannerTests.swift @@ -6,26 +6,23 @@ import XCTest final class OpenCodeUsageScannerTests: XCTestCase { private func d(_ iso: String) -> Date { OpenUsageISO8601.date(from: iso)! } private func epochMs(_ iso: String) -> Int { Int(d(iso).timeIntervalSince1970 * 1000) } - private func row(_ iso: String, _ cost: String, _ tokens: Int, _ model: String, _ provider: String) -> String { - "[\(epochMs(iso)),\(cost),\(tokens),\"\(model)\",\"\(provider)\"]" - } private let now = OpenUsageISO8601.date(from: "2026-07-12T12:00:00.000Z")! private var db1: String { "[" + [ - row("2026-07-12T11:00:00.000Z", "2.0", 1000, "glm-5.2", "opencode-go"), - row("2026-07-12T10:00:00.000Z", "1.0", 500, "gpt-5.5", "opencode"), - row("2026-07-11T10:00:00.000Z", "3.0", 2000, "kimi-k2.6", "opencode-go"), - row("2026-07-12T11:00:00.000Z", "null", 100, "x", "opencode-go"), + openCodeRow("2026-07-12T11:00:00.000Z", "2.0", 1000, "glm-5.2", "opencode-go"), + openCodeRow("2026-07-12T10:00:00.000Z", "1.0", 500, "gpt-5.5", "opencode"), + openCodeRow("2026-07-11T10:00:00.000Z", "3.0", 2000, "kimi-k2.6", "opencode-go"), + openCodeRow("2026-07-12T11:00:00.000Z", "null", 100, "x", "opencode-go"), "\"garbage\"" ].joined(separator: ",") + "]" } private var db2: String { - "[" + row("2026-07-12T09:00:00.000Z", "4.0", 800, "deepseek-v4-pro", "opencode-go") + "]" + "[" + openCodeRow("2026-07-12T09:00:00.000Z", "4.0", 800, "deepseek-v4-pro", "opencode-go") + "]" } private func standardScanner() -> OpenCodeUsageScanner { - let sqlite = FakeSQLite(data: [ + let sqlite = OpenCodeFakeSQLite(data: [ "/oc/opencode.db": db1, "/oc/opencode-next.db": db2 ]) @@ -41,25 +38,15 @@ final class OpenCodeUsageScannerTests: XCTestCase { XCTAssertEqual(totalTokens, 4300) // 1000 + 500 + 2000 + 800 } - func testZenOnlyUsageStillScans() async throws { - let db = "[" + row("2026-07-12T10:00:00.000Z", "1.0", 500, "gpt-5.5", "opencode") + "]" - let scanner = OpenCodeUsageScanner( - sqlite: FakeSQLite(data: ["/oc/opencode.db": db]), - databasePaths: { ["/oc/opencode.db"] } - ) - guard let scan = try await scanner.scan(now: now) else { return XCTFail("expected a scan") } - XCTAssertEqual(scan.logScan.series.daily.compactMap(\.costUSD).reduce(0, +), 1.0, accuracy: 0.0001) - } - func testMissingDatabaseReturnsNil() async throws { - let scanner = OpenCodeUsageScanner(sqlite: FakeSQLite(), databasePaths: { [] }) + let scanner = OpenCodeUsageScanner(sqlite: OpenCodeFakeSQLite(), databasePaths: { [] }) let scan = try await scanner.scan(now: now) XCTAssertNil(scan) } func testEmptyDatabaseYieldsEmptyScanNotNil() async throws { let scanner = OpenCodeUsageScanner( - sqlite: FakeSQLite(data: ["/oc/opencode.db": "[]"]), + sqlite: OpenCodeFakeSQLite(data: ["/oc/opencode.db": "[]"]), databasePaths: { ["/oc/opencode.db"] } ) guard let scan = try await scanner.scan(now: now) else { return XCTFail("expected a scan") } @@ -68,7 +55,7 @@ final class OpenCodeUsageScannerTests: XCTestCase { func testFailingDatabaseIsSkippedNotFatal() async throws { let scanner = OpenCodeUsageScanner( - sqlite: FakeSQLite(data: ["/oc/opencode-next.db": db2], failing: ["/oc/opencode.db"]), + sqlite: OpenCodeFakeSQLite(data: ["/oc/opencode-next.db": db2], failing: ["/oc/opencode.db"]), databasePaths: { ["/oc/opencode.db", "/oc/opencode-next.db"] } ) guard let scan = try await scanner.scan(now: now) else { return XCTFail("expected a scan") } @@ -77,7 +64,7 @@ final class OpenCodeUsageScannerTests: XCTestCase { func testAllDatabasesFailingThrowsInsteadOfEmptyScan() async { let scanner = OpenCodeUsageScanner( - sqlite: FakeSQLite(failing: ["/oc/opencode.db", "/oc/opencode-next.db"]), + sqlite: OpenCodeFakeSQLite(failing: ["/oc/opencode.db", "/oc/opencode-next.db"]), databasePaths: { ["/oc/opencode.db", "/oc/opencode-next.db"] } ) do { @@ -90,7 +77,7 @@ final class OpenCodeUsageScannerTests: XCTestCase { func testUnreadableDataDirectoryThrowsInsteadOfNil() async { let scanner = OpenCodeUsageScanner( - sqlite: FakeSQLite(), + sqlite: OpenCodeFakeSQLite(), databasePaths: { throw CocoaError(.fileReadNoPermission) } ) do { @@ -102,15 +89,15 @@ final class OpenCodeUsageScannerTests: XCTestCase { } func testHasHostedUsageProbe() { - let db = "[" + row("2026-07-12T10:00:00.000Z", "1.0", 500, "gpt-5.5", "opencode") + "]" + let db = "[" + openCodeRow("2026-07-12T10:00:00.000Z", "1.0", 500, "gpt-5.5", "opencode") + "]" let withUsage = OpenCodeUsageScanner( - sqlite: FakeSQLite(data: ["/oc/opencode.db": db]), + sqlite: OpenCodeFakeSQLite(data: ["/oc/opencode.db": db]), databasePaths: { ["/oc/opencode.db"] } ) XCTAssertTrue(withUsage.hasHostedUsage()) let empty = OpenCodeUsageScanner( - sqlite: FakeSQLite(data: ["/oc/opencode.db": "[]"]), + sqlite: OpenCodeFakeSQLite(data: ["/oc/opencode.db": "[]"]), databasePaths: { ["/oc/opencode.db"] } ) XCTAssertFalse(empty.hasHostedUsage()) @@ -118,7 +105,7 @@ final class OpenCodeUsageScannerTests: XCTestCase { func testSQLCutoffMatchesCalendarTileWindow() async throws { let now = d("2026-07-12T18:00:00.000Z") - let sqlite = FakeSQLite(data: ["/oc/opencode.db": "[]"]) + let sqlite = OpenCodeFakeSQLite(data: ["/oc/opencode.db": "[]"]) let scanner = OpenCodeUsageScanner(sqlite: sqlite, databasePaths: { ["/oc/opencode.db"] }) _ = try await scanner.scan(now: now) @@ -130,7 +117,7 @@ final class OpenCodeUsageScannerTests: XCTestCase { func testAbsurdTokenCountIsClampedNotCrashing() async throws { let db = "[[\(epochMs("2026-07-12T10:00:00.000Z")),1.0,1e19,\"glm-5.2\",\"opencode-go\"]]" let scanner = OpenCodeUsageScanner( - sqlite: FakeSQLite(data: ["/oc/opencode.db": db]), + sqlite: OpenCodeFakeSQLite(data: ["/oc/opencode.db": db]), databasePaths: { ["/oc/opencode.db"] } ) guard let scan = try await scanner.scan(now: now) else { return XCTFail("expected a scan") } @@ -139,8 +126,16 @@ final class OpenCodeUsageScannerTests: XCTestCase { } } +/// One `[time_created, cost, tokens, model, provider]` row in the `json_group_array` shape both +/// OpenCode test suites feed the stub. +func openCodeRow(_ iso: String, _ cost: String, _ tokens: Int, _ model: String, _ provider: String) -> String { + let epochMs = Int(OpenUsageISO8601.date(from: iso)!.timeIntervalSince1970 * 1000) + return "[\(epochMs),\(cost),\(tokens),\"\(model)\",\"\(provider)\"]" +} + /// Stub that returns crafted payloads per database path and classifies the query by SQL shape. -private final class FakeSQLite: SQLiteAccessing, @unchecked Sendable { +/// Shared by the OpenCode scanner and provider tests. +final class OpenCodeFakeSQLite: SQLiteAccessing, @unchecked Sendable { var data: [String: String] var failing: Set var lastDataSQL: String? diff --git a/Tests/OpenUsageTests/PanelHeightCoordinatorTests.swift b/Tests/OpenUsageTests/PanelHeightCoordinatorTests.swift index b255ca3ad..fa1373b77 100644 --- a/Tests/OpenUsageTests/PanelHeightCoordinatorTests.swift +++ b/Tests/OpenUsageTests/PanelHeightCoordinatorTests.swift @@ -65,14 +65,6 @@ final class PanelHeightCoordinatorTests: XCTestCase { XCTAssertEqual(c.target(for: .dashboard), 500) // in-range ideals pass through } - func testLaterMeasurementRecomposesIdeal() { - let c = PanelHeightCoordinator(topBarHeight: topBar) - c.setScrollContent(300, for: .dashboard) - XCTAssertEqual(c.measuredIdeal[.dashboard], 300) - c.setScrollContent(500, for: .dashboard) // content grew (rows loaded) - XCTAssertEqual(c.measuredIdeal[.dashboard], 500) - } - func testRepeatedMeasurementsDoNotInvalidateHeightObservers() { let coordinator = PanelHeightCoordinator(topBarHeight: topBar) coordinator.setScrollContent(300, for: .dashboard) diff --git a/Tests/OpenUsageTests/ProviderAccountsStoreTests.swift b/Tests/OpenUsageTests/ProviderAccountsStoreTests.swift index 264c25486..10be368c0 100644 --- a/Tests/OpenUsageTests/ProviderAccountsStoreTests.swift +++ b/Tests/OpenUsageTests/ProviderAccountsStoreTests.swift @@ -122,6 +122,5 @@ final class ProviderAccountsStoreTests: XCTestCase { func testFamilyHelperSplitsCardIDs() { XCTAssertEqual(ProviderAccountID.family(of: "claude"), "claude") XCTAssertEqual(ProviderAccountID.family(of: "claude@ab12cd34"), "claude") - XCTAssertEqual(ProviderAccountID.family(of: "cursor"), "cursor") } } diff --git a/Tests/OpenUsageTests/ReorderGeometryTests.swift b/Tests/OpenUsageTests/ReorderGeometryTests.swift index 2fe696f3d..0f9fa8f1f 100644 --- a/Tests/OpenUsageTests/ReorderGeometryTests.swift +++ b/Tests/OpenUsageTests/ReorderGeometryTests.swift @@ -35,10 +35,9 @@ final class ReorderGeometryTests: XCTestCase { excluding: "dragged", orderedIDs: ["dragged", "target"] ), "target") - XCTAssertTrue(frameStore === dragConsumer) } - func testRegularRowsKeepCrossingThreshold() { + func testCrossingThresholdWhenDraggingDown() { let frames = [ "dragged": CGRect(x: 0, y: 0, width: 100, height: 40), "target": CGRect(x: 0, y: 40, width: 100, height: 40) @@ -58,27 +57,7 @@ final class ReorderGeometryTests: XCTestCase { ), "target") } - func testDividerCanUseRegularRowThresholdWhenDraggingDown() { - let frames = [ - "dragged": CGRect(x: 0, y: 0, width: 100, height: 40), - "divider": CGRect(x: 0, y: 40, width: 100, height: 40) - ] - - XCTAssertNil(reorderTarget( - at: CGPoint(x: 20, y: 44), - in: frames, - excluding: "dragged", - orderedIDs: ["dragged", "divider"] - )) - XCTAssertEqual(reorderTarget( - at: CGPoint(x: 20, y: 48), - in: frames, - excluding: "dragged", - orderedIDs: ["dragged", "divider"] - ), "divider") - } - - func testDividerCanUseRegularRowThresholdWhenDraggingUp() { + func testCrossingThresholdWhenDraggingUp() { let frames = [ "above": CGRect(x: 0, y: 0, width: 100, height: 40), "divider": CGRect(x: 0, y: 40, width: 100, height: 40), diff --git a/Tests/OpenUsageTests/ShareCardRendererTests.swift b/Tests/OpenUsageTests/ShareCardRendererTests.swift index 32be94ce9..914ce9578 100644 --- a/Tests/OpenUsageTests/ShareCardRendererTests.swift +++ b/Tests/OpenUsageTests/ShareCardRendererTests.swift @@ -48,30 +48,19 @@ final class ShareCardRendererTests: XCTestCase { XCTAssertGreaterThan(rep.pixelsHigh, 0) } - func testCondensedTextRowIndicesFollowsNeighborRule() { - let rows = MockData.descriptors(for: MockData.claude.id).map { $0.sample } - XCTAssertGreaterThan(rows.count, 1, "sample fixture should have multiple rows") - let condensed = ShareCardView.condensedTextRowIndices(rows) - XCTAssertFalse(condensed.contains(0), "the first row is never condensed") - for i in 1.. WidgetData { + WidgetData(title: "Row", icon: .providerMark("claude"), kind: .percent, used: 0, limit: bounded ? 100 : nil) } - } + // Meter, then a run of three text-only rows. + let rows = [row(bounded: true), row(bounded: false), row(bounded: false), row(bounded: false)] - func testCondensedTextRowIndicesRespectExpandBoundary() { - let rows = MockData.descriptors(for: MockData.claude.id).map { $0.sample } - XCTAssertGreaterThan(rows.count, 1, "sample fixture should have multiple rows") - let boundary = rows.count / 2 - let condensed = ShareCardView.condensedTextRowIndices(rows, boundary: boundary) - XCTAssertFalse(condensed.contains(boundary), "the first expanded row (at the boundary) is never condensed") - for i in 1.. ProviderSnapshot { - if refreshed { return second } - refreshed = true - return first + let snapshot = snapshots[min(refreshCount, snapshots.count - 1)] + refreshCount += 1 + return snapshot + } +} + +/// An unsigned Cursor-style JWT whose payload carries the `sub`/`exp` claims the auth store parses. +/// Pass `sub: nil` for a token without a subject claim. +func makeCursorJWT(sub: String? = "google-oauth2|user", exp: Double = 9_999_999_999) -> String { + let payload = sub.map { #"{"sub":"\#($0)","exp":\#(exp)}"# } ?? #"{"exp":\#(exp)}"# + let encoded = Data(payload.utf8).base64EncodedString() + .replacingOccurrences(of: "=", with: "") + .replacingOccurrences(of: "+", with: "-") + .replacingOccurrences(of: "/", with: "_") + return "a.\(encoded).c" +} + +/// A key/value-table SQLite stand-in (Cursor's `state.vscdb` shape): `queryValue` returns the value +/// whose key appears in the SQL, and `execute` records `(key, value) VALUES (…)` upserts so tests +/// can assert what was persisted. +final class KeyValueSQLite: SQLiteAccessing, @unchecked Sendable { + var values: [String: String] + var writtenValues: [String: String] = [:] + + init(values: [String: String] = [:]) { + self.values = values + } + + func queryValue(path: String, sql: String) throws -> String? { + for (key, value) in values where sql.contains(key) { + return value + } + return nil + } + + func execute(path: String, sql: String) throws { + guard let key = sqlValue(after: "(key, value) VALUES ('", in: sql), + let value = sqlValue(after: "', '", in: sql) + else { + return + } + writtenValues[key] = value + } + + private func sqlValue(after marker: String, in sql: String) -> String? { + guard let start = sql.range(of: marker)?.upperBound, + let end = sql[start...].range(of: "'")?.lowerBound + else { + return nil + } + return String(sql[start..