codeburn/mac/Tests/CodeBurnMenubarTests/UsageDataChangeGuardTests.swift
ozymandiashh 157347aa99
feat: Amp support - read the local thread mirror (#1572) (#1578)
* 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
2026-10-02 10:36:05 +02:00

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