Files
2026-07-13 12:22:33 +08:00

1243 lines
46 KiB
Swift

import Foundation
import Testing
@testable import CodexBarCore
struct MiniMaxAPISettingsReaderTests {
@Test
func `api token prefers coding plan specific environment key`() {
let token = MiniMaxAPISettingsReader.apiToken(environment: [
"MINIMAX_API_KEY": "sk-api-standard",
"MINIMAX_CODING_API_KEY": "sk-cp-coding-plan",
])
#expect(token == "sk-cp-coding-plan")
#expect(MiniMaxAPISettingsReader.apiKeyKind(token: token) == .codingPlan)
}
@Test
func `api token falls back to generic environment key`() {
let token = MiniMaxAPISettingsReader.apiToken(environment: [
"MINIMAX_API_KEY": "\"sk-api-standard\"",
])
#expect(token == "sk-api-standard")
#expect(MiniMaxAPISettingsReader.apiKeyKind(token: token) == .standard)
}
}
struct MiniMaxEndpointOverrideSettingsTests {
@Test
func `strict endpoint overrides reject non MiniMax hosts`() {
let env = [
MiniMaxSettingsReader.requireProviderEndpointOverridesKey: "true",
MiniMaxSettingsReader.hostKey: "https://attacker.example",
MiniMaxSettingsReader.codingPlanURLKey: "https://attacker.example/coding-plan",
MiniMaxSettingsReader.remainsURLKey: "https://attacker.example/remains",
MiniMaxSettingsReader.billingHistoryURLKey: "https://attacker.example/account/amount",
]
#expect(MiniMaxSettingsReader.hostOverride(environment: env) == nil)
#expect(MiniMaxSettingsReader.codingPlanURL(environment: env) == nil)
#expect(MiniMaxSettingsReader.remainsURL(environment: env) == nil)
#expect(MiniMaxSettingsReader.billingHistoryURL(environment: env) == nil)
}
@Test
func `endpoint overrides reject encoded host delimiters before suffix matching`() {
let encodedSlash = "https://attacker.example%2f.platform.minimax.io"
let doubleEncodedSlash = "https://attacker.example%252f.platform.minimax.io"
let env = [
MiniMaxSettingsReader.hostKey: encodedSlash,
MiniMaxSettingsReader.codingPlanURLKey: "\(encodedSlash)/coding-plan",
MiniMaxSettingsReader.remainsURLKey: "\(encodedSlash)/remains",
MiniMaxSettingsReader.billingHistoryURLKey: "\(encodedSlash)/account/amount",
]
#expect(MiniMaxSettingsReader.hostOverride(environment: env) == nil)
#expect(MiniMaxSettingsReader.codingPlanURL(environment: env) == nil)
#expect(MiniMaxSettingsReader.remainsURL(environment: env) == nil)
#expect(MiniMaxSettingsReader.billingHistoryURL(environment: env) == nil)
#expect(MiniMaxSettingsReader.hostOverride(environment: [
MiniMaxSettingsReader.hostKey: doubleEncodedSlash,
]) == nil)
}
@Test
func `endpoint overrides reject whitespace and control characters in hosts`() {
for host in ["https://bad host", "https://bad%20host", "https://bad%09host"] {
#expect(MiniMaxSettingsReader.hostOverride(environment: [
MiniMaxSettingsReader.hostKey: host,
]) == nil)
#expect(MiniMaxSettingsReader.remainsURL(environment: [
MiniMaxSettingsReader.remainsURLKey: "\(host)/remains",
]) == nil)
}
}
@Test
func `endpoint overrides require https and no userinfo`() {
#expect(MiniMaxSettingsReader.hostOverride(environment: [
MiniMaxSettingsReader.hostKey: "http://platform.minimax.io",
]) == nil)
#expect(MiniMaxSettingsReader.remainsURL(environment: [
MiniMaxSettingsReader.remainsURLKey: "https://user:pass@platform.minimax.io/remains",
]) == nil)
#expect(MiniMaxSettingsReader.rejectedEndpointOverrideKey(environment: [
MiniMaxSettingsReader.hostKey: ":443",
]) == MiniMaxSettingsReader.hostKey)
}
@Test
func `endpoint overrides allow MiniMax and custom https hosts`() {
let env = [
MiniMaxSettingsReader.hostKey: "platform.minimaxi.com",
MiniMaxSettingsReader.remainsURLKey: "https://platform.minimax.io/custom/remains",
]
#expect(MiniMaxSettingsReader.hostOverride(environment: env) == "platform.minimaxi.com")
#expect(MiniMaxSettingsReader.remainsURL(environment: env)?.host == "platform.minimax.io")
let customEnv = [
MiniMaxSettingsReader.hostKey: "proxy.example.test",
MiniMaxSettingsReader.remainsURLKey: "https://proxy.example.test/custom/remains",
]
#expect(MiniMaxSettingsReader.hostOverride(environment: customEnv) == "proxy.example.test")
#expect(MiniMaxSettingsReader.remainsURL(environment: customEnv)?.host == "proxy.example.test")
#expect(MiniMaxSettingsReader.rejectedEndpointOverrideKey(environment: customEnv) == nil)
}
@Test
func `host endpoint overrides preserve explicit port`() {
let env = [MiniMaxSettingsReader.hostKey: "proxy.example.test:8443"]
#expect(MiniMaxSettingsReader.hostOverride(environment: env) == "proxy.example.test:8443")
#expect(
MiniMaxUsageFetcher.resolveCodingPlanURL(region: .global, environment: env).absoluteString ==
"https://proxy.example.test:8443/user-center/payment/coding-plan?cycle_type=3")
#expect(
MiniMaxUsageFetcher.resolveRemainsURL(region: .global, environment: env).absoluteString ==
"https://proxy.example.test:8443/v1/api/openplatform/coding_plan/remains")
}
@Test
func `subscription metadata accepts host names beginning with http`() throws {
let url = try MiniMaxSubscriptionMetadataFetcher.resolveComboURL(
region: .global,
environment: [MiniMaxSettingsReader.hostKey: "https://http-proxy.example.test"])
#expect(url.host == "http-proxy.example.test")
#expect(url.scheme == "https")
}
@Test
func `scheme less endpoint preserves colon in path`() {
let env = [MiniMaxSettingsReader.remainsURLKey: "proxy.example.test/api:v1"]
#expect(
MiniMaxSettingsReader.remainsURL(environment: env)?.absoluteString ==
"https://proxy.example.test/api:v1")
}
@Test
func `custom https endpoints allow bracketed IPv6 literals`() {
let env = [
MiniMaxSettingsReader.hostKey: "[::1]:8443",
MiniMaxSettingsReader.remainsURLKey: "https://[::1]:8443/remains",
]
#expect(MiniMaxSettingsReader.hostOverride(environment: env) == "[::1]:8443")
#expect(MiniMaxSettingsReader.remainsURL(environment: env)?.host == "::1")
#expect(MiniMaxSettingsReader.rejectedEndpointOverrideKey(environment: env) == nil)
}
@Test
func `strict provider endpoint mode rejects custom hosts`() {
let env = [
MiniMaxSettingsReader.requireProviderEndpointOverridesKey: "true",
MiniMaxSettingsReader.hostKey: "proxy.example.test",
MiniMaxSettingsReader.remainsURLKey: "https://proxy.example.test/custom/remains",
]
#expect(MiniMaxSettingsReader.hostOverride(environment: env) == nil)
#expect(MiniMaxSettingsReader.remainsURL(environment: env) == nil)
#expect(MiniMaxSettingsReader.rejectedEndpointOverrideKey(environment: env) == MiniMaxSettingsReader.hostKey)
}
@Test
func `custom https compatibility mode still rejects http and userinfo`() {
#expect(MiniMaxSettingsReader.hostOverride(environment: [
MiniMaxSettingsReader.hostKey: "http://proxy.example.test",
]) == nil)
#expect(MiniMaxSettingsReader.rejectedEndpointOverrideKey(environment: [
MiniMaxSettingsReader.remainsURLKey: "https://user:pass@proxy.example.test/remains",
]) == MiniMaxSettingsReader.remainsURLKey)
}
@Test
func `explicit endpoint override rejects invalid scheme before network`() async {
await #expect(throws: ProviderEndpointOverrideError.minimax(MiniMaxSettingsReader.codingPlanURLKey)) {
_ = try await MiniMaxUsageFetcher.fetchUsage(
cookieHeader: "session=abc123",
environment: [MiniMaxSettingsReader.codingPlanURLKey: "http://platform.minimax.io/coding-plan"],
includeBillingHistory: false)
}
}
}
struct MiniMaxProviderStrategyTests {
private struct StubClaudeFetcher: ClaudeUsageFetching {
func loadLatestUsage(model _: String) async throws -> ClaudeUsageSnapshot {
throw ClaudeUsageError.parseFailed("stub")
}
func debugRawProbe(model _: String) async -> String {
"stub"
}
func detectVersion() -> String? {
nil
}
}
@Test
func `browser cookie import is user initiated app only`() {
let appContext = self.makeContext(runtime: .app)
let cliContext = self.makeContext(runtime: .cli)
#expect(MiniMaxCodingPlanFetchStrategy.allowsBrowserCookieImport(context: appContext) == false)
#expect(MiniMaxCodingPlanFetchStrategy.allowsBrowserCookieImport(context: cliContext) == false)
ProviderInteractionContext.$current.withValue(.userInitiated) {
#expect(MiniMaxCodingPlanFetchStrategy.allowsBrowserCookieImport(context: appContext))
#expect(MiniMaxCodingPlanFetchStrategy.allowsBrowserCookieImport(context: cliContext) == false)
}
}
private func makeContext(runtime: ProviderRuntime) -> ProviderFetchContext {
let env: [String: String] = [:]
return ProviderFetchContext(
runtime: runtime,
sourceMode: .web,
includeCredits: false,
webTimeout: 1,
webDebugDumpHTML: false,
verbose: false,
env: env,
settings: nil,
fetcher: UsageFetcher(environment: env),
claudeFetcher: StubClaudeFetcher(),
browserDetection: BrowserDetection(cacheTTL: 0))
}
}
struct MiniMaxCookieHeaderTests {
@Test
func `normalizes raw cookie header`() {
let raw = "foo=bar; session=abc123"
let normalized = MiniMaxCookieHeader.normalized(from: raw)
#expect(normalized == "foo=bar; session=abc123")
}
@Test
func `extracts from cookie header line`() {
let raw = "Cookie: foo=bar; session=abc123"
let normalized = MiniMaxCookieHeader.normalized(from: raw)
#expect(normalized == "foo=bar; session=abc123")
}
@Test
func `extracts from curl header`() {
let raw = "curl https://platform.minimax.io -H 'Cookie: foo=bar; session=abc123' -H 'accept: */*'"
let normalized = MiniMaxCookieHeader.normalized(from: raw)
#expect(normalized == "foo=bar; session=abc123")
}
@Test
func `extracts from curl cookie flag`() {
let raw = "curl https://platform.minimax.io --cookie 'foo=bar; session=abc123'"
let normalized = MiniMaxCookieHeader.normalized(from: raw)
#expect(normalized == "foo=bar; session=abc123")
}
@Test
func `extracts auth and group ID from curl`() {
let raw = """
curl 'https://platform.minimax.io/v1/api/openplatform/coding_plan/remains?GroupId=123456' \
-H 'authorization: Bearer token123' \
-H 'Cookie: foo=bar; session=abc123'
"""
let override = MiniMaxCookieHeader.override(from: raw)
#expect(override?.cookieHeader == "foo=bar; session=abc123")
#expect(override?.authorizationToken == "token123")
#expect(override?.groupID == "123456")
}
@Test
func `extracts auth from uppercase header`() {
let raw = """
curl 'https://platform.minimax.io/v1/api/openplatform/coding_plan/remains?GROUP_ID=98765' \
-H 'Authorization: Bearer token-abc' \
-H 'Cookie: foo=bar; session=abc123'
"""
let override = MiniMaxCookieHeader.override(from: raw)
#expect(override?.authorizationToken == "token-abc")
#expect(override?.groupID == "98765")
}
@Test
func `extracts group ID from combo curl header and cookie`() {
let raw = """
curl 'https://www.minimaxi.com/v1/api/openplatform/charge/combo/cycle_audio_resource_package' \
-b 'foo=bar; minimax_group_id_v2=2013894056999916075' \
-H 'x-group-id: 2013894056999916075'
"""
let override = MiniMaxCookieHeader.override(from: raw)
#expect(override?.cookieHeader == "foo=bar; minimax_group_id_v2=2013894056999916075")
#expect(override?.groupID == "2013894056999916075")
}
}
struct MiniMaxUsageParserTests {
@Test
func `signed out check ignores login copy inside scripts`() {
let html = """
<html>
<head>
<script id="__NEXT_DATA__" type="application/json">
{
"props": {
"pageProps": {
"_nextI18Next": {
"initialI18nStore": {
"zh": {
"common": {
"landing_common_login": "登录",
"login": "Log in"
}
}
}
}
}
}
}
</script>
</head>
<body><div id="__next">Coding Plan</div></body>
</html>
"""
#expect(!MiniMaxUsageFetcher._looksSignedOutForTesting(html: html))
}
@Test
func `signed out check still detects visible login copy`() {
let html = """
<html>
<head><script>{"landing_common_login":"登录"}</script></head>
<body><main><a>Log in</a></main></body>
</html>
"""
#expect(MiniMaxUsageFetcher._looksSignedOutForTesting(html: html))
}
@Test
func `parses planName from concrete fields in remains response`() throws {
let now = Date(timeIntervalSince1970: 1_700_000_000)
// 1. plan_name
let jsonPlanName = """
{
"base_resp": { "status_code": 0 },
"plan_name": "MiniMax Star",
"model_remains": [{"model_name": "abab6.5"}]
}
"""
let snapshot1 = try MiniMaxUsageParser.parseCodingPlanRemains(data: Data(jsonPlanName.utf8), now: now)
#expect(snapshot1.planName == "MiniMax Star")
// 2. current_plan_title
let jsonCurrentPlan = """
{
"base_resp": { "status_code": 0 },
"current_plan_title": "Coding Plan Pro",
"model_remains": [{"model_name": "abab6.5"}]
}
"""
let snapshot2 = try MiniMaxUsageParser.parseCodingPlanRemains(data: Data(jsonCurrentPlan.utf8), now: now)
#expect(snapshot2.planName == "Coding Plan Pro")
// 3. current_subscribe_title
let jsonSubscribe = """
{
"base_resp": { "status_code": 0 },
"current_subscribe_title": "Max",
"model_remains": [{"model_name": "abab6.5"}]
}
"""
let snapshot3 = try MiniMaxUsageParser.parseCodingPlanRemains(data: Data(jsonSubscribe.utf8), now: now)
#expect(snapshot3.planName == "Max")
// 4. combo_title
let jsonCombo = """
{
"base_resp": { "status_code": 0 },
"combo_title": "Combo Star",
"model_remains": [{"model_name": "abab6.5"}]
}
"""
let snapshot4 = try MiniMaxUsageParser.parseCodingPlanRemains(data: Data(jsonCombo.utf8), now: now)
#expect(snapshot4.planName == "Combo Star")
// 5. current_combo_card.title
let jsonComboCard = """
{
"base_resp": { "status_code": 0 },
"current_combo_card": { "title": "Card Title" },
"model_remains": [{"model_name": "abab6.5"}]
}
"""
let snapshot5 = try MiniMaxUsageParser.parseCodingPlanRemains(data: Data(jsonComboCard.utf8), now: now)
#expect(snapshot5.planName == "Card Title")
}
@Test
func `toUsageSnapshot maps planName to loginMethod`() {
let now = Date()
let snapshot1 = MiniMaxUsageSnapshot(
planName: "MiniMax Star",
availablePrompts: nil,
currentPrompts: nil,
remainingPrompts: nil,
windowMinutes: nil,
usedPercent: nil,
resetsAt: nil,
updatedAt: now)
let usage1 = snapshot1.toUsageSnapshot()
#expect(usage1.identity?.loginMethod == "MiniMax Star")
let snapshot2 = MiniMaxUsageSnapshot(
planName: nil,
availablePrompts: nil,
currentPrompts: nil,
remainingPrompts: nil,
windowMinutes: nil,
usedPercent: nil,
resetsAt: nil,
updatedAt: now)
let usage2 = snapshot2.toUsageSnapshot()
#expect(usage2.identity?.loginMethod == nil)
}
@Test
func `parses coding plan snapshot`() throws {
let now = Date(timeIntervalSince1970: 1_700_000_000)
let html = """
<div>Coding Plan</div>
<div>Max</div>
<div>Available usage: 1,000 prompts / 5 hours</div>
<div>Current Usage</div>
<div>0% Used</div>
<div>Resets in 4 min</div>
"""
let snapshot = try MiniMaxUsageParser.parse(html: html, now: now)
#expect(snapshot.planName == "Max")
#expect(snapshot.availablePrompts == 1000)
#expect(snapshot.windowMinutes == 300)
#expect(snapshot.usedPercent == 0)
#expect(snapshot.resetsAt == now.addingTimeInterval(240))
let usage = snapshot.toUsageSnapshot()
#expect(usage.primary?.resetDescription == "1000 prompts / 5 hours")
}
@Test
func `parses coding plan remains response`() throws {
let now = Date(timeIntervalSince1970: 1_700_000_000)
let start = 1_700_000_000_000
let end = start + 5 * 60 * 60 * 1000
let json = """
{
"base_resp": { "status_code": 0 },
"current_subscribe_title": "Max",
"model_remains": [
{
"current_interval_total_count": 1000,
"current_interval_usage_count": 250,
"start_time": \(start),
"end_time": \(end),
"remains_time": 240000
}
]
}
"""
let snapshot = try MiniMaxUsageParser.parseCodingPlanRemains(data: Data(json.utf8), now: now)
let expectedReset = Date(timeIntervalSince1970: TimeInterval(end) / 1000)
#expect(snapshot.planName == "Max")
#expect(snapshot.availablePrompts == 1000)
#expect(snapshot.currentPrompts == 750)
#expect(snapshot.remainingPrompts == 250)
#expect(snapshot.windowMinutes == 300)
#expect(snapshot.usedPercent == 75)
#expect(snapshot.resetsAt == expectedReset)
}
@Test
func `parses model remains services using used quota semantics`() throws {
let now = Date(timeIntervalSince1970: 1_700_000_000)
let start = 1_700_000_000_000
let end = start + 5 * 60 * 60 * 1000
let json = """
{
"base_resp": { "status_code": 0 },
"current_subscribe_title": "Max",
"model_remains": [
{
"model_name": "M2.7-highspeed",
"current_interval_total_count": 1000,
"current_interval_usage_count": 250,
"start_time": \(start),
"end_time": \(end),
"remains_time": 240000
}
]
}
"""
let snapshot = try MiniMaxUsageParser.parseCodingPlanRemains(data: Data(json.utf8), now: now)
let service = try #require(snapshot.services?.first)
#expect(service.displayName == "Text Generation")
#expect(service.usage == 750)
#expect(service.remaining == 250)
#expect(service.limit == 1000)
#expect(service.percent == 75)
#expect(snapshot.toUsageSnapshot().primary?.usedPercent == 75)
}
@Test
func `text generation includes weekly window when provided`() throws {
let now = Date(timeIntervalSince1970: 1_700_000_000)
let start = 1_700_000_000_000
let end = start + 5 * 60 * 60 * 1000
let weekStart = start - 2 * 24 * 60 * 60 * 1000
let weekEnd = weekStart + 7 * 24 * 60 * 60 * 1000
let json = """
{
"base_resp": { "status_code": 0 },
"model_remains": [
{
"model_name": "MiniMax-M1",
"current_interval_total_count": 1000,
"current_interval_usage_count": 250,
"start_time": \(start),
"end_time": \(end),
"current_weekly_total_count": 6000,
"current_weekly_usage_count": 5376,
"weekly_start_time": \(weekStart),
"weekly_end_time": \(weekEnd)
}
]
}
"""
let snapshot = try MiniMaxUsageParser.parseCodingPlanRemains(data: Data(json.utf8), now: now)
let services = try #require(snapshot.services)
#expect(services.count == 2)
#expect(services[0].serviceType == "Text Generation")
#expect(services[0].windowType == "5 hours")
#expect(services[1].serviceType == "Text Generation")
#expect(services[1].windowType == "Weekly")
#expect(services[1].usage == 624)
#expect(services[1].limit == 6000)
#expect(services[1].timeRange.contains("/"))
#expect(services[1].timeRange.contains("UTC+8"))
#expect(!services[1].timeRange.hasPrefix("10:00-10:00"))
}
@Test
func `legacy plan hides weekly when weekly total is missing or zero`() throws {
let now = Date(timeIntervalSince1970: 1_700_000_000)
let start = 1_700_000_000_000
let end = start + 5 * 60 * 60 * 1000
let json = """
{
"base_resp": { "status_code": 0 },
"model_remains": [
{
"model_name": "MiniMax-M1",
"current_interval_total_count": 1000,
"current_interval_usage_count": 250,
"start_time": \(start),
"end_time": \(end),
"current_weekly_total_count": 0,
"current_weekly_usage_count": 0
}
]
}
"""
let snapshot = try MiniMaxUsageParser.parseCodingPlanRemains(data: Data(json.utf8), now: now)
let services = try #require(snapshot.services)
#expect(services.count == 1)
#expect(services[0].windowType == "5 hours")
}
@Test
func `parses multi service payload and utc offset reset`() throws {
var calendar = Calendar(identifier: .gregorian)
calendar.timeZone = try #require(TimeZone(secondsFromGMT: 8 * 3600))
let now = try #require(calendar.date(from: DateComponents(
year: 2026,
month: 3,
day: 25,
hour: 11,
minute: 0)))
let expectedReset = try #require(calendar.date(from: DateComponents(
year: 2026,
month: 3,
day: 25,
hour: 15,
minute: 0)))
let json = """
{
"data": {
"services": [
{
"service_type": "Text Generation",
"window_type": "5 hours",
"time_range": "10:00-15:00(UTC+8)",
"usage": 2,
"limit": 10
},
{
"service_type": "Image",
"window_type": "Today",
"time_range": "2026/03/25 00:00 - 2026/03/26 00:00",
"usage": "5",
"limit": "50",
"percent": "10"
}
]
}
}
"""
let snapshot = try MiniMaxUsageParser.parseCodingPlanRemains(data: Data(json.utf8), now: now)
let services = try #require(snapshot.services)
#expect(services.count == 2)
#expect(services[0].usage == 2)
#expect(services[0].remaining == 8)
#expect(services[0].percent == 20)
#expect(services[0].resetsAt == expectedReset)
#expect(services[1].usage == 5)
#expect(services[1].remaining == 45)
#expect(services[1].percent == 10)
}
@Test
func `parses coding plan remains from data wrapper`() throws {
let now = Date(timeIntervalSince1970: 1_700_000_100)
let start = 1_700_000_000_000
let end = start + 5 * 60 * 60 * 1000
let json = """
{
"base_resp": { "status_code": "0" },
"data": {
"current_subscribe_title": "Max",
"model_remains": [
{
"current_interval_total_count": "15000",
"current_interval_usage_count": "14989",
"start_time": \(start),
"end_time": \(end),
"remains_time": 8941292
}
]
}
}
"""
let snapshot = try MiniMaxUsageParser.parseCodingPlanRemains(data: Data(json.utf8), now: now)
let expectedUsed = Double(11) / Double(15000) * 100
let expectedReset = Date(timeIntervalSince1970: TimeInterval(end) / 1000)
#expect(snapshot.planName == "Max")
#expect(snapshot.availablePrompts == 15000)
#expect(snapshot.currentPrompts == 11)
#expect(snapshot.remainingPrompts == 14989)
#expect(snapshot.windowMinutes == 300)
#expect(abs((snapshot.usedPercent ?? 0) - expectedUsed) < 0.01)
#expect(snapshot.resetsAt == expectedReset)
}
@Test
func `parses coding plan from next data`() throws {
let now = Date(timeIntervalSince1970: 1_700_000_000)
let start = 1_700_000_000_000
let end = start + 5 * 60 * 60 * 1000
let json = """
{
"props": {
"pageProps": {
"data": {
"base_resp": { "status_code": 0 },
"current_subscribe_title": "Max",
"model_remains": [
{
"current_interval_total_count": 1000,
"current_interval_usage_count": 250,
"start_time": \(start),
"end_time": \(end),
"remains_time": 240000
}
]
}
}
}
}
"""
let html = """
<html>
<script id="__NEXT_DATA__" type="application/json">\(json)</script>
</html>
"""
let snapshot = try MiniMaxUsageParser.parse(html: html, now: now)
let expectedReset = Date(timeIntervalSince1970: TimeInterval(end) / 1000)
#expect(snapshot.planName == "Max")
#expect(snapshot.availablePrompts == 1000)
#expect(snapshot.currentPrompts == 750)
#expect(snapshot.remainingPrompts == 250)
#expect(snapshot.windowMinutes == 300)
#expect(snapshot.usedPercent == 75)
#expect(snapshot.resetsAt == expectedReset)
}
@Test
func `parses HTML with used prefix and reset time`() throws {
var calendar = Calendar(identifier: .gregorian)
calendar.timeZone = TimeZone(identifier: "UTC") ?? .current
let now = try #require(calendar.date(from: DateComponents(year: 2025, month: 1, day: 1, hour: 10, minute: 0)))
let expectedReset = try #require(calendar.date(from: DateComponents(
year: 2025,
month: 1,
day: 1,
hour: 23,
minute: 30)))
let html = """
<div>Coding Plan Pro</div>
<div>Available usage: 1,500 prompts / 1.5 hours</div>
<div>Used 75%</div>
<div>Resets at 23:30 (UTC)</div>
"""
let snapshot = try MiniMaxUsageParser.parse(html: html, now: now)
#expect(snapshot.planName == "Pro")
#expect(snapshot.availablePrompts == 1500)
#expect(snapshot.windowMinutes == 90)
#expect(snapshot.usedPercent == 75)
#expect(snapshot.resetsAt == expectedReset)
}
@Test
func `throws on missing cookie response`() {
let json = """
{
"base_resp": { "status_code": 1004, "status_msg": "cookie is missing, log in again" }
}
"""
#expect(throws: MiniMaxUsageError.invalidCredentials) {
try MiniMaxUsageParser.parseCodingPlanRemains(data: Data(json.utf8))
}
}
@Test
func `throws on string status code when logged out`() {
let json = """
{
"base_resp": { "status_code": "1004", "status_msg": "login required" }
}
"""
#expect(throws: MiniMaxUsageError.invalidCredentials) {
try MiniMaxUsageParser.parseCodingPlanRemains(data: Data(json.utf8))
}
}
@Test
func `throws on error in data wrapper`() {
let json = """
{
"data": {
"base_resp": { "status_code": 1004, "status_msg": "unauthorized" }
}
}
"""
#expect(throws: MiniMaxUsageError.invalidCredentials) {
try MiniMaxUsageParser.parseCodingPlanRemains(data: Data(json.utf8))
}
}
@Test
func `billing history aggregates records locally`() throws {
var calendar = Calendar(identifier: .gregorian)
calendar.timeZone = try #require(TimeZone(secondsFromGMT: 0))
let now = try #require(ISO8601DateFormatter().date(from: "2026-05-17T12:00:00Z"))
let json = """
{
"base_resp": { "status_code": 0 },
"consume_token_sum": 999999,
"total_cnt": 4,
"charge_records": [
{
"consume_token": 1000,
"consume_cash_after_voucher": 1.25,
"ymd": "2026-05-17",
"method": "chat",
"model": "MiniMax-M1"
},
{
"consume_token": "2000",
"consume_cash": "2.50",
"ymd": "2026-05-16",
"method": "chat",
"model": "MiniMax-M2"
},
{
"consume_input_token": 1200,
"consume_output_token": 1800,
"ymd": "2026-04-18",
"method": "audio",
"model": "speech-2.8"
},
{
"consume_token": 4000,
"ymd": "2026-04-17",
"method": "old",
"model": "ignored"
}
]
}
"""
let summary = try MiniMaxBillingHistoryParser.parse(
data: Data(json.utf8),
now: now,
calendar: calendar)
#expect(summary.todayTokens == 1000)
#expect(summary.last30DaysTokens == 6000)
#expect(summary.todayCash == 1.25)
#expect(summary.last30DaysCash == 3.75)
#expect(summary.daily.map(\.day) == ["2026-04-18", "2026-05-16", "2026-05-17"])
#expect(summary.topMethods.first?.name == "audio")
#expect(summary.topModels.first?.name == "speech-2.8")
}
@Test
func `billing history preserves date only days in local calendar`() throws {
var calendar = Calendar(identifier: .gregorian)
calendar.timeZone = try #require(TimeZone(secondsFromGMT: -7 * 60 * 60))
let now = try #require(ISO8601DateFormatter().date(from: "2026-05-17T12:00:00Z"))
let json = """
{
"base_resp": { "status_code": 0 },
"total_cnt": 1,
"charge_records": [
{
"consume_token": 1234,
"ymd": "2026-05-17",
"method": "chat",
"model": "MiniMax-M1"
}
]
}
"""
let summary = try MiniMaxBillingHistoryParser.parse(
data: Data(json.utf8),
now: now,
calendar: calendar)
#expect(summary.todayTokens == 1234)
#expect(summary.daily.map(\.day) == ["2026-05-17"])
}
@Test
func `billing history filters failed records`() throws {
var calendar = Calendar(identifier: .gregorian)
calendar.timeZone = try #require(TimeZone(secondsFromGMT: 0))
let now = try #require(ISO8601DateFormatter().date(from: "2026-05-17T12:00:00Z"))
let json = """
{
"base_resp": { "status_code": 0 },
"total_cnt": 5,
"charge_records": [
{
"consume_token": 1000,
"ymd": "2026-05-17",
"method": "chat",
"model": "MiniMax-M1",
"result": "SUCCESS"
},
{
"consume_token": 2000,
"ymd": "2026-05-17",
"method": "chat",
"model": "MiniMax-M1",
"result": "FAILED"
},
{
"consume_token": 3000,
"ymd": "2026-05-17",
"method": "chat",
"model": "MiniMax-M1",
"status": "fail"
},
{
"consume_token": 4000,
"ymd": "2026-05-17",
"method": "audio",
"model": "speech-2.8"
},
{
"consume_token": 5000,
"ymd": "2026-05-17",
"method": "video",
"model": "video-1",
"status": 0
}
]
}
"""
let summary = try MiniMaxBillingHistoryParser.parse(
data: Data(json.utf8),
now: now,
calendar: calendar)
// Only SUCCESS (1000) and missing/empty result status (4000) should be included.
// FAILED (2000), status "fail" (3000), and numeric status 0 (5000) should be skipped.
#expect(summary.todayTokens == 5000)
#expect(summary.last30DaysTokens == 5000)
#expect(summary.daily.map(\.day) == ["2026-05-17"])
// Top methods should aggregate only SUCCESS/missing records.
#expect(summary.topMethods.count == 2)
#expect(summary.topMethods[0].name == "audio")
#expect(summary.topMethods[0].tokens == 4000)
#expect(summary.topMethods[1].name == "chat")
#expect(summary.topMethods[1].tokens == 1000)
}
@Test
func `web usage fetch attaches billing history when available`() async throws {
let now = try #require(ISO8601DateFormatter().date(from: "2026-05-17T12:00:00Z"))
let transport = ProviderHTTPTransportStub { request in
let url = try #require(request.url)
if url.path.contains("coding-plan") {
return Self.httpResponse(
url: url,
body: Self.codingPlanJSON,
contentType: "application/json")
}
#expect(url.path == "/account/amount")
#expect(url.query?.contains("aggregate=false") == true)
#expect(request.value(forHTTPHeaderField: "Cookie") == "HERTZ-SESSION=abc")
let body = """
{
"base_resp": { "status_code": 0 },
"total_cnt": 1,
"charge_records": [
{
"consume_token": 1234,
"ymd": "2026-05-17",
"method": "chat",
"model": "MiniMax-M1"
}
]
}
"""
return Self.httpResponse(url: url, body: body, contentType: "application/json")
}
let snapshot = try await MiniMaxUsageFetcher.fetchUsage(
cookieHeader: "HERTZ-SESSION=abc",
region: .global,
environment: [:],
session: transport,
now: now)
#expect(snapshot.currentPrompts == 2)
#expect(snapshot.billingSummary?.todayTokens == 1234)
#expect(snapshot.billingSummary?.last30DaysTokens == 1234)
}
@Test
func `web usage fetch keeps paginating billing history until 30 day cutoff`() async throws {
let now = try #require(ISO8601DateFormatter().date(from: "2026-05-17T12:00:00Z"))
let transport = ProviderHTTPTransportStub { request in
let url = try #require(request.url)
if url.path.contains("coding-plan") {
return Self.httpResponse(
url: url,
body: Self.codingPlanJSON,
contentType: "application/json")
}
let page = URLComponents(url: url, resolvingAgainstBaseURL: false)?
.queryItems?
.first { $0.name == "page" }?
.value ?? "1"
let recordDay = page == "3" ? "2026-04-17" : "2026-05-17"
let records = (0..<100)
.map { _ in
"""
{"consume_token":1,"ymd":"\(recordDay)","method":"chat","model":"MiniMax-M1"}
"""
}
.joined(separator: ",")
let body = """
{
"base_resp": { "status_code": 0 },
"total_cnt": 250,
"charge_records": [\(records)]
}
"""
return Self.httpResponse(url: url, body: body, contentType: "application/json")
}
let snapshot = try await MiniMaxUsageFetcher.fetchUsage(
cookieHeader: "HERTZ-SESSION=abc",
region: .global,
environment: [:],
session: transport,
now: now)
let billingRequests = await transport.requests().filter { $0.url?.path == "/account/amount" }
#expect(billingRequests.count == 3)
#expect(snapshot.billingSummary?.last30DaysTokens == 200)
}
@Test
func `web usage fetch skips billing history when optional usage is disabled`() async throws {
let now = try #require(ISO8601DateFormatter().date(from: "2026-05-17T12:00:00Z"))
let transport = ProviderHTTPTransportStub { request in
let url = try #require(request.url)
#expect(url.path.contains("coding-plan"))
return Self.httpResponse(
url: url,
body: Self.codingPlanJSON,
contentType: "application/json")
}
let snapshot = try await MiniMaxUsageFetcher.fetchUsage(
cookieHeader: "HERTZ-SESSION=abc",
region: .global,
environment: [:],
includeBillingHistory: false,
session: transport,
now: now)
let requests = await transport.requests()
#expect(snapshot.currentPrompts == 2)
#expect(snapshot.billingSummary == nil)
#expect(requests.count == 1)
}
@Test
func `web usage fetch keeps quota when billing history is forbidden`() async throws {
let now = try #require(ISO8601DateFormatter().date(from: "2026-05-17T12:00:00Z"))
let transport = ProviderHTTPTransportStub { request in
let url = try #require(request.url)
if url.path.contains("coding-plan") {
return Self.httpResponse(
url: url,
body: Self.codingPlanJSON,
contentType: "application/json")
}
return Self.httpResponse(url: url, body: "{}", statusCode: 403, contentType: "application/json")
}
let snapshot = try await MiniMaxUsageFetcher.fetchUsage(
cookieHeader: "HERTZ-SESSION=abc",
region: .global,
environment: [:],
session: transport,
now: now)
#expect(snapshot.currentPrompts == 2)
#expect(snapshot.billingSummary == nil)
}
@Test
func `web usage fetch preserves stale bearer failure during billing history`() async throws {
let now = try #require(ISO8601DateFormatter().date(from: "2026-05-17T12:00:00Z"))
let transport = ProviderHTTPTransportStub { request in
let url = try #require(request.url)
if url.path.contains("coding-plan") {
return Self.httpResponse(
url: url,
body: Self.codingPlanJSON,
contentType: "application/json")
}
#expect(request.value(forHTTPHeaderField: "Authorization") == "Bearer stale")
return Self.httpResponse(url: url, body: "{}", statusCode: 403, contentType: "application/json")
}
await #expect(throws: MiniMaxUsageError.invalidCredentials) {
try await MiniMaxUsageFetcher.fetchUsage(
cookieHeader: "HERTZ-SESSION=abc",
authorizationToken: "stale",
region: .global,
environment: [:],
session: transport,
now: now)
}
}
@Test
func `web usage fetch preserves billing history cancellation`() async throws {
let now = try #require(ISO8601DateFormatter().date(from: "2026-05-17T12:00:00Z"))
let transport = ProviderHTTPTransportStub { request in
let url = try #require(request.url)
if url.path.contains("coding-plan") {
return Self.httpResponse(
url: url,
body: Self.codingPlanJSON,
contentType: "application/json")
}
throw CancellationError()
}
await #expect(throws: CancellationError.self) {
try await MiniMaxUsageFetcher.fetchUsage(
cookieHeader: "HERTZ-SESSION=abc",
region: .global,
environment: [:],
session: transport,
now: now)
}
}
private static let codingPlanJSON = """
{
"base_resp": { "status_code": 0 },
"data": {
"plan_name": "Max",
"model_remains": [
{
"model_name": "MiniMax-M1",
"current_interval_total_count": 10,
"current_interval_usage_count": 8,
"start_time": 1779019200,
"end_time": 1779037200,
"remains_time": 3600
}
]
}
}
"""
private static func httpResponse(
url: URL,
body: String,
statusCode: Int = 200,
contentType: String) -> (Data, URLResponse)
{
let response = HTTPURLResponse(
url: url,
statusCode: statusCode,
httpVersion: "HTTP/1.1",
headerFields: ["Content-Type": contentType])!
return (Data(body.utf8), response)
}
}
struct MiniMaxAPIRegionTests {
@Test
func `defaults to global hosts`() {
let codingPlan = MiniMaxUsageFetcher.resolveCodingPlanURL(region: .global, environment: [:])
let remains = MiniMaxUsageFetcher.resolveRemainsURL(region: .global, environment: [:])
#expect(codingPlan.host == "platform.minimax.io")
#expect(remains.host == "platform.minimax.io")
}
@Test
func `uses china mainland hosts`() {
let codingPlan = MiniMaxUsageFetcher.resolveCodingPlanURL(region: .chinaMainland, environment: [:])
let remains = MiniMaxUsageFetcher.resolveRemainsURL(region: .chinaMainland, environment: [:])
#expect(codingPlan.host == "platform.minimaxi.com")
#expect(remains.host == "platform.minimaxi.com")
#expect(codingPlan.query == "cycle_type=3")
}
@Test
func `resolves web remains fallback hosts`() {
let global = MiniMaxUsageFetcher.resolveRemainsURLs(region: .global, environment: [:])
let china = MiniMaxUsageFetcher.resolveRemainsURLs(region: .chinaMainland, environment: [:])
#expect(global.map(\.host).contains("platform.minimax.io"))
#expect(global.map(\.host).contains("www.minimax.io"))
#expect(china.map(\.host).contains("platform.minimaxi.com"))
#expect(china.map(\.host).contains("www.minimaxi.com"))
}
@Test
func `resolves official token plan remains URL`() {
let url = MiniMaxUsageFetcher.resolveTokenPlanRemainsURL(region: .chinaMainland)
#expect(url.host == "api.minimaxi.com")
#expect(url.path == "/v1/token_plan/remains")
}
@Test
func `host override wins for remains and coding plan`() {
let env = [MiniMaxSettingsReader.hostKey: "api.minimaxi.com"]
let codingPlan = MiniMaxUsageFetcher.resolveCodingPlanURL(region: .global, environment: env)
let remains = MiniMaxUsageFetcher.resolveRemainsURL(region: .global, environment: env)
#expect(codingPlan.host == "api.minimaxi.com")
#expect(remains.host == "api.minimaxi.com")
}
@Test
func `billing history url uses account amount endpoint`() {
let url = MiniMaxUsageFetcher.resolveBillingHistoryURL(region: .chinaMainland, environment: [:], page: 2)
#expect(url.host == "platform.minimaxi.com")
#expect(url.path == "/account/amount")
#expect(url.query?.contains("page=2") == true)
#expect(url.query?.contains("limit=100") == true)
#expect(url.query?.contains("aggregate=false") == true)
}
@Test
func `remains url override beats host`() {
let env = [MiniMaxSettingsReader.remainsURLKey: "https://platform.minimaxi.com/custom/remains"]
let remains = MiniMaxUsageFetcher.resolveRemainsURL(region: .global, environment: env)
#expect(remains.absoluteString == "https://platform.minimaxi.com/custom/remains")
}
@Test
func `origin uses coding plan override host`() {
let env = [MiniMaxSettingsReader.codingPlanURLKey: "https://api.minimaxi.com/custom/path?cycle_type=3"]
let codingPlan = MiniMaxUsageFetcher.resolveCodingPlanURL(region: .global, environment: env)
let origin = MiniMaxUsageFetcher.originURL(from: codingPlan)
#expect(origin.absoluteString == "https://api.minimaxi.com")
}
@Test
func `origin strips host override path`() {
let env = [MiniMaxSettingsReader.hostKey: "https://api.minimaxi.com/custom/path"]
let codingPlan = MiniMaxUsageFetcher.resolveCodingPlanURL(region: .global, environment: env)
let origin = MiniMaxUsageFetcher.originURL(from: codingPlan)
#expect(origin.absoluteString == "https://api.minimaxi.com")
}
}