mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-08-25 16:33:14 +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.
130 lines
3.6 KiB
Swift
130 lines
3.6 KiB
Swift
import Foundation
|
|
import Testing
|
|
@testable import CodeBurnMenubar
|
|
|
|
@Suite("MenubarPayload combined")
|
|
struct MenubarPayloadCombinedTests {
|
|
@Test("decodes combined block")
|
|
func decodesCombinedBlock() throws {
|
|
let json = combinedPayloadJSON()
|
|
|
|
let payload = try JSONDecoder().decode(MenubarPayload.self, from: json)
|
|
|
|
#expect(payload.combined?.perDevice.count == 2)
|
|
#expect(payload.combined?.perDevice.first?.id == "local-device")
|
|
#expect(payload.combined?.perDevice.first?.local == true)
|
|
#expect(payload.combined?.perDevice.last?.error == "offline")
|
|
#expect(payload.combined?.combined.cost == 4.5)
|
|
#expect(payload.combined?.combined.calls == 7)
|
|
#expect(payload.combined?.combined.deviceCount == 2)
|
|
#expect(payload.combined?.combined.reachableCount == 1)
|
|
}
|
|
|
|
@Test("combined hero token total excludes cache tokens")
|
|
func combinedHeroTokenTotalExcludesCacheTokens() throws {
|
|
let payload = try JSONDecoder().decode(MenubarPayload.self, from: combinedPayloadJSON())
|
|
let totals = HeroTotals(payload: payload, activeScope: .combined)
|
|
|
|
#expect(payload.combined?.combined.totalTokens == 2000)
|
|
#expect(totals.inputTokens == 1000)
|
|
#expect(totals.outputTokens == 500)
|
|
#expect(totals.totalTokens == 1500)
|
|
}
|
|
|
|
@Test("combined block is nil when absent")
|
|
func combinedBlockIsNilWhenAbsent() throws {
|
|
let json = Data("""
|
|
{
|
|
"generated": "2026-06-24T00:00:00Z",
|
|
"current": {
|
|
"label": "Today",
|
|
"cost": 1.25,
|
|
"calls": 2,
|
|
"sessions": 1,
|
|
"inputTokens": 100,
|
|
"outputTokens": 50
|
|
},
|
|
"optimize": {
|
|
"findingCount": 0,
|
|
"savingsUSD": 0,
|
|
"topFindings": []
|
|
},
|
|
"history": {
|
|
"daily": []
|
|
}
|
|
}
|
|
""".utf8)
|
|
|
|
let payload = try JSONDecoder().decode(MenubarPayload.self, from: json)
|
|
|
|
#expect(payload.combined == nil)
|
|
}
|
|
}
|
|
|
|
private func combinedPayloadJSON() -> Data {
|
|
Data("""
|
|
{
|
|
"generated": "2026-06-24T00:00:00Z",
|
|
"current": {
|
|
"label": "Today",
|
|
"cost": 1.25,
|
|
"calls": 2,
|
|
"sessions": 1,
|
|
"inputTokens": 100,
|
|
"outputTokens": 50
|
|
},
|
|
"optimize": {
|
|
"findingCount": 0,
|
|
"savingsUSD": 0,
|
|
"topFindings": []
|
|
},
|
|
"history": {
|
|
"daily": []
|
|
},
|
|
"combined": {
|
|
"perDevice": [
|
|
{
|
|
"id": "local-device",
|
|
"name": "MacBook Pro",
|
|
"local": true,
|
|
"error": null,
|
|
"cost": 4.5,
|
|
"calls": 7,
|
|
"sessions": 3,
|
|
"inputTokens": 1000,
|
|
"outputTokens": 500,
|
|
"cacheCreateTokens": 200,
|
|
"cacheReadTokens": 300,
|
|
"totalTokens": 2000
|
|
},
|
|
{
|
|
"id": "remote-device",
|
|
"name": "Studio",
|
|
"local": false,
|
|
"error": "offline",
|
|
"cost": 0,
|
|
"calls": 0,
|
|
"sessions": 0,
|
|
"inputTokens": 0,
|
|
"outputTokens": 0,
|
|
"cacheCreateTokens": 0,
|
|
"cacheReadTokens": 0,
|
|
"totalTokens": 0
|
|
}
|
|
],
|
|
"combined": {
|
|
"cost": 4.5,
|
|
"calls": 7,
|
|
"sessions": 3,
|
|
"inputTokens": 1000,
|
|
"outputTokens": 500,
|
|
"cacheCreateTokens": 200,
|
|
"cacheReadTokens": 300,
|
|
"totalTokens": 2000,
|
|
"deviceCount": 2,
|
|
"reachableCount": 1
|
|
}
|
|
}
|
|
}
|
|
""".utf8)
|
|
}
|