codeburn/mac/Tests/CodeBurnMenubarTests/MenubarPayloadCombinedTests.swift
ozymandiashh 805d2ffa49
Some checks are pending
CI / semgrep (push) Waiting to run
feat(menubar): add Local/Combined usage toggle (#566) (#568)
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.
2026-06-28 22:01:01 +02:00

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)
}