Files
steipete--codexbar/Tests/CodexBarTests/ProviderStorageFootprintTests.swift
2026-07-13 12:22:33 +08:00

553 lines
24 KiB
Swift

import AppKit
import CodexBarCore
import Foundation
import Observation
import Testing
@testable import CodexBar
struct ProviderStorageFootprintTests {
private final class ObservationFlag: @unchecked Sendable {
private let lock = NSLock()
private var value = false
func set() {
self.lock.lock()
self.value = true
self.lock.unlock()
}
func get() -> Bool {
self.lock.lock()
defer { self.lock.unlock() }
return self.value
}
}
@Test
func `scanner sums nested regular files and skips symlink targets`() throws {
let root = try Self.makeTemporaryDirectory()
defer { try? FileManager.default.removeItem(at: root) }
let nested = root.appendingPathComponent("nested", isDirectory: true)
try FileManager.default.createDirectory(at: nested, withIntermediateDirectories: true)
try Data(repeating: 1, count: 5).write(to: root.appendingPathComponent("a.jsonl"))
try Data(repeating: 2, count: 7).write(to: nested.appendingPathComponent("b.jsonl"))
let external = try Self.makeTemporaryDirectory()
defer { try? FileManager.default.removeItem(at: external) }
let target = external.appendingPathComponent("outside.bin")
try Data(repeating: 3, count: 100).write(to: target)
let link = root.appendingPathComponent("linked.bin")
try FileManager.default.createSymbolicLink(at: link, withDestinationURL: target)
let footprint = ProviderStorageScanner().scan(provider: .codex, candidatePaths: [root.path])
#expect(footprint.totalBytes == 12)
#expect(footprint.paths == [root.path])
#expect(footprint.missingPaths.isEmpty)
#expect(footprint.components.map(\.name) == ["nested", "a.jsonl"])
#expect(footprint.components.map(\.totalBytes) == [7, 5])
}
@Test
func `scanner does not follow symlinked candidate roots`() throws {
let root = try Self.makeTemporaryDirectory()
defer { try? FileManager.default.removeItem(at: root) }
let target = root.appendingPathComponent("target", isDirectory: true)
try FileManager.default.createDirectory(at: target, withIntermediateDirectories: true)
try Data(repeating: 1, count: 64).write(to: target.appendingPathComponent("session.jsonl"))
let symlinkRoot = root.appendingPathComponent("codex-link", isDirectory: true)
try FileManager.default.createSymbolicLink(at: symlinkRoot, withDestinationURL: target)
let footprint = ProviderStorageScanner().scan(provider: .codex, candidatePaths: [symlinkRoot.path])
#expect(footprint.paths == [symlinkRoot.path])
#expect(footprint.totalBytes == 0)
#expect(footprint.components.isEmpty)
}
@Test
func `scanner records missing paths without failing`() throws {
let root = try Self.makeTemporaryDirectory()
let missing = root.appendingPathComponent("missing")
defer { try? FileManager.default.removeItem(at: root) }
let footprint = ProviderStorageScanner().scan(provider: .claude, candidatePaths: [missing.path])
#expect(footprint.totalBytes == 0)
#expect(footprint.paths.isEmpty)
#expect(footprint.missingPaths == [missing.path])
}
@Test
func `codex path catalog uses CODEX_HOME and managed homes`() {
let managed = ManagedCodexAccount(
id: UUID(),
email: "user@example.com",
managedHomePath: "/tmp/codex-managed-home",
createdAt: 1,
updatedAt: 1,
lastAuthenticatedAt: nil)
let paths = ProviderStoragePathCatalog.candidatePaths(
for: .codex,
environment: ["CODEX_HOME": "/tmp/codex-home"],
managedCodexAccounts: [managed])
#expect(paths == ["/tmp/codex-home", "/tmp/codex-managed-home"])
}
@Test
func `codex path catalog falls back to default home`() {
let paths = ProviderStoragePathCatalog.candidatePaths(for: .codex, environment: [:])
let expected = FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent(".codex", isDirectory: true)
.path
#expect(paths.first == expected)
}
@Test
func `cursor path catalog includes application data and caches`() {
let home = FileManager.default.homeDirectoryForCurrentUser
let paths = ProviderStoragePathCatalog.candidatePaths(for: .cursor, environment: [:])
#expect(paths == [
home.appendingPathComponent("Library/Application Support/Cursor", isDirectory: true).path,
home.appendingPathComponent(
"Library/Application Support/Caches/cursor-updater",
isDirectory: true).path,
home.appendingPathComponent(".cursor", isDirectory: true).path,
home.appendingPathComponent("Library/Caches/Cursor", isDirectory: true).path,
home.appendingPathComponent("Library/Caches/com.todesktop.230313mzl4w4u92", isDirectory: true).path,
home.appendingPathComponent("Library/Caches/com.todesktop.230313mzl4w4u92.ShipIt", isDirectory: true).path,
home.appendingPathComponent("Library/Caches/cursor-compile-cache", isDirectory: true).path,
home.appendingPathComponent(
"Library/HTTPStorages/com.todesktop.230313mzl4w4u92",
isDirectory: true).path,
])
}
@Test
func `claude recommendations use documented cleanup categories`() {
let root = "/Users/test/.claude"
let footprint = ProviderStorageFootprint(
provider: .claude,
totalBytes: 28,
paths: [root],
missingPaths: [],
unreadablePaths: [],
components: [
.init(path: "\(root)/projects", totalBytes: 10),
.init(path: "\(root)/file-history", totalBytes: 8),
.init(path: "\(root)/paste-cache", totalBytes: 6),
.init(path: "\(root)/settings.json", totalBytes: 4),
],
updatedAt: Date(timeIntervalSince1970: 0))
let recommendations = footprint.cleanupRecommendations
#expect(recommendations.map(\.path) == [
"\(root)/projects",
"\(root)/file-history",
"\(root)/paste-cache",
])
#expect(recommendations[0].consequence.contains("resume"))
#expect(recommendations.allSatisfy { $0.riskLevel == .manualCleanup })
}
@Test
func `codex recommendations stay under known homes and exclude auth and config`() {
let root = "/Users/test/.codex"
let footprint = ProviderStorageFootprint(
provider: .codex,
totalBytes: 51,
paths: [root],
missingPaths: [],
unreadablePaths: [],
components: [
.init(path: "\(root)/sessions", totalBytes: 20),
.init(path: "\(root)/archived_sessions", totalBytes: 15),
.init(path: "\(root)/log", totalBytes: 12),
.init(path: "\(root)/logs_2.sqlite", totalBytes: 11),
.init(path: "\(root)/cache", totalBytes: 10),
.init(path: "\(root)/shell_snapshots", totalBytes: 9),
.init(path: "\(root)/auth.json", totalBytes: 4),
.init(path: "\(root)/config.toml", totalBytes: 2),
.init(path: "/tmp/outside/sessions", totalBytes: 99),
],
updatedAt: Date(timeIntervalSince1970: 0))
let recommendations = footprint.cleanupRecommendations
#expect(recommendations.map(\.path) == [
"\(root)/sessions",
"\(root)/archived_sessions",
"\(root)/cache",
"\(root)/log",
"\(root)/logs_2.sqlite",
"\(root)/shell_snapshots",
])
#expect(recommendations.map(\.bytes) == [20, 15, 10, 12, 11, 9])
}
@Test
func `unknown provider storage returns no cleanup recommendations`() {
let footprint = ProviderStorageFootprint(
provider: .gemini,
totalBytes: 10,
paths: ["/Users/test/.gemini"],
missingPaths: [],
unreadablePaths: [],
components: [
.init(path: "/Users/test/.gemini/cache", totalBytes: 10),
],
updatedAt: Date(timeIntervalSince1970: 0))
#expect(footprint.cleanupRecommendations.isEmpty)
}
@Test
@MainActor
func `overview row carries storage text outside provider detail model`() throws {
let now = Date()
let metadata = try #require(ProviderDefaults.metadata[.claude])
let snapshot = UsageSnapshot(
primary: RateWindow(
usedPercent: 25,
windowMinutes: nil,
resetsAt: now.addingTimeInterval(3600),
resetDescription: nil),
secondary: nil,
updatedAt: now,
identity: nil)
let model = UsageMenuCardView.Model.make(.init(
provider: .claude,
metadata: metadata,
snapshot: snapshot,
credits: nil,
creditsError: nil,
dashboard: nil,
dashboardError: nil,
tokenSnapshot: nil,
tokenError: nil,
account: AccountInfo(email: nil, plan: nil),
isRefreshing: false,
lastError: nil,
usageBarsShowUsed: false,
resetTimeDisplayStyle: .countdown,
tokenCostUsageEnabled: false,
showOptionalCreditsAndExtraUsage: true,
hidePersonalInfo: false,
now: now))
let overview = OverviewMenuCardRowView(model: model, storageText: "1.5 GB", width: 310)
let detail = UsageMenuCardView(model: model, width: 310)
#expect(overview.storageText == "1.5 GB")
#expect(detail.model.provider == UsageProvider.claude)
}
@Test
@MainActor
func `storage detail view exposes cleanup recommendations while overview remains number only`() throws {
let root = "/Users/test/.claude"
let footprint = ProviderStorageFootprint(
provider: .claude,
totalBytes: 10,
paths: [root],
missingPaths: [],
unreadablePaths: [],
components: [
.init(path: "\(root)/projects", totalBytes: 10),
],
updatedAt: Date(timeIntervalSince1970: 0))
let detailView = StorageBreakdownMenuView(footprint: footprint, width: 310)
let metadata = try #require(ProviderDefaults.metadata[.claude])
let model = UsageMenuCardView.Model.make(.init(
provider: .claude,
metadata: metadata,
snapshot: nil,
credits: nil,
creditsError: nil,
dashboard: nil,
dashboardError: nil,
tokenSnapshot: nil,
tokenError: nil,
account: AccountInfo(email: nil, plan: nil),
isRefreshing: false,
lastError: nil,
usageBarsShowUsed: false,
resetTimeDisplayStyle: .countdown,
tokenCostUsageEnabled: false,
showOptionalCreditsAndExtraUsage: true,
hidePersonalInfo: false,
now: Date(timeIntervalSince1970: 0)))
let overview = OverviewMenuCardRowView(model: model, storageText: "10 B", width: 310)
#expect(detailView.cleanupRecommendations.map(\.path) == ["\(root)/projects"])
#expect(overview.storageText == "10 B")
}
@Test
@MainActor
func `storage detail view exposes copyable exact paths`() {
let root = "/Users/test/.claude"
let footprint = ProviderStorageFootprint(
provider: .claude,
totalBytes: 110,
paths: [root],
missingPaths: [],
unreadablePaths: [],
components: [
.init(path: "\(root)/projects", totalBytes: 100),
.init(path: "\(root)/file-history", totalBytes: 10),
],
updatedAt: Date(timeIntervalSince1970: 0))
let detailView = StorageBreakdownMenuView(footprint: footprint, width: 310)
#expect(detailView.copyablePaths.contains("\(root)/projects"))
#expect(detailView.copyablePaths.contains("\(root)/file-history"))
}
@Test
@MainActor
func `manual storage refresh updates deleted provider data`() async throws {
let home = try Self.makeTemporaryDirectory()
defer { try? FileManager.default.removeItem(at: home) }
let codexHome = home.appendingPathComponent(".codex", isDirectory: true)
let sessions = codexHome.appendingPathComponent("sessions", isDirectory: true)
try FileManager.default.createDirectory(at: sessions, withIntermediateDirectories: true)
try Data(repeating: 1, count: 32).write(to: sessions.appendingPathComponent("session.jsonl"))
let suite = "ProviderStorageFootprintTests-storage-refresh-\(UUID().uuidString)"
let defaults = try #require(UserDefaults(suiteName: suite))
defaults.removePersistentDomain(forName: suite)
let settings = SettingsStore(
userDefaults: defaults,
configStore: testConfigStore(suiteName: suite),
zaiTokenStore: NoopZaiTokenStore(),
syntheticTokenStore: NoopSyntheticTokenStore())
if let codexMetadata = ProviderDefaults.metadata[.codex] {
settings.setProviderEnabled(provider: .codex, metadata: codexMetadata, enabled: true)
}
let store = UsageStore(
fetcher: UsageFetcher(),
browserDetection: BrowserDetection(cacheTTL: 0),
settings: settings,
environmentBase: ["CODEX_HOME": codexHome.path])
settings.providerStorageFootprintsEnabled = true
store.managedCodexAccountsForStorageOverride = []
await store.refreshStorageFootprintsForOverviewNow()
#expect(store.storageFootprint(for: .codex)?.totalBytes == 32)
try FileManager.default.removeItem(at: sessions)
await store.refreshStorageFootprintsForOverviewNow()
#expect(store.storageFootprint(for: .codex)?.totalBytes == 0)
#expect(store.storageFootprintText(for: .codex) == "No local data found")
}
@Test
@MainActor
func `repeated identical storage refresh does not republish observable footprints`() async throws {
let home = try Self.makeTemporaryDirectory()
defer { try? FileManager.default.removeItem(at: home) }
let codexHome = home.appendingPathComponent(".codex", isDirectory: true)
let sessions = codexHome.appendingPathComponent("sessions", isDirectory: true)
try FileManager.default.createDirectory(at: sessions, withIntermediateDirectories: true)
try Data(repeating: 1, count: 32).write(to: sessions.appendingPathComponent("session.jsonl"))
let suite = "ProviderStorageFootprintTests-identity-\(UUID().uuidString)"
let defaults = try #require(UserDefaults(suiteName: suite))
defaults.removePersistentDomain(forName: suite)
let settings = SettingsStore(
userDefaults: defaults,
configStore: testConfigStore(suiteName: suite),
zaiTokenStore: NoopZaiTokenStore(),
syntheticTokenStore: NoopSyntheticTokenStore())
if let codexMetadata = ProviderDefaults.metadata[.codex] {
settings.setProviderEnabled(provider: .codex, metadata: codexMetadata, enabled: true)
}
let store = UsageStore(
fetcher: UsageFetcher(),
browserDetection: BrowserDetection(cacheTTL: 0),
settings: settings,
environmentBase: ["CODEX_HOME": codexHome.path])
settings.providerStorageFootprintsEnabled = true
store.managedCodexAccountsForStorageOverride = []
await store.refreshStorageFootprintsNow(for: [.codex])
#expect(store.storageFootprint(for: .codex)?.totalBytes == 32)
// A second scan over identical on-disk data must not re-assign the observable property.
// Storage scans run on every menu open and every ~5 min; an unconditional re-publish wakes
// the controller's `menuObservationToken` -> `invalidateMenus` path for no value change.
let didRepublish = ObservationFlag()
withObservationTracking {
_ = store.providerStorageFootprints
} onChange: {
didRepublish.set()
}
await store.refreshStorageFootprintsNow(for: [.codex])
try? await Task.sleep(for: .milliseconds(50))
#expect(didRepublish.get() == false)
#expect(store.storageFootprint(for: .codex)?.totalBytes == 32)
}
@Test
@MainActor
func `storage refresh is opt in and clears stale footprints when disabled`() async throws {
let home = try Self.makeTemporaryDirectory()
defer { try? FileManager.default.removeItem(at: home) }
let codexHome = home.appendingPathComponent(".codex", isDirectory: true)
try FileManager.default.createDirectory(at: codexHome, withIntermediateDirectories: true)
try Data(repeating: 1, count: 16).write(to: codexHome.appendingPathComponent("session.jsonl"))
let suite = "ProviderStorageFootprintTests-storage-opt-in-\(UUID().uuidString)"
let defaults = try #require(UserDefaults(suiteName: suite))
defaults.removePersistentDomain(forName: suite)
let settings = SettingsStore(
userDefaults: defaults,
configStore: testConfigStore(suiteName: suite),
zaiTokenStore: NoopZaiTokenStore(),
syntheticTokenStore: NoopSyntheticTokenStore())
if let codexMetadata = ProviderDefaults.metadata[.codex] {
settings.setProviderEnabled(provider: .codex, metadata: codexMetadata, enabled: true)
}
let store = UsageStore(
fetcher: UsageFetcher(),
browserDetection: BrowserDetection(cacheTTL: 0),
settings: settings,
environmentBase: ["CODEX_HOME": codexHome.path])
store.managedCodexAccountsForStorageOverride = []
await store.refreshStorageFootprintsForOverviewNow()
#expect(store.storageFootprint(for: .codex) == nil)
settings.providerStorageFootprintsEnabled = true
await store.refreshStorageFootprintsForOverviewNow()
#expect(store.storageFootprint(for: .codex)?.totalBytes == 16)
settings.providerStorageFootprintsEnabled = false
await store.refreshStorageFootprintsForOverviewNow()
#expect(store.storageFootprint(for: .codex) == nil)
#expect(store.providerStorageFootprints.isEmpty)
}
@Test
@MainActor
func `forced scheduled storage refresh does not restart identical in flight scan`() throws {
let home = try Self.makeTemporaryDirectory()
defer { try? FileManager.default.removeItem(at: home) }
let codexHome = home.appendingPathComponent(".codex", isDirectory: true)
try FileManager.default.createDirectory(at: codexHome, withIntermediateDirectories: true)
let suite = "ProviderStorageFootprintTests-storage-in-flight-\(UUID().uuidString)"
let defaults = try #require(UserDefaults(suiteName: suite))
defaults.removePersistentDomain(forName: suite)
let settings = SettingsStore(
userDefaults: defaults,
configStore: testConfigStore(suiteName: suite),
zaiTokenStore: NoopZaiTokenStore(),
syntheticTokenStore: NoopSyntheticTokenStore())
let store = UsageStore(
fetcher: UsageFetcher(),
browserDetection: BrowserDetection(cacheTTL: 0),
settings: settings,
environmentBase: ["CODEX_HOME": codexHome.path])
settings.providerStorageFootprintsEnabled = true
store.storageRefreshGeneration = 41
store.storageRefreshInFlightSignature = "codex=\(codexHome.path)"
store.storageRefreshTask = Task.detached {
try? await Task.sleep(for: .seconds(30))
}
defer {
store.storageRefreshTask?.cancel()
store.storageRefreshTask = nil
store.storageRefreshInFlightSignature = nil
}
store.scheduleStorageFootprintRefresh(for: [.codex], force: true)
#expect(store.storageRefreshGeneration == 41)
}
@Test
@MainActor
func `scheduled storage refresh notices managed Codex home changes`() async throws {
let home = try Self.makeTemporaryDirectory()
defer { try? FileManager.default.removeItem(at: home) }
let ambientHome = home.appendingPathComponent("ambient", isDirectory: true)
let firstManagedHome = home.appendingPathComponent("managed-a", isDirectory: true)
let secondManagedHome = home.appendingPathComponent("managed-b", isDirectory: true)
try FileManager.default.createDirectory(at: ambientHome, withIntermediateDirectories: true)
try FileManager.default.createDirectory(at: firstManagedHome, withIntermediateDirectories: true)
try FileManager.default.createDirectory(at: secondManagedHome, withIntermediateDirectories: true)
try Data(repeating: 1, count: 16).write(to: firstManagedHome.appendingPathComponent("session.jsonl"))
try Data(repeating: 2, count: 32).write(to: secondManagedHome.appendingPathComponent("session.jsonl"))
let suite = "ProviderStorageFootprintTests-managed-refresh-\(UUID().uuidString)"
let defaults = try #require(UserDefaults(suiteName: suite))
defaults.removePersistentDomain(forName: suite)
let settings = SettingsStore(
userDefaults: defaults,
configStore: testConfigStore(suiteName: suite),
zaiTokenStore: NoopZaiTokenStore(),
syntheticTokenStore: NoopSyntheticTokenStore())
if let codexMetadata = ProviderDefaults.metadata[.codex] {
settings.setProviderEnabled(provider: .codex, metadata: codexMetadata, enabled: true)
}
let store = UsageStore(
fetcher: UsageFetcher(),
browserDetection: BrowserDetection(cacheTTL: 0),
settings: settings,
environmentBase: ["CODEX_HOME": ambientHome.path])
settings.providerStorageFootprintsEnabled = true
store.managedCodexAccountsForStorageOverride = [
Self.managedCodexAccount(homePath: firstManagedHome.path),
]
store.scheduleStorageFootprintRefresh(for: [.codex])
for _ in 0..<100 where store.isStorageRefreshInFlight {
try await Task.sleep(for: .milliseconds(10))
}
#expect(store.storageFootprint(for: .codex)?.totalBytes == 16)
store.managedCodexAccountsForStorageOverride = [
Self.managedCodexAccount(homePath: secondManagedHome.path),
]
store.scheduleStorageFootprintRefresh(for: [.codex])
for _ in 0..<100 where store.isStorageRefreshInFlight {
try await Task.sleep(for: .milliseconds(10))
}
#expect(store.storageFootprint(for: .codex)?.totalBytes == 32)
}
private static func managedCodexAccount(homePath: String) -> ManagedCodexAccount {
ManagedCodexAccount(
id: UUID(),
email: "storage@example.com",
managedHomePath: homePath,
createdAt: 1,
updatedAt: 1,
lastAuthenticatedAt: nil)
}
private static func makeTemporaryDirectory() throws -> URL {
let url = FileManager.default.temporaryDirectory
.appendingPathComponent("ProviderStorageFootprintTests-\(UUID().uuidString)", isDirectory: true)
try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true)
return url
}
}