chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:26:02 +08:00
commit a0a48b5e45
437 changed files with 100767 additions and 0 deletions
@@ -0,0 +1,389 @@
import Foundation
import Testing
@testable import CodeBurnMenubar
private func combinedUsage(cost: Double = 12.5) -> CombinedUsage {
CombinedUsage(
perDevice: [
CombinedDeviceUsage(
id: "local",
name: "MacBook",
local: true,
error: nil,
cost: cost,
calls: 3,
sessions: 2,
inputTokens: 100,
outputTokens: 50,
cacheCreateTokens: 10,
cacheReadTokens: 20,
totalTokens: 180
)
],
combined: CombinedUsageTotals(
cost: cost,
calls: 3,
sessions: 2,
inputTokens: 100,
outputTokens: 50,
cacheCreateTokens: 10,
cacheReadTokens: 20,
totalTokens: 180,
deviceCount: 1,
reachableCount: 1
)
)
}
private func claudeConfigSelector(selectedId: String? = nil) -> ClaudeConfigSelector {
ClaudeConfigSelector(
selectedId: selectedId,
options: [
ClaudeConfigOption(id: "claude-config:work", label: "claude-work", path: "/tmp/claude-work"),
ClaudeConfigOption(id: "claude-config:personal", label: "claude-personal", path: "/tmp/claude-personal")
]
)
}
private func menubarPayload(cost: Double,
combined: CombinedUsage? = nil,
claudeConfigs: ClaudeConfigSelector? = nil) -> MenubarPayload {
MenubarPayload(
generated: "test",
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: combined,
claudeConfigs: claudeConfigs
)
}
@Suite("AppStore refresh recovery")
@MainActor
struct AppStoreRefreshRecoveryTests {
@Test("stale visible payload triggers hard recovery without clearing cache")
func stalePayloadTriggersHardRecoveryWithoutClearingCache() {
let store = AppStore()
store.setCachedPayloadForTesting(
menubarPayload(cost: 92.33),
period: .today,
provider: .all,
fetchedAt: Date().addingTimeInterval(-180)
)
#expect(store.todayPayload?.current.cost == 92.33)
#expect(store.needsInteractivePayloadRefresh)
#expect(store.needsStatusPayloadRefresh)
#expect(store.hasStaleInteractivePayload)
#expect(store.shouldResetInteractiveRefreshPipeline)
store.resetRefreshState(clearCache: false)
#expect(store.todayPayload?.current.cost == 92.33)
}
@Test("fresh visible payload does not trigger hard recovery")
func freshPayloadDoesNotTriggerHardRecovery() {
let store = AppStore()
store.setCachedPayloadForTesting(
menubarPayload(cost: 164.06),
period: .today,
provider: .all,
fetchedAt: Date()
)
#expect(!store.needsInteractivePayloadRefresh)
#expect(!store.needsStatusPayloadRefresh)
#expect(!store.hasStaleInteractivePayload)
#expect(!store.shouldResetInteractiveRefreshPipeline)
}
@Test("payload cache partitions local and combined scope")
func payloadCachePartitionsByScope() {
let store = AppStore()
store.setCachedPayloadForTesting(
menubarPayload(cost: 10),
scope: .local,
period: .today,
provider: .all,
fetchedAt: Date()
)
store.setCachedPayloadForTesting(
menubarPayload(cost: 99, combined: combinedUsage(cost: 42)),
scope: .combined,
period: .today,
provider: .all,
fetchedAt: Date()
)
#expect(store.cachedPayloadForTesting(scope: .local, period: .today, provider: .all)?.current.cost == 10)
#expect(store.cachedPayloadForTesting(scope: .combined, period: .today, provider: .all)?.current.cost == 99)
store.selectedScope = .combined
#expect(store.payload.current.cost == 10)
#expect(store.payload.combined?.combined.cost == 42)
}
@Test("multi-day combined selection uses local cache path")
func multiDayCombinedSelectionUsesLocalCachePath() {
let store = AppStore()
let days: Set<String> = ["2026-06-01", "2026-06-02"]
store.selectedScope = .combined
store.selectedDays = days
store.setCachedPayloadForTesting(
menubarPayload(cost: 18),
scope: .local,
period: .today,
provider: .all,
days: days,
fetchedAt: Date()
)
store.setCachedPayloadForTesting(
menubarPayload(cost: 99, combined: combinedUsage(cost: 44)),
scope: .combined,
period: .today,
provider: .all,
days: days,
fetchedAt: Date()
)
#expect(store.activeScope == .local)
#expect(store.payload.current.cost == 18)
#expect(store.payload.combined == nil)
}
@Test("combined failure state does not invalidate local badge payload")
func combinedFailureDoesNotInvalidateLocalBadgePayload() {
let store = AppStore()
store.setCachedPayloadForTesting(
menubarPayload(cost: 31),
scope: .local,
period: .today,
provider: .all,
fetchedAt: Date()
)
store.selectedScope = .combined
store.setLastErrorForTesting(
"timeout",
scope: .combined,
period: .today,
provider: .all
)
#expect(store.lastError == "timeout")
#expect(store.menubarPayload?.current.cost == 31)
#expect(!store.needsStatusPayloadRefresh)
#expect(store.payload.current.cost == 31)
#expect(store.payload.combined == nil)
}
@Test("switching to combined resets selected provider to all")
func switchingToCombinedResetsSelectedProviderToAll() {
let store = AppStore()
store.suppressRefreshesForTesting()
store.selectedScope = .local
store.selectedProvider = .claude
store.switchTo(scope: .combined)
#expect(store.selectedScope == .combined)
#expect(store.selectedProvider == .all)
}
@Test("selected Claude config partitions payload cache")
func selectedClaudeConfigPartitionsPayloadCache() {
let store = AppStore()
store.setCachedPayloadForTesting(
menubarPayload(cost: 10, claudeConfigs: claudeConfigSelector()),
scope: .local,
period: .today,
provider: .all,
fetchedAt: Date()
)
store.setCachedPayloadForTesting(
menubarPayload(cost: 4, claudeConfigs: claudeConfigSelector(selectedId: "claude-config:work")),
scope: .local,
period: .today,
provider: .all,
claudeConfigSourceId: "claude-config:work",
fetchedAt: Date()
)
#expect(store.payload.current.cost == 10)
store.selectedClaudeConfigSourceId = "claude-config:work"
#expect(store.payload.current.cost == 4)
#expect(store.cachedPayloadForTesting(scope: .local, period: .today, provider: .all)?.current.cost == 10)
#expect(store.cachedPayloadForTesting(scope: .local, period: .today, provider: .all, claudeConfigSourceId: "claude-config:work")?.current.cost == 4)
}
@Test("Claude config selector is hidden until multiple configs are available")
func claudeConfigSelectorVisibilityRequiresMultipleConfigs() {
let store = AppStore()
store.setCachedPayloadForTesting(
menubarPayload(cost: 1),
scope: .local,
period: .today,
provider: .all,
fetchedAt: Date()
)
#expect(!store.shouldShowClaudeConfigSelector)
store.setCachedPayloadForTesting(
menubarPayload(cost: 2, claudeConfigs: claudeConfigSelector()),
scope: .local,
period: .today,
provider: .all,
fetchedAt: Date()
)
#expect(store.shouldShowClaudeConfigSelector)
#expect(store.claudeConfigOptions.map(\.label) == ["claude-work", "claude-personal"])
}
@Test("selecting Claude config resets provider and combined scope")
func selectingClaudeConfigResetsProviderAndCombinedScope() {
let store = AppStore()
store.suppressRefreshesForTesting()
store.selectedScope = .combined
store.selectedProvider = .codex
store.switchTo(claudeConfigSourceId: "claude-config:work")
#expect(store.selectedClaudeConfigSourceId == "claude-config:work")
#expect(store.selectedScope == .local)
#expect(store.selectedProvider == .all)
}
@Test("daily budget warning is suppressed for combined scope")
func dailyBudgetWarningIsSuppressedForCombinedScope() {
let defaults = UserDefaults.standard
let previousDisplayMetric = defaults.object(forKey: "CodeBurnDisplayMetric")
let previousDailyBudget = defaults.object(forKey: "CodeBurnDailyBudget")
defer {
if let previousDisplayMetric {
defaults.set(previousDisplayMetric, forKey: "CodeBurnDisplayMetric")
} else {
defaults.removeObject(forKey: "CodeBurnDisplayMetric")
}
if let previousDailyBudget {
defaults.set(previousDailyBudget, forKey: "CodeBurnDailyBudget")
} else {
defaults.removeObject(forKey: "CodeBurnDailyBudget")
}
}
let store = AppStore()
store.selectedScope = .local
store.selectedDays = []
store.displayMetric = .cost
store.dailyBudget = 10
store.setCachedPayloadForTesting(
menubarPayload(cost: 12.5),
scope: .local,
period: .today,
provider: .all,
fetchedAt: Date()
)
#expect(store.isOverDailyBudget)
#expect(store.shouldShowDailyBudgetWarning)
store.selectedScope = .combined
#expect(store.isOverDailyBudget)
#expect(!store.shouldShowDailyBudgetWarning)
}
@Test("missing today status payload needs status refresh")
func missingTodayStatusPayloadNeedsStatusRefresh() {
let store = AppStore()
#expect(store.todayPayload == nil)
#expect(store.needsStatusPayloadRefresh)
}
@Test("missing unattempted payload triggers hard recovery")
func missingUnattemptedPayloadTriggersHardRecovery() {
let store = AppStore()
#expect(!store.hasCachedData)
#expect(!store.hasAttemptedCurrentKeyLoad)
#expect(store.needsInteractivePayloadRefresh)
#expect(store.hasMissingInteractivePayloadWithoutAttempt)
#expect(store.shouldResetInteractiveRefreshPipeline)
}
@Test("orphaned stale in-flight entry does not block stuck-loading recovery")
func staleInFlightDoesNotBlockRecovery() {
let store = AppStore()
// A quiet refresh torn down across sleep/wake can leave an in-flight
// entry behind for the current key with no cache and no active loading
// counter, far older than the watchdog window. Recovery must clear it
// and proceed instead of bailing on the in-flight guard forever.
store.seedInFlightForTesting(period: .today, provider: .all, insertedAt: Date().addingTimeInterval(-3600))
#expect(store.isInFlightForTesting(period: .today, provider: .all))
let canRecover = store.prepareStuckLoadingRecovery()
#expect(canRecover)
#expect(!store.isInFlightForTesting(period: .today, provider: .all))
}
@Test("healthy in-flight fetch is not killed by recovery")
func healthyInFlightFetchSurvivesRecovery() {
let store = AppStore()
store.seedInFlightForTesting(period: .today, provider: .all, insertedAt: Date())
let canRecover = store.prepareStuckLoadingRecovery()
#expect(!canRecover)
#expect(store.isInFlightForTesting(period: .today, provider: .all))
}
@Test("prepareStuckLoadingRecovery clears stale loading bookkeeping for the current key")
func popoverRecoveryClearsStuckLoading() {
let store = AppStore()
// Seed an orphaned in-flight entry older than the 60s watchdog so the
// stale-clear path runs, mimicking a fetch torn down across sleep/wake.
store.seedInFlightForTesting(
period: .today,
provider: .all,
insertedAt: Date().addingTimeInterval(-120)
)
#expect(store.isInFlightForTesting(period: .today, provider: .all))
let willFetch = store.prepareStuckLoadingRecovery()
#expect(willFetch)
#expect(!store.isInFlightForTesting(period: .today, provider: .all))
}
}
@@ -0,0 +1,19 @@
import Testing
@testable import CodeBurnMenubar
@Suite("AppVersion")
struct AppVersionTests {
@Test("display avoids duplicate v prefix")
func displayAvoidsDuplicatePrefix() {
#expect(AppVersion.display("0.9.8") == "v0.9.8")
#expect(AppVersion.display("v0.9.8") == "v0.9.8")
#expect(AppVersion.display("mac-v0.9.8") == "v0.9.8")
}
@Test("bundle metadata stores unprefixed semver")
func normalizeBundleVersion() {
#expect(AppVersion.normalize("v0.9.8") == "0.9.8")
#expect(AppVersion.normalize("mac-v0.9.8") == "0.9.8")
#expect(AppVersion.normalize("dev") == "dev")
}
}
@@ -0,0 +1,97 @@
import Foundation
import Testing
@testable import CodeBurnMenubar
@Suite("CLI Devin config", .serialized)
struct CLIDevinConfigTests {
private func withTemporaryStore(_ body: (URL, CodeburnCLIConfigStore) throws -> Void) throws {
let root = FileManager.default.temporaryDirectory
.appendingPathComponent("codeburn-devin-config-\(UUID().uuidString)", isDirectory: true)
try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
defer {
try? FileManager.default.removeItem(at: root)
}
try body(root, CodeburnCLIConfigStore(homeDirectory: root.path))
}
private func configURL(in home: URL) -> URL {
home
.appendingPathComponent(".config", isDirectory: true)
.appendingPathComponent("codeburn", isDirectory: true)
.appendingPathComponent("config.json")
}
private func writeConfig(_ object: [String: Any], in home: URL) throws {
let url = configURL(in: home)
try FileManager.default.createDirectory(
at: url.deletingLastPathComponent(),
withIntermediateDirectories: true
)
let data = try JSONSerialization.data(withJSONObject: object, options: [.prettyPrinted, .sortedKeys])
try data.write(to: url)
}
private func readConfig(in home: URL) throws -> [String: Any] {
let data = try Data(contentsOf: configURL(in: home))
return try #require(JSONSerialization.jsonObject(with: data) as? [String: Any])
}
@Test("missing config has no ACU rate")
func missingConfigHasNoRate() throws {
try withTemporaryStore { _, store in
#expect(store.loadDevinAcuUsdRate() == nil)
}
}
@Test("persists and loads ACU rate")
func persistsAndLoadsRate() throws {
try withTemporaryStore { _, store in
store.persistDevinAcuUsdRate(2.25)
#expect(store.loadDevinAcuUsdRate() == 2.25)
}
}
@Test("preserves existing config while adding Devin rate")
func preservesExistingConfig() throws {
try withTemporaryStore { home, store in
try writeConfig([
"currency": [
"code": "EUR",
"symbol": "\u{20AC}"
]
], in: home)
store.persistDevinAcuUsdRate(3.5)
let json = try readConfig(in: home)
let currency = try #require(json["currency"] as? [String: Any])
let devin = try #require(json["devin"] as? [String: Any])
#expect(currency["code"] as? String == "EUR")
#expect(devin["acuUsdRate"] as? Double == 3.5)
}
}
@Test("ignores invalid rates")
func ignoresInvalidRates() throws {
try withTemporaryStore { _, store in
store.persistDevinAcuUsdRate(1.75)
store.persistDevinAcuUsdRate(0)
store.persistDevinAcuUsdRate(-2)
store.persistDevinAcuUsdRate(.infinity)
#expect(store.loadDevinAcuUsdRate() == 1.75)
}
}
@Test("loads only positive finite numeric rates")
func loadsOnlyPositiveFiniteNumericRates() throws {
try withTemporaryStore { home, store in
try writeConfig(["devin": ["acuUsdRate": 0]], in: home)
#expect(store.loadDevinAcuUsdRate() == nil)
try writeConfig(["devin": ["acuUsdRate": "2.25"]], in: home)
#expect(store.loadDevinAcuUsdRate() == nil)
}
}
}
@@ -0,0 +1,158 @@
import Foundation
import Testing
@testable import CodeBurnMenubar
private let now = Date(timeIntervalSince1970: 1_734_000_000)
private func snap(_ percent: Double, _ tokens: Double, ageDays: Double = 0) -> CapacitySnapshot {
CapacitySnapshot(
percent: percent,
effectiveTokens: tokens,
capturedAt: now.addingTimeInterval(-ageDays * 86400)
)
}
@Suite("CapacityEstimator -- gating")
struct CapacityEstimatorGatingTests {
@Test("returns nil with no snapshots")
func emptyReturnsNil() {
#expect(CapacityEstimator.estimate([], asOf: now) == nil)
}
@Test("returns nil with fewer than 5 snapshots")
func tooFewReturnsNil() {
let snaps = (1...4).map { snap(Double($0 * 10), Double($0) * 100_000) }
#expect(CapacityEstimator.estimate(snaps, asOf: now) == nil)
}
@Test("returns nil when percent range is below 15 points")
func tooNarrowReturnsNil() {
let snaps = [
snap(40, 4_000_000),
snap(42, 4_200_000),
snap(44, 4_400_000),
snap(46, 4_600_000),
snap(48, 4_800_000),
snap(50, 5_000_000),
]
#expect(CapacityEstimator.estimate(snaps, asOf: now) == nil)
}
}
@Suite("CapacityEstimator -- recovery")
struct CapacityEstimatorRecoveryTests {
@Test("recovers capacity from 10 noise-free snapshots within 0.5%")
func recoverFromCleanData() {
let trueCapacity: Double = 10_000_000
let percents = [5.0, 12, 20, 28, 35, 47, 55, 68, 80, 92]
let snaps = percents.map { p in snap(p, p / 100 * trueCapacity) }
let est = CapacityEstimator.estimate(snaps, asOf: now)
#expect(est != nil)
#expect(est!.capacity > trueCapacity * 0.995)
#expect(est!.capacity < trueCapacity * 1.005)
// 10 perfect samples is below the solid sample threshold (15) but easily medium.
#expect(est!.confidence == .medium || est!.confidence == .solid)
}
@Test("recovers capacity within 5% from 30 noisy snapshots")
func recoverFromNoisyData() {
let trueCapacity: Double = 8_000_000
var rng = LinearCongruentialGenerator(seed: 42)
let snaps: [CapacitySnapshot] = (0..<30).map { i in
let p = 5.0 + Double(i) * 3.0 // 5..92, spanning enough
let noise = (rng.nextDouble() - 0.5) * 0.10 // ±5%
let tokens = (p / 100) * trueCapacity * (1 + noise)
return snap(p, tokens)
}
let est = CapacityEstimator.estimate(snaps, asOf: now)
#expect(est != nil)
let ratio = est!.capacity / trueCapacity
#expect(ratio > 0.95 && ratio < 1.05)
#expect(est!.confidence == .solid || est!.confidence == .medium)
}
}
@Suite("CapacityEstimator -- confidence tiers")
struct CapacityEstimatorConfidenceTests {
@Test("six clean snapshots span sufficient range -> at least medium")
func sixCleanSnapshotsMedium() {
let trueCapacity: Double = 5_000_000
let percents = [5.0, 18, 32, 51, 70, 88]
let snaps = percents.map { p in snap(p, p / 100 * trueCapacity) }
let est = CapacityEstimator.estimate(snaps, asOf: now)
#expect(est != nil)
#expect(est!.confidence == .medium || est!.confidence == .solid)
}
@Test("noisy small-sample data falls to low confidence")
func noisySmallSampleLow() {
let trueCapacity: Double = 5_000_000
var rng = LinearCongruentialGenerator(seed: 7)
let percents = [5.0, 22, 40, 60, 80, 95]
let snaps: [CapacitySnapshot] = percents.map { p in
let noise = (rng.nextDouble() - 0.5) * 1.6 // ±80% noise -> drops R^2 below medium gate
return snap(p, p / 100 * trueCapacity * (1 + noise))
}
let est = CapacityEstimator.estimate(snaps, asOf: now)
#expect(est != nil)
#expect(est!.confidence == .low)
}
}
@Suite("CapacityEstimator -- recency weighting")
struct CapacityEstimatorRecencyTests {
@Test("recent snapshots dominate over old ones with different capacity")
func recencyShiftsEstimate() {
// Old data: capacity = 5M (45 days ago)
// New data: capacity = 10M (today)
// With 30-day half-life, recent data should win.
let oldSnaps = (0..<10).map { i -> CapacitySnapshot in
let p = 10.0 + Double(i) * 8
return snap(p, p / 100 * 5_000_000, ageDays: 45)
}
let newSnaps = (0..<10).map { i -> CapacitySnapshot in
let p = 10.0 + Double(i) * 8
return snap(p, p / 100 * 10_000_000, ageDays: 1)
}
let est = CapacityEstimator.estimate(oldSnaps + newSnaps, asOf: now)
#expect(est != nil)
// Recent capacity is 10M; estimate should be closer to 10M than 5M.
#expect(est!.capacity > 7_500_000)
}
}
@Suite("CapacityEstimator -- non-linearity")
struct CapacityEstimatorNonLinearityTests {
@Test("flags non-linearity when residuals show systematic sign pattern")
func detectsKneePattern() {
// Data follows a knee: linear up to 60%, then flatter (Anthropic capping).
let snaps: [CapacitySnapshot] = (0..<20).map { i in
let p = 5.0 + Double(i) * 5
let tokens: Double = p < 60 ? p / 100 * 8_000_000 : 0.6 * 8_000_000 + (p - 60) / 100 * 4_000_000
return snap(p, tokens)
}
let est = CapacityEstimator.estimate(snaps, asOf: now)
#expect(est != nil)
#expect(est!.nonLinearityWarning == true)
}
@Test("does not flag clean linear data")
func cleanLinearNoFlag() {
let trueCapacity: Double = 6_000_000
let percents = stride(from: 5.0, to: 95.0, by: 5.0).map { $0 }
let snaps = percents.map { p in snap(p, p / 100 * trueCapacity) }
let est = CapacityEstimator.estimate(snaps, asOf: now)
#expect(est != nil)
#expect(est!.nonLinearityWarning == false)
}
}
// Lightweight deterministic RNG for reproducible noise in tests.
struct LinearCongruentialGenerator {
private var state: UInt64
init(seed: UInt64) { self.state = seed }
mutating func nextDouble() -> Double {
state = state &* 6364136223846793005 &+ 1442695040888963407
return Double(state >> 11) / Double(1 << 53)
}
}
@@ -0,0 +1,77 @@
import Foundation
import Testing
@testable import CodeBurnMenubar
@Suite("Claude usage response parsing")
struct ClaudeSubscriptionParsingTests {
// Shape captured from the live oauth/usage endpoint (2026-07): named
// windows plus a `limits` array carrying model-scoped weekly buckets.
private let liveShape = """
{
"five_hour": { "utilization": 13.0, "resets_at": "2026-07-02T22:09:59.599633+00:00", "limit_dollars": null, "used_dollars": null, "remaining_dollars": null },
"seven_day": { "utilization": 63.0, "resets_at": "2026-07-03T06:59:59.599658+00:00", "limit_dollars": null, "used_dollars": null, "remaining_dollars": null },
"seven_day_oauth_apps": null,
"seven_day_opus": null,
"seven_day_sonnet": null,
"extra_usage": { "is_enabled": false },
"limits": [
{ "kind": "session", "group": "session", "percent": 13, "severity": "normal", "resets_at": "2026-07-02T22:09:59.456907+00:00", "scope": null, "is_active": false },
{ "kind": "weekly_all", "group": "weekly", "percent": 63, "severity": "normal", "resets_at": "2026-07-03T06:59:59.456926+00:00", "scope": null, "is_active": false },
{ "kind": "weekly_scoped", "group": "weekly", "percent": 94, "severity": "critical", "resets_at": "2026-07-03T06:59:59.457220+00:00", "scope": { "model": { "id": null, "display_name": "Fable" }, "surface": null }, "is_active": true }
]
}
"""
@Test("model-scoped weekly bucket surfaces with its display name")
func scopedWeeklyParsed() throws {
let usage = try ClaudeSubscriptionService.parseUsage(Data(liveShape.utf8), rawTier: "max_20x")
#expect(usage.scopedWeekly.count == 1)
let fable = try #require(usage.scopedWeekly.first)
#expect(fable.label == "Fable")
#expect(fable.percent == 94)
#expect(fable.resetsAt != nil)
}
@Test("named windows still map alongside limits")
func namedWindowsUnaffected() throws {
let usage = try ClaudeSubscriptionService.parseUsage(Data(liveShape.utf8), rawTier: "max_20x")
#expect(usage.fiveHourPercent == 13.0)
#expect(usage.sevenDayPercent == 63.0)
#expect(usage.sevenDayOpusPercent == nil)
#expect(usage.sevenDaySonnetPercent == nil)
#expect(usage.tier == .max20x)
}
@Test("session and weekly_all limits are not duplicated as scoped rows")
func unscopedKindsSkipped() throws {
let usage = try ClaudeSubscriptionService.parseUsage(Data(liveShape.utf8), rawTier: nil)
#expect(!usage.scopedWeekly.contains { $0.percent == 13 || $0.percent == 63 })
}
@Test("response without a limits array parses with no scoped windows")
func missingLimitsIsBackCompat() throws {
let old = """
{
"five_hour": { "utilization": 40.0, "resets_at": "2026-07-02T22:00:00+00:00" },
"seven_day": { "utilization": 12.5, "resets_at": "2026-07-03T07:00:00+00:00" }
}
"""
let usage = try ClaudeSubscriptionService.parseUsage(Data(old.utf8), rawTier: "pro")
#expect(usage.scopedWeekly.isEmpty)
#expect(usage.fiveHourPercent == 40.0)
#expect(usage.tier == .pro)
}
@Test("weekly_scoped without a model display name is skipped")
func scopedWithoutNameSkipped() throws {
let body = """
{
"limits": [
{ "kind": "weekly_scoped", "percent": 50, "resets_at": "2026-07-03T07:00:00+00:00", "scope": { "model": null } }
]
}
"""
let usage = try ClaudeSubscriptionService.parseUsage(Data(body.utf8), rawTier: nil)
#expect(usage.scopedWeekly.isEmpty)
}
}
@@ -0,0 +1,36 @@
import Foundation
import Testing
@testable import CodeBurnMenubar
@Suite("Codex terminal failure classification")
struct CodexTerminalFailureTests {
@Test("expired refresh token (4xx) is terminal")
func expiredRefreshTokenIsTerminal() {
let err = CodexCredentialStore.StoreError.refreshHTTPError(400, "invalid_grant: refresh_token_expired")
#expect(err.isTerminal)
}
@Test("missing refresh token is terminal")
func missingRefreshTokenIsTerminal() {
#expect(CodexCredentialStore.StoreError.noRefreshToken.isTerminal)
}
@Test("a 5xx refresh error is not terminal")
func serverErrorIsNotTerminal() {
let err = CodexCredentialStore.StoreError.refreshHTTPError(503, "service unavailable")
#expect(!err.isTerminal)
}
@Test("a network error is not terminal")
func networkErrorIsNotTerminal() {
let err = CodexCredentialStore.StoreError.refreshNetworkError(URLError(.timedOut))
#expect(!err.isTerminal)
}
@Test("FetchError wrapping a terminal credential error is terminal")
func fetchErrorPropagatesTerminal() {
let store = CodexCredentialStore.StoreError.refreshHTTPError(401, "invalid_grant")
let fetch = CodexSubscriptionService.FetchError.credential(store)
#expect(fetch.isTerminal)
}
}
@@ -0,0 +1,117 @@
import Foundation
import Testing
@testable import CodeBurnMenubar
private let heatmapNow: Date = {
var calendar = Calendar(identifier: .gregorian)
calendar.timeZone = TimeZone(secondsFromGMT: 0)!
return calendar.date(from: DateComponents(year: 2026, month: 6, day: 3, hour: 12))!
}()
private let heatmapCalendar: Calendar = {
var calendar = Calendar(identifier: .gregorian)
calendar.timeZone = TimeZone(secondsFromGMT: 0)!
return calendar
}()
private let heatmapFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
formatter.timeZone = TimeZone(secondsFromGMT: 0)!
return formatter
}()
private func historyEntry(
_ date: String,
cost: Double,
calls: Int = 1,
inputTokens: Int = 100,
outputTokens: Int = 20
) -> DailyHistoryEntry {
DailyHistoryEntry(
date: date,
cost: cost,
savingsUSD: 0,
calls: calls,
inputTokens: inputTokens,
outputTokens: outputTokens,
cacheReadTokens: 0,
cacheWriteTokens: 0,
topModels: []
)
}
@Suite("Contribution heatmap")
struct ContributionHeatmapTests {
@Test("builds Monday-start weeks ending with the current week")
func buildsMondayStartWeeks() {
let weeks = buildContributionWeeks(
from: [historyEntry("2026-06-03", cost: 10)],
weekCount: 2,
now: heatmapNow,
calendar: heatmapCalendar,
formatter: heatmapFormatter
)
#expect(weeks.map(\.startDate) == ["2026-05-25", "2026-06-01"])
#expect(weeks.allSatisfy { $0.days.count == 7 })
#expect(weeks[1].days[0].date == "2026-06-01")
#expect(weeks[1].days[2].date == "2026-06-03")
#expect(weeks[1].days[2].isToday)
}
@Test("marks future cells after today")
func marksFutureCells() {
let weeks = buildContributionWeeks(
from: [
historyEntry("2026-06-03", cost: 10),
historyEntry("2026-06-04", cost: 99),
],
weekCount: 1,
now: heatmapNow,
calendar: heatmapCalendar,
formatter: heatmapFormatter
)
let currentWeek = weeks[0].days
#expect(currentWeek[2].date == "2026-06-03")
#expect(currentWeek[2].cost == 10)
#expect(!currentWeek[2].isFuture)
#expect(currentWeek[3].date == "2026-06-04")
#expect(currentWeek[3].cost == 0)
#expect(currentWeek[3].isFuture)
}
@Test("maps costs to four nonzero intensity levels")
func mapsIntensityLevels() {
#expect(contributionLevel(value: 0, maxValue: 100) == 0)
#expect(contributionLevel(value: 10, maxValue: 100) == 1)
#expect(contributionLevel(value: 25, maxValue: 100) == 2)
#expect(contributionLevel(value: 50, maxValue: 100) == 3)
#expect(contributionLevel(value: 75, maxValue: 100) == 4)
#expect(contributionLevel(value: 100, maxValue: 100) == 4)
}
@MainActor
@Test("computes total, active days, average, and current streak")
func computesStats() {
let weeks = buildContributionWeeks(
from: [
historyEntry("2026-06-01", cost: 3),
historyEntry("2026-06-02", cost: 0),
historyEntry("2026-06-03", cost: 9),
],
weekCount: 1,
now: heatmapNow,
calendar: heatmapCalendar,
formatter: heatmapFormatter
)
let stats = computeContributionStats(weeks: weeks)
#expect(stats.total == 12)
#expect(stats.activeDays == 2)
#expect(stats.avgActive == 6)
#expect(stats.currentStreak == 1)
#expect(stats.peakLabel.contains("Jun 3"))
}
}
@@ -0,0 +1,224 @@
import Foundation
import Testing
@testable import CodeBurnMenubar
@Suite("DataClient process")
struct DataClientProcessTests {
@Test("status argv local omits scope")
func statusSubcommandLocalOmitsScope() {
let args = DataClient.statusSubcommand(
period: .today,
provider: .claude,
includeOptimize: false,
scope: .local
)
#expect(!args.contains("--scope"))
#expect(value(after: "--provider", in: args) == "claude")
#expect(args.contains("--no-optimize"))
}
@Test("status argv combined adds scope and forces provider all")
func statusSubcommandCombinedAddsScopeAndForcesAllProvider() {
let args = DataClient.statusSubcommand(
period: .today,
provider: .codex,
includeOptimize: true,
scope: .combined
)
#expect(value(after: "--scope", in: args) == "combined")
#expect(value(after: "--provider", in: args) == "all")
#expect(!args.contains("--no-optimize"))
}
@Test("status argv combined multi-day coerces to local")
func statusSubcommandCombinedMultiDayCoercesToLocal() {
let args = DataClient.statusSubcommand(
period: .today,
days: ["2026-06-01", "2026-06-02"],
provider: .codex,
includeOptimize: false,
scope: .combined
)
#expect(!args.contains("--scope"))
#expect(value(after: "--provider", in: args) == "codex")
#expect(value(after: "--days", in: args) == "2026-06-01,2026-06-02")
}
@Test("status argv local includes Claude config source")
func statusSubcommandLocalIncludesClaudeConfigSource() {
let args = DataClient.statusSubcommand(
period: .today,
provider: .all,
includeOptimize: false,
scope: .local,
claudeConfigSourceId: "claude-config:work"
)
#expect(value(after: "--claude-config-source", in: args) == "claude-config:work")
}
@Test("status argv combined omits Claude config source")
func statusSubcommandCombinedOmitsClaudeConfigSource() {
let args = DataClient.statusSubcommand(
period: .today,
provider: .all,
includeOptimize: false,
scope: .combined,
claudeConfigSourceId: "claude-config:work"
)
#expect(value(after: "--scope", in: args) == "combined")
#expect(!args.contains("--claude-config-source"))
}
@Test("status argv supports LingTai TUI provider")
func statusSubcommandSupportsLingTaiTUI() {
let args = DataClient.statusSubcommand(
period: .month,
provider: .lingtaiTui,
includeOptimize: false,
scope: .local
)
#expect(value(after: "--provider", in: args) == "lingtai-tui")
#expect(value(after: "--period", in: args) == "month")
}
/// Concurrency + timeout smoke test: launch more hung subprocesses than
/// there are cooperative threads, all at once, with a short timeout, and
/// assert every call returns once the timeout kills its sleep.
///
/// NOTE: this does NOT reproduce the production permanent deadlock (16/16
/// cooperative threads parked in waitUntilExit). In a short-lived unit-test
/// process libdispatch spins up replacement threads for blocked workers, so
/// even the old blocking-on-the-pool code completes here. The real deadlock
/// built up over ~2 days under the @MainActor refresh loop and is confirmed
/// by the live `sample`, not by this test. Kept as a guard that the
/// off-pool wait + timeout path stays correct under concurrency.
@Test("concurrent timed-out processes all complete")
func concurrentTimedOutProcessesAllComplete() {
let count = ProcessInfo.processInfo.activeProcessorCount * 2 + 4
let done = DispatchSemaphore(value: 0)
Task {
await withTaskGroup(of: Void.self) { group in
for _ in 0..<count {
group.addTask {
let process = Process()
process.executableURL = URL(fileURLWithPath: "/bin/sleep")
process.arguments = ["30"]
_ = try? await DataClient.runProcess(process, timeoutSeconds: 1, label: "sleep 30")
}
}
}
done.signal()
}
// Wait on the test thread (a real thread, not the cooperative pool) so
// the deadlock is detectable even when the pool is fully starved.
let outcome = done.wait(timeout: .now() + 15)
#expect(outcome == .success, "runProcess deadlocked: \(count) concurrent CLIs starved the cooperative pool")
}
/// A decode failure surfaces the CLI's actual stdout/stderr so a stray banner
/// on stdout (see #515) is self-diagnosing instead of an opaque "not valid JSON".
@Test("decode failure surfaces output")
func decodeFailureSurfacesOutput() {
struct Boom: Error {}
let failure = CLIDecodeFailure(
underlying: Boom(),
stdoutByteCount: 13,
stdoutSnippet: "(node) banner",
stderr: "warn: x"
)
let text = String(describing: failure)
#expect(text.contains("(node) banner"), "should include the stdout snippet")
#expect(text.contains("13 bytes"), "should include the stdout byte count")
#expect(text.contains("warn: x"), "should include stderr")
}
/// Empty stdout is reported distinctly (the JSONDecoder-on-empty-Data case).
@Test("decode failure with empty stdout")
func decodeFailureWithEmptyStdout() {
struct Boom: Error {}
let failure = CLIDecodeFailure(underlying: Boom(), stdoutByteCount: 0, stdoutSnippet: "", stderr: "")
let text = String(describing: failure)
#expect(text.contains("0 bytes"))
#expect(text.contains("<empty>"))
}
/// A normally-exiting process returns its real output and exit code through
/// the off-pool wait path.
@Test("process returns output and exit code")
func processReturnsOutputAndExitCode() async throws {
let process = Process()
process.executableURL = URL(fileURLWithPath: "/bin/echo")
process.arguments = ["hello"]
let result = try await DataClient.runProcess(process, timeoutSeconds: 5, label: "echo hello")
#expect(result.exitCode == 0)
#expect(String(data: result.stdout, encoding: .utf8) == "hello\n")
}
/// Many NORMALLY-exiting processes, all at once, must every one complete
/// through the terminationHandler wait path. Guards against the wait path
/// leaking or wedging under concurrency (the production bug was the wait and
/// its timeout sharing one queue that saturated under sustained load).
@Test("many normal processes all complete")
func manyNormalProcessesAllComplete() async {
let count = 50
let codes = await withTaskGroup(of: Int32?.self) { group -> [Int32?] in
for _ in 0..<count {
group.addTask {
let process = Process()
process.executableURL = URL(fileURLWithPath: "/bin/echo")
process.arguments = ["ok"]
return try? await DataClient.runProcess(process, timeoutSeconds: 5, label: "echo ok").exitCode
}
}
var out: [Int32?] = []
for await code in group { out.append(code) }
return out
}
#expect(codes.count == count)
#expect(codes.allSatisfy { $0 == 0 },
"every concurrent process should exit 0 via the terminationHandler wait path")
}
/// The async semaphore never lets more than its count run concurrently.
@Test("async semaphore caps concurrency")
func asyncSemaphoreCapsConcurrency() async {
let sem = AsyncSemaphore(2)
let peak = PeakCounter()
await withTaskGroup(of: Void.self) { group in
for _ in 0..<12 {
group.addTask {
await sem.acquire()
await peak.enter()
try? await Task.sleep(nanoseconds: 8_000_000)
await peak.leave()
await sem.release()
}
}
}
let observed = await peak.peak
#expect(observed <= 2, "semaphore should cap concurrency at 2, saw \(observed)")
#expect(observed > 0)
}
}
private func value(after flag: String, in args: [String]) -> String? {
guard let index = args.firstIndex(of: flag), args.indices.contains(index + 1) else {
return nil
}
return args[index + 1]
}
private actor PeakCounter {
private var current = 0
private(set) var peak = 0
func enter() { current += 1; peak = max(peak, current) }
func leave() { current -= 1 }
}
@@ -0,0 +1,130 @@
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)
}
@@ -0,0 +1,71 @@
import Foundation
import Testing
@testable import CodeBurnMenubar
@Suite("Menubar period settings")
struct MenubarPeriodSettingsTests {
@Test("settings picker exposes requested periods")
func settingsPickerExposesRequestedPeriods() {
#expect(Period.menubarMetricCases == [.today, .sevenDays, .month, .all])
}
@Test("defaults values map to periods")
func defaultsValuesMapToPeriods() {
#expect(Period(menubarDefaultsValue: "today") == .today)
#expect(Period(menubarDefaultsValue: "week") == .sevenDays)
#expect(Period(menubarDefaultsValue: "month") == .month)
#expect(Period(menubarDefaultsValue: "sixMonths") == .all)
#expect(Period(menubarDefaultsValue: "all") == .all)
#expect(Period(menubarDefaultsValue: "30days") == .today)
#expect(Period(menubarDefaultsValue: "bogus") == .today)
#expect(Period(menubarDefaultsValue: nil) == .today)
}
@Test("periods persist canonical defaults values")
func periodsPersistCanonicalDefaultsValues() {
let suiteName = "CodeBurnMenubarTests.\(UUID().uuidString)"
let defaults = UserDefaults(suiteName: suiteName)!
defer { defaults.removePersistentDomain(forName: suiteName) }
Period.sevenDays.persistAsMenubarDefault(defaults: defaults)
#expect(defaults.string(forKey: "CodeBurnMenubarPeriod") == "week")
#expect(Period.savedMenubarPeriod(defaults: defaults) == .sevenDays)
Period.all.persistAsMenubarDefault(defaults: defaults)
#expect(defaults.string(forKey: "CodeBurnMenubarPeriod") == "sixMonths")
#expect(Period.savedMenubarPeriod(defaults: defaults) == .all)
Period.thirtyDays.persistAsMenubarDefault(defaults: defaults)
#expect(defaults.string(forKey: "CodeBurnMenubarPeriod") == "today")
#expect(Period.savedMenubarPeriod(defaults: defaults) == .today)
}
@Test("menubar scope persistence defaults to local and round-trips")
func menubarScopePersistenceDefaultsToLocalAndRoundTrips() {
let suiteName = "CodeBurnMenubarTests.\(UUID().uuidString)"
let defaults = UserDefaults(suiteName: suiteName)!
defer { defaults.removePersistentDomain(forName: suiteName) }
#expect(MenubarScope.savedMenubarScope(defaults: defaults) == .local)
MenubarScope.combined.persistAsMenubarDefault(defaults: defaults)
#expect(defaults.string(forKey: "CodeBurnMenubarScope") == "combined")
#expect(MenubarScope.savedMenubarScope(defaults: defaults) == .combined)
MenubarScope.local.persistAsMenubarDefault(defaults: defaults)
#expect(defaults.string(forKey: "CodeBurnMenubarScope") == "local")
#expect(MenubarScope.savedMenubarScope(defaults: defaults) == .local)
#expect(MenubarScope(menubarDefaultsValue: "bogus") == .local)
}
@Test("non-today periods render compact and regular suffixes")
func nonTodayPeriodsRenderCompactAndRegularSuffixes() {
#expect(Period.today.menubarSuffix(compact: false) == "")
#expect(Period.sevenDays.menubarSuffix(compact: false) == " / wk")
#expect(Period.month.menubarSuffix(compact: false) == " / mo")
#expect(Period.all.menubarSuffix(compact: false) == " / 6mo")
#expect(Period.sevenDays.menubarSuffix(compact: true) == "/wk")
#expect(Period.month.menubarSuffix(compact: true) == "/mo")
#expect(Period.all.menubarSuffix(compact: true) == "/6mo")
}
}
@@ -0,0 +1,87 @@
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)
}
}
@@ -0,0 +1,80 @@
import XCTest
@testable import CodeBurnMenubar
final class RefreshCadenceTests: XCTestCase {
func testAutoPopoverOpenAlwaysUsesActiveCadence() {
XCTAssertEqual(
RefreshCadence.interval(mode: .auto, popoverOpen: true, onBattery: true, lowPowerMode: true),
RefreshCadence.activeSeconds
)
}
func testAutoIdleOnACStaysActive() {
XCTAssertEqual(
RefreshCadence.interval(mode: .auto, popoverOpen: false, onBattery: false, lowPowerMode: false),
RefreshCadence.activeSeconds
)
}
func testAutoIdleOnBatteryBacksOff() {
XCTAssertEqual(
RefreshCadence.interval(mode: .auto, popoverOpen: false, onBattery: true, lowPowerMode: false),
RefreshCadence.batteryIdleSeconds
)
}
func testAutoLowPowerModeBacksOffFurthest() {
XCTAssertEqual(
RefreshCadence.interval(mode: .auto, popoverOpen: false, onBattery: true, lowPowerMode: true),
RefreshCadence.lowPowerIdleSeconds
)
XCTAssertEqual(
RefreshCadence.interval(mode: .auto, popoverOpen: false, onBattery: false, lowPowerMode: true),
RefreshCadence.lowPowerIdleSeconds
)
}
func testManualNeverAutoSpawns() {
XCTAssertNil(RefreshCadence.interval(mode: .manual, popoverOpen: false, onBattery: false, lowPowerMode: false))
XCTAssertNil(RefreshCadence.interval(mode: .manual, popoverOpen: true, onBattery: false, lowPowerMode: false))
}
func testFixedCadenceIgnoresPowerState() {
XCTAssertEqual(
RefreshCadence.interval(mode: .fiveMinutes, popoverOpen: false, onBattery: true, lowPowerMode: true),
300
)
XCTAssertEqual(
RefreshCadence.interval(mode: .fifteenMinutes, popoverOpen: false, onBattery: false, lowPowerMode: false),
900
)
}
func testFixedCadenceGoesActiveWhilePopoverOpen() {
XCTAssertEqual(
RefreshCadence.interval(mode: .fiveMinutes, popoverOpen: true, onBattery: true, lowPowerMode: false),
RefreshCadence.activeSeconds
)
}
func testBackoffOrdering() {
XCTAssertLessThan(RefreshCadence.activeSeconds, RefreshCadence.batteryIdleSeconds)
XCTAssertLessThan(RefreshCadence.batteryIdleSeconds, RefreshCadence.lowPowerIdleSeconds)
}
func testCadenceDefaultsToAutoWhenUnset() {
let key = UsageRefreshCadence.defaultsKey
let saved = UserDefaults.standard.object(forKey: key)
defer {
if let saved { UserDefaults.standard.set(saved, forKey: key) }
else { UserDefaults.standard.removeObject(forKey: key) }
}
UserDefaults.standard.removeObject(forKey: key)
XCTAssertEqual(UsageRefreshCadence.current, .auto)
UsageRefreshCadence.current = .manual
XCTAssertEqual(UsageRefreshCadence.current, .manual)
UsageRefreshCadence.current = .fiveMinutes
XCTAssertEqual(UsageRefreshCadence.current, .fiveMinutes)
}
}
@@ -0,0 +1,60 @@
import Testing
@testable import CodeBurnMenubar
@Suite("UpdateChecker")
struct UpdateCheckerTests {
@Test("selects newest mac release with zip and checksum")
func selectsNewestMacReleaseWithChecksum() {
let releases = [
GitHubRelease(
tag_name: "v0.9.9",
assets: [GitHubAsset(name: "codeburn-0.9.9.tgz", browser_download_url: "https://example.test/cli")]
),
GitHubRelease(
tag_name: "mac-v0.9.8",
assets: [
GitHubAsset(name: "CodeBurnMenubar-v0.9.8.zip", browser_download_url: "https://example.test/app"),
GitHubAsset(name: "CodeBurnMenubar-v0.9.8.zip.sha256", browser_download_url: "https://example.test/app.sha256"),
]
),
]
let resolved = UpdateChecker.resolveLatestMenubarRelease(in: releases)
#expect(resolved?.release.tag_name == "mac-v0.9.8")
#expect(resolved?.asset.name == "CodeBurnMenubar-v0.9.8.zip")
}
@Test("ignores mac release missing checksum")
func ignoresMacReleaseMissingChecksum() {
let releases = [
GitHubRelease(
tag_name: "mac-v0.9.8",
assets: [GitHubAsset(name: "CodeBurnMenubar-v0.9.8.zip", browser_download_url: "https://example.test/app")]
),
]
#expect(UpdateChecker.resolveLatestMenubarRelease(in: releases) == nil)
}
@Test("flags CLI older than the menubar-update fix as too old")
func flagsCliBelowMinimumAsTooOld() {
#expect(UpdateChecker.isCliTooOld(installed: "0.9.8"))
#expect(UpdateChecker.isCliTooOld(installed: "v0.9.8"))
#expect(UpdateChecker.isCliTooOld(installed: "0.8.12"))
}
@Test("accepts CLI at or above the menubar-update fix version")
func acceptsCliAtOrAboveMinimum() {
#expect(!UpdateChecker.isCliTooOld(installed: "0.9.9"))
#expect(!UpdateChecker.isCliTooOld(installed: "0.9.10"))
#expect(!UpdateChecker.isCliTooOld(installed: "0.9.14"))
#expect(!UpdateChecker.isCliTooOld(installed: "1.0.0"))
}
@Test("does not flag when the CLI version is unknown")
func ignoresUnknownCliVersion() {
#expect(!UpdateChecker.isCliTooOld(installed: nil))
#expect(!UpdateChecker.isCliTooOld(installed: ""))
}
}