mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-07-10 01:29:41 +00:00
feat(menubar): add MenubarStatusCache read side for badge backstop
This commit is contained in:
parent
75edff64ec
commit
bdbd7b19f6
2 changed files with 115 additions and 0 deletions
43
mac/Sources/CodeBurnMenubar/Data/MenubarStatusCache.swift
Normal file
43
mac/Sources/CodeBurnMenubar/Data/MenubarStatusCache.swift
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import Foundation
|
||||
|
||||
/// On-disk badge backstop. A static LaunchAgent runs `menubar-refresh.sh` every
|
||||
/// 30s and atomically writes `menubar-status.json`; the app reads it as a badge
|
||||
/// fallback when the in-app refresh loop is behind or dead. Shares the
|
||||
/// `MenubarPayload` decoder with the live path — no separate data model.
|
||||
struct MenubarStatusCache {
|
||||
let statusPath: String
|
||||
let scriptPath: String
|
||||
|
||||
/// Default locations under `~/.cache/codeburn/`.
|
||||
static func standard() -> MenubarStatusCache {
|
||||
let home = FileManager.default.homeDirectoryForCurrentUser.path
|
||||
let dir = "\(home)/.cache/codeburn"
|
||||
return MenubarStatusCache(
|
||||
statusPath: "\(dir)/menubar-status.json",
|
||||
scriptPath: "\(dir)/menubar-refresh.sh"
|
||||
)
|
||||
}
|
||||
|
||||
struct BadgeRead {
|
||||
let payload: MenubarPayload
|
||||
let ageSeconds: TimeInterval
|
||||
}
|
||||
|
||||
/// Decodes the status file and returns it with its age (from the file's
|
||||
/// mtime). Returns nil when the file is missing, corrupt, unreadable, or
|
||||
/// older than `maxAgeSeconds` — every failure mode silently falls back to
|
||||
/// the in-memory payload, never crashes.
|
||||
func readBadgePayload(maxAgeSeconds: TimeInterval) -> BadgeRead? {
|
||||
guard let attrs = try? FileManager.default.attributesOfItem(atPath: statusPath),
|
||||
let mtime = attrs[.modificationDate] as? Date else {
|
||||
return nil
|
||||
}
|
||||
let age = Date().timeIntervalSince(mtime)
|
||||
guard age >= 0, age <= maxAgeSeconds else { return nil }
|
||||
guard let data = try? SafeFile.read(from: statusPath),
|
||||
let payload = try? JSONDecoder().decode(MenubarPayload.self, from: data) else {
|
||||
return nil
|
||||
}
|
||||
return BadgeRead(payload: payload, ageSeconds: age)
|
||||
}
|
||||
}
|
||||
72
mac/Tests/CodeBurnMenubarTests/MenubarStatusCacheTests.swift
Normal file
72
mac/Tests/CodeBurnMenubarTests/MenubarStatusCacheTests.swift
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
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,
|
||||
topActivities: [], topModels: [], 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: [])
|
||||
)
|
||||
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, scriptPath: dir + "/menubar-refresh.sh")
|
||||
|
||||
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", scriptPath: dir + "/s.sh")
|
||||
#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, scriptPath: dir + "/s.sh")
|
||||
#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, scriptPath: dir + "/s.sh")
|
||||
#expect(cache.readBadgePayload(maxAgeSeconds: 3600) == nil)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue