Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
- LiteLLM: show personal and team spend amounts directly on budget rows while suppressing duplicate budget sections. Thanks @hololee!

### Fixed
- Xiaomi MiMo: retry another imported browser session when a stale session redirects API requests to login. Thanks @Yuxin-Qiao!
- Kiro: keep parsed usage available when the optional account probe times out or fails. Thanks @Yuxin-Qiao!
- Memory: release idle OpenAI WebViews under system pressure without blocking the main thread. Thanks @ProspectOre!
- Memory: trim rebuildable menu and OpenAI debug caches under system pressure. Thanks @ProspectOre!
Expand Down
3 changes: 3 additions & 0 deletions Sources/CodexBarCore/Providers/MiMo/MiMoUsageFetcher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,9 @@ public enum MiMoUsageFetcher {
switch response.statusCode {
case 200:
break
// Expired browser sessions can redirect API requests to the login flow.
case 300..<400:
throw MiMoUsageError.loginRequired
case 401:
throw MiMoUsageError.loginRequired
case 403:
Expand Down
86 changes: 86 additions & 0 deletions Tests/CodexBarTests/MiMoProviderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,25 @@ struct MiMoProviderTests {

#expect(elapsed < .seconds(1), "Required failure was delayed by optional requests: \(elapsed)")
}

@Test
func `fetch usage treats auth redirect as login required`() async throws {
let transport = ProviderHTTPTransportStub { request in
let url = try #require(request.url)
let (response, data) = Self.makeResponse(url: url, body: "", statusCode: 302)
return (data, response)
}

do {
_ = try await MiMoUsageFetcher.fetchUsage(
cookieHeader: "userId=123; api-platform_serviceToken=expired-token",
environment: ["MIMO_API_URL": "https://mimo.test/api/v1"],
session: transport)
Issue.record("Expected MiMo auth redirect to require login")
} catch MiMoUsageError.loginRequired {
// Expected.
}
}
}

private actor MiMoOptionalRequestGate {
Expand Down Expand Up @@ -942,6 +961,73 @@ extension MiMoProviderTests {
#expect(CookieHeaderCache.load(provider: .mimo)?.sourceLabel == "Active Chrome")
}

@Test
func `mimo web strategy retries safari after stale chrome auth redirect`() async throws {
KeychainCacheStore.setTestStoreForTesting(true)
defer { KeychainCacheStore.setTestStoreForTesting(false) }
let registered = URLProtocol.registerClass(MiMoStubURLProtocol.self)
defer {
if registered {
URLProtocol.unregisterClass(MiMoStubURLProtocol.self)
}
MiMoStubURLProtocol.handler = nil
MiMoCookieImporter.importSessionsOverrideForTesting = nil
CookieHeaderCache.clear(provider: .mimo)
}

CookieHeaderCache.clear(provider: .mimo)
CookieHeaderCache.store(
provider: .mimo,
cookieHeader: "api-platform_serviceToken=stale-chrome-token; userId=111",
sourceLabel: "Chrome")

MiMoCookieImporter.importSessionsOverrideForTesting = { _, _ in
[
.init(
cookieHeader: "api-platform_serviceToken=stale-chrome-token; userId=111",
sourceLabel: "Chrome"),
.init(
cookieHeader: "api-platform_serviceToken=valid-safari-token; userId=222",
sourceLabel: "Safari"),
]
}

let lock = NSLock()
var requestedCookies: [String] = []
MiMoStubURLProtocol.handler = { request in
guard let url = request.url else { throw URLError(.badURL) }
let cookie = request.value(forHTTPHeaderField: "Cookie") ?? ""
lock.withLock {
requestedCookies.append(cookie)
}

if cookie.contains("stale-chrome-token") {
return Self.makeResponse(url: url, body: "", statusCode: 302)
}

let body = """
{
"code": 0,
"message": "",
"data": {
"balance": "25.51",
"currency": "USD"
}
}
"""
return Self.makeResponse(url: url, body: body)
}

let strategy = MiMoWebFetchStrategy()
let result = try await strategy
.fetch(self.makeContext(environment: ["MIMO_API_URL": "https://mimo.test/api/v1"]))

#expect(requestedCookies.contains(where: { $0.contains("stale-chrome-token") }))
#expect(requestedCookies.contains(where: { $0.contains("valid-safari-token") }))
#expect(result.usage.mimoUsage?.balanceDetail == "$25.51")
#expect(CookieHeaderCache.load(provider: .mimo)?.sourceLabel == "Safari")
}

#if os(macOS)
@Test
func `mimo importer merges profile stores before validating auth cookies`() {
Expand Down