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

1022 lines
34 KiB
Swift

import Foundation
import Testing
@testable import CodexBarCore
struct ZaiSettingsReaderTests {
@Test
func `api token reads from environment`() {
let token = ZaiSettingsReader.apiToken(environment: ["Z_AI_API_KEY": "abc123"])
#expect(token == "abc123")
}
@Test
func `api token strips quotes`() {
let token = ZaiSettingsReader.apiToken(environment: ["Z_AI_API_KEY": "\"token-xyz\""])
#expect(token == "token-xyz")
}
@Test
func `api host reads from environment`() {
let host = ZaiSettingsReader.apiHost(environment: [ZaiSettingsReader.apiHostKey: " open.bigmodel.cn "])
#expect(host == "open.bigmodel.cn")
}
@Test
func `quota URL infers scheme`() {
let url = ZaiSettingsReader
.quotaURL(environment: [ZaiSettingsReader.quotaURLKey: "open.bigmodel.cn/api/coding"])
#expect(url?.absoluteString == "https://open.bigmodel.cn/api/coding")
}
@Test
func `endpoint override validation accepts HTTPS and bare hosts`() throws {
try ZaiSettingsReader.validateEndpointOverrides(environment: [
ZaiSettingsReader.quotaURLKey: "https://open.bigmodel.cn/api/coding",
])
try ZaiSettingsReader.validateEndpointOverrides(environment: [
ZaiSettingsReader.apiHostKey: "open.bigmodel.cn",
])
}
@Test
func `endpoint override validation rejects insecure URLs`() {
#expect(throws: ZaiSettingsError.invalidEndpointOverride(ZaiSettingsReader.quotaURLKey)) {
try ZaiSettingsReader.validateEndpointOverrides(environment: [
ZaiSettingsReader.quotaURLKey: "http://attacker.test/quota",
])
}
#expect(throws: ZaiSettingsError.invalidEndpointOverride(ZaiSettingsReader.apiHostKey)) {
try ZaiSettingsReader.validateEndpointOverrides(environment: [
ZaiSettingsReader.apiHostKey: "http://attacker.test",
])
}
}
}
struct ZaiUsageSnapshotTests {
@Test
func `maps usage snapshot windows`() {
let reset = Date(timeIntervalSince1970: 123)
let tokenLimit = ZaiLimitEntry(
type: .tokensLimit,
unit: .hours,
number: 5,
usage: 100,
currentValue: 20,
remaining: 80,
percentage: 25,
usageDetails: [],
nextResetTime: reset)
let timeLimit = ZaiLimitEntry(
type: .timeLimit,
unit: .days,
number: 30,
usage: 200,
currentValue: 40,
remaining: 160,
percentage: 50,
usageDetails: [],
nextResetTime: nil)
let snapshot = ZaiUsageSnapshot(
tokenLimit: tokenLimit,
timeLimit: timeLimit,
planName: nil,
updatedAt: reset)
let usage = snapshot.toUsageSnapshot()
#expect(usage.primary?.usedPercent == 20)
#expect(usage.primary?.windowMinutes == 300)
#expect(usage.primary?.resetsAt == reset)
#expect(usage.primary?.resetDescription == "5 hours window")
#expect(usage.secondary?.usedPercent == 20)
#expect(usage.secondary?.resetDescription == "30 days window")
#expect(usage.tertiary == nil)
#expect(usage.zaiUsage?.tokenLimit?.usage == 100)
#expect(usage.zaiUsage?.sessionTokenLimit == nil)
}
@Test
func `maps usage snapshot windows with missing fields`() {
let reset = Date(timeIntervalSince1970: 123)
let tokenLimit = ZaiLimitEntry(
type: .tokensLimit,
unit: .hours,
number: 5,
usage: nil,
currentValue: nil,
remaining: nil,
percentage: 25,
usageDetails: [],
nextResetTime: reset)
let snapshot = ZaiUsageSnapshot(
tokenLimit: tokenLimit,
timeLimit: nil,
planName: nil,
updatedAt: reset)
let usage = snapshot.toUsageSnapshot()
#expect(usage.primary?.usedPercent == 25)
#expect(usage.primary?.windowMinutes == 300)
#expect(usage.primary?.resetsAt == reset)
#expect(usage.primary?.resetDescription == "5 hours window")
#expect(usage.zaiUsage?.tokenLimit?.usage == nil)
}
@Test
func `maps usage snapshot windows with missing remaining uses current value`() {
let reset = Date(timeIntervalSince1970: 123)
let tokenLimit = ZaiLimitEntry(
type: .tokensLimit,
unit: .hours,
number: 5,
usage: 100,
currentValue: 20,
remaining: nil,
percentage: 25,
usageDetails: [],
nextResetTime: reset)
let snapshot = ZaiUsageSnapshot(
tokenLimit: tokenLimit,
timeLimit: nil,
planName: nil,
updatedAt: reset)
let usage = snapshot.toUsageSnapshot()
#expect(usage.primary?.usedPercent == 20)
}
@Test
func `maps usage snapshot windows with missing current value uses remaining`() {
let reset = Date(timeIntervalSince1970: 123)
let tokenLimit = ZaiLimitEntry(
type: .tokensLimit,
unit: .hours,
number: 5,
usage: 100,
currentValue: nil,
remaining: 80,
percentage: 25,
usageDetails: [],
nextResetTime: reset)
let snapshot = ZaiUsageSnapshot(
tokenLimit: tokenLimit,
timeLimit: nil,
planName: nil,
updatedAt: reset)
let usage = snapshot.toUsageSnapshot()
#expect(usage.primary?.usedPercent == 20)
}
@Test
func `maps usage snapshot windows with missing remaining and current value falls back to percentage`() {
let reset = Date(timeIntervalSince1970: 123)
let tokenLimit = ZaiLimitEntry(
type: .tokensLimit,
unit: .hours,
number: 5,
usage: 100,
currentValue: nil,
remaining: nil,
percentage: 25,
usageDetails: [],
nextResetTime: reset)
let snapshot = ZaiUsageSnapshot(
tokenLimit: tokenLimit,
timeLimit: nil,
planName: nil,
updatedAt: reset)
let usage = snapshot.toUsageSnapshot()
#expect(usage.primary?.usedPercent == 25)
}
}
struct ZaiUsageParsingTests {
@Test
func `empty body returns parse failed`() {
#expect {
_ = try ZaiUsageFetcher.parseUsageSnapshot(from: Data())
} throws: { error in
guard case let ZaiUsageError.parseFailed(message) = error else { return false }
return message == "Empty response body"
}
}
@Test
func `parses usage response`() throws {
let json = """
{
"code": 200,
"msg": "Operation successful",
"data": {
"limits": [
{
"type": "TIME_LIMIT",
"unit": 5,
"number": 1,
"usage": 100,
"currentValue": 102,
"remaining": 0,
"percentage": 100,
"usageDetails": [
{ "modelCode": "search-prime", "usage": 95 }
]
},
{
"type": "TOKENS_LIMIT",
"unit": 3,
"number": 5,
"usage": 40000000,
"currentValue": 13628365,
"remaining": 26371635,
"percentage": 34,
"nextResetTime": 1768507567547
}
],
"planName": "Pro"
},
"success": true
}
"""
let snapshot = try ZaiUsageFetcher.parseUsageSnapshot(from: Data(json.utf8))
#expect(snapshot.planName == "Pro")
#expect(snapshot.tokenLimit?.usage == 40_000_000)
#expect(snapshot.timeLimit?.usageDetails.first?.modelCode == "search-prime")
#expect(snapshot.tokenLimit?.percentage == 34.0)
let usage = snapshot.toUsageSnapshot()
#expect(usage.secondary?.windowMinutes == nil)
#expect(usage.secondary?.resetDescription == "Monthly")
}
@Test
func `zai mcp time limit displays monthly instead of one minute window`() throws {
let json = """
{
"code": 200,
"msg": "Operation successful",
"data": {
"limits": [
{
"type": "TIME_LIMIT",
"unit": 5,
"number": 1,
"usage": 100,
"currentValue": 50,
"remaining": 50,
"percentage": 50,
"usageDetails": []
},
{
"type": "TOKENS_LIMIT",
"unit": 3,
"number": 5,
"percentage": 34,
"nextResetTime": 1768507567547
}
]
},
"success": true
}
"""
let snapshot = try ZaiUsageFetcher.parseUsageSnapshot(from: Data(json.utf8))
let usage = snapshot.toUsageSnapshot()
#expect(snapshot.timeLimit?.windowDescription == "1 minute")
#expect(usage.secondary?.windowMinutes == nil)
#expect(usage.secondary?.resetDescription == "Monthly")
}
@Test
func `missing data returns api error`() {
let json = """
{ "code": 1001, "msg": "Authorization Token Missing", "success": false }
"""
#expect {
_ = try ZaiUsageFetcher.parseUsageSnapshot(from: Data(json.utf8))
} throws: { error in
guard case let ZaiUsageError.apiError(message) = error else { return false }
return message == "Authorization Token Missing"
}
}
@Test
func `failed response without message reports the API code`() {
let json = """
{ "code": 1001, "success": false }
"""
#expect {
_ = try ZaiUsageFetcher.parseUsageSnapshot(from: Data(json.utf8))
} throws: { error in
guard case let ZaiUsageError.apiError(message) = error else { return false }
return message == "Z.ai quota API returned code 1001"
}
}
@Test
func `success without data returns parse failed`() {
let json = """
{ "code": 200, "msg": "Operation successful", "success": true }
"""
#expect {
_ = try ZaiUsageFetcher.parseUsageSnapshot(from: Data(json.utf8))
} throws: { error in
guard case let ZaiUsageError.parseFailed(message) = error else { return false }
return message == "Missing data"
}
}
@Test
func `success without limits parses empty usage`() throws {
let json = """
{
"code": 200,
"msg": "Operation successful",
"data": { "planName": "Pro" },
"success": true
}
"""
let snapshot = try ZaiUsageFetcher.parseUsageSnapshot(from: Data(json.utf8))
#expect(snapshot.planName == "Pro")
#expect(snapshot.tokenLimit == nil)
#expect(snapshot.timeLimit == nil)
}
@Test
func `parses new schema with missing token limit fields`() throws {
let json = """
{
"code": 200,
"msg": "Operation successful",
"data": {
"limits": [
{
"type": "TIME_LIMIT",
"unit": 5,
"number": 1,
"usage": 100,
"currentValue": 0,
"remaining": 100,
"percentage": 0,
"usageDetails": [
{ "modelCode": "search-prime", "usage": 0 },
{ "modelCode": "web-reader", "usage": 1 },
{ "modelCode": "zread", "usage": 0 }
]
},
{
"type": "TOKENS_LIMIT",
"unit": 3,
"number": 5,
"percentage": 1,
"nextResetTime": 1770724088678
}
]
},
"success": true
}
"""
let snapshot = try ZaiUsageFetcher.parseUsageSnapshot(from: Data(json.utf8))
#expect(snapshot.tokenLimit?.percentage == 1.0)
#expect(snapshot.tokenLimit?.usage == nil)
#expect(snapshot.tokenLimit?.currentValue == nil)
#expect(snapshot.tokenLimit?.remaining == nil)
#expect(snapshot.tokenLimit?.usedPercent == 1.0)
#expect(snapshot.tokenLimit?.windowMinutes == 300)
#expect(snapshot.timeLimit?.usage == 100)
}
@Test
func `parses BigModel CN quota response without message`() throws {
let json = """
{
"code": 200,
"data": {
"limits": [
{
"type": "TIME_LIMIT",
"unit": 5,
"number": 1,
"usage": 1000,
"currentValue": 147,
"remaining": 853,
"percentage": 14,
"nextResetTime": 1784706344993,
"usageDetails": [
{ "modelCode": "search-prime", "usage": 84 },
{ "modelCode": "web-reader", "usage": 41 },
{ "modelCode": "zread", "usage": 8 }
]
},
{
"type": "TOKENS_LIMIT",
"unit": 3,
"number": 5,
"percentage": 8,
"nextResetTime": 1783049703178
},
{
"type": "TOKENS_LIMIT",
"unit": 6,
"number": 1,
"percentage": 7,
"nextResetTime": 1783496744998
}
],
"level": "pro"
},
"success": true
}
"""
let snapshot = try ZaiUsageFetcher.parseUsageSnapshot(from: Data(json.utf8))
let usage = snapshot.toUsageSnapshot()
#expect(usage.primary?.usedPercent == 7)
#expect(usage.secondary?.usedPercent == 14.7)
#expect(usage.tertiary?.usedPercent == 8)
}
}
struct ZaiBigModelTeamScopeTests {
@Test
func `team scope appends type 2 and sends BigModel project headers`() async throws {
let transport = ProviderHTTPTransportStub { request in
let json = """
{
"code": 200,
"msg": "操作成功",
"data": {
"level": "pro",
"limits": [
{
"type": "TIME_LIMIT",
"unit": 5,
"number": 1,
"usage": 1000,
"currentValue": 224,
"remaining": 776,
"percentage": 22,
"nextResetTime": 1777575229998,
"usageDetails": []
},
{
"type": "TOKENS_LIMIT",
"unit": 3,
"number": 5,
"percentage": 25,
"nextResetTime": 1775020168897
},
{
"type": "TOKENS_LIMIT",
"unit": 6,
"number": 1,
"percentage": 9,
"nextResetTime": 1775588029998
}
]
},
"success": true
}
"""
return (
Data(json.utf8),
HTTPURLResponse(
url: request.url!,
statusCode: 200,
httpVersion: nil,
headerFields: nil)!)
}
let snapshot = try await ZaiUsageFetcher.fetchUsage(
apiKey: "zai-test-token",
region: .bigmodelCN,
usageScope: .team,
teamContext: ZaiBigModelTeamContext(
organizationID: "org-test",
projectID: "proj-test"),
environment: [:],
transport: transport)
let requests = await transport.requests()
let request = try #require(requests.first)
#expect(request.url?.absoluteString == "https://open.bigmodel.cn/api/monitor/usage/quota/limit?type=2")
#expect(request.value(forHTTPHeaderField: "Authorization") == "Bearer zai-test-token")
#expect(request.value(forHTTPHeaderField: "Bigmodel-Organization") == "org-test")
#expect(request.value(forHTTPHeaderField: "Bigmodel-Project") == "proj-test")
#expect(snapshot.tokenLimit?.unit == .weeks)
#expect(snapshot.sessionTokenLimit?.unit == .hours)
#expect(snapshot.timeLimit?.usage == 1000)
}
@Test
func `personal scope keeps existing quota URL and omits team headers`() async throws {
let transport = ProviderHTTPTransportStub { request in
let json = """
{
"code": 200,
"msg": "Operation successful",
"data": {
"limits": [
{
"type": "TOKENS_LIMIT",
"unit": 3,
"number": 5,
"percentage": 34,
"nextResetTime": 1768507567547
}
]
},
"success": true
}
"""
return (
Data(json.utf8),
HTTPURLResponse(
url: request.url!,
statusCode: 200,
httpVersion: nil,
headerFields: nil)!)
}
_ = try await ZaiUsageFetcher.fetchUsage(
apiKey: "zai-test-token",
region: .bigmodelCN,
usageScope: .personal,
teamContext: ZaiBigModelTeamContext(
organizationID: "org-test",
projectID: "proj-test"),
environment: [:],
transport: transport)
let requests = await transport.requests()
let request = try #require(requests.first)
#expect(request.url?.absoluteString == "https://open.bigmodel.cn/api/monitor/usage/quota/limit")
#expect(request.value(forHTTPHeaderField: "Bigmodel-Organization") == nil)
#expect(request.value(forHTTPHeaderField: "Bigmodel-Project") == nil)
}
@Test
func `team scope requires complete BigModel context`() async {
let transport = ProviderHTTPTransportStub { request in
(
Data(),
HTTPURLResponse(
url: request.url!,
statusCode: 200,
httpVersion: nil,
headerFields: nil)!)
}
await self.expectMissingTeamContext {
_ = try await ZaiUsageFetcher.fetchUsage(
apiKey: "zai-test-token",
region: .bigmodelCN,
usageScope: .team,
teamContext: nil,
environment: [:],
transport: transport)
}
let requests = await transport.requests()
#expect(requests.isEmpty)
await self.expectMissingTeamContext {
_ = try await ZaiUsageFetcher.fetchUsage(
apiKey: "zai-test-token",
region: .bigmodelCN,
usageScope: .team,
teamContext: nil,
environment: [ZaiSettingsReader.bigModelOrganizationKey: "org-only"],
transport: transport)
}
await self.expectMissingTeamContext {
_ = try await ZaiUsageFetcher.fetchUsage(
apiKey: "zai-test-token",
region: .bigmodelCN,
usageScope: .team,
teamContext: nil,
environment: [ZaiSettingsReader.bigModelProjectKey: "proj-only"],
transport: transport)
}
}
@Test
func `team model usage appends type 3 and sends BigModel project headers`() async throws {
let transport = ProviderHTTPTransportStub { request in
let json = """
{
"code": 200,
"msg": "success",
"success": true,
"data": {
"x_time": ["2026-06-21 08:00"],
"modelDataList": [
{ "modelName": "glm-4.6", "tokensUsage": [100] }
]
}
}
"""
return (
Data(json.utf8),
HTTPURLResponse(
url: request.url!,
statusCode: 200,
httpVersion: nil,
headerFields: nil)!)
}
let usage = try await ZaiUsageFetcher.fetchModelUsage(
apiKey: "zai-test-token",
region: .bigmodelCN,
usageScope: .team,
teamContext: ZaiBigModelTeamContext(
organizationID: "org-test",
projectID: "proj-test"),
environment: [:],
transport: transport)
let requests = await transport.requests()
let request = try #require(requests.first)
let requestURL = try #require(request.url)
let components = try #require(URLComponents(url: requestURL, resolvingAgainstBaseURL: false))
#expect(components.path == "/api/monitor/usage/model-usage")
#expect(components.queryItems?.first { $0.name == "type" }?.value == "3")
#expect(request.value(forHTTPHeaderField: "Authorization") == "Bearer zai-test-token")
#expect(request.value(forHTTPHeaderField: "Bigmodel-Organization") == "org-test")
#expect(request.value(forHTTPHeaderField: "Bigmodel-Project") == "proj-test")
#expect(usage.modelNames == ["glm-4.6"])
}
@Test
func `team quota rejects insecure override before sending credentials`() async throws {
let transport = ProviderHTTPTransportStub { request in
Issue.record("Unexpected z.ai team quota request to \(request.url?.absoluteString ?? "<nil>")")
throw URLError(.badURL)
}
await #expect(throws: ZaiSettingsError.invalidEndpointOverride(ZaiSettingsReader.quotaURLKey)) {
try await ZaiUsageFetcher.fetchUsage(
apiKey: "zai-test-token",
region: .bigmodelCN,
usageScope: .team,
teamContext: ZaiBigModelTeamContext(
organizationID: "org-test",
projectID: "proj-test"),
environment: [ZaiSettingsReader.quotaURLKey: "http://attacker.test/quota"],
transport: transport)
}
let requests = await transport.requests()
#expect(requests.isEmpty)
}
@Test
func `team model usage rejects insecure API host before sending credentials`() async throws {
let transport = ProviderHTTPTransportStub { request in
Issue.record("Unexpected z.ai team model usage request to \(request.url?.absoluteString ?? "<nil>")")
throw URLError(.badURL)
}
await #expect(throws: ZaiSettingsError.invalidEndpointOverride(ZaiSettingsReader.apiHostKey)) {
try await ZaiUsageFetcher.fetchModelUsage(
apiKey: "zai-test-token",
region: .bigmodelCN,
usageScope: .team,
teamContext: ZaiBigModelTeamContext(
organizationID: "org-test",
projectID: "proj-test"),
environment: [ZaiSettingsReader.apiHostKey: "http://attacker.test"],
transport: transport)
}
let requests = await transport.requests()
#expect(requests.isEmpty)
}
private func expectMissingTeamContext(_ operation: () async throws -> Void) async {
do {
try await operation()
Issue.record("Expected z.ai missing team context error.")
} catch ZaiUsageError.missingTeamContext {
// Expected.
} catch {
Issue.record("Expected z.ai missing team context error, got \(error).")
}
}
@Test
func `team context can be resolved from environment`() {
let env = [
ZaiSettingsReader.bigModelOrganizationKey: " org-env ",
ZaiSettingsReader.bigModelProjectKey: " proj-env ",
]
#expect(ZaiBigModelTeamContext(environment: env)?.organizationID == "org-env")
#expect(ZaiBigModelTeamContext(environment: env)?.projectID == "proj-env")
}
}
struct ZaiHourlyUsageTests {
@Test
func `model usage parser decodes hourly model payload`() throws {
let json = """
{
"code": 200,
"msg": "success",
"success": true,
"data": {
"x_time": ["2026-05-14 08:00", "2026-05-14 09:00"],
"modelDataList": [
{ "modelName": "glm-4.6", "tokensUsage": [100, null] },
{ "modelName": "glm-4.5", "tokensUsage": [50, 25] }
]
}
}
"""
let usage = try ZaiUsageFetcher.parseModelUsage(from: Data(json.utf8))
#expect(usage.xTime == ["2026-05-14 08:00", "2026-05-14 09:00"])
#expect(usage.modelNames == ["glm-4.6", "glm-4.5"])
#expect(usage.modelDataList[0].tokensUsage == [100, nil])
#expect(usage.modelDataList[1].tokensUsage == [50, 25])
}
@Test
func `today hourly bars filter earlier days and skip empty hours`() {
let reference = Self.localDate(year: 2026, month: 5, day: 14, hour: 12)
let yesterday = Calendar.current.date(byAdding: .day, value: -1, to: reference) ?? reference
let modelData = ZaiModelUsageData(
xTime: [
Self.hourString(yesterday),
"2026-05-14 08:00",
"2026-05-14 09:00",
],
modelDataList: [
ZaiModelDataItem(modelName: "glm-4.6", tokensUsage: [999, 100, 0]),
ZaiModelDataItem(modelName: "glm-4.5", tokensUsage: [0, 50, nil]),
])
let bars = ZaiHourlyBars.from(modelData: modelData, range: .today(referenceDate: reference), now: reference)
#expect(bars.map(\.label) == ["08"])
#expect(bars.first?.totalTokens == 150)
#expect(bars.first?.segments.count == 2)
}
@Test
func `last 24 hour bars filter data outside trailing window`() {
let reference = Self.localDate(year: 2026, month: 5, day: 14, hour: 12)
let old = Calendar.current.date(byAdding: .hour, value: -25, to: reference) ?? reference
let inWindow = Calendar.current.date(byAdding: .hour, value: -23, to: reference) ?? reference
let modelData = ZaiModelUsageData(
xTime: [
Self.hourString(old),
Self.hourString(inWindow),
Self.hourString(reference),
],
modelDataList: [
ZaiModelDataItem(modelName: "glm-4.6", tokensUsage: [10, 20, 30]),
])
let bars = ZaiHourlyBars.from(modelData: modelData, range: .last24h, now: reference)
#expect(bars.map(\.label) == [Self.hourLabel(inWindow), Self.hourLabel(reference)])
#expect(bars.map(\.totalTokens) == [20, 30])
}
private static func localDate(year: Int, month: Int, day: Int, hour: Int) -> Date {
Calendar.current.date(from: DateComponents(year: year, month: month, day: day, hour: hour)) ?? Date()
}
private static func hourString(_ date: Date) -> String {
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm"
formatter.locale = Locale(identifier: "en_US_POSIX")
return formatter.string(from: date)
}
private static func hourLabel(_ date: Date) -> String {
let formatter = DateFormatter()
formatter.dateFormat = "HH"
formatter.locale = Locale(identifier: "en_US_POSIX")
return formatter.string(from: date)
}
}
struct ZaiThreeLimitTests {
@Test
func `parses three limit entries into session weekly and mcp slots`() throws {
let json = """
{
"code": 200,
"msg": "操作成功",
"data": {
"limits": [
{
"type": "TOKENS_LIMIT",
"unit": 3,
"number": 5,
"percentage": 25,
"nextResetTime": 1775020168897
},
{
"type": "TOKENS_LIMIT",
"unit": 6,
"number": 1,
"percentage": 9,
"nextResetTime": 1775588029998
},
{
"type": "TIME_LIMIT",
"unit": 5,
"number": 1,
"usage": 1000,
"currentValue": 224,
"remaining": 776,
"percentage": 22,
"nextResetTime": 1777575229998,
"usageDetails": [
{ "modelCode": "search-prime", "usage": 210 },
{ "modelCode": "web-reader", "usage": 14 }
]
}
],
"level": "pro"
},
"success": true
}
"""
let snapshot = try ZaiUsageFetcher.parseUsageSnapshot(from: Data(json.utf8))
// Weekly token limit (unit:6=weeks, longer window) → tokenLimit (primary)
#expect(snapshot.tokenLimit?.unit == .weeks)
#expect(snapshot.tokenLimit?.number == 1)
#expect(snapshot.tokenLimit?.percentage == 9.0)
#expect(snapshot.tokenLimit?.windowMinutes == 10080)
// 5-hour token limit (unit:3=hours, number:5 → 300 min) → sessionTokenLimit (tertiary)
#expect(snapshot.sessionTokenLimit?.unit == .hours)
#expect(snapshot.sessionTokenLimit?.number == 5)
#expect(snapshot.sessionTokenLimit?.percentage == 25.0)
#expect(snapshot.sessionTokenLimit?.windowMinutes == 300)
// MCP time limit → timeLimit (secondary)
#expect(snapshot.timeLimit?.usage == 1000)
#expect(snapshot.timeLimit?.usageDetails.first?.modelCode == "search-prime")
// UsageSnapshot slot mapping
let usage = snapshot.toUsageSnapshot()
#expect(usage.primary?.usedPercent == 9.0)
#expect(usage.primary?.windowMinutes == 10080)
#expect(usage.secondary != nil) // MCP
#expect(usage.tertiary?.usedPercent == 25.0)
#expect(usage.tertiary?.windowMinutes == 300)
}
@Test
func `unit 6 maps to weeks with correct window minutes`() {
let entry = ZaiLimitEntry(
type: .tokensLimit,
unit: .weeks,
number: 1,
usage: nil,
currentValue: nil,
remaining: nil,
percentage: 9,
usageDetails: [],
nextResetTime: nil)
#expect(entry.windowMinutes == 10080)
#expect(entry.windowDescription == "1 week")
#expect(entry.windowLabel == "1 week window")
}
@Test
func `two limit entries remain backward compatible`() throws {
let json = """
{
"code": 200,
"msg": "Operation successful",
"data": {
"limits": [
{
"type": "TIME_LIMIT",
"unit": 5,
"number": 1,
"usage": 100,
"currentValue": 50,
"remaining": 50,
"percentage": 50,
"usageDetails": []
},
{
"type": "TOKENS_LIMIT",
"unit": 3,
"number": 5,
"percentage": 34,
"nextResetTime": 1768507567547
}
]
},
"success": true
}
"""
let snapshot = try ZaiUsageFetcher.parseUsageSnapshot(from: Data(json.utf8))
#expect(snapshot.tokenLimit != nil)
#expect(snapshot.sessionTokenLimit == nil)
#expect(snapshot.timeLimit != nil)
let usage = snapshot.toUsageSnapshot()
#expect(usage.primary != nil)
#expect(usage.secondary != nil)
#expect(usage.tertiary == nil)
}
}
struct ZaiAPIRegionTests {
@Test
func `dashboard URLs follow selected region`() {
#expect(
ZaiAPIRegion.global.dashboardURL.absoluteString ==
"https://z.ai/manage-apikey/coding-plan/personal/my-plan")
#expect(
ZaiAPIRegion.bigmodelCN.dashboardURL.absoluteString ==
"https://bigmodel.cn/coding-plan/personal/usage")
#expect(
ZaiProviderDescriptor.descriptor.metadata.dashboardURL ==
ZaiAPIRegion.global.dashboardURL.absoluteString)
}
@Test
func `defaults to global endpoint`() {
let url = ZaiUsageFetcher.resolveQuotaURL(region: .global, environment: [:])
#expect(url.absoluteString == "https://api.z.ai/api/monitor/usage/quota/limit")
}
@Test
func `uses big model region when selected`() {
let url = ZaiUsageFetcher.resolveQuotaURL(region: .bigmodelCN, environment: [:])
#expect(url.absoluteString == "https://open.bigmodel.cn/api/monitor/usage/quota/limit")
}
@Test
func `quota url environment override wins`() {
let env = [ZaiSettingsReader.quotaURLKey: "https://open.bigmodel.cn/api/coding/paas/v4"]
let url = ZaiUsageFetcher.resolveQuotaURL(region: .global, environment: env)
#expect(url.absoluteString == "https://open.bigmodel.cn/api/coding/paas/v4")
}
@Test
func `api host environment appends quota path`() {
let env = [ZaiSettingsReader.apiHostKey: "open.bigmodel.cn"]
let url = ZaiUsageFetcher.resolveQuotaURL(region: .global, environment: env)
#expect(url.absoluteString == "https://open.bigmodel.cn/api/monitor/usage/quota/limit")
}
@Test
func `dashboard follows known endpoint overrides`() {
let china = ZaiUsageFetcher.resolveDashboardURL(
region: .global,
environment: [ZaiSettingsReader.apiHostKey: "open.bigmodel.cn"])
#expect(china == ZaiAPIRegion.bigmodelCN.dashboardURL)
let global = ZaiUsageFetcher.resolveDashboardURL(
region: .bigmodelCN,
environment: [ZaiSettingsReader.apiHostKey: "api.z.ai"])
#expect(global == ZaiAPIRegion.global.dashboardURL)
}
@Test
func `dashboard keeps selected region for custom endpoint override`() {
let dashboard = ZaiUsageFetcher.resolveDashboardURL(
region: .bigmodelCN,
environment: [ZaiSettingsReader.apiHostKey: "zai.internal.example"])
#expect(dashboard == ZaiAPIRegion.bigmodelCN.dashboardURL)
}
}