import Foundation import SwiftUI // for ColorScheme equality in the theme-token checks import CCSBarCore // Lightweight assert harness used in place of XCTest (unavailable without a // full Xcode install). Run with `swift run ccs-bar-check`; exits non-zero on // any failure so it works as a CI/test gate on a CommandLineTools toolchain. var failures = 0 func check(_ condition: Bool, _ message: String) { if condition { print("[OK] \(message)") } else { print("[X] \(message)") failures += 1 } } // Recording transport so CCSBarClient can be exercised without a live server. final class RequestRecorder: @unchecked Sendable { var lastRequest: URLRequest? var responseData = Data("[]".utf8) var status = 200 } struct RecordingTransport: HTTPTransport { let recorder: RequestRecorder func send(_ request: URLRequest) async throws -> (Data, HTTPURLResponse) { recorder.lastRequest = request let http = HTTPURLResponse( url: request.url!, statusCode: recorder.status, httpVersion: nil, headerFields: nil)! return (recorder.responseData, http) } } // MARK: BarSummaryRow decoding (mixed snake_case / camelCase keys) let summaryJSON = """ [ { "account_id": "alice@example.com", "provider": "agy", "displayName": "Alice (Ultra)", "tier": "ultra", "paused": false, "quota_percentage": 82.4, "quotaStatus": "ok", "next_reset": "2026-06-08T00:00:00Z", "is_default": true, "last_activity_at": "2026-04-29T10:00:00Z", "today_cost": 3.2, "health": "ok", "cached": true, "fetchedAt": "2026-06-07T19:00:00Z", "needsReauth": false }, { "account_id": "bob@example.com", "provider": "codex", "displayName": null, "tier": null, "paused": true, "quota_percentage": null, "quotaStatus": "error", "next_reset": null, "is_default": false, "last_activity_at": null, "today_cost": null, "health": "error", "cached": false, "fetchedAt": null, "needsReauth": true } ] """ do { let rows = try JSONDecoder().decode([BarSummaryRow].self, from: Data(summaryJSON.utf8)) check(rows.count == 2, "decodes two rows") check(rows[0].accountId == "alice@example.com", "maps account_id") check(rows[0].quotaPercentage == 82.4, "maps quota_percentage") check(rows[0].quotaStatus == "ok", "maps quotaStatus (ok)") check(rows[0].isDefault == true, "maps is_default") check(rows[0].lastActivityAt == "2026-04-29T10:00:00Z", "maps last_activity_at") check(rows[0].todayCost == 3.2, "maps today_cost") check(rows[0].id == "agy:alice@example.com", "stable id is provider:account") check(rows[1].quotaPercentage == nil, "null quota decodes to nil") check(rows[1].quotaStatus == "error", "maps quotaStatus (error)") check(rows[1].isDefault == false, "is_default false decodes") check(rows[1].lastActivityAt == nil, "null last_activity_at decodes to nil") check(rows[1].paused == true, "maps paused") check(rows[1].needsReauth == true, "maps needsReauth") check(rows[1].healthDot == "X", "error -> X dot") check(rows[0].healthDot == "OK", "ok -> OK dot") } // An "unsupported" provider (ghcp/kiro) decodes with quotaStatus "unsupported" // and health "ok" — the backend's key correctness fix (no permanent orange dot). do { let unsupportedJSON = """ [{ "account_id": "copilot-kaitranntt", "provider": "ghcp", "displayName": "copilot-kaitranntt", "tier": null, "paused": false, "quota_percentage": null, "quotaStatus": "unsupported", "next_reset": null, "is_default": true, "last_activity_at": null, "today_cost": null, "health": "ok", "cached": false, "fetchedAt": "2026-06-08T16:40:00.000Z", "needsReauth": false }] """ let rows = try JSONDecoder().decode([BarSummaryRow].self, from: Data(unsupportedJSON.utf8)) check(rows[0].quotaStatus == "unsupported", "ghcp decodes quotaStatus unsupported") check(rows[0].health == "ok", "unsupported provider is health ok (no orange dot)") } // MARK: BarDiscovery loading let tmp = NSTemporaryDirectory() + "ccs-bar-check-\(ProcessInfo.processInfo.globallyUniqueString)" let ccsDir = tmp + "/.ccs" try? FileManager.default.createDirectory(atPath: ccsDir, withIntermediateDirectories: true) let barJSON = """ { "baseUrl": "http://127.0.0.1:3210", "port": 3210, "authMode": "loopback" } """ try? barJSON.write(toFile: ccsDir + "/bar.json", atomically: true, encoding: .utf8) switch BarDiscovery.load(home: tmp) { case .success(let d): check(d.port == 3210, "discovery reads port") check(d.authMode == "loopback", "discovery reads authMode") check(d.resolvedURL?.absoluteString == "http://127.0.0.1:3210", "resolvedURL from baseUrl") case .failure(let e): check(false, "discovery load failed: \(e)") } let missingHome = NSTemporaryDirectory() + "ccs-bar-check-missing-\(ProcessInfo.processInfo.globallyUniqueString)" switch BarDiscovery.load(home: missingHome) { case .failure(.missing): check(true, "absent bar.json -> .missing (offline state)") case .success: check(false, "expected .missing for absent file") case .failure(let e): check(false, "expected .missing, got \(e)") } // MARK: BarFormatting // Tri-state quota label: never a bare "--". check(BarFormatting.quotaLabel(percentage: 82.4, status: "ok") == "82%", "ok+pct -> NN%") check( BarFormatting.quotaLabel(percentage: nil, status: "unsupported") == "no quota", "unsupported -> 'no quota'") check( BarFormatting.quotaLabel(percentage: nil, status: "error") == "quota ?", "error -> 'quota ?'") check(BarFormatting.costLabel(3.2) == "$3.20", "cost label formats") check(BarFormatting.costLabel(0) == "", "zero cost hidden") check(BarFormatting.costLabel(nil) == "", "nil cost hidden") // MARK: statusTitle fallback chain (NEVER a bare "--", NEVER a lifetime figure when today==0) // Shared analytics fixtures for the chain. func mkAnalytics(allTimeCost: Double, todayCost: Double = 0) -> BarAnalytics { BarAnalytics( today: .init(cost: todayCost, requests: todayCost > 0 ? 10 : 0), last7d: .init(cost: 0, requests: 0), last30d: .init(cost: 0, requests: 0), allTime: .init(cost: allTimeCost, requests: 5314), byDay: [], topModels: [], topModelsWindow: "all", lastActivityAt: "2026-04-29T10:00:00Z", daysSinceLastActivity: 40, hasRecentData: false, generatedAt: "2026-06-08T13:00:00Z") } // (1) QUOTA wins: a quota row yields " NN%". do { let rows = try JSONDecoder().decode([BarSummaryRow].self, from: Data(summaryJSON.utf8)) let title = BarFormatting.statusTitle(rows: rows, analytics: mkAnalytics(allTimeCost: 2609.1)) check(title == "agy 82%", "title (1) quota wins -> 'agy 82%'") } // (1) lowest-remaining among quota rows is chosen; unsupported/error skipped. let quotaMix = [ BarSummaryRow(accountId: "a", provider: "agy", quotaPercentage: 90, quotaStatus: "ok"), BarSummaryRow(accountId: "b", provider: "agy", quotaPercentage: 12, quotaStatus: "ok"), BarSummaryRow(accountId: "c", provider: "ghcp", quotaStatus: "unsupported"), ] let mixTitle = BarFormatting.statusTitle(rows: quotaMix, analytics: nil) check(mixTitle == "agy 12%", "title features lowest-remaining quota, skips unsupported") check(!mixTitle.contains("--"), "quota-mix title never contains '--'") // (2) TODAY COST wins when no quota but analytics.today.cost > 0. // The title reads the fresh analytics aggregate, not per-row today_cost. let todayRows = [ BarSummaryRow(accountId: "a", provider: "ghcp", quotaStatus: "unsupported"), ] let todayAnalytics = mkAnalytics(allTimeCost: 40844, todayCost: 3.2) check( BarFormatting.statusTitle(rows: todayRows, analytics: todayAnalytics) == "$3.20", "title (2) today cost from analytics -> '$3.20'") // (2) today cost zero with non-zero all-time must NOT produce a "~$..." lifetime // figure — the all-time step was removed to prevent confusion with live spend. let staleRows = [ BarSummaryRow(accountId: "a", provider: "ghcp", quotaStatus: "unsupported", todayCost: 0), BarSummaryRow(accountId: "b", provider: "kiro", quotaStatus: "unsupported", todayCost: nil), ] let staleAnalytics = mkAnalytics(allTimeCost: 2609.1, todayCost: 0) let staleTitle = BarFormatting.statusTitle(rows: staleRows, analytics: staleAnalytics) check(!staleTitle.contains("~$"), "today==0 title never shows a '~$' lifetime figure") check(!staleTitle.contains("2.6k"), "today==0 title never shows the all-time dollar amount") check(!staleTitle.contains("--"), "stale title never contains '--'") // With no quota and today==0 and 2 active rows, chain falls to COUNT. check(staleTitle == "CCS 2", "title (3) no quota + today==0 falls to active count 'CCS 2'") // (3) COUNT / ATTENTION when no quota and no today spend. let reauthRows = [ BarSummaryRow(accountId: "a", provider: "ghcp", quotaStatus: "error", needsReauth: true), BarSummaryRow(accountId: "b", provider: "kiro", quotaStatus: "unsupported"), ] check( BarFormatting.statusTitle(rows: reauthRows, analytics: mkAnalytics(allTimeCost: 0)) == "CCS 1!", "title (3) reauth attention -> 'CCS 1!'") let activeRows = [ BarSummaryRow(accountId: "a", provider: "ghcp", quotaStatus: "unsupported"), BarSummaryRow(accountId: "b", provider: "kiro", paused: true, quotaStatus: "unsupported"), ] check( BarFormatting.statusTitle(rows: activeRows, analytics: nil) == "CCS 1", "title (3) active count -> 'CCS 1'") // (4) empty rows -> "CCS". check(BarFormatting.statusTitle(rows: [], analytics: nil) == "CCS", "title (4) empty -> 'CCS'") // All-empty-quota fixture: title never a bare "--". check( !BarFormatting.statusTitle(rows: activeRows, analytics: nil).contains("--"), "no-quota title never contains a bare '--'") // leadRow is deterministic with no quota: the is_default row, not rows.first. let leadRows = [ BarSummaryRow(accountId: "z", provider: "ghcp", quotaStatus: "unsupported", isDefault: false), BarSummaryRow(accountId: "a", provider: "kiro", quotaStatus: "unsupported", isDefault: true), ] check(BarFormatting.leadRow(leadRows)?.accountId == "a", "leadRow prefers is_default row") // MARK: RefreshDebouncer (arms at decision time) var deb = RefreshDebouncer(interval: 15) let t0 = Date(timeIntervalSince1970: 1000) check(deb.shouldRefresh(now: t0), "first force-refresh proceeds") check(!deb.shouldRefresh(now: t0.addingTimeInterval(5)), "within 15s blocked") check(!deb.shouldRefresh(now: t0.addingTimeInterval(14.9)), "just before window end blocked") check(deb.shouldRefresh(now: t0.addingTimeInterval(15)), "at/after 15s proceeds") // MARK: CCSBarClient (recording transport) let recorder = RequestRecorder() recorder.responseData = Data(summaryJSON.utf8) let client = CCSBarClient( baseURL: URL(string: "http://127.0.0.1:3210")!, transport: RecordingTransport(recorder: recorder) ) do { let rows = try await client.summary(refresh: true) check(rows.count == 2, "client.summary decodes rows") check( recorder.lastRequest?.url?.query?.contains("refresh=true") == true, "summary(refresh: true) adds ?refresh=true") } catch { check(false, "client.summary threw: \(error)") } recorder.responseData = Data("{}".utf8) recorder.lastRequest = nil do { try await client.pause(provider: "agy", accountId: "alice@example.com") check(recorder.lastRequest?.httpMethod == "POST", "pause is POST") check( recorder.lastRequest?.url?.path.hasSuffix("bulk-pause") == true, "pause hits bulk-pause endpoint") } catch { check(false, "pause threw: \(error)") } recorder.lastRequest = nil do { try await client.tierLock(provider: "agy", tier: nil) let body = recorder.lastRequest?.httpBody ?? Data() let obj = (try? JSONSerialization.jsonObject(with: body)) as? [String: Any] check(obj?["tier"] is NSNull, "tierLock(nil) serializes tier: null") check((obj?["provider"] as? String) == "agy", "tierLock sends provider") } catch { check(false, "tierLock threw: \(error)") } recorder.status = 409 do { _ = try await client.summary(refresh: false) check(false, "non-200 summary should throw") } catch CCSBarClientError.httpStatus(let code) { check(code == 409, "non-200 -> httpStatus(409)") } catch { check(false, "wrong error type: \(error)") } recorder.status = 200 // Analytics formatting + decode. check(BarFormatting.money(0) == "$0.00", "money shows zero") check(BarFormatting.money(2609.1) == "$2.6k", "money compacts thousands") check(BarFormatting.count(5314) == "5.3k", "count compacts thousands") // Mirrors the stale-snapshot reality: recent windows zero, all-time populated, // hasRecentData false, last-active in late April, 30-entry zero-filled byDay. func buildByDayJSON(count: Int) -> String { (0.. minutes only") check(BarQuotaGauge.compactDuration(minutes: 60) == "1h 0m", "compactDuration 1h exactly") check(BarQuotaGauge.compactDuration(minutes: 195) == "3h 15m", "compactDuration hours+minutes") // 44h 38m = 2678 min → 1d 20h (44%24=20, 44/24=1) check(BarQuotaGauge.compactDuration(minutes: 2678) == "1d 20h", "compactDuration 44h38m -> '1d 20h'") // 110h 27m = 6627 min → 4d 14h (110/24=4, 110%24=14) check(BarQuotaGauge.compactDuration(minutes: 6627) == "4d 14h", "compactDuration 110h27m -> '4d 14h'") check(BarQuotaGauge.compactDuration(minutes: 1440) == "1d 0h", "compactDuration exactly 24h -> '1d 0h'") // MARK: BarQuotaGauge band + fillFraction + countdown check(BarQuotaGauge.band(percentage: 82, status: "ok") == .green, "band >50 -> green") check(BarQuotaGauge.band(percentage: 51, status: "ok") == .green, "band 51 -> green (boundary)") check(BarQuotaGauge.band(percentage: 50, status: "ok") == .yellow, "band 50 -> yellow (boundary)") check(BarQuotaGauge.band(percentage: 21, status: "ok") == .yellow, "band 21 -> yellow") check(BarQuotaGauge.band(percentage: 20, status: "ok") == .orange, "band 20 -> orange (boundary)") check(BarQuotaGauge.band(percentage: 11, status: "ok") == .orange, "band 11 -> orange") check(BarQuotaGauge.band(percentage: 10, status: "ok") == .red, "band 10 -> red (boundary)") check(BarQuotaGauge.band(percentage: 0, status: "ok") == .red, "band 0 -> red") check(BarQuotaGauge.band(percentage: nil, status: "ok") == .none, "band nil pct -> none") check(BarQuotaGauge.band(percentage: 50, status: "unsupported") == .none, "band unsupported -> none") check(BarQuotaGauge.band(percentage: 50, status: "error") == .none, "band error -> none") check(BarQuotaGauge.fillFraction(percentage: 82, status: "ok") == 0.82, "fillFraction 82 -> 0.82") check(BarQuotaGauge.fillFraction(percentage: 0, status: "ok") == 0.0, "fillFraction 0 -> 0") check(BarQuotaGauge.fillFraction(percentage: 150, status: "ok") == 1.0, "fillFraction clamps >100 to 1") check(BarQuotaGauge.fillFraction(percentage: nil, status: "ok") == nil, "fillFraction nil pct -> nil") check( BarQuotaGauge.fillFraction(percentage: 50, status: "unsupported") == nil, "fillFraction unsupported -> nil") do { let now = Date(timeIntervalSince1970: 1_000_000) let in3h12m = now.addingTimeInterval(3 * 3600 + 12 * 60) let iso = ISO8601DateFormatter().string(from: in3h12m) check( BarQuotaGauge.resetCountdown(nextReset: iso, now: now) == "resets in 3h 12m", "resetCountdown formats hours+minutes") let in12m = now.addingTimeInterval(12 * 60) let iso12 = ISO8601DateFormatter().string(from: in12m) check( BarQuotaGauge.resetCountdown(nextReset: iso12, now: now) == "resets in 12m", "resetCountdown formats minutes-only") // Days tier: >=24h -> "Nd Nh" — 44h 38m -> "1d 20h", 110h 27m -> "4d 14h". let in44h38m = now.addingTimeInterval(44 * 3600 + 38 * 60) let iso44h38m = ISO8601DateFormatter().string(from: in44h38m) check( BarQuotaGauge.resetCountdown(nextReset: iso44h38m, now: now) == "resets in 1d 20h", "resetCountdown >=24h -> days tier '1d 20h'") let in110h27m = now.addingTimeInterval(110 * 3600 + 27 * 60) let iso110h27m = ISO8601DateFormatter().string(from: in110h27m) check( BarQuotaGauge.resetCountdown(nextReset: iso110h27m, now: now) == "resets in 4d 14h", "resetCountdown large duration '4d 14h'") let past = now.addingTimeInterval(-60) let isoPast = ISO8601DateFormatter().string(from: past) check( BarQuotaGauge.resetCountdown(nextReset: isoPast, now: now) == "resets soon", "resetCountdown past -> 'resets soon'") check(BarQuotaGauge.resetCountdown(nextReset: nil, now: now) == nil, "resetCountdown nil -> nil") check( BarQuotaGauge.resetCountdown(nextReset: "not-a-date", now: now) == nil, "resetCountdown unparseable -> nil") } // MARK: Glance-mode title resolution (lifetime dollar NEVER appears) do { let allTimeBig = mkAnalytics(allTimeCost: 40844, todayCost: 0) // allTime is the largest window let lifetime = BarFormatting.money(allTimeBig.allTime.cost) let rows = [ BarSummaryRow(accountId: "a", provider: "ghcp", quotaStatus: "unsupported"), BarSummaryRow(accountId: "b", provider: "kiro", quotaStatus: "unsupported"), ] // For EVERY mode, the title must never be the lifetime figure. for mode in BarGlanceMode.allCases { let t = BarFormatting.statusTitle(rows: rows, analytics: allTimeBig, mode: mode) check(t != lifetime, "glance mode \(mode.rawValue): title never == lifetime dollar") } // .todaySpend leads with today when > 0, else falls back to .auto chain. let todayA = mkAnalytics(allTimeCost: 40844, todayCost: 3.2) check( BarFormatting.statusTitle(rows: rows, analytics: todayA, mode: .todaySpend) == "$3.20", "glance .todaySpend leads with today's spend") check( BarFormatting.statusTitle(rows: rows, analytics: allTimeBig, mode: .todaySpend) == "CCS 2", "glance .todaySpend falls back to .auto when today==0") // .monthSpend leads with monthToDate when > 0, else falls back. func mkMonth(_ mtd: Double, today: Double = 0) -> BarAnalytics { BarAnalytics( today: .init(cost: today, requests: 0), last7d: .init(cost: 0, requests: 0), last30d: .init(cost: 35000, requests: 0), monthToDate: .init(cost: mtd, requests: 0), allTime: .init(cost: 40844, requests: 0), byDay: [], topModels: [], topModelsWindow: "30d", generatedAt: "2026-06-09T00:00:00Z") } check( BarFormatting.statusTitle(rows: rows, analytics: mkMonth(1200.5), mode: .monthSpend) == BarFormatting.money(1200.5), "glance .monthSpend leads with calendar MTD") check( BarFormatting.statusTitle(rows: rows, analytics: mkMonth(0), mode: .monthSpend) == "CCS 2", "glance .monthSpend falls back to .auto when MTD==0") // MTD mode must NOT leak last30d (35000) even though it is far larger. let mtdTitle = BarFormatting.statusTitle(rows: rows, analytics: mkMonth(1200.5), mode: .monthSpend) check(!mtdTitle.contains("35"), "glance .monthSpend never shows last30d figure") // .lowestQuota leads with the lowest ok-quota row, else falls back. let quotaRows = [ BarSummaryRow(accountId: "a", provider: "agy", quotaPercentage: 90, quotaStatus: "ok"), BarSummaryRow(accountId: "b", provider: "agy", quotaPercentage: 12, quotaStatus: "ok"), ] check( BarFormatting.statusTitle(rows: quotaRows, analytics: nil, mode: .lowestQuota) == "agy 12%", "glance .lowestQuota leads with lowest remaining quota") check( BarFormatting.statusTitle(rows: rows, analytics: allTimeBig, mode: .lowestQuota) == "CCS 2", "glance .lowestQuota falls back to .auto with no ok-quota rows") // .accountCount = non-paused count, never appends "!". let pausedMix = [ BarSummaryRow(accountId: "a", provider: "ghcp", quotaStatus: "unsupported"), BarSummaryRow(accountId: "b", provider: "kiro", paused: true, quotaStatus: "unsupported"), ] check( BarFormatting.statusTitle(rows: pausedMix, analytics: nil, mode: .accountCount) == "CCS 1", "glance .accountCount = non-paused count") let allPaused = [ BarSummaryRow(accountId: "a", provider: "ghcp", paused: true, quotaStatus: "unsupported"), BarSummaryRow(accountId: "b", provider: "kiro", paused: true, quotaStatus: "unsupported"), ] check( BarFormatting.statusTitle(rows: allPaused, analytics: nil, mode: .accountCount) == "CCS 2", "glance .accountCount falls back to total when all paused") let reauthCount = [ BarSummaryRow(accountId: "a", provider: "ghcp", quotaStatus: "error", needsReauth: true) ] check( !BarFormatting.statusTitle(rows: reauthCount, analytics: nil, mode: .accountCount).contains("!"), "glance .accountCount never appends attention '!'") } // MARK: BarAlertPrefsStore dictionary codec + level parsing do { // Empty dict -> all defaults. let p = BarAlertPrefsStore.decode(from: [:]) check(p.quotaEnabled == true, "prefs default quotaEnabled true") check(p.quotaLevels == [20, 10, 0], "prefs default quota levels 20,10,0") check(p.dailyCapUSD == 500, "prefs default daily cap 500") check(p.monthCapUSD == 10000, "prefs default month cap 10000") check(p.glanceMode == .auto, "prefs default glance mode auto") // Round-trip encode -> decode is identity. let custom = BarAlertPrefs( quotaEnabled: false, quotaLevels: [5, 25, 50], dailyCapUSD: 12.5, monthCapUSD: 2000, glanceMode: .monthSpend) let back = BarAlertPrefsStore.decode(from: BarAlertPrefsStore.encode(custom)) check(back.quotaEnabled == false, "prefs round-trip quotaEnabled") check(back.quotaLevels == [50, 25, 5], "prefs round-trip levels sorted desc") check(back.dailyCapUSD == 12.5, "prefs round-trip daily cap") check(back.glanceMode == .monthSpend, "prefs round-trip glance mode") // Level parsing: clamp, dedupe, sort desc; garbage tokens dropped. check( BarAlertPrefsStore.parseQuotaLevels("0, 10, 200, 10, -5, foo") == [100, 10, 0], "parseQuotaLevels clamps/dedupes/sorts and drops garbage") } // MARK: BarAlertEngine — pure rule engine let engineNow = Date(timeIntervalSince1970: 1_700_000_000) // fixed clock let utc = { () -> Calendar in var c = Calendar(identifier: .gregorian) c.timeZone = TimeZone(identifier: "UTC")! return c }() func mkSpendAnalytics(today: Double, month: Double) -> BarAnalytics { BarAnalytics( today: .init(cost: today, requests: 0), last7d: .init(cost: 0, requests: 0), last30d: .init(cost: 99999, requests: 0), // intentionally huge to prove it's unused monthToDate: .init(cost: month, requests: 0), allTime: .init(cost: 999999, requests: 0), // intentionally huge to prove it's unused byDay: [], topModels: [], topModelsWindow: "30d", generatedAt: "2026-06-09T00:00:00Z") } // (A) Threshold cross: quota dropping below levels fires exactly ONE notif at the // most-severe crossed level (L10 for remaining 5 with levels [20,10,0]). do { let rows = [ BarSummaryRow( accountId: "a", provider: "agy", quotaPercentage: 5, quotaStatus: "ok", nextReset: "2026-07-01T00:00:00Z") ] let prefs = BarAlertPrefs(quotaLevels: [20, 10, 0]) let ev = BarAlertEngine.evaluate( rows: rows, analytics: nil, prefs: prefs, priorFiredKeys: [], now: engineNow, calendar: utc) let quotaNotifs = ev.toDeliver.filter { $0.kind == .quotaRemainingBelow } check(quotaNotifs.count == 1, "quota cross fires exactly ONE notif (most-severe level)") check(quotaNotifs.first?.id.hasSuffix("|L10") == true, "quota fires at most-severe crossed L10") check(quotaNotifs.first?.body.contains("5%") == true, "quota body reports remaining %") } // (B) dailySpendAbove strict > ; silent at ==cap ; driven by today (not last30d/allTime). do { let prefs = BarAlertPrefs(dailyCapUSD: 50) let over = BarAlertEngine.evaluate( rows: [], analytics: mkSpendAnalytics(today: 50.01, month: 0), prefs: prefs, priorFiredKeys: [], now: engineNow, calendar: utc) check( over.toDeliver.contains { $0.kind == .dailySpendAbove }, "daily spend fires when today > cap") let atCap = BarAlertEngine.evaluate( rows: [], analytics: mkSpendAnalytics(today: 50, month: 0), prefs: prefs, priorFiredKeys: [], now: engineNow, calendar: utc) check( !atCap.toDeliver.contains { $0.kind == .dailySpendAbove }, "daily spend silent at == cap (strict >)") } // (C) monthSpendAbove driven by monthToDate ONLY — huge last30d/allTime must not trigger. do { let prefs = BarAlertPrefs(monthCapUSD: 1000) // monthToDate UNDER cap but last30d (99999) and allTime (999999) huge → must NOT fire. let under = BarAlertEngine.evaluate( rows: [], analytics: mkSpendAnalytics(today: 0, month: 500), prefs: prefs, priorFiredKeys: [], now: engineNow, calendar: utc) check( !under.toDeliver.contains { $0.kind == .monthSpendAbove }, "month alert ignores last30d/allTime — silent when MTD under cap") let over = BarAlertEngine.evaluate( rows: [], analytics: mkSpendAnalytics(today: 0, month: 1500), prefs: prefs, priorFiredKeys: [], now: engineNow, calendar: utc) check( over.toDeliver.contains { $0.kind == .monthSpendAbove }, "month alert fires on MTD > cap") } // (D) Dedupe: a second identical evaluate with same now + same firedKeys → empty toDeliver. do { let rows = [ BarSummaryRow(accountId: "a", provider: "agy", quotaPercentage: 5, quotaStatus: "ok", nextReset: "2026-07-01T00:00:00Z") ] let prefs = BarAlertPrefs(dailyCapUSD: 10) let a1 = BarAlertEngine.evaluate( rows: rows, analytics: mkSpendAnalytics(today: 100, month: 5000), prefs: prefs, priorFiredKeys: [], now: engineNow, calendar: utc) check(!a1.toDeliver.isEmpty, "first poll delivers notifs") let a2 = BarAlertEngine.evaluate( rows: rows, analytics: mkSpendAnalytics(today: 100, month: 5000), prefs: prefs, priorFiredKeys: a1.firedKeys, now: engineNow, calendar: utc) check(a2.toDeliver.isEmpty, "second identical poll delivers nothing (dedupe)") check(a2.firedKeys == a1.firedKeys, "fired keys stable across identical polls") } // (E) Re-arm period: +1 day re-fires daily; +1 month re-fires month; new nextReset re-fires quota. do { let prefs = BarAlertPrefs(dailyCapUSD: 10, monthCapUSD: 100) let day0 = mkSpendAnalytics(today: 100, month: 500) let p0 = BarAlertEngine.evaluate( rows: [], analytics: day0, prefs: prefs, priorFiredKeys: [], now: engineNow, calendar: utc) let nextDay = engineNow.addingTimeInterval(24 * 3600) let p1 = BarAlertEngine.evaluate( rows: [], analytics: day0, prefs: prefs, priorFiredKeys: p0.firedKeys, now: nextDay, calendar: utc) check(p1.toDeliver.contains { $0.kind == .dailySpendAbove }, "daily re-fires next calendar day") let nextMonth = engineNow.addingTimeInterval(40 * 24 * 3600) let p2 = BarAlertEngine.evaluate( rows: [], analytics: day0, prefs: prefs, priorFiredKeys: p0.firedKeys, now: nextMonth, calendar: utc) check(p2.toDeliver.contains { $0.kind == .monthSpendAbove }, "month re-fires next calendar month") // New nextReset string => new quota bucket => quota re-fires. let q0rows = [ BarSummaryRow(accountId: "a", provider: "agy", quotaPercentage: 5, quotaStatus: "ok", nextReset: "2026-07-01T00:00:00Z") ] let q0 = BarAlertEngine.evaluate( rows: q0rows, analytics: nil, prefs: BarAlertPrefs(quotaLevels: [10]), priorFiredKeys: [], now: engineNow, calendar: utc) let q1rows = [ BarSummaryRow(accountId: "a", provider: "agy", quotaPercentage: 5, quotaStatus: "ok", nextReset: "2026-08-01T00:00:00Z") ] let q1 = BarAlertEngine.evaluate( rows: q1rows, analytics: nil, prefs: BarAlertPrefs(quotaLevels: [10]), priorFiredKeys: q0.firedKeys, now: engineNow, calendar: utc) check( q1.toDeliver.contains { $0.kind == .quotaRemainingBelow }, "quota re-fires on a new next_reset bucket") } // (F) Re-arm clears-then-recurs: reauth true->fires->key set; false->key dropped; true->re-fires. do { let onRows = [BarSummaryRow(accountId: "a", provider: "agy", needsReauth: true)] let offRows = [BarSummaryRow(accountId: "a", provider: "agy", needsReauth: false)] let s1 = BarAlertEngine.evaluate( rows: onRows, analytics: nil, prefs: BarAlertPrefs(), priorFiredKeys: [], now: engineNow, calendar: utc) check(s1.toDeliver.contains { $0.kind == .reauthNeeded }, "reauth fires when needsReauth true") check( s1.firedKeys.contains { $0.hasPrefix("reauthNeeded|") }, "reauth key present after firing") let s2 = BarAlertEngine.evaluate( rows: offRows, analytics: nil, prefs: BarAlertPrefs(), priorFiredKeys: s1.firedKeys, now: engineNow, calendar: utc) check( !s2.firedKeys.contains { $0.hasPrefix("reauthNeeded|") }, "reauth key dropped when condition clears") let s3 = BarAlertEngine.evaluate( rows: onRows, analytics: nil, prefs: BarAlertPrefs(), priorFiredKeys: s2.firedKeys, now: engineNow, calendar: utc) check( s3.toDeliver.contains { $0.kind == .reauthNeeded }, "reauth re-fires after clear") // Same clears-then-recurs behavior for accountCooldownOrPaused on `paused`. let pOn = [BarSummaryRow(accountId: "a", provider: "agy", paused: true)] let pOff = [BarSummaryRow(accountId: "a", provider: "agy", paused: false)] let c1 = BarAlertEngine.evaluate( rows: pOn, analytics: nil, prefs: BarAlertPrefs(), priorFiredKeys: [], now: engineNow, calendar: utc) check( c1.toDeliver.contains { $0.kind == .accountCooldownOrPaused }, "cooldown/paused fires on paused") let c2 = BarAlertEngine.evaluate( rows: pOff, analytics: nil, prefs: BarAlertPrefs(), priorFiredKeys: c1.firedKeys, now: engineNow, calendar: utc) check( !c2.firedKeys.contains { $0.hasPrefix("accountCooldownOrPaused|") }, "cooldown/paused key dropped when unpaused") } // (G) Prune / bounded: after day+month+reset rollover and account churn, fired set // holds only current-bucket keys and no keys for absent accounts. do { let prefs = BarAlertPrefs(quotaLevels: [10], dailyCapUSD: 10, monthCapUSD: 100) let rowsT0 = [ BarSummaryRow(accountId: "a", provider: "agy", quotaPercentage: 5, quotaStatus: "ok", nextReset: "2026-07-01T00:00:00Z", needsReauth: true) ] let t0 = BarAlertEngine.evaluate( rows: rowsT0, analytics: mkSpendAnalytics(today: 100, month: 500), prefs: prefs, priorFiredKeys: [], now: engineNow, calendar: utc) check(t0.firedKeys.count >= 3, "t0 accumulates quota+daily+month+reauth keys") // Roll a full month forward AND drop account "a", introduce account "b". let later = engineNow.addingTimeInterval(40 * 24 * 3600) let rowsT1 = [BarSummaryRow(accountId: "b", provider: "kiro", quotaStatus: "unsupported")] let t1 = BarAlertEngine.evaluate( rows: rowsT1, analytics: mkSpendAnalytics(today: 0, month: 0), prefs: prefs, priorFiredKeys: t0.firedKeys, now: later, calendar: utc) check( !t1.firedKeys.contains { $0.contains("|agy:a|") || $0.hasSuffix("|agy:a|on") }, "prune drops keys for departed account a") // Stale daily/month/quota buckets from t0 are gone (bucket rolled), so the set // collapses to only what's currently true (account b has no alerts) → empty. check(t1.firedKeys.isEmpty, "prune collapses stale-bucket + absent-account keys (bounded)") } // (H) Duplicate account ids from a malformed summary must not crash pruning. do { let rows = [ BarSummaryRow( accountId: "dup@example.com", provider: "agy", quotaPercentage: 5, quotaStatus: "ok", nextReset: "2026-07-01T00:00:00Z"), BarSummaryRow( accountId: "dup@example.com", provider: "agy", quotaPercentage: 4, quotaStatus: "ok", nextReset: "2026-08-01T00:00:00Z"), ] let prior: Set = [ "quotaRemainingBelow|agy:dup@example.com|2026-07-01T00:00:00Z|L10", "quotaRemainingBelow|agy:dup@example.com|2026-08-01T00:00:00Z|L10", "quotaRemainingBelow|agy:dup@example.com|2026-09-01T00:00:00Z|L10", ] let ev = BarAlertEngine.evaluate( rows: rows, analytics: nil, prefs: BarAlertPrefs(quotaLevels: [10]), priorFiredKeys: prior, now: engineNow, calendar: utc) check( ev.firedKeys.contains("quotaRemainingBelow|agy:dup@example.com|2026-07-01T00:00:00Z|L10"), "duplicate ids: prune keeps first present reset bucket") check( ev.firedKeys.contains("quotaRemainingBelow|agy:dup@example.com|2026-08-01T00:00:00Z|L10"), "duplicate ids: prune keeps second present reset bucket") check( !ev.firedKeys.contains("quotaRemainingBelow|agy:dup@example.com|2026-09-01T00:00:00Z|L10"), "duplicate ids: prune drops absent reset bucket") } // (H) Deterministic order: shuffled rows produce notifs in stable id order. do { let rows = [ BarSummaryRow(accountId: "z", provider: "agy", needsReauth: true), BarSummaryRow(accountId: "a", provider: "agy", needsReauth: true), ] let ev = BarAlertEngine.evaluate( rows: rows, analytics: nil, prefs: BarAlertPrefs(), priorFiredKeys: [], now: engineNow, calendar: utc) let reauthIds = ev.toDeliver.filter { $0.kind == .reauthNeeded }.map { $0.id } check( reauthIds == reauthIds.sorted(), "engine emits notifs in stable account-id order") } // (I) Delivery wrapper (structural): RecordingNotifier (Core protocol) gets exactly // the toDeliver ids; a repeat poll delivers nothing. final class RecordingNotifier: NotificationDelivering, @unchecked Sendable { var delivered: [String] = [] func deliver(_ n: BarNotification) { delivered.append(n.id) } } do { let notifier = RecordingNotifier() let rows = [BarSummaryRow(accountId: "a", provider: "agy", needsReauth: true)] let ev = BarAlertEngine.evaluate( rows: rows, analytics: nil, prefs: BarAlertPrefs(), priorFiredKeys: [], now: engineNow, calendar: utc) for n in ev.toDeliver { notifier.deliver(n) } check(notifier.delivered == ev.toDeliver.map { $0.id }, "notifier delivered ids == toDeliver ids") let ev2 = BarAlertEngine.evaluate( rows: rows, analytics: nil, prefs: BarAlertPrefs(), priorFiredKeys: ev.firedKeys, now: engineNow, calendar: utc) let before = notifier.delivered.count for n in ev2.toDeliver { notifier.deliver(n) } check(notifier.delivered.count == before, "repeat poll delivers nothing through notifier") } // (J) Disabled rule: quota disabled => no quota notif even on a deep cross. do { let rows = [ BarSummaryRow(accountId: "a", provider: "agy", quotaPercentage: 1, quotaStatus: "ok", nextReset: "2026-07-01T00:00:00Z") ] let ev = BarAlertEngine.evaluate( rows: rows, analytics: nil, prefs: BarAlertPrefs(quotaEnabled: false), priorFiredKeys: [], now: engineNow, calendar: utc) check( !ev.toDeliver.contains { $0.kind == .quotaRemainingBelow }, "disabled quota rule never fires") } // MARK: Native subscription rows (Claude Code / Codex) // // The backend folds first-party subscription quota into /api/bar/summary as // ordinary BarSummaryRow values (provider "claude-code" / "codex"). They must // decode, drive the Tier-1 gauge + alert engine with ZERO new engine code, and // group/label distinctly from CLIProxy pool accounts. // (N1) Decode the exact native-row payload the collector emits. let nativeJSON = """ [ { "account_id": "claude-code", "provider": "claude-code", "displayName": "Claude Code", "tier": "max", "paused": false, "quota_percentage": 15, "quotaStatus": "ok", "next_reset": "2026-06-09T20:00:00.000Z", "is_default": false, "last_activity_at": null, "today_cost": null, "health": "ok", "cached": true, "fetchedAt": "2026-06-09T13:00:00.000Z", "needsReauth": false }, { "account_id": "codex", "provider": "codex", "displayName": "Codex", "tier": "pro", "paused": false, "quota_percentage": 52, "quotaStatus": "ok", "next_reset": "2026-06-09T19:00:00.000Z", "is_default": false, "last_activity_at": null, "today_cost": null, "health": "ok", "cached": false, "fetchedAt": "2026-06-09T13:00:00.000Z", "needsReauth": false } ] """ do { let rows = try JSONDecoder().decode([BarSummaryRow].self, from: Data(nativeJSON.utf8)) check(rows.count == 2, "native: decodes claude-code + codex rows") check(rows[0].provider == "claude-code", "native: claude-code provider decodes") check(rows[0].id == "claude-code:claude-code", "native: claude-code stable id") check(rows[0].tier == "max", "native: claude-code tier decodes (max)") check(rows[0].quotaStatus == "ok", "native: claude-code quotaStatus ok") check(rows[0].todayCost == nil, "native: today_cost null decodes to nil (honest, not fake $0)") check(rows[1].provider == "codex", "native: codex provider decodes") check(rows[1].tier == "pro", "native: codex tier decodes (pro)") } // (N2) Native rows drive the Tier-1 gauge with no new code: an "ok" claude-code // row at 15% lands in the orange band with a 0.15 fill. do { let claude = BarSummaryRow( accountId: "claude-code", provider: "claude-code", displayName: "Claude Code", tier: "max", quotaPercentage: 15, quotaStatus: "ok", nextReset: "2026-06-09T20:00:00Z") check( BarQuotaGauge.band(percentage: claude.quotaPercentage, status: claude.quotaStatus) == .orange, "native: claude-code 15% -> orange gauge band") check( BarQuotaGauge.fillFraction(percentage: claude.quotaPercentage, status: claude.quotaStatus) == 0.15, "native: claude-code 15% -> 0.15 gauge fill") } // (N3) Native rows feed the alert engine with no new code: a claude-code row at 8% // with levels [20,10,0] fires quotaRemainingBelow scoped to its id. do { let rows = [ BarSummaryRow( accountId: "claude-code", provider: "claude-code", displayName: "Claude Code", tier: "max", quotaPercentage: 8, quotaStatus: "ok", nextReset: "2026-06-09T20:00:00Z") ] let ev = BarAlertEngine.evaluate( rows: rows, analytics: nil, prefs: BarAlertPrefs(quotaLevels: [20, 10, 0]), priorFiredKeys: [], now: engineNow, calendar: utc) let quota = ev.toDeliver.filter { $0.kind == .quotaRemainingBelow } check(quota.count == 1, "native: claude-code low quota fires exactly one alert") check( quota.first?.id.contains("|claude-code:claude-code|") == true, "native: quota alert scoped to claude-code:claude-code") check(quota.first?.body.contains("Claude Code") == true, "native: alert names 'Claude Code'") } // (N4) Native reauth: a claude-code row that needs reauth fires reauthNeeded // (the 401-from-usage-endpoint path on the backend). do { let rows = [ BarSummaryRow( accountId: "claude-code", provider: "claude-code", displayName: "Claude Code", quotaStatus: "error", health: "error", needsReauth: true) ] let ev = BarAlertEngine.evaluate( rows: rows, analytics: nil, prefs: BarAlertPrefs(), priorFiredKeys: [], now: engineNow, calendar: utc) check( ev.toDeliver.contains { $0.kind == .reauthNeeded }, "native: claude-code needsReauth fires reauthNeeded alert") } // (N5) Friendly provider labels + subscription classification. check( BarFormatting.providerLabel("claude-code") == "Claude Code", "native: providerLabel maps claude-code -> 'Claude Code'") check( BarFormatting.providerLabel("codex") == "Codex", "native: providerLabel maps codex -> 'Codex'") check(BarFormatting.providerLabel("agy") == "agy", "native: providerLabel passes CLIProxy keys through") check( BarFormatting.isNativeSubscription( BarSummaryRow(accountId: "claude-code", provider: "claude-code")), "native: claude-code is a native subscription") check( BarFormatting.isNativeSubscription(BarSummaryRow(accountId: "codex", provider: "codex")), "native: codex is a native subscription") check( !BarFormatting.isNativeSubscription(BarSummaryRow(accountId: "agy-a", provider: "agy")), "native: agy (CLIProxy pool) is NOT a native subscription") check( !BarFormatting.isNativeSubscription( BarSummaryRow(accountId: "pool-codex-oauth-1", provider: "codex")), "native: codex CLIProxy pool row is NOT a native subscription") // (N6) Grouping: a mixed list splits into native subscriptions (top) and pool // accounts, preserving backend order within each group. do { let mixed = [ BarSummaryRow(accountId: "pool-a", provider: "agy", quotaPercentage: 80, quotaStatus: "ok"), BarSummaryRow( accountId: "claude-code", provider: "claude-code", quotaPercentage: 40, quotaStatus: "ok"), BarSummaryRow(accountId: "pool-b", provider: "ghcp", quotaStatus: "unsupported"), BarSummaryRow( accountId: "pool-codex-oauth-1", provider: "codex", quotaPercentage: 65, quotaStatus: "ok"), BarSummaryRow(accountId: "codex", provider: "codex", quotaPercentage: 52, quotaStatus: "ok"), ] let parts = BarFormatting.partitionSubscriptions(mixed) check(parts.subscriptions.count == 2, "native: partition pulls 2 subscriptions") check(parts.pool.count == 3, "native: partition leaves 3 pool accounts") check( parts.subscriptions.map { $0.provider } == ["claude-code", "codex"], "native: subscriptions keep backend order (claude-code, codex)") check( parts.pool.map { $0.id } == ["agy:pool-a", "ghcp:pool-b", "codex:pool-codex-oauth-1"], "native: pool keeps backend order and retains CLIProxy codex row") } // (N7) Pool-only list does NOT get split (single "Accounts" header path): both // groups non-empty is the only trigger for the two-section render. do { let poolOnly = [ BarSummaryRow(accountId: "a", provider: "agy", quotaPercentage: 70, quotaStatus: "ok"), BarSummaryRow(accountId: "b", provider: "ghcp", quotaStatus: "unsupported"), ] let parts = BarFormatting.partitionSubscriptions(poolOnly) check(parts.subscriptions.isEmpty, "native: pool-only list has no subscriptions to split out") check(parts.pool.count == 2, "native: pool-only list keeps all rows in pool group") } // MARK: Per-window quota detail decode (resilient, backward compatible) do { let nativeJSON = """ [ { "account_id": "claude-code", "provider": "claude-code", "displayName": "Claude Code", "tier": "max", "paused": false, "quota_percentage": 44, "quotaStatus": "ok", "next_reset": "2026-06-09T20:00:00.000Z", "is_default": false, "last_activity_at": null, "today_cost": null, "health": "ok", "cached": false, "fetchedAt": "2026-06-09T14:00:00.000Z", "needsReauth": false, "quota_windows": [ { "key": "five_hour", "label": "5h", "usedPercent": 56, "remainingPercent": 44, "resetAt": "2026-06-09T20:00:00.000Z", "windowMinutes": 300 }, { "key": "seven_day", "label": "week", "usedPercent": 30, "remainingPercent": 70, "resetAt": "2026-06-15T00:00:00.000Z", "windowMinutes": 10080 }, { "key": "seven_day_opus", "label": "Opus · week", "usedPercent": 25, "remainingPercent": 75, "resetAt": "2026-06-15T00:00:00.000Z", "windowMinutes": 10080 }, { "key": "seven_day_sonnet", "label": "Sonnet · week", "usedPercent": 60, "remainingPercent": 40, "resetAt": "2026-06-15T00:00:00.000Z", "windowMinutes": 10080 } ] }, { "account_id": "codex", "provider": "codex", "displayName": "Codex", "tier": "pro", "paused": false, "quota_percentage": 70, "quotaStatus": "ok", "next_reset": "2026-06-09T19:00:00.000Z", "is_default": false, "last_activity_at": null, "today_cost": null, "health": "warning", "cached": false, "fetchedAt": "2026-06-09T14:00:00.000Z", "needsReauth": false, "quota_windows": [ { "key": "five_hour", "label": "5h", "usedPercent": 19, "remainingPercent": 81, "resetAt": "2026-06-09T19:00:00.000Z", "windowMinutes": 300 } ], "stale_as_of": "2026-06-06T11:00:00.000Z" } ] """ do { let rows = try JSONDecoder().decode([BarSummaryRow].self, from: Data(nativeJSON.utf8)) check(rows.count == 2, "qw: decodes two native rows") let claude = rows[0] check(claude.quotaWindows?.count == 4, "qw: claude decodes 4 windows") check(claude.quotaWindows?[0].key == "five_hour", "qw: first window key five_hour") check(claude.quotaWindows?[0].label == "5h", "qw: first window label 5h") check(claude.quotaWindows?[0].usedPercent == 56, "qw: usedPercent decodes") check(claude.quotaWindows?[0].remainingPercent == 44, "qw: remainingPercent decodes") check(claude.quotaWindows?[0].windowMinutes == 300, "qw: windowMinutes 300 decodes") check( claude.quotaWindows?[0].resetAt == "2026-06-09T20:00:00.000Z", "qw: resetAt decodes (ISO)") check( claude.quotaWindows?[2].key == "seven_day_opus", "qw: opus window present (Max)") check( claude.quotaWindows?[3].key == "seven_day_sonnet", "qw: sonnet window present (Max)") check(claude.staleAsOf == nil, "qw: claude (fresh) has nil staleAsOf") check(claude.quotaWindows?[0].id == "five_hour", "qw: window id is its key") let codex = rows[1] check(codex.quotaWindows?.count == 1, "qw: codex decodes 1 window") check(codex.staleAsOf == "2026-06-06T11:00:00.000Z", "qw: codex staleAsOf decodes (stale)") } catch { check(false, "qw: native decode failed: \(error)") } } // Legacy payload WITHOUT the two native-only keys still decodes (quotaWindows // and staleAsOf -> nil). This is the backward-compatibility guarantee. do { let legacyJSON = """ [ { "account_id": "alice@example.com", "provider": "agy", "displayName": "Alice", "tier": "ultra", "paused": false, "quota_percentage": 80, "quotaStatus": "ok", "next_reset": null, "is_default": true, "last_activity_at": null, "today_cost": null, "health": "ok", "cached": true, "fetchedAt": "2026-06-07T19:00:00Z", "needsReauth": false } ] """ do { let rows = try JSONDecoder().decode([BarSummaryRow].self, from: Data(legacyJSON.utf8)) check(rows.count == 1, "qw-legacy: legacy row decodes") check(rows[0].quotaWindows == nil, "qw-legacy: missing quota_windows -> nil") check(rows[0].staleAsOf == nil, "qw-legacy: missing stale_as_of -> nil") } catch { check(false, "qw-legacy: legacy decode failed: \(error)") } } // MARK: burnMinutesRemaining (linear single-window projection) do { // Mid-window pace: a 300-min window resets in 150 min -> elapsed = 150 min. // usedPercent = 50 -> rate = 50/150 = 1/3 %/min -> remaining 50% / (1/3) = 150. let now = Date(timeIntervalSince1970: 1_700_000_000) let resetIn150 = now.addingTimeInterval(150 * 60) let t = BarQuotaGauge.burnMinutesRemaining( usedPercent: 50, resetAt: resetIn150, windowMinutes: 300, now: now) check(t == 150, "burn: mid-window 50% over 150min elapsed -> 150 min remaining") // Near-zero usage -> nil ("plenty"); avoids an absurd projection. let lowUse = BarQuotaGauge.burnMinutesRemaining( usedPercent: 0.5, resetAt: resetIn150, windowMinutes: 300, now: now) check(lowUse == nil, "burn: near-zero usage -> nil (plenty)") // Exhausted -> 0 ("limit reached"). let exhausted = BarQuotaGauge.burnMinutesRemaining( usedPercent: 100, resetAt: resetIn150, windowMinutes: 300, now: now) check(exhausted == 0, "burn: usedPercent>=100 -> 0 (limit reached)") // Unknown window length or reset -> nil (omit pace). check( BarQuotaGauge.burnMinutesRemaining( usedPercent: 50, resetAt: resetIn150, windowMinutes: nil, now: now) == nil, "burn: nil windowMinutes -> nil") check( BarQuotaGauge.burnMinutesRemaining( usedPercent: 50, resetAt: nil, windowMinutes: 300, now: now) == nil, "burn: nil resetAt -> nil") // elapsed <= 0 (reset already further out than the full window) -> nil. let resetTooFar = now.addingTimeInterval(400 * 60) check( BarQuotaGauge.burnMinutesRemaining( usedPercent: 50, resetAt: resetTooFar, windowMinutes: 300, now: now) == nil, "burn: elapsed<=0 -> nil") } // MARK: selectBindingWindow do { let now = Date(timeIntervalSince1970: 1_700_000_000) _ = now let fiveHour = QuotaWindowDetail( key: "five_hour", label: "5h", usedPercent: 56, remainingPercent: 44, windowMinutes: 300) let week = QuotaWindowDetail( key: "seven_day", label: "week", usedPercent: 30, remainingPercent: 70, windowMinutes: 10080) let opus = QuotaWindowDetail( key: "seven_day_opus", label: "Opus · week", usedPercent: 75, remainingPercent: 25, windowMinutes: 10080) let binding = BarQuotaGauge.selectBindingWindow([fiveHour, week, opus]) check(binding?.key == "seven_day_opus", "binding: lowest remaining (opus 25%) wins") // Tie on remaining% -> shorter window first. let weekTie = QuotaWindowDetail( key: "seven_day", label: "week", usedPercent: 60, remainingPercent: 40, windowMinutes: 10080) let fiveTie = QuotaWindowDetail( key: "five_hour", label: "5h", usedPercent: 60, remainingPercent: 40, windowMinutes: 300) let tieBinding = BarQuotaGauge.selectBindingWindow([weekTie, fiveTie]) check(tieBinding?.key == "five_hour", "binding: remaining tie -> shorter window (5h) wins") check(BarQuotaGauge.selectBindingWindow([]) == nil, "binding: empty input -> nil") } // MARK: paceClause phrasing do { let now = Date(timeIntervalSince1970: 1_700_000_000) // At-risk scenario: window=300min, reset in 200min → elapsed=100min. // usedPercent=75 → burn = (100-75)*100/75 ≈ 33 min. 33 < 200 → at-risk → shows pace. let resetIn200 = ISO8601DateFormatter().string(from: now.addingTimeInterval(200 * 60)) let finite = BarQuotaGauge.paceClause( usedPercent: 75, remainingPercent: 25, resetAt: resetIn200, windowMinutes: 300, now: now) check(finite?.hasPrefix("~") == true && finite?.hasSuffix("left at this pace") == true, "pace: at-risk (burn < reset) -> shows pace clause") // NOT at-risk: burn > minutesToReset — pace must be nil (the core fix). // window=300min, reset in 50min → elapsed=250min. usedPercent=50 → // burn = (100-50)*250/50 = 250 min. 250 > 50 → NOT at risk → nil. let resetIn50 = ISO8601DateFormatter().string(from: now.addingTimeInterval(50 * 60)) let notAtRisk = BarQuotaGauge.paceClause( usedPercent: 50, remainingPercent: 50, resetAt: resetIn50, windowMinutes: 300, now: now) check(notAtRisk == nil, "pace: burn > reset (not at risk) -> nil (omit — core bug fix)") // Lots of headroom -> "plenty at this pace". let resetIn150 = ISO8601DateFormatter().string(from: now.addingTimeInterval(150 * 60)) let plenty = BarQuotaGauge.paceClause( usedPercent: 10, remainingPercent: 90, resetAt: resetIn150, windowMinutes: 300, now: now) check(plenty == "plenty at this pace", "pace: >=85% remaining -> 'plenty at this pace'") // Near-zero usage -> "plenty at this pace" (burn nil but window known). let plentyLow = BarQuotaGauge.paceClause( usedPercent: 0.5, remainingPercent: 99.5, resetAt: resetIn150, windowMinutes: 300, now: now) check(plentyLow == "plenty at this pace", "pace: near-zero usage -> 'plenty at this pace'") // Exhausted -> "limit reached, resets in ...". let reached = BarQuotaGauge.paceClause( usedPercent: 100, remainingPercent: 0, resetAt: resetIn150, windowMinutes: 300, now: now) check( reached?.hasPrefix("limit reached, resets in") == true, "pace: 0% remaining -> 'limit reached, resets in ...'") // < 5 min floor -> limit-reached path, never a scary "~0m". // window=300min, reset in 150min → elapsed=150min. usedPercent=99 → // burn = (100-99)*150/99 ≈ 1.5 min → floor hit → "limit reached". let nearFloor = BarQuotaGauge.paceClause( usedPercent: 99, remainingPercent: 1, resetAt: resetIn150, windowMinutes: 300, now: now) check( nearFloor?.hasPrefix("limit reached") == true && !(nearFloor?.contains("~0m") ?? true), "pace: <5m floor -> limit-reached, never '~0m'") // Unknown window -> nil (omit). let unknown = BarQuotaGauge.paceClause( usedPercent: 50, remainingPercent: 50, resetAt: nil, windowMinutes: nil, now: now) check(unknown == nil, "pace: unknown window -> nil (omit)") // resetAt in the past -> nil pace. let pastReset = ISO8601DateFormatter().string(from: now.addingTimeInterval(-60)) let past = BarQuotaGauge.paceClause( usedPercent: 50, remainingPercent: 50, resetAt: pastReset, windowMinutes: 300, now: now) check(past == nil, "pace: past resetAt -> nil pace") } // MARK: atRisk boolean do { let now = Date(timeIntervalSince1970: 1_700_000_000) // At-risk: burn(33m) < minutesToReset(200m) → true. let resetIn200 = ISO8601DateFormatter().string(from: now.addingTimeInterval(200 * 60)) let isAtRisk = BarQuotaGauge.atRisk( usedPercent: 75, remainingPercent: 25, resetAt: resetIn200, windowMinutes: 300, now: now) check(isAtRisk == true, "atRisk: burn < reset -> true") // NOT at-risk: burn(250m) > minutesToReset(50m) → false. let resetIn50 = ISO8601DateFormatter().string(from: now.addingTimeInterval(50 * 60)) let notAtRisk = BarQuotaGauge.atRisk( usedPercent: 50, remainingPercent: 50, resetAt: resetIn50, windowMinutes: 300, now: now) check(notAtRisk == false, "atRisk: burn > reset -> false") // Exhausted (remaining=0) → false (handled by limit-reached path, not atRisk). let resetIn150 = ISO8601DateFormatter().string(from: now.addingTimeInterval(150 * 60)) check( BarQuotaGauge.atRisk( usedPercent: 100, remainingPercent: 0, resetAt: resetIn150, windowMinutes: 300, now: now) == false, "atRisk: exhausted -> false") // Unknown window → false. check( BarQuotaGauge.atRisk( usedPercent: 75, remainingPercent: 25, resetAt: nil, windowMinutes: nil, now: now) == false, "atRisk: unknown window -> false") // Large weekly burn in screenshot scenario: weekly window resets in ~9h 2m (542 min), // usedPercent=59 over elapsed=(10080-542)=9538 min → burn ≈ (41*9538)/59 ≈ 6626 min. // 6626 > 542 → NOT at risk (this was the nonsensical "~110h left" shown to user). let resetIn542m = ISO8601DateFormatter().string(from: now.addingTimeInterval(542 * 60)) let weeklyNotAtRisk = BarQuotaGauge.atRisk( usedPercent: 59, remainingPercent: 41, resetAt: resetIn542m, windowMinutes: 10080, now: now) check(weeklyNotAtRisk == false, "atRisk: weekly window resetting in 9h, burn projecting 100+h -> false (screenshot fix)") } // MARK: headroomLeader do { func subRow(_ provider: String, _ name: String, remaining: Double) -> BarSummaryRow { BarSummaryRow( accountId: provider, provider: provider, displayName: name, quotaStatus: "ok", quotaWindows: [ QuotaWindowDetail( key: "five_hour", label: "5h", usedPercent: 100 - remaining, remainingPercent: remaining, windowMinutes: 300) ]) } let claude = subRow("claude-code", "Claude Code", remaining: 44) let codex = subRow("codex", "Codex", remaining: 81) let errorRow = BarSummaryRow( accountId: "broken", provider: "claude-code", displayName: "Broken", quotaStatus: "error") let leader = BarQuotaGauge.headroomLeader([claude, codex, errorRow]) check(leader?.label == "Codex", "headroom: highest-binding-remaining (Codex 81%) leads") check(leader?.remainingPercent == 81, "headroom: leader carries its remaining%") // Error/reauth rows (no binding window) are excluded from the count. let single = BarQuotaGauge.headroomLeader([claude, errorRow]) check(single == nil, "headroom: <2 eligible subscriptions -> nil") check(BarQuotaGauge.headroomLeader([]) == nil, "headroom: empty -> nil") } // MARK: - Theme token model (BarPalette / BarAppearance / BarTheme) // // These assert on the raw RGB Doubles, NOT on SwiftUI Color equality (which is // unreliable — identical components are not guaranteed `==`). The dark values // are the regression lock: they must equal the original Sparkline constants. // Forced-scheme mapping: .system inherits OS, .light/.dark override. check(BarAppearance.system.forced == nil, "appearance .system -> forced nil (inherit OS)") check(BarAppearance.light.forced == .light, "appearance .light -> forced .light") check(BarAppearance.dark.forced == .dark, "appearance .dark -> forced .dark") check(BarAppearance.allCases.count == 3, "appearance has exactly 3 cases") // DARK == today (regression lock). Exact constants from the original // Sparkline BarTheme enum — any drift fails the build. check(BarPalette.dark.accentRGB == RGB(0.886, 0.451, 0.137), "dark accent == #E2732A (locked)") check( BarPalette.dark.subscriptionRGB == RGB(0.357, 0.388, 0.851), "dark subscription == #5B63D9 (locked)") check(BarPalette.dark.bandGreenRGB == RGB(0.36, 0.74, 0.56), "dark bandGreen == #5CBC8F (locked)") check(BarPalette.dark.bandAmberRGB == RGB(0.86, 0.67, 0.31), "dark bandAmber == #DBAB4F (locked)") check(BarPalette.dark.bandCoralRGB == RGB(0.91, 0.46, 0.36), "dark bandCoral == #E8755C (locked)") check(BarPalette.dark.bandRedRGB == RGB(0.85, 0.34, 0.31), "dark bandRed == #D9564F (locked)") // LIGHT differs from DARK for every themed token (so light can't silently // collapse back to dark) AND equals the locked light values. check(BarPalette.light.accentRGB != BarPalette.dark.accentRGB, "light accent differs from dark") check( BarPalette.light.subscriptionRGB != BarPalette.dark.subscriptionRGB, "light subscription differs from dark") check( BarPalette.light.bandGreenRGB != BarPalette.dark.bandGreenRGB, "light bandGreen differs from dark") check( BarPalette.light.bandAmberRGB != BarPalette.dark.bandAmberRGB, "light bandAmber differs from dark") check( BarPalette.light.bandCoralRGB != BarPalette.dark.bandCoralRGB, "light bandCoral differs from dark") check(BarPalette.light.bandRedRGB != BarPalette.dark.bandRedRGB, "light bandRed differs from dark") check(BarPalette.light.accentRGB == RGB(0.812, 0.357, 0.063), "light accent == #CF5B10 (locked)") check( BarPalette.light.subscriptionRGB == RGB(0.275, 0.302, 0.745), "light subscription == #464DBE (locked)") check(BarPalette.light.bandGreenRGB == RGB(0.106, 0.580, 0.357), "light bandGreen == #1B945B (locked)") check(BarPalette.light.bandAmberRGB == RGB(0.722, 0.490, 0.043), "light bandAmber == #B87D0B (locked)") check(BarPalette.light.bandCoralRGB == RGB(0.831, 0.302, 0.157), "light bandCoral == #D44D28 (locked)") check(BarPalette.light.bandRedRGB == RGB(0.776, 0.157, 0.137), "light bandRed == #C62823 (locked)") // Light ramp separability: the four band colors must be mutually distinct so // the green→amber→coral→red status ramp stays readable on the light plate. // (We assert distinctness, not a lightness ordering: hue, not luminance, is what // separates these bands — amber and coral are intentionally near in luminance.) let lightBands = [ BarPalette.light.bandGreenRGB, BarPalette.light.bandAmberRGB, BarPalette.light.bandCoralRGB, BarPalette.light.bandRedRGB, ] check(Set(lightBands.map { "\($0.r),\($0.g),\($0.b)" }).count == 4, "light ramp: 4 distinct bands") // Red is the deepest/most-saturated end of the ramp: it has the lowest green // channel, anchoring "critical" as the visually heaviest band. check( BarPalette.light.bandRedRGB.g < BarPalette.light.bandGreenRGB.g && BarPalette.light.bandRedRGB.g < BarPalette.light.bandAmberRGB.g && BarPalette.light.bandRedRGB.g < BarPalette.light.bandCoralRGB.g, "light ramp: red has the lowest green channel (critical anchor)") // Resolver picks the right palette per scheme (verified via the stored palette // ref, staying Color-equality-free). check(BarTheme.resolve(.dark).palette == BarPalette.dark, "resolve(.dark) draws from dark palette") check( BarTheme.resolve(.light).palette == BarPalette.light, "resolve(.light) draws from light palette") check(BarTheme.dark.palette == BarPalette.dark, "BarTheme.dark preset uses dark palette") check(BarTheme.light.palette == BarPalette.light, "BarTheme.light preset uses light palette") check(BarThemeKey.defaultValue.palette == BarPalette.dark, "environment default is the dark preset") // BarAppearanceStore round-trips, defaults to .dark on an absent key. Use an // isolated suite so we never pollute (or depend on) real user defaults. do { let suiteName = "ccs-bar-check-\(ProcessInfo.processInfo.globallyUniqueString)" let defaults = UserDefaults(suiteName: suiteName)! defaults.removeObject(forKey: BarAppearanceStore.defaultsKey) // Absent key -> .dark (the no-registration fallback). let raw = defaults.string(forKey: BarAppearanceStore.defaultsKey) ?? BarAppearance.dark.rawValue check(BarAppearance(rawValue: raw) == .dark, "appearance store defaults to .dark on absent key") // Round-trip save(.light) -> load(). defaults.set(BarAppearance.light.rawValue, forKey: BarAppearanceStore.defaultsKey) let back = BarAppearance(rawValue: defaults.string(forKey: BarAppearanceStore.defaultsKey) ?? "") check(back == .light, "appearance store round-trips save(.light) -> load() == .light") // Garbage string -> nil (load() coalesces to .dark). defaults.set("nonsense", forKey: BarAppearanceStore.defaultsKey) let g = BarAppearance(rawValue: defaults.string(forKey: BarAppearanceStore.defaultsKey) ?? "") check(g == nil, "appearance store: garbage raw -> nil (load() coalesces to .dark)") defaults.removePersistentDomain(forName: suiteName) } // MARK: SpendChartStyle — enum stability + store round-trip // Default is .bars (the pre-existing render mode; no visual regression on upgrade). check(SpendChartStyle.bars.rawValue == "bars", "SpendChartStyle .bars rawValue stable") check(SpendChartStyle.line.rawValue == "line", "SpendChartStyle .line rawValue stable") check(SpendChartStyle(rawValue: "bars") == .bars, "SpendChartStyle round-trip bars") check(SpendChartStyle(rawValue: "line") == .line, "SpendChartStyle round-trip line") check(SpendChartStyle(rawValue: "unknown") == nil, "SpendChartStyle unknown rawValue -> nil") check(SpendChartStyle.allCases.count == 2, "SpendChartStyle has exactly 2 cases") // Store round-trip: save .line -> load() == .line; absent key defaults to .bars. do { let suiteName = "ccs-bar-check-chart-\(ProcessInfo.processInfo.globallyUniqueString)" let defaults = UserDefaults(suiteName: suiteName)! // Absent key -> .bars (the default). let raw = defaults.string(forKey: SpendChartStyleStore.defaultsKey) ?? SpendChartStyle.bars.rawValue check(SpendChartStyle(rawValue: raw) == .bars, "SpendChartStyleStore defaults to .bars on absent key") // Save .line -> load .line. defaults.set(SpendChartStyle.line.rawValue, forKey: SpendChartStyleStore.defaultsKey) let back = SpendChartStyle(rawValue: defaults.string(forKey: SpendChartStyleStore.defaultsKey) ?? "") check(back == .line, "SpendChartStyleStore round-trips save(.line) -> .line") // Garbage -> nil (coalesces to .bars on live load). defaults.set("nonsense", forKey: SpendChartStyleStore.defaultsKey) let g = SpendChartStyle(rawValue: defaults.string(forKey: SpendChartStyleStore.defaultsKey) ?? "") check(g == nil, "SpendChartStyleStore garbage raw -> nil (coalesces to .bars)") defaults.removePersistentDomain(forName: suiteName) } // MARK: BarScreenPicker — pure geometry helpers (#1502 / #1503) // // BarScreenPicker lives in CCSBarApp (AppKit-only target), so we can't import it // here. We test the equivalent geometry directly: distanceSquared and the // index-selection logic expressed over plain CGRect/CGPoint values — the same // arithmetic BarScreenPicker runs at runtime. NSPoint/NSRect are CGPoint/CGRect // on macOS, so the results are identical. do { // distanceSquared: point inside rect -> 0. let r = CGRect(x: 0, y: 0, width: 1920, height: 1080) let inside = CGPoint(x: 960, y: 540) let dxIn = max(r.minX - inside.x, 0, inside.x - r.maxX) let dyIn = max(r.minY - inside.y, 0, inside.y - r.maxY) check(dxIn * dxIn + dyIn * dyIn == 0, "screenPicker: point inside rect -> distance 0") // distanceSquared: point to the right of rect. let rightOf = CGPoint(x: 2000, y: 540) let dx = max(r.minX - rightOf.x, 0, rightOf.x - r.maxX) // 2000 - 1920 = 80 let dy = max(r.minY - rightOf.y, 0, rightOf.y - r.maxY) // 0 check(dx == 80 && dy == 0, "screenPicker: point right of rect -> dx=80, dy=0") check(dx * dx + dy * dy == 6400, "screenPicker: rightOf distance^2 = 6400") // Screen selection: pick the screen containing the point. // Simulate two side-by-side displays: left (0..1920) and right (1920..3840), // each 1080 tall. A click at x=2500 is on the right display. let leftFrame = CGRect(x: 0, y: 0, width: 1920, height: 1080) let rightFrame = CGRect(x: 1920, y: 0, width: 1920, height: 1080) let clickOnRight = CGPoint(x: 2500, y: 50) // Contains check (exact hit on right). let exactHit = rightFrame.contains(clickOnRight) check(exactHit, "screenPicker: click at x=2500 is contained by right display frame") let noHitLeft = leftFrame.contains(clickOnRight) check(!noHitLeft, "screenPicker: click at x=2500 is NOT on the left display frame") // Gap scenario: click at x=-10 (left of leftFrame), should pick left (closest). let gapClick = CGPoint(x: -10, y: 540) let dLeft: CGFloat = { let ddx = max(leftFrame.minX - gapClick.x, 0, gapClick.x - leftFrame.maxX) let ddy = max(leftFrame.minY - gapClick.y, 0, gapClick.y - leftFrame.maxY) return ddx * ddx + ddy * ddy }() let dRight: CGFloat = { let ddx = max(rightFrame.minX - gapClick.x, 0, gapClick.x - rightFrame.maxX) let ddy = max(rightFrame.minY - gapClick.y, 0, gapClick.y - rightFrame.maxY) return ddx * ddx + ddy * ddy }() check(dLeft < dRight, "screenPicker: gap click left of left display -> left is closest") // Screen A is the one whose frame.contains(click) returns true; that is the // screen that should be selected for panel anchoring (the fix for #1502). let screens = [leftFrame, rightFrame] let selectedIdx = screens.firstIndex(where: { $0.contains(clickOnRight) }) check(selectedIdx == 1, "screenPicker: exact hit selects right screen (index 1) for #1502 anchor") } // MARK: BarVersionDisplay — pure display-string helper check(BarVersionDisplay.displayString(for: nil) == nil, "version nil raw -> nil (no dangling 'v')") check(BarVersionDisplay.displayString(for: "") == nil, "version empty raw -> nil (no dangling 'v')") check(BarVersionDisplay.displayString(for: "1.5.0") == "v1.5.0", "version '1.5.0' -> 'v1.5.0'") check(BarVersionDisplay.displayString(for: "2.0.0") == "v2.0.0", "version '2.0.0' -> 'v2.0.0'") // Outside a bundle, string() must not crash and returns nil (no assertion on value — bundle absent). let _ = BarVersionDisplay.string() // MARK: - BarLaunchDescriptor JSON decoding // (L1) Full descriptor with ccsHome round-trips correctly. do { let json = """ { "schema": 1, "runtime": "/usr/local/bin/node", "args": ["/home/kai/.ccs/bin/ccs.js", "bar", "serve"], "home": "/home/kai", "ccsHome": "/home/kai/.ccs-custom" } """ let d = try JSONDecoder().decode(BarLaunchDescriptor.self, from: Data(json.utf8)) check(d.schema == 1, "descriptor: schema decodes to 1") check(d.runtime == "/usr/local/bin/node", "descriptor: runtime decodes") check(d.args == ["/home/kai/.ccs/bin/ccs.js", "bar", "serve"], "descriptor: args decode") check(d.home == "/home/kai", "descriptor: home decodes") check(d.ccsHome == "/home/kai/.ccs-custom", "descriptor: ccsHome decodes when present") } // (L2) Descriptor without ccsHome (optional field absent) decodes to nil. do { let json = """ { "schema": 1, "runtime": "/opt/homebrew/bin/bun", "args": ["/usr/local/lib/ccs/dist/index.js", "bar", "serve"], "home": "/Users/kai" } """ let d = try JSONDecoder().decode(BarLaunchDescriptor.self, from: Data(json.utf8)) check(d.ccsHome == nil, "descriptor: absent ccsHome decodes to nil") check(d.runtime == "/opt/homebrew/bin/bun", "descriptor: bun runtime decodes") check(d.args.count == 3, "descriptor: args count is 3") check(d.args.last == "serve", "descriptor: last arg is 'serve'") } // (L3) defaultPath uses ~/.ccs/bar/launch.json. do { let home = "/Users/testuser" let path = BarLaunchDescriptor.defaultPath(home: home) check(path.hasSuffix("/.ccs/bar/launch.json"), "descriptor: defaultPath ends with /.ccs/bar/launch.json") check(path.hasPrefix(home), "descriptor: defaultPath starts with home dir") } // (L4) Malformed JSON is non-decodable (no crash, just nil from try?). do { let bad = Data("{\"schema\": \"not-an-int\"}".utf8) let d = try? JSONDecoder().decode(BarLaunchDescriptor.self, from: bad) check(d == nil, "descriptor: malformed JSON decodes to nil (no crash)") } // MARK: - BarServerProbe ordering and selection // Mock transport that returns 200 for exactly one (host, port) combination and // times out (throws) for everything else. Used to verify probe ordering. final class SelectiveTransport: HTTPTransport, @unchecked Sendable { /// The URL prefix that should succeed (e.g. "http://127.0.0.1:3001"). let successPrefix: String /// Track every URL probed, in order. var probed: [String] = [] init(successPrefix: String) { self.successPrefix = successPrefix } func send(_ request: URLRequest) async throws -> (Data, HTTPURLResponse) { let urlStr = request.url?.absoluteString ?? "" probed.append(urlStr) if urlStr.hasPrefix(successPrefix) { let http = HTTPURLResponse( url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)! return (Data(), http) } // Simulate connection refused — throw so the probe moves on. throw URLError(.cannotConnectToHost) } } // (P1) bar.json port is probed FIRST, then fallbacks in order. // With no live server the probe visits: bar.json port (127.0.0.1 then ::1), // then 3000, 3001, ... until it finds one or exhausts all. do { // bar.json says port 9999 (unusual, not in fallbacks). We make 127.0.0.1:9999 succeed. let transport = SelectiveTransport(successPrefix: "http://127.0.0.1:9999") let probe = BarServerProbe(transport: transport) let discovery = BarDiscovery(baseUrl: "http://127.0.0.1:9999", port: 9999, authMode: "loopback") let result = await probe.findLiveServer(discovery: discovery) // The first URL probed must be the bar.json port on 127.0.0.1. check( transport.probed.first?.hasPrefix("http://127.0.0.1:9999") == true, "probe: bar.json port is tried first (127.0.0.1)") check(result?.absoluteString.hasPrefix("http://127.0.0.1:9999") == true, "probe: returns live URL when bar.json port responds 200") } // (P2) Falls through to fallback list when bar.json port is dead. // Make port 3001 on 127.0.0.1 the live one. bar.json points at dead port 9999. do { let transport = SelectiveTransport(successPrefix: "http://127.0.0.1:3001") let probe = BarServerProbe(transport: transport) let discovery = BarDiscovery(baseUrl: "http://127.0.0.1:9999", port: 9999, authMode: "loopback") let result = await probe.findLiveServer(discovery: discovery) check(result?.absoluteString.hasPrefix("http://127.0.0.1:3001") == true, "probe: falls through to fallback 3001 when bar.json port 9999 is dead") // bar.json port 9999 must have been probed before any fallback. let idx9999 = transport.probed.firstIndex(where: { $0.contains(":9999/") }) let idx3001 = transport.probed.firstIndex(where: { $0.contains(":3001/") }) if let i = idx9999, let j = idx3001 { check(i < j, "probe: bar.json port (9999) tried before fallback port (3001)") } else { check(false, "probe: expected probes for ports 9999 and 3001") } } // (P3) IPv4 tried before IPv6 for each port. // Make only the IPv6 address of 3000 succeed so we can verify that 127.0.0.1 // was tried before [::1] for the same port. do { let transport = SelectiveTransport(successPrefix: "http://[::1]:3000") let probe = BarServerProbe(transport: transport) // No bar.json — nil discovery. let result = await probe.findLiveServer(discovery: nil) check(result?.absoluteString.hasPrefix("http://[::1]:3000") == true, "probe: IPv6 [::1]:3000 selected when 127.0.0.1:3000 is dead") let idx4 = transport.probed.firstIndex(where: { $0.contains("127.0.0.1:3000") }) let idx6 = transport.probed.firstIndex(where: { $0.contains("[::1]:3000") }) if let i = idx4, let j = idx6 { check(i < j, "probe: 127.0.0.1 tried before [::1] for same port") } else { check(false, "probe: expected probes for both 127.0.0.1:3000 and [::1]:3000") } } // (P4) Returns nil when no server responds. do { // A transport that always throws — simulates total offline. struct DeadTransport: HTTPTransport { func send(_ request: URLRequest) async throws -> (Data, HTTPURLResponse) { throw URLError(.cannotConnectToHost) } } let probe = BarServerProbe(transport: DeadTransport()) let result = await probe.findLiveServer(discovery: nil) check(result == nil, "probe: returns nil when no server responds") } // (P5) Non-200 response is treated as dead (not live). do { struct Status404Transport: HTTPTransport { func send(_ request: URLRequest) async throws -> (Data, HTTPURLResponse) { let http = HTTPURLResponse( url: request.url!, statusCode: 404, httpVersion: nil, headerFields: nil)! return (Data(), http) } } let probe = BarServerProbe(transport: Status404Transport()) let result = await probe.findLiveServer(discovery: nil) check(result == nil, "probe: 404 response treated as dead (not live)") } // (P6) Fallback list deduplication: when bar.json port equals a fallback port // (e.g. 3000), that port is not probed twice. do { let transport = SelectiveTransport(successPrefix: "http://127.0.0.1:3000") let probe = BarServerProbe(transport: transport) // bar.json port == 3000, which is also in the fallback list. let discovery = BarDiscovery(baseUrl: "http://127.0.0.1:3000", port: 3000, authMode: "loopback") _ = await probe.findLiveServer(discovery: discovery) // Count how many times :3000/ appears across all probed URLs. let port3000Count = transport.probed.filter { $0.contains(":3000/") }.count // Expect exactly 1 probe for 3000 on 127.0.0.1 (finds it immediately; stops before ::1). check(port3000Count == 1, "probe: bar.json port 3000 deduped — probed once, not twice") } // (BarUpdateChecker) Semver comparison drives the in-app "Update available" // affordance, so ordering must be numeric (1.10.0 > 1.9.0), tolerate // pre-release suffixes, and reject malformed input. do { check(BarUpdateChecker.isNewer("1.8.0", than: "1.7.0"), "isNewer: newer minor (1.8.0 > 1.7.0)") check(BarUpdateChecker.isNewer("1.7.1", than: "1.7.0"), "isNewer: newer patch (1.7.1 > 1.7.0)") check(BarUpdateChecker.isNewer("2.0.0", than: "1.9.9"), "isNewer: newer major (2.0.0 > 1.9.9)") check( BarUpdateChecker.isNewer("1.10.0", than: "1.9.0"), "isNewer: numeric not lexicographic (1.10.0 > 1.9.0)") check(!BarUpdateChecker.isNewer("1.7.0", than: "1.7.0"), "isNewer: equal is not newer") check(!BarUpdateChecker.isNewer("1.6.0", than: "1.7.0"), "isNewer: older is not newer") check( BarUpdateChecker.isNewer("1.8.0-beta.1", than: "1.7.0"), "isNewer: pre-release suffix stripped, core still newer") check( !BarUpdateChecker.isNewer("1.7.0-beta.1", than: "1.7.0"), "isNewer: equal core after stripping suffix is not newer") check(!BarUpdateChecker.isNewer("garbage", than: "1.7.0"), "isNewer: malformed latest rejected") check(!BarUpdateChecker.isNewer("1.7", than: "1.7.0"), "isNewer: fewer than 3 parts rejected") } // MARK: Multi-profile fields decode (GH-1595) // // Three new optional wire keys: surface, profile, is_subscription. // Native rows carry all three; CLIProxy pool rows omit them — legacy decoders // must not fail and must yield nil for the absent fields. let multiProfileJSON = """ [ { "account_id": "ccs:work", "provider": "claude-code", "surface": "ccs", "profile": "work", "is_subscription": true, "displayName": "work", "tier": "max", "paused": false, "quota_percentage": 62, "quotaStatus": "ok", "next_reset": "2026-06-24T01:00:00.000Z", "is_default": true, "last_activity_at": null, "today_cost": null, "health": "ok", "cached": false, "fetchedAt": "2026-06-23T20:40:00.000Z", "needsReauth": false }, { "account_id": "ccsx:ck", "provider": "codex", "surface": "ccsx", "profile": "ck", "is_subscription": true, "displayName": "ck", "tier": "pro", "paused": true, "quota_percentage": 80, "quotaStatus": "ok", "next_reset": null, "is_default": false, "last_activity_at": null, "today_cost": null, "health": "ok", "cached": true, "fetchedAt": "2026-06-23T16:19:00.000Z", "needsReauth": false, "stale_as_of": "2026-06-23T16:19:00.000Z" }, { "account_id": "alice@example.com", "provider": "agy", "displayName": "Alice (Ultra)", "tier": "ultra", "paused": false, "quota_percentage": 70, "quotaStatus": "ok", "next_reset": null, "is_default": false, "last_activity_at": null, "today_cost": null, "health": "ok", "cached": true, "fetchedAt": "2026-06-23T20:00:00.000Z", "needsReauth": false } ] """ do { let rows = try JSONDecoder().decode([BarSummaryRow].self, from: Data(multiProfileJSON.utf8)) check(rows.count == 3, "mp: decodes 3 rows (2 native + 1 legacy CLIProxy)") // (MP1) Native Claude active row let claudeRow = rows[0] check(claudeRow.accountId == "ccs:work", "mp: native Claude account_id = 'ccs:work'") check(claudeRow.surface == "ccs", "mp: native Claude surface = 'ccs'") check(claudeRow.profile == "work", "mp: native Claude profile = 'work'") check(claudeRow.isSubscription == true, "mp: native Claude is_subscription = true") check(claudeRow.paused == false, "mp: active Claude profile is not paused") // (MP2) Parked Codex row let codexRow = rows[1] check(codexRow.accountId == "ccsx:ck", "mp: parked Codex account_id = 'ccsx:ck'") check(codexRow.surface == "ccsx", "mp: parked Codex surface = 'ccsx'") check(codexRow.profile == "ck", "mp: parked Codex profile = 'ck'") check(codexRow.isSubscription == true, "mp: parked Codex is_subscription = true") check(codexRow.paused == true, "mp: parked Codex row has paused = true") // (MP3) Legacy CLIProxy row — new fields decode to nil without failure let legacyRow = rows[2] check(legacyRow.surface == nil, "mp: CLIProxy row surface decodes to nil (backward compat)") check(legacyRow.profile == nil, "mp: CLIProxy row profile decodes to nil (backward compat)") check(legacyRow.isSubscription == nil, "mp: CLIProxy row is_subscription decodes to nil") // (MP4) isNativeSubscription uses is_subscription flag when present check( BarFormatting.isNativeSubscription(claudeRow), "mp: isNativeSubscription true when is_subscription=true (new flag path)") check( BarFormatting.isNativeSubscription(codexRow), "mp: isNativeSubscription true for parked Codex row (is_subscription=true)") check( !BarFormatting.isNativeSubscription(legacyRow), "mp: isNativeSubscription false for CLIProxy pool row (no is_subscription)") // (MP5) Legacy rows without is_subscription use fallback heuristic let legacyClaudeRow = BarSummaryRow(accountId: "claude-code", provider: "claude-code") check( BarFormatting.isNativeSubscription(legacyClaudeRow), "mp: legacy claude-code row uses heuristic fallback -> is native") let legacyCodexRow = BarSummaryRow(accountId: "codex", provider: "codex") check( BarFormatting.isNativeSubscription(legacyCodexRow), "mp: legacy codex row uses heuristic fallback -> is native") let poolCodexRow = BarSummaryRow(accountId: "pool-codex-oauth-1", provider: "codex") check( !BarFormatting.isNativeSubscription(poolCodexRow), "mp: CLIProxy codex pool row with no is_subscription -> not native (heuristic)") // (MP6) partitionSubscriptions correctly splits with new flag let parts = BarFormatting.partitionSubscriptions(rows) check(parts.subscriptions.count == 2, "mp: partition yields 2 native subscriptions") check(parts.pool.count == 1, "mp: partition yields 1 CLIProxy pool row") check( parts.subscriptions.map { $0.accountId } == ["ccs:work", "ccsx:ck"], "mp: subscriptions keep backend order") // (MP7) surfaceProfileLabel helper check( BarFormatting.surfaceProfileLabel(claudeRow) == "ccs · work", "mp: surfaceProfileLabel for ccs:work -> 'ccs · work'") check( BarFormatting.surfaceProfileLabel(codexRow) == "ccsx · ck", "mp: surfaceProfileLabel for ccsx:ck -> 'ccsx · ck'") check( BarFormatting.surfaceProfileLabel(legacyRow) == nil, "mp: surfaceProfileLabel for CLIProxy row (no profile) -> nil") // (MP8) surfaceProfileLabel falls back to provider when surface is absent let noSurfaceRow = BarSummaryRow( accountId: "ccsx:personal", provider: "codex", surface: nil, profile: "personal", isSubscription: true) check( BarFormatting.surfaceProfileLabel(noSurfaceRow) == "codex · personal", "mp: surfaceProfileLabel falls back to provider when surface is nil") // (MP9) base/default account vs named profile presentation let defaultCodexRow = BarSummaryRow( accountId: "ccsx:default", provider: "codex", surface: "ccsx", profile: "default", isSubscription: true) check( BarFormatting.isBaseAccount(defaultCodexRow), "mp: 'default' profile is the base account") check( !BarFormatting.isBaseAccount(codexRow), "mp: named profile 'ck' is not the base account") check( BarFormatting.accountTitle(defaultCodexRow) == "ccsx", "mp: accountTitle for default account -> 'ccsx' (bare command)") check( BarFormatting.accountTitle(codexRow) == "ck", "mp: accountTitle for named profile -> 'ck'") check( BarFormatting.accountTag(defaultCodexRow) == "default", "mp: accountTag for default account -> 'default'") check( BarFormatting.accountTag(codexRow) == "ccsx", "mp: accountTag for named profile -> 'ccsx' (surface)") check( BarFormatting.accountTag(legacyRow) == nil, "mp: accountTag for CLIProxy pool row -> nil") } catch { check(false, "mp: multi-profile JSON decode failed: \(error)") } // cleanup try? FileManager.default.removeItem(atPath: tmp) if failures > 0 { print("\nFAILED: \(failures) check(s)") exit(1) } else { print("\nALL CHECKS PASSED") exit(0) }