Skip to content

Commit ad37578

Browse files
committed
Honor Cursor source policy in dashboard
1 parent 0e00869 commit ad37578

4 files changed

Lines changed: 176 additions & 27 deletions

File tree

‎Sources/CodexBarCLI/CLICostCommand.swift‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -374,7 +374,7 @@ extension CodexBarCLI {
374374

375375
/// Resolve the configured Cursor cookie settings (source + manual header) the same way the CLI
376376
/// usage path does, so Cursor cost honors Off/Manual instead of always auto-resolving a session.
377-
/// Shared by `cost` and the serve `/cost` route.
377+
/// Shared by `cost`, the serve `/cost` route, and dashboard snapshot collection.
378378
static func cursorCookieSettings(
379379
config: CodexBarConfig,
380380
providers: [UsageProvider]) throws -> ProviderSettingsSnapshot.CursorProviderSettings?

‎Sources/CodexBarCLI/CLIDashboardCommand.swift‎

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ struct DashboardSnapshotResult {
3131
/// existing authenticated HTTP cache.
3232
struct DashboardSnapshotProducer: Sendable {
3333
let collectUsage: @Sendable ([UsageProvider]) async throws -> UsageCommandOutput
34-
let collectCost: @Sendable ([UsageProvider]) async -> [CostPayload]
34+
let collectCost: @Sendable ([UsageProvider], CodexBarConfig) async -> [CostPayload]
3535
let now: @Sendable () -> Date
3636

3737
func collect(
@@ -43,7 +43,9 @@ struct DashboardSnapshotProducer: Sendable {
4343
rawOverride: nil,
4444
enabled: config.enabledProviders())
4545
let usageOutput = try await self.collectUsage(selection.asList)
46-
let costPayloads = await self.collectCost(CodexBarCLI.costProviders(from: selection))
46+
let costPayloads = await self.collectCost(
47+
CodexBarCLI.costProviders(from: selection),
48+
config)
4749

4850
let payload = DashboardSnapshotBuilder.makeSnapshot(
4951
usagePayloads: usageOutput.payload,
@@ -65,16 +67,18 @@ struct DashboardSnapshotProducer: Sendable {
6567
selection: .custom(providers),
6668
context: context.usage)
6769
},
68-
collectCost: { providers in
70+
collectCost: { providers, config in
6971
let costFetcher = CostUsageFetcher()
70-
return await CodexBarCLI.serveCollectCostPayloads(
72+
return await CodexBarCLI.collectConfiguredCostPayloads(
7173
providers: providers,
74+
config: config,
7275
context: context.costCollection)
73-
{ provider in
76+
{ provider, cursorCookieHeaderOverride in
7477
do {
7578
let snapshot = try await costFetcher.loadTokenSnapshot(
7679
provider: provider,
7780
forceRefresh: false,
81+
cursorCookieHeaderOverride: cursorCookieHeaderOverride,
7882
refreshPricingInBackground: context.costRefreshesPricingInBackground)
7983
return CodexBarCLI.makeCostPayload(provider: provider, snapshot: snapshot, error: nil)
8084
} catch {

‎Sources/CodexBarCLI/CLIServeCommand.swift‎

Lines changed: 36 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1287,21 +1287,48 @@ extension CodexBarCLI {
12871287
message: "cost is only supported for \(Self.costSupportedProviderNames())")
12881288
}
12891289

1290-
// Cursor cost honors the same cookie policy here as the `cost` command: return a provider
1291-
// error when the source is Off and forward the Manual header for an enabled fetch.
1290+
let fetcher = CostUsageFetcher()
1291+
let payload = await Self.collectConfiguredCostPayloads(
1292+
providers: providers,
1293+
config: context.config,
1294+
context: context.collection)
1295+
{ provider, cursorCookieHeaderOverride in
1296+
do {
1297+
let snapshot = try await fetcher.loadTokenSnapshot(
1298+
provider: provider,
1299+
forceRefresh: false,
1300+
cursorCookieHeaderOverride: cursorCookieHeaderOverride,
1301+
refreshPricingInBackground: Self.serveCostRefreshesPricingInBackground)
1302+
return Self.makeCostPayload(provider: provider, snapshot: snapshot, error: nil)
1303+
} catch {
1304+
return Self.makeCostPayload(provider: provider, snapshot: nil, error: error)
1305+
}
1306+
}
1307+
1308+
return Self.serveJSON(payload)
1309+
}
1310+
1311+
static func collectConfiguredCostPayloads(
1312+
providers: [UsageProvider],
1313+
config: CodexBarConfig,
1314+
context: ServeCostCollectionContext,
1315+
fetch: @Sendable @escaping (UsageProvider, String?) async -> CostPayload) async -> [CostPayload]
1316+
{
1317+
// Keep every dashboard transport aligned with the configured Cursor credential source.
1318+
// Policy failures remain row-local so other providers still render.
12921319
let cursorCookieSettings: ProviderSettingsSnapshot.CursorProviderSettings?
12931320
let cursorCookieSettingsError: Error?
12941321
do {
1295-
cursorCookieSettings = try Self.cursorCookieSettings(config: context.config, providers: providers)
1322+
cursorCookieSettings = try Self.cursorCookieSettings(config: config, providers: providers)
12961323
cursorCookieSettingsError = nil
12971324
} catch {
12981325
cursorCookieSettings = nil
12991326
cursorCookieSettingsError = error
13001327
}
1301-
let fetcher = CostUsageFetcher()
1302-
let payload = await Self.serveCollectCostPayloads(
1328+
1329+
return await Self.serveCollectCostPayloads(
13031330
providers: providers,
1304-
context: context.collection)
1331+
context: context)
13051332
{ provider in
13061333
if let error = Self.cursorCostAvailabilityError(
13071334
provider,
@@ -1310,21 +1337,10 @@ extension CodexBarCLI {
13101337
{
13111338
return Self.makeCostPayload(provider: provider, snapshot: nil, error: error)
13121339
}
1313-
do {
1314-
let snapshot = try await fetcher.loadTokenSnapshot(
1315-
provider: provider,
1316-
forceRefresh: false,
1317-
cursorCookieHeaderOverride: Self.cursorCostHeaderOverride(
1318-
provider,
1319-
settings: cursorCookieSettings),
1320-
refreshPricingInBackground: Self.serveCostRefreshesPricingInBackground)
1321-
return Self.makeCostPayload(provider: provider, snapshot: snapshot, error: nil)
1322-
} catch {
1323-
return Self.makeCostPayload(provider: provider, snapshot: nil, error: error)
1324-
}
1340+
return await fetch(
1341+
provider,
1342+
Self.cursorCostHeaderOverride(provider, settings: cursorCookieSettings))
13251343
}
1326-
1327-
return Self.serveJSON(payload)
13281344
}
13291345

13301346
static func serveCollectCostPayloads(

‎Tests/CodexBarTests/DashboardSnapshotBuilderTests.swift‎

Lines changed: 130 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,85 @@ import Testing
44
@testable import CodexBarCLI
55

66
struct DashboardSnapshotBuilderTests {
7+
@Test
8+
func `dashboard cost collection does not fetch Cursor when cookie source is off`() async {
9+
let recorder = DashboardCostFetchRecorder()
10+
let config = CodexBarConfig(providers: [
11+
ProviderConfig(id: .cursor, enabled: true, cookieSource: .off),
12+
])
13+
14+
let payload = await CodexBarCLI.collectConfiguredCostPayloads(
15+
providers: [.cursor],
16+
config: config,
17+
context: self.costCollectionContext())
18+
{ provider, header in
19+
await recorder.record(provider: provider, cursorCookieHeaderOverride: header)
20+
return CodexBarCLI.makeCostPayload(provider: provider, snapshot: nil, error: nil)
21+
}
22+
23+
#expect(await recorder.calls().isEmpty)
24+
#expect(payload.count == 1)
25+
#expect(payload[0].provider == "cursor")
26+
#expect(payload[0].error?.message.contains("cookie source is set to Off") == true)
27+
}
28+
29+
@Test
30+
func `dashboard cost collection forwards configured Cursor manual cookie`() async {
31+
let recorder = DashboardCostFetchRecorder()
32+
let config = CodexBarConfig(providers: [
33+
ProviderConfig(
34+
id: .cursor,
35+
enabled: true,
36+
cookieHeader: " session=manual ",
37+
cookieSource: .manual),
38+
])
39+
40+
let payload = await CodexBarCLI.collectConfiguredCostPayloads(
41+
providers: [.cursor],
42+
config: config,
43+
context: self.costCollectionContext())
44+
{ provider, header in
45+
await recorder.record(provider: provider, cursorCookieHeaderOverride: header)
46+
return CodexBarCLI.makeCostPayload(provider: provider, snapshot: nil, error: nil)
47+
}
48+
49+
let calls = await recorder.calls()
50+
#expect(calls.count == 1)
51+
#expect(calls[0].provider == .cursor)
52+
#expect(calls[0].cursorCookieHeaderOverride == "session=manual")
53+
#expect(payload.count == 1)
54+
#expect(payload[0].error == nil)
55+
}
56+
57+
@Test
58+
func `dashboard producer forwards its config to cost collection`() async throws {
59+
let recorder = DashboardCostConfigRecorder()
60+
let config = CodexBarConfig(providers: [
61+
ProviderConfig(
62+
id: .cursor,
63+
enabled: true,
64+
cookieHeader: "session=manual",
65+
cookieSource: .manual),
66+
])
67+
let producer = DashboardSnapshotProducer(
68+
collectUsage: { _ in UsageCommandOutput() },
69+
collectCost: { providers, config in
70+
await recorder.record(providers: providers, config: config)
71+
return []
72+
},
73+
now: { Date(timeIntervalSince1970: 1_800_000_000) })
74+
75+
_ = try await producer.collect(
76+
config: config,
77+
refreshInterval: 0,
78+
codexBarVersion: "9.8.7")
79+
80+
let call = try #require(await recorder.call())
81+
#expect(call.providers == [.cursor])
82+
#expect(call.cookieSource == .manual)
83+
#expect(call.cookieHeader == "session=manual")
84+
}
85+
786
@Test
887
func `producer keeps stable order redaction and partial errors`() async throws {
988
let generatedAt = Date(timeIntervalSince1970: 1_800_000_000)
@@ -27,7 +106,7 @@ struct DashboardSnapshotBuilderTests {
27106
output.exitCode = .failure
28107
return output
29108
},
30-
collectCost: { _ in [] },
109+
collectCost: { _, _ in [] },
31110
now: { generatedAt })
32111
let config = CodexBarConfig(providers: [
33112
ProviderConfig(id: .claude, enabled: true),
@@ -53,6 +132,15 @@ struct DashboardSnapshotBuilderTests {
53132
#expect(object["generatedAt"] as? String == "2027-01-15T08:00:00Z")
54133
}
55134

135+
private func costCollectionContext() -> ServeCostCollectionContext {
136+
ServeCostCollectionContext(
137+
configFingerprint: "dashboard-cost-policy",
138+
providerTimeout: nil,
139+
requestDeadline: nil,
140+
now: { ContinuousClock().now },
141+
providerOperations: CLIServeOperationCoordinator())
142+
}
143+
56144
@Test
57145
func `builds stable display-oriented dashboard snapshot`() throws {
58146
let generatedAt = Date(timeIntervalSince1970: 1_800_000_000)
@@ -496,3 +584,44 @@ struct DashboardSnapshotBuilderTests {
496584
return try #require(JSONSerialization.jsonObject(with: data) as? [String: Any])
497585
}
498586
}
587+
588+
private actor DashboardCostFetchRecorder {
589+
struct Call: Sendable {
590+
let provider: UsageProvider
591+
let cursorCookieHeaderOverride: String?
592+
}
593+
594+
private var recordedCalls: [Call] = []
595+
596+
func record(provider: UsageProvider, cursorCookieHeaderOverride: String?) {
597+
self.recordedCalls.append(Call(
598+
provider: provider,
599+
cursorCookieHeaderOverride: cursorCookieHeaderOverride))
600+
}
601+
602+
func calls() -> [Call] {
603+
self.recordedCalls
604+
}
605+
}
606+
607+
private actor DashboardCostConfigRecorder {
608+
struct Call: Sendable {
609+
let providers: [UsageProvider]
610+
let cookieSource: ProviderCookieSource?
611+
let cookieHeader: String?
612+
}
613+
614+
private var recordedCall: Call?
615+
616+
func record(providers: [UsageProvider], config: CodexBarConfig) {
617+
let cursor = config.providerConfig(for: .cursor)
618+
self.recordedCall = Call(
619+
providers: providers,
620+
cookieSource: cursor?.cookieSource,
621+
cookieHeader: cursor?.cookieHeader)
622+
}
623+
624+
func call() -> Call? {
625+
self.recordedCall
626+
}
627+
}

0 commit comments

Comments
 (0)