Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions Sources/CodexBar/StatusItemController+IconObservation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,30 @@ extension StatusItemController {
"text=\(displayText ?? "nil")",
"layoutCost=\(layoutCostSignature ?? "nil")",
"layoutAccount=\(layoutAccountSignature ?? "nil")",
"accountSnapshots=\(self.accountSnapshotSignature(for: provider))",
].joined(separator: "|")
}

private func accountSnapshotSignature(for provider: UsageProvider) -> String {
let accountSnapshots = self.store.accountSnapshots[provider] ?? []
let accountPart = accountSnapshots.map { snap in
let label = snap.account.label
return "\(snap.account.id.uuidString):\(label)"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Include mutable account state in the signature

The new signature projection is fresh evidence that the previously reported gate remains incomplete: an ordinary refresh replaces an existing TokenAccountUsageSnapshot with updated snapshot, error, or sourceLabel values while preserving its ID and label, so Observation fires but this string remains unchanged and observeStoreIconChanges() returns without updating the status item. The Codex projection has the same problem because it retains only ID, email, and active state; include the mutable fields that are intended to trigger icon work and cover a same-account refresh rather than only insertion into an empty collection.

Useful? React with 👍 / 👎.

}.joined(separator: ",")

let codexPart: String = if provider == .codex {
self.store.codexAccountSnapshots.map { snap in
let email = snap.account.email
let active = snap.account.isActive ? "1" : "0"
return "\(snap.id):\(email):\(active)"
}.joined(separator: ",")
} else {
""
}

return "\(accountPart)|\(codexPart)"
}

private func storedMenuBarLayoutAccountSignature(
for provider: UsageProvider,
snapshot: UsageSnapshot?)
Expand Down
2 changes: 2 additions & 0 deletions Sources/CodexBar/UsageStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ extension UsageStore {
_ = self.statuses
_ = self.tokenSnapshotPublications
_ = self.historicalPaceRevision
_ = self.accountSnapshots
_ = self.codexAccountSnapshots
Comment on lines +59 to +60

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Feed account snapshots into the icon work signature

Once the initial icon signature is stored, a refresh that changes only either per-account collection triggers this observation but still does no icon work: observeStoreIconChanges() recomputes storeIconObservationSignature() and returns because that signature reads store.snapshot(for:) and never accountSnapshots or codexAccountSnapshots. The status indicator therefore remains stale in the scenario these dependencies are intended to fix; include the relevant account state in the signature/render source (or publish it into snapshots) and test through the controller's signature gate rather than only asserting raw Observation invalidation.

AGENTS.md reference: AGENTS.md:L4-L5

Useful? React with 👍 / 👎.

return 0
}

Expand Down
73 changes: 73 additions & 0 deletions Tests/CodexBarTests/StatusItemIconObservationSignatureTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,79 @@ struct StatusItemIconObservationSignatureTests {
#expect(controller.storeIconObservationSignature() != baseline)
}

@Test
func `store icon observation token invalidates when account snapshots change`() {
let (_, store, controller) = self.makeController(
suiteName: "StatusItemIconObservationSignatureTests-account-snapshots")
defer { controller.releaseStatusItemsForTesting() }

let baselineSignature = controller.storeIconObservationSignature()

nonisolated(unsafe) var invalidated = false
withObservationTracking {
_ = store.iconObservationToken
} onChange: {
invalidated = true
}

let account = ProviderTokenAccount(
id: UUID(),
label: "Account 1",
token: "token1",
addedAt: 1,
lastUsed: nil)

store.accountSnapshots[.codex] = [
TokenAccountUsageSnapshot(
account: account,
snapshot: nil,
error: nil,
sourceLabel: "fixture",
cacheKey: store.tokenAccountSnapshotCacheKey(provider: .codex, account: account)),
]

#expect(invalidated)
#expect(controller.storeIconObservationSignature() != baselineSignature)
}

@Test
func `store icon observation token invalidates when codex account snapshots change`() {
let (_, store, controller) = self.makeController(
suiteName: "StatusItemIconObservationSignatureTests-codex-account-snapshots")
defer { controller.releaseStatusItemsForTesting() }

let baselineSignature = controller.storeIconObservationSignature()

nonisolated(unsafe) var invalidated = false
withObservationTracking {
_ = store.iconObservationToken
} onChange: {
invalidated = true
}

let visibleAccount = CodexVisibleAccount(
id: "test-codex-account",
email: "email@example.com",
authFingerprint: nil,
storedAccountID: nil,
selectionSource: .profileHome(path: "/tmp"),
isActive: true,
isLive: true,
canReauthenticate: true,
canRemove: true)

store.codexAccountSnapshots = [
CodexAccountUsageSnapshot(
account: visibleAccount,
snapshot: nil,
error: nil,
sourceLabel: "cached"),
]

#expect(invalidated)
#expect(controller.storeIconObservationSignature() != baselineSignature)
}

@Test
func `store icon observation signature changes when hide critters toggles`() {
let (settings, _, controller) = self.makeController(
Expand Down