diff --git a/Sources/CodeCaps/MonitorModel.swift b/Sources/CodeCaps/MonitorModel.swift index af622fd4..8c260e36 100644 --- a/Sources/CodeCaps/MonitorModel.swift +++ b/Sources/CodeCaps/MonitorModel.swift @@ -2388,7 +2388,7 @@ final class MonitorModel: ObservableObject { // This Mac's own push fills in only a provider no local reader // produced — with local readers off, that is every provider. - let adopted = ownPush.filter { !localProviders.contains($0.canonicalProviderKey) } + let adopted = QuotaLocalScope.adoptedOwnPush(ownPush, localWindows: self.localWindows) // A window that carries no reading does not claim its provider. // This Mac's plan details are its own push coming back, and they // named the Antigravity key, so the fleet's live reading for @@ -2405,7 +2405,7 @@ final class MonitorModel: ObservableObject { return !localProviders.contains(key) && !adoptedReadingKeys.contains(key) && expectedQuotaProviderKeys.contains(key) } - let merged = self.localWindows + adopted + fleetFallback + let merged = QuotaLocalScope.localWindowsForMerge(self.localWindows, adopted: adopted) + adopted + fleetFallback var origins: [String: QuotaOrigin] = [:] for key in Set(merged.map(\.canonicalProviderKey)) { origins[key] = .local } self.originByProvider = origins @@ -2555,9 +2555,9 @@ final class MonitorModel: ObservableObject { // as a local reading here put the xAI row back to no reading at all // within a minute of the refresh fixing it (owner, 2026-10-23). let localProviders = QuotaLocalScope.providers(withReadings: localWindows) - let split = FleetOrigin.split(serverWindows, now: now) + let split = FleetOrigin.split(serverWindows, localWindows: localWindows, now: now) let ownPush = split.ownPush - let adopted = ownPush.filter { !localProviders.contains($0.canonicalProviderKey) } + let adopted = QuotaLocalScope.adoptedOwnPush(ownPush, localWindows: localWindows) // A window that carries no reading does not claim its provider, the // same rule the refresh path uses: the file poll overwrites the // merged response, so a plan-details window that held the fleet's @@ -2572,7 +2572,7 @@ final class MonitorModel: ObservableObject { return !localProviders.contains(key) && !adoptedReadingKeys.contains(key) && expectedQuotaProviderKeys.contains(key) } - let merged = localWindows + adopted + fleetFallback + let merged = QuotaLocalScope.localWindowsForMerge(localWindows, adopted: adopted) + adopted + fleetFallback originByProvider = Dictionary(uniqueKeysWithValues: Set(merged.map(\.canonicalProviderKey)).map { ($0, .local) }) response = QuotaResponse(generatedAt: ISO8601DateFormatter().string(from: now), windows: merged) if let writeForTesting = handoffWriteForTesting { diff --git a/Sources/QuotaCore/FleetOrigin.swift b/Sources/QuotaCore/FleetOrigin.swift index 47fdee0c..0327d692 100644 --- a/Sources/QuotaCore/FleetOrigin.swift +++ b/Sources/QuotaCore/FleetOrigin.swift @@ -30,6 +30,11 @@ public enum FleetOrigin { .replacingOccurrences(of: " ", with: "") } + private static func localPayloadId(_ id: String) -> String { + let prefix = QuotaWindowVocabulary.localDeviceWindowPrefix + return id.hasPrefix(prefix) ? String(id.dropFirst(prefix.count)) : id + } + /// Whether a pulled window is this Mac's own push coming back. Such a /// window belongs under This Mac and must never be duplicated under Fleet. public static func isOwnPush( @@ -61,7 +66,10 @@ public enum FleetOrigin { let matchesLocal = localWindows.contains { local in local.canonicalProviderKey == window.canonicalProviderKey && local.occurredAt == occurred - && (local.window == window.window || local.id == window.id) + // Missing cadence tokens do not identify the same payload. + // Compare the same normalized IDs used by local adoption. + && (localPayloadId(local.id) == localPayloadId(window.id) + || (local.window != nil && local.window == window.window)) } if matchesLocal { return true } } diff --git a/Sources/QuotaCore/QuotaModels.swift b/Sources/QuotaCore/QuotaModels.swift index 8db34836..580e8655 100644 --- a/Sources/QuotaCore/QuotaModels.swift +++ b/Sources/QuotaCore/QuotaModels.swift @@ -452,6 +452,34 @@ public enum QuotaLocalScope { public static func providers(withReadings windows: [QuotaWindow]) -> Set { Set(windows.filter { $0.hasReading }.map(\.canonicalProviderKey)) } + + /// Own pushes have already been separated from foreign readings by + /// FleetOrigin.split. Keep an informative local copy when its push returns; + /// an informative push can replace an empty local copy at the merge. + /// Foreign producers are never deduped by this local adoption rule. + public static func adoptedOwnPush(_ ownPush: [QuotaWindow], localWindows: [QuotaWindow]) -> [QuotaWindow] { + let localProviders = providers(withReadings: localWindows) + let localKeys = Set(localWindows.map(windowKey)) + return ownPush.filter { + // A local reading still owns its provider. An empty local copy + // must not hide an informative returning push with the same key. + !localProviders.contains($0.canonicalProviderKey) + && (!localKeys.contains(windowKey($0)) || $0.hasReading) + } + } + + /// Only an informative adopted own push replaces its empty local copy. + /// The collector's actual local windows are not changed by this merge view. + public static func localWindowsForMerge(_ localWindows: [QuotaWindow], adopted: [QuotaWindow]) -> [QuotaWindow] { + let replacementKeys = Set(adopted.filter { $0.hasReading }.map(windowKey)) + return localWindows.filter { $0.hasReading || !replacementKeys.contains(windowKey($0)) } + } + + private static func windowKey(_ window: QuotaWindow) -> [String] { + let prefix = QuotaWindowVocabulary.localDeviceWindowPrefix + let id = window.id.hasPrefix(prefix) ? String(window.id.dropFirst(prefix.count)) : window.id + return [window.canonicalProviderKey, id] + } } /// Response from GET /api/quota-windows. `providerGroups` is additive and is diff --git a/Sources/QuotaCore/QuotaWindowVocabulary.swift b/Sources/QuotaCore/QuotaWindowVocabulary.swift index 3daa0b74..1c4e4479 100644 --- a/Sources/QuotaCore/QuotaWindowVocabulary.swift +++ b/Sources/QuotaCore/QuotaWindowVocabulary.swift @@ -600,13 +600,16 @@ public enum QuotaWindowVocabulary { producerInstanceId: String?, ownInstanceId: String? = nil ) -> Bool { - if id.hasPrefix(localDeviceWindowPrefix) { return true } + // The producer instance decides before the prefix, the same + // precedence `FleetOrigin.isOwnPush` applies: a window carrying this + // Mac's `local-mac:` id prefix but ANOTHER installation's producer + // instance is a foreign copy of a same-named payload, and the two + // surfaces must agree on one payload. The comparison ignores case. let instance = (producerInstanceId ?? "").trimmingCharacters(in: .whitespacesAndNewlines) let own = (ownInstanceId ?? "").trimmingCharacters(in: .whitespacesAndNewlines) - // The owning installation's own reading, decided the way the Mac - // decides it: the instance decides, and the comparison ignores case. - if !instance.isEmpty, !own.isEmpty, instance.lowercased() == own.lowercased() { return true } + if !instance.isEmpty, !own.isEmpty { return instance.lowercased() == own.lowercased() } if !instance.isEmpty { return false } + if id.hasPrefix(localDeviceWindowPrefix) { return true } let named = (machine ?? "").trimmingCharacters(in: .whitespacesAndNewlines) if !named.isEmpty { return false } let src = (source ?? "").trimmingCharacters(in: .whitespacesAndNewlines).lowercased() diff --git a/Tests/CodeCapsTests/FleetRowSourceTests.swift b/Tests/CodeCapsTests/FleetRowSourceTests.swift index a0d87d87..75348ff6 100644 --- a/Tests/CodeCapsTests/FleetRowSourceTests.swift +++ b/Tests/CodeCapsTests/FleetRowSourceTests.swift @@ -409,6 +409,8 @@ final class FleetRowSourceTests: XCTestCase { await model.refreshTaskForTesting?.value let merged = model.response.windows.filter { $0.canonicalProviderKey == "google-antigravity" } + XCTAssertEqual(merged.filter { $0.id.hasSuffix("plan-metadata") }.count, 1, + "local plan details and their own returning push are one payload") let readings = merged.filter { $0.remainingPercent != nil } XCTAssertEqual(readings.count, 1, "the fleet's live reading reaches the row") @@ -424,7 +426,89 @@ final class FleetRowSourceTests: XCTestCase { await model.sessionFileTaskForTesting?.value let rebuilt = model.response.windows.filter { $0.canonicalProviderKey == "google-antigravity" } + XCTAssertEqual(rebuilt.filter { $0.id.hasSuffix("plan-metadata") }.count, 1, + "the file poll also keeps one plan-details payload") XCTAssertEqual(rebuilt.filter { $0.remainingPercent != nil }.count, 1, "the file poll keeps the fleet's live reading in the row") } + /// xAI plan metadata survives normalization, so both paths really merge + /// a local copy with its own returning push rather than only the push. + func testBothMergePathsDedupePlanMetadataThatSurvivesNormalization() async throws { + for variant in ["metadata", "reading", "legacy-reading"] { + let carriesReading = variant != "metadata" + let isLegacy = variant == "legacy-reading" + let iso = ISO8601DateFormatter() + let now = Date() + let producer = QuotaPublisher.producerInstanceId + let machine = QuotaPublisher.machineName + let suffix = carriesReading ? "unknown" : "plan-metadata" + let local = QuotaWindow(id: "local-mac:xai:\(suffix)", provider: "xai", + providerKey: "xai", label: "Plan Details", remainingUnknown: true, + occurredAt: iso.string(from: now), source: "CodeCaps Settings", + producerInstanceId: producer, machine: machine) + var pushed = local + pushed.id = "xai:\(suffix)" + if carriesReading { + pushed.remainingPercent = 73 + pushed.remainingUnknown = false + } + if isLegacy { + // No prefix, producer, machine or app-name shortcut: only + // the matching local timestamp/window proves this own push. + pushed.producerInstanceId = nil + pushed.machine = nil + pushed.source = "synthetic-legacy-source" + } + var foreign = pushed + foreign.id = "xai:foreign-reading" + foreign.remainingPercent = 11 + foreign.remainingUnknown = false + foreign.producerInstanceId = isLegacy ? nil : "other-mac" + foreign.machine = isLegacy ? nil : "other-mac" + // The legacy foreign payload shares provider/time and missing cadence, + // but not payload ID. It must remain in Fleet on BOTH merge paths. + let server = QuotaResponse(generatedAt: iso.string(from: now), windows: [pushed, foreign]) + let model = MonitorModel(defaults: defaults()) + model.skipsSnapshotIOForTesting = true + // The fixture itself must survive the same normalization used by + // both merge paths. localWindows is private to MonitorModel. + XCTAssertEqual(AntigravityQuotaGroups.normalize([local]).map(\.id), [local.id], + "the local fixture survives normalization before either merge") + model.localReadForTesting = { LocalQuotaResult(windows: [local]) } + model.serverFetchForTesting = { server } + model.refresh() + await model.refreshTaskForTesting?.value + func assertMerge(_ path: String, file: StaticString = #filePath, line: UInt = #line) { + XCTAssertEqual(model.response.windows.filter { $0.id == local.id }.count, carriesReading ? 0 : 1, + "\(path): only an informative own push replaces the empty local copy", file: file, line: line) + // Only refresh publishes fleetWindowGroups; polling must be + // checked through its actual merged response below, not a stale group. + if path == "refresh" { + let groupedForeign = model.fleetWindowGroups.flatMap(\.windows) + .filter { $0.id == foreign.id } + XCTAssertEqual(groupedForeign.count, 1, + "\(path): distinct legacy payload remains in Fleet", file: file, line: line) + XCTAssertEqual(groupedForeign.first?.remainingPercent, 11, file: file, line: line) + } + let merged = model.response.windows.filter { $0.canonicalProviderKey == "xai" } + if carriesReading { + XCTAssertEqual(merged.filter { $0.id == local.id || $0.id == "xai:\(suffix)" }.count, 1, + "\(path): the exact-key payload appears once", file: file, line: line) + let readings = merged.filter { $0.hasReading } + XCTAssertEqual(readings.map(\.remainingPercent), [73], + "\(path): the own reading wins before foreign fallback", file: file, line: line) + XCTAssertEqual(readings.first?.producerInstanceId, isLegacy ? nil : producer, file: file, line: line) + } else { + XCTAssertEqual(merged.filter { $0.id.hasSuffix("plan-metadata") }.count, 1, + "\(path): two metadata copies dedupe to one", file: file, line: line) + } + } + assertMerge("refresh") + model.sessionFileReadForTesting = { LocalQuotaResult(windows: [local]) } + model.refreshSessionFiles() + await model.sessionFileTaskForTesting?.value + assertMerge("file poll") + } + } + } diff --git a/Tests/QuotaCoreTests/FleetOriginTests.swift b/Tests/QuotaCoreTests/FleetOriginTests.swift index c64f454c..f6c0a304 100644 --- a/Tests/QuotaCoreTests/FleetOriginTests.swift +++ b/Tests/QuotaCoreTests/FleetOriginTests.swift @@ -89,6 +89,53 @@ final class FleetOriginTests: XCTestCase { XCTAssertTrue(split.groups.isEmpty) } + /// An arbitrary source avoids the separate legacy app-name fallback. + func testMissingCadenceDoesNotIdentifyDistinctLegacyPayloadAsOwn() { + let local = window(id: "local-mac:anthropic:five_hour", source: "synthetic-local", sourceApp: nil) + let foreign = window(id: "anthropic:weekly", source: "synthetic-remote", sourceApp: nil) + XCTAssertNil(local.window) + XCTAssertNil(foreign.window) + XCTAssertEqual(local.occurredAt, foreign.occurredAt) + XCTAssertFalse(FleetOrigin.isOwnPush(foreign, host: "injected-host", localWindows: [local])) + let split = FleetOrigin.split([foreign], host: "injected-host", localWindows: [local], + now: foreign.occurredDate ?? Date()) + XCTAssertTrue(split.ownPush.isEmpty) + XCTAssertEqual(split.groups.flatMap(\.windows).map(\.id), [foreign.id]) + } + + func testLegacyMatchingPayloadIdSurvivesLocalPrefixNormalization() { + let local = window(id: "local-mac:anthropic:five_hour", source: "synthetic-local", sourceApp: nil) + let echo = window(id: "anthropic:five_hour", source: "synthetic-remote", sourceApp: nil) + XCTAssertTrue(FleetOrigin.isOwnPush(echo, host: "injected-host", localWindows: [local])) + } + + func testExplicitMatchingCadenceStillIdentifiesLegacyPayload() { + var local = window(id: "local-mac:anthropic:a", source: "synthetic-local", sourceApp: nil) + var echo = window(id: "anthropic:b", source: "synthetic-remote", sourceApp: nil) + local.window = "five_hour" + echo.window = "five_hour" + XCTAssertTrue(FleetOrigin.isOwnPush(echo, host: "injected-host", localWindows: [local])) + echo.window = "weekly" + XCTAssertFalse(FleetOrigin.isOwnPush(echo, host: "injected-host", localWindows: [local])) + } + + func testExplicitRemoteProducerOverridesMatchingTimestampAndId() { + let local = window(id: "local-mac:anthropic:five_hour", source: "synthetic-local", sourceApp: nil) + var remote = window(id: "anthropic:five_hour", source: "synthetic-remote", sourceApp: nil) + remote.producerInstanceId = "other-installation" + XCTAssertFalse(FleetOrigin.isOwnPush(remote, host: "injected-host", localWindows: [local])) + } + + func testNilCadenceFixDoesNotChangeLegacyAppSourceProviderFallback() { + let local = window(id: "local-mac:anthropic:a", source: "synthetic-local", sourceApp: nil) + for source in ["codecaps", "agent-bar"] { + var legacy = window(id: "anthropic:b", source: source, sourceApp: nil) + legacy.occurredAt = "2026-09-18T03:37:53.456Z" + XCTAssertTrue(FleetOrigin.isOwnPush(legacy, host: "injected-host", localWindows: [local]), + "the separate legacy provider fallback is deliberately unchanged") + } + } + func testLocalMacPrefixAndSourceAppIdentifiesOwnPush() { let prefixed = window(id: "local-mac:anthropic:five_hour", source: "Claude Code", sourceApp: nil) XCTAssertTrue(FleetOrigin.isOwnPush(prefixed, host: "unrelated-host")) diff --git a/Tests/QuotaCoreTests/PlanDetailsDedupeTests.swift b/Tests/QuotaCoreTests/PlanDetailsDedupeTests.swift new file mode 100644 index 00000000..b9a8e89d --- /dev/null +++ b/Tests/QuotaCoreTests/PlanDetailsDedupeTests.swift @@ -0,0 +1,88 @@ +import XCTest +@testable import QuotaCore + +final class PlanDetailsDedupeTests: XCTestCase { + private func window(_ id: String, producer: String? = nil) -> QuotaWindow { + QuotaWindow(id: id, provider: "xai", providerKey: "xai", label: "Plan Details", + remainingUnknown: true, occurredAt: "2026-10-24T07:00:00Z", + source: "test", producerInstanceId: producer) + } + + func testALocalPlanDetailsWindowAndItsOwnPushSurviveOnce() { + let local = [window("local-mac:xai:plan-metadata")] + let split = FleetOrigin.split([window("xai:plan-metadata", producer: "this-mac")], host: "this-mac") + let adopted = QuotaLocalScope.adoptedOwnPush(split.ownPush, localWindows: local) + XCTAssertEqual((local + adopted).count, 1) + } + + func testADifferentWindowUnderTheSameProviderSurvives() { + let local = [window("local-mac:xai:plan-metadata")] + let own = [window("xai:plan-metadata"), window("xai:subscription")] + let adopted = QuotaLocalScope.adoptedOwnPush(own, localWindows: local) + XCTAssertEqual(adopted.map(\.id), ["xai:subscription"]) + } + + func testAnotherProducersSameIdWindowSurvives() { + let local = [window("local-mac:xai:plan-metadata")] + let split = FleetOrigin.split([ + window("xai:plan-metadata", producer: "this-mac"), + window("xai:plan-metadata", producer: "other-mac") + ], host: "this-mac") + let adopted = QuotaLocalScope.adoptedOwnPush(split.ownPush, localWindows: local) + XCTAssertTrue(adopted.isEmpty) + XCTAssertEqual(split.groups.flatMap(\.windows).map(\.producerInstanceId), ["other-mac"]) + let response = QuotaResponse(generatedAt: "", windows: local + split.groups.flatMap(\.windows)) + XCTAssertEqual(response.platformSections().first(where: { $0.providerKey == "xai" })?.windows.count, 2) + } + func testAnEmptyLocalCopyDoesNotHideItsReturningReading() { + let local = [window("local-mac:xai:unknown")] + var pushed = window("xai:unknown") + pushed.remainingPercent = 73 + pushed.remainingUnknown = false + let adopted = QuotaLocalScope.adoptedOwnPush([pushed], localWindows: local) + XCTAssertEqual(adopted.map(\.remainingPercent), [73]) + } + + func testALocalReadingStillOwnsItsProviderAcrossDistinctWindowIds() { + var local = window("local-mac:xai:weekly") + local.remainingPercent = 81 + local.remainingUnknown = false + var pushed = window("xai:subscription") + pushed.remainingPercent = 23 + pushed.remainingUnknown = false + XCTAssertTrue(QuotaLocalScope.adoptedOwnPush([pushed], localWindows: [local]).isEmpty) + } + + func testOnlyAnAdoptedReadingReplacesItsExactEmptyLocalCopy() { + let empty = window("local-mac:xai:unknown") + let distinct = window("local-mac:xai:plan-metadata") + var reading = window("xai:unknown") + reading.remainingPercent = 73 + reading.remainingUnknown = false + XCTAssertEqual(QuotaLocalScope.localWindowsForMerge([empty, distinct], adopted: [reading]).map(\.id), + [distinct.id]) + XCTAssertEqual(QuotaLocalScope.localWindowsForMerge([empty], adopted: []).map(\.id), [empty.id]) + XCTAssertEqual(QuotaLocalScope.localWindowsForMerge([empty], adopted: [window("xai:unknown")]).map(\.id), [empty.id]) + var localReading = empty + localReading.remainingPercent = 81 + localReading.remainingUnknown = false + XCTAssertEqual(QuotaLocalScope.localWindowsForMerge([localReading], adopted: [reading]).map(\.remainingPercent), [81]) + } + + func testLegacyProducerlessOwnPushSplitsAdoptsAndReplacesItsEmptyCopy() { + let local = window("local-mac:xai:unknown") + var legacyPush = window("local-mac:xai:unknown") + legacyPush.remainingPercent = 73 + legacyPush.remainingUnknown = false + let split = FleetOrigin.split([legacyPush], host: "injected-host", localWindows: [local]) + XCTAssertEqual(split.ownPush.count, 1) + XCTAssertTrue(split.groups.isEmpty) + let adopted = QuotaLocalScope.adoptedOwnPush(split.ownPush, localWindows: [local]) + XCTAssertEqual(adopted.map(\.remainingPercent), [73]) + let merged = QuotaLocalScope.localWindowsForMerge([local], adopted: adopted) + adopted + XCTAssertEqual(merged.count, 1) + XCTAssertEqual(merged.first?.remainingPercent, 73) + XCTAssertNil(merged.first?.producerInstanceId) + } + +} diff --git a/Tests/QuotaCoreTests/QuotaWindowVocabularyTests.swift b/Tests/QuotaCoreTests/QuotaWindowVocabularyTests.swift index 68165b09..bb3a8bb1 100644 --- a/Tests/QuotaCoreTests/QuotaWindowVocabularyTests.swift +++ b/Tests/QuotaCoreTests/QuotaWindowVocabularyTests.swift @@ -370,6 +370,26 @@ final class QuotaWindowVocabularyTests: XCTestCase { "a window from another reader is a copy") } + /// A window carrying this Mac's `local-mac:` prefix but ANOTHER + /// installation's producer instance id is a foreign copy of a same-named + /// payload: the instance outranks the prefix, the same precedence + /// `FleetOrigin.isOwnPush` applies, so the phone, the widget and the Mac + /// decide one payload the same way. + func testAForeignProducerInstanceOutranksTheLocalMacPrefix() { + XCTAssertFalse(QuotaWindowVocabulary.isOwnDeviceWindow(id: "local-mac:xai:subscription", + source: "CodeCaps", + machine: nil, + producerInstanceId: "other-mac", + ownInstanceId: "this-mac"), + "a foreign producer instance makes the payload a copy even under the local-mac prefix") + XCTAssertTrue(QuotaWindowVocabulary.isOwnDeviceWindow(id: "local-mac:xai:subscription", + source: "CodeCaps", + machine: nil, + producerInstanceId: "this-mac", + ownInstanceId: "this-mac"), + "this installation's own instance confirms its own payload") + } + /// The owning installation's own window is this device's own reading, even /// though it names a producer instance. Every fleet-sourced window names /// one, so without this the phone and the widget read this Mac's own