mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-10-21 14:12:34 +00:00
* feat: Amp support - read the local thread mirror (#1572) Amp (Sourcegraph's coding agent) mirrors every thread it serves as one JSON document per thread at ~/.local/share/amp/threads/ (AMP_DATA_DIR honored, comma-separated). Each file is { id, messages, usageLedger?{events} }; the ledger's events are the per-request record (tokens.input/output, cache tokens joined from the billed message through toMessageId -> messages[].messageId), with a per-assistant- message usage fallback for threads without a ledger and a total-only record billing as output. Amp bills a subscription and records no charged dollars, so every call is priced from the underlying model's token rates and marked costIsEstimated; the recorded credits field is ignored (no published per-credit dollar rate). Thread files carry no cwd, so sessions group under the provider name. Registered eager; parse version usage-ledger-v1; no artwork yet (mono-initial fallback, like Codebuff). Surfaces: CLI registry + doctor probe roots, Electron sandbox read paths + menubar catalog (Amp after Cursor, matching the Swift order), Windows tray tab color + usage_guard root, GNOME prefs dropdown, macOS ProviderConnectionCatalog, docs/providers/amp.md, README tool list, CHANGELOG. * fix: address Amp provider review feedback
141 lines
5.7 KiB
Swift
141 lines
5.7 KiB
Swift
import Foundation
|
|
import Testing
|
|
@testable import CodeBurnMenubar
|
|
|
|
@Suite("Usage data change guard")
|
|
struct UsageDataChangeGuardTests {
|
|
private let now = Date(timeIntervalSince1970: 1_000_000)
|
|
|
|
@Test("fresh snapshot skips")
|
|
func freshSnapshotSkips() {
|
|
let snapshot = makeSnapshot(10)
|
|
#expect(UsageDataChangeGuard.shouldSkip(
|
|
current: snapshot,
|
|
lastSuccessful: snapshot,
|
|
lastSuccessAt: now,
|
|
now: now,
|
|
force: false
|
|
))
|
|
}
|
|
|
|
@Test("stale snapshot does not skip")
|
|
func staleSnapshotDoesNotSkip() {
|
|
#expect(!UsageDataChangeGuard.shouldSkip(
|
|
current: makeSnapshot(20),
|
|
lastSuccessful: makeSnapshot(10),
|
|
lastSuccessAt: now,
|
|
now: now,
|
|
force: false
|
|
))
|
|
}
|
|
|
|
@Test("first run does not skip")
|
|
func firstRunDoesNotSkip() {
|
|
#expect(!UsageDataChangeGuard.shouldSkip(
|
|
current: makeSnapshot(10),
|
|
lastSuccessful: nil,
|
|
lastSuccessAt: nil,
|
|
now: now,
|
|
force: false
|
|
))
|
|
}
|
|
|
|
@Test("force refresh bypasses fresh snapshot")
|
|
func forceRefreshBypassesFreshSnapshot() {
|
|
let snapshot = makeSnapshot(10)
|
|
#expect(!UsageDataChangeGuard.shouldSkip(
|
|
current: snapshot,
|
|
lastSuccessful: snapshot,
|
|
lastSuccessAt: now,
|
|
now: now,
|
|
force: true
|
|
))
|
|
}
|
|
|
|
@Test("unchanged snapshot stops skipping after the backstop interval")
|
|
func backstopForcesRefreshAfterMaxSkipInterval() {
|
|
let snapshot = makeSnapshot(10)
|
|
let justInside = now.addingTimeInterval(UsageDataChangeGuard.maxSkipIntervalSeconds - 1)
|
|
let atBoundary = now.addingTimeInterval(UsageDataChangeGuard.maxSkipIntervalSeconds)
|
|
#expect(UsageDataChangeGuard.shouldSkip(
|
|
current: snapshot,
|
|
lastSuccessful: snapshot,
|
|
lastSuccessAt: now,
|
|
now: justInside,
|
|
force: false
|
|
))
|
|
#expect(!UsageDataChangeGuard.shouldSkip(
|
|
current: snapshot,
|
|
lastSuccessful: snapshot,
|
|
lastSuccessAt: now,
|
|
now: atBoundary,
|
|
force: false
|
|
))
|
|
}
|
|
|
|
/// Warp's own database sits in a group container behind the "access data from
|
|
/// other apps" consent, where a stat blocks rather than fails, so the default
|
|
/// location is never watched. A path the person named themselves is.
|
|
@Test("WARP_DB_PATH is watched when set, and nothing Warp-shaped when it is not")
|
|
func warpIsWatchedOnlyWhenExplicitlyConfigured() throws {
|
|
let home = URL(fileURLWithPath: NSTemporaryDirectory())
|
|
.appendingPathComponent("CodeBurnMenubarTests.\(UUID().uuidString)", isDirectory: true)
|
|
try FileManager.default.createDirectory(at: home, withIntermediateDirectories: true)
|
|
defer { try? FileManager.default.removeItem(at: home) }
|
|
let database = home.appendingPathComponent("warp.sqlite")
|
|
try Data().write(to: database)
|
|
|
|
// Named so the guard never falls through to the real config on this Mac.
|
|
let environment = ["CLAUDE_CONFIG_DIR": home.path]
|
|
|
|
let unset = UsageDataChangeGuard.snapshot(environment: environment, homeDirectory: home.path)
|
|
#expect(!unset.modificationDates.keys.contains { $0.contains("Group Containers") })
|
|
#expect(unset.modificationDates[database.path] == nil)
|
|
|
|
let set = UsageDataChangeGuard.snapshot(
|
|
environment: environment.merging(["WARP_DB_PATH": database.path]) { _, new in new },
|
|
homeDirectory: home.path
|
|
)
|
|
#expect(set.modificationDates[database.path] != nil)
|
|
#expect(!set.modificationDates.keys.contains { $0.contains("Group Containers") })
|
|
}
|
|
|
|
@Test("Amp watches the default root and comma-separated overrides")
|
|
func ampDataRootsAreWatched() throws {
|
|
let home = URL(fileURLWithPath: NSTemporaryDirectory())
|
|
.appendingPathComponent("CodeBurnMenubarTests.\(UUID().uuidString)", isDirectory: true)
|
|
defer { try? FileManager.default.removeItem(at: home) }
|
|
let defaultRoot = home.appendingPathComponent(".local/share/amp")
|
|
let first = home.appendingPathComponent("amp-a")
|
|
let second = home.appendingPathComponent("amp-b")
|
|
for root in [defaultRoot, first, second] {
|
|
try FileManager.default.createDirectory(
|
|
at: root.appendingPathComponent("threads"), withIntermediateDirectories: true
|
|
)
|
|
}
|
|
let environment = ["CLAUDE_CONFIG_DIR": home.path]
|
|
let unset = UsageDataChangeGuard.snapshot(environment: environment, homeDirectory: home.path)
|
|
#expect(unset.modificationDates[defaultRoot.path] != nil)
|
|
#expect(unset.modificationDates[defaultRoot.appendingPathComponent("threads").path] != nil)
|
|
|
|
let set = UsageDataChangeGuard.snapshot(
|
|
environment: environment.merging(["AMP_DATA_DIR": " \(first.path), ,\(second.path), "]) { _, new in new },
|
|
homeDirectory: home.path
|
|
)
|
|
for root in [first, second] {
|
|
#expect(set.modificationDates[root.path] != nil)
|
|
#expect(set.modificationDates[root.appendingPathComponent("threads").path] != nil)
|
|
}
|
|
#expect(set.modificationDates[defaultRoot.path] == nil)
|
|
|
|
let empty = UsageDataChangeGuard.snapshot(
|
|
environment: environment.merging(["AMP_DATA_DIR": " , , "]) { _, new in new },
|
|
homeDirectory: home.path
|
|
)
|
|
#expect(empty.modificationDates[defaultRoot.path] != nil)
|
|
}
|
|
|
|
private func makeSnapshot(_ seconds: TimeInterval) -> UsageDataSnapshot {
|
|
UsageDataSnapshot(modificationDates: ["provider-root": Date(timeIntervalSince1970: seconds)])
|
|
}
|
|
}
|