mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-07-28 10:25:13 +00:00
Some checks are pending
CI / semgrep (push) Waiting to run
Mirror the combined multi-device dashboard in the macOS menubar via a Local/Combined scope toggle (Settings -> Display). Builds on the menubar-json `--scope combined` contract. - MenubarScope enum (.local default) persisted in UserDefaults; PayloadCacheKey gains a scope dimension so local and combined payloads cache separately. - DataClient.fetch(scope:): a pure statusSubcommand builder appends `--scope combined` only for combined, forces `--provider all` (the CLI rejects combined with a provider/project/exclude filter), and coerces multi-day selections to local (the CLI rejects combined + --days). - MenubarPayload decodes the optional `combined` block (per-device + totals); nil when absent (back-compat). - The badge always stays on the local payload; the combined network pull lives on a separate cache key and falls back to local with a "Combined unavailable" indicator, so a slow/offline peer never blocks the badge. - Hero shows combined totals + a per-device breakdown in Combined scope; the token metric stays input+output (cache excluded) to match local; the per-device daily-budget warning is suppressed for a multi-device total; selecting Combined resets the provider tab to All. Tests run via `swift test` (CommandLineTools Testing.framework on the rpath, no Xcode): scope persistence, argv (local/combined/provider-forcing/multi-day), cache-key partition, badge-survives-combined-failure, combined decode, token consistency, provider reset, budget suppression.
87 lines
3.5 KiB
Swift
87 lines
3.5 KiB
Swift
import Foundation
|
|
import Testing
|
|
@testable import CodeBurnMenubar
|
|
|
|
@Suite("MenubarStatusCache")
|
|
struct MenubarStatusCacheTests {
|
|
private func tempDir() -> String {
|
|
let dir = NSTemporaryDirectory() + "menubar-status-test-" + UUID().uuidString
|
|
try? FileManager.default.createDirectory(atPath: dir, withIntermediateDirectories: true)
|
|
return dir
|
|
}
|
|
|
|
private func validPayloadJSON(cost: Double) -> Data {
|
|
let p = MenubarPayload(
|
|
generated: "2026-05-29T00:00:00Z",
|
|
current: CurrentBlock(
|
|
label: "Today", cost: cost, calls: 1, sessions: 1, oneShotRate: nil,
|
|
inputTokens: 1, outputTokens: 1, cacheHitPercent: 0,
|
|
codexCredits: nil,
|
|
topActivities: [], topModels: [], localModelSavings: LocalModelSavings(totalUSD: 0, calls: 0, byModel: [], byProvider: []), providers: ["claude": cost],
|
|
topProjects: [], modelEfficiency: [], topSessions: [],
|
|
retryTax: RetryTax(totalUSD: 0, retries: 0, editTurns: 0, byModel: []),
|
|
routingWaste: RoutingWaste(totalSavingsUSD: 0, baselineModel: "", baselineCostPerEdit: 0, byModel: []),
|
|
tools: [], skills: [], subagents: [], mcpServers: []
|
|
),
|
|
optimize: OptimizeBlock(findingCount: 0, savingsUSD: 0, topFindings: []),
|
|
history: HistoryBlock(daily: []),
|
|
combined: nil
|
|
)
|
|
return try! JSONEncoder().encode(p)
|
|
}
|
|
|
|
@Test("reads a fresh, valid status file")
|
|
func readsValidFile() throws {
|
|
let dir = tempDir()
|
|
let path = dir + "/menubar-status.json"
|
|
try validPayloadJSON(cost: 42.5).write(to: URL(fileURLWithPath: path))
|
|
let cache = MenubarStatusCache(statusPath: path)
|
|
|
|
let result = cache.readBadgePayload(maxAgeSeconds: 3600)
|
|
|
|
#expect(result?.payload.current.cost == 42.5)
|
|
#expect((result?.ageSeconds ?? .infinity) < 60)
|
|
}
|
|
|
|
@Test("returns nil for a missing file")
|
|
func missingFileReturnsNil() {
|
|
let dir = tempDir()
|
|
let cache = MenubarStatusCache(statusPath: dir + "/nope.json")
|
|
#expect(cache.readBadgePayload(maxAgeSeconds: 3600) == nil)
|
|
}
|
|
|
|
@Test("returns nil for a corrupt file")
|
|
func corruptFileReturnsNil() throws {
|
|
let dir = tempDir()
|
|
let path = dir + "/menubar-status.json"
|
|
try Data("{ not json".utf8).write(to: URL(fileURLWithPath: path))
|
|
let cache = MenubarStatusCache(statusPath: path)
|
|
#expect(cache.readBadgePayload(maxAgeSeconds: 3600) == nil)
|
|
}
|
|
|
|
@Test("returns nil for an over-age file")
|
|
func overAgeFileReturnsNil() throws {
|
|
let dir = tempDir()
|
|
let path = dir + "/menubar-status.json"
|
|
let url = URL(fileURLWithPath: path)
|
|
try validPayloadJSON(cost: 7).write(to: url)
|
|
try FileManager.default.setAttributes(
|
|
[.modificationDate: Date().addingTimeInterval(-7200)], ofItemAtPath: path
|
|
)
|
|
let cache = MenubarStatusCache(statusPath: path)
|
|
#expect(cache.readBadgePayload(maxAgeSeconds: 3600) == nil)
|
|
}
|
|
|
|
@Test("writeStatus round-trips through readBadgePayload")
|
|
func writeStatusRoundTrips() throws {
|
|
let dir = tempDir()
|
|
let path = dir + "/menubar-status.json"
|
|
let cache = MenubarStatusCache(statusPath: path)
|
|
|
|
let payload = try JSONDecoder().decode(MenubarPayload.self, from: validPayloadJSON(cost: 13.5))
|
|
try cache.writeStatus(payload)
|
|
|
|
let result = cache.readBadgePayload(maxAgeSeconds: 3600)
|
|
#expect(result?.payload.current.cost == 13.5)
|
|
}
|
|
}
|