diff --git a/Sources/QuotaCore/QuotaModels.swift b/Sources/QuotaCore/QuotaModels.swift index cf1fe902..8ef5fc0b 100644 --- a/Sources/QuotaCore/QuotaModels.swift +++ b/Sources/QuotaCore/QuotaModels.swift @@ -625,7 +625,7 @@ private enum ISO8601Date { } } -private enum QuotaProviders { +enum QuotaProviders { struct Expected { let key: String let label: String @@ -672,7 +672,12 @@ private enum QuotaProviders { "minimax": "minimax", "minimax-code": "minimax", "muse-assist": "muse-assist", "muse assist": "muse-assist", "muse_assist": "muse-assist", "muse-assistant": "muse-assist", "muse assistant": "muse-assist", "muse-code": "muse-code", "muse code": "muse-code", "muse_code": "muse-code", - "muse": "muse-code", "muse-cli": "muse-code", "muse-sdk": "muse-code", + // `muse` is what `subscription-status-cli` reports, and its windows + // are "Additional tokens" / "Free weekly limit" -- Muse Assist, per + // the owner 2026-10-09. It was aliased to `muse-code` here, which + // filed every Assist reading under Muse Code: both rows then showed + // "no report" while the data sat in the handoff, correctly formed. + "muse": "muse-assist", "muse-cli": "muse-assist", "muse-sdk": "muse-assist", "kimi": "kimi", "moonshot": "kimi", "moonshot-ai": "kimi", "gemini-cli": "gemini-cli", "copilot": "github-copilot", "github-copilot": "github-copilot", "github_copilot": "github-copilot", diff --git a/Tests/QuotaCoreTests/MuseProviderIdentityTests.swift b/Tests/QuotaCoreTests/MuseProviderIdentityTests.swift new file mode 100644 index 00000000..10e44931 --- /dev/null +++ b/Tests/QuotaCoreTests/MuseProviderIdentityTests.swift @@ -0,0 +1,54 @@ +import XCTest +@testable import QuotaCore + +/// Muse Assist and Muse Code are two different subscriptions, and the app +/// showed "no report" for both while the readings sat in the handoff. +/// +/// The alias table mapped `muse` to `muse-code`. `subscription-status-cli` +/// reports Muse Assist's windows under the key `muse` ("Additional tokens", +/// "Free weekly limit"), so every one of them was rewritten to `muse-code`. +/// The expected-provider list asks for `muse-assist`, and nothing ever emitted +/// that key — so the Muse Assist row had no windows to show, and the rows that +/// did resolve carried the wrong subscription's numbers. +final class MuseProviderIdentityTests: XCTestCase { + + /// The bug, stated directly: Muse Assist's own key must not be rewritten to + /// Muse Code's. + func testMuseIsMuseAssistNotMuseCode() { + XCTAssertEqual(QuotaProviders.canonicalKey(provider: "muse", providerKey: "muse", via: nil), + "muse-assist", + "'muse' is the key subscription-status-cli reports for Muse Assist") + } + + /// The CLI spellings follow the same rule. + func testTheMuseCliAliasesFollowTheSameRule() { + for raw in ["muse-cli", "muse-sdk", "muse-assistant", "muse assistant"] { + XCTAssertEqual(QuotaProviders.canonicalKey(provider: raw, providerKey: raw, via: nil), + "muse-assist", "'\(raw)' should resolve to Muse Assist") + } + } + + /// Muse Code keeps its own identity, from its own source. + func testMuseCodeKeepsItsOwnIdentity() { + for raw in ["muse-code", "muse code", "muse_code"] { + XCTAssertEqual(QuotaProviders.canonicalKey(provider: raw, providerKey: raw, via: nil), + "muse-code", "'\(raw)' should resolve to Muse Code") + } + } + + /// Both rows are expected providers, so a missing reading reads as a real + /// absence rather than an unknown platform. + func testBothMuseProvidersAreExpected() { + let keys = Set(QuotaProviders.expected.map(\.key)) + XCTAssertTrue(keys.contains("muse-assist"), "Muse Assist is missing from the expected list") + XCTAssertTrue(keys.contains("muse-code"), "Muse Code is missing from the expected list") + } + + /// The regression that would bring the bug back: a window reported under + /// `muse` must land on the Muse Assist section, not Muse Code's. + func testAWindowKeyedMuseLandsOnMuseAssist() { + let canonical = QuotaProviders.canonicalKey(provider: "muse", providerKey: "muse", via: nil) + XCTAssertNotEqual(canonical, "muse-code", + "Muse Assist's readings are being filed under Muse Code again") + } +}