Files
steipete--codexbar/TestsPlugin/ZaiPluginBalanceTests.swift
Xigua cdc456ae09 Add BigModel CN account balance to z.ai provider (#3109)
* Add BigModel CN account balance to z.ai provider

* Bound balance timeout, region-aware override validation, null-safe amounts

---------

Co-authored-by: RunhuaHuang <RunhuaHuang@users.noreply.github.com>
2026-08-20 07:43:03 -07:00

170 lines
6.6 KiB
Swift

import CodexBarCore
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
import Testing
/// BigModel CN pay-as-you-go account balance, surfaced by the bundled zai plugin
/// as a best-effort detail row (endpoint verified against the live console API:
/// `GET www.bigmodel.cn/api/biz/account/query-customer-account-report`, which
/// accepts both `Bearer <key>` and raw-key Authorization).
struct ZaiPluginBalanceTests {
@Test
func `bigmodel CN snapshot renders account balance row`() async throws {
let recorder = BalanceRequestRecorder()
let snapshot = try await Self.fetch(
region: "bigmodel-cn",
balanceBody: """
{
"code": 200,
"msg": "操作成功",
"success": true,
"data": {
"balance": 42.5,
"availableBalance": 40.0,
"rechargeAmount": 100.0,
"giveAmount": 20.0,
"totalSpendAmount": 77.5,
"frozenBalance": 2.5
}
}
""",
recorder: recorder)
// availableBalance wins over balance; the secondary line summarizes spend provenance
#expect(snapshot.detailRow(label: "Account balance")?.value == "¥40.00")
#expect(
snapshot.detailRow(label: "Account balance")?.secondaryValue
== "recharged ¥100.00 · granted ¥20.00 · spent ¥77.50")
let balanceRequest = try #require(await recorder.requests.first { $0.url?.host == "www.bigmodel.cn" })
#expect(balanceRequest.url?.path == "/api/biz/account/query-customer-account-report")
// Optional lookup must be bounded well below the fetch deadline (review P1).
#expect(balanceRequest.timeoutInterval == 5)
}
@Test
func `null availableBalance falls back to balance and hides null secondary fields`() async throws {
// Number(null) is 0 — without an explicit null guard the row would read ¥0.00.
let snapshot = try await Self.fetch(
region: "bigmodel-cn",
balanceBody: """
{
"code": 200,
"success": true,
"data": {
"balance": 42.5,
"availableBalance": null,
"rechargeAmount": null,
"giveAmount": 5.0,
"totalSpendAmount": null
}
}
""")
#expect(snapshot.detailRow(label: "Account balance")?.value == "¥42.50")
#expect(snapshot.detailRow(label: "Account balance")?.secondaryValue == "granted ¥5.00")
}
@Test
func `region-aware validation rejects invalid balance override`() {
#expect(throws: ZaiSettingsError.self) {
try ZaiSettingsReader.validateEndpointOverrides(
region: .bigmodelCN,
environment: [ZaiSettingsReader.balanceURLKey: "http://insecure.test/report"])
}
#expect(throws: ZaiSettingsError.self) {
try ZaiSettingsReader.validateEndpointOverrides(
environment: [ZaiSettingsReader.balanceURLKey: "http://insecure.test/report"])
}
}
@Test
func `balance endpoint failure keeps quota snapshot intact`() async throws {
let snapshot = try await Self.fetch(region: "bigmodel-cn", balanceBody: "{}", balanceStatus: 500)
#expect(snapshot.primary?.usedPercent == 42)
#expect(snapshot.detailRow(label: "Account balance") == nil)
}
@Test
func `global region skips the balance request entirely`() async throws {
let recorder = BalanceRequestRecorder()
let snapshot = try await Self.fetch(region: "global", balanceBody: "{}", recorder: recorder)
#expect(snapshot.primary?.usedPercent == 42)
let balanceHostRequests = await recorder.requests.filter { $0.url?.host == "www.bigmodel.cn" }
#expect(balanceHostRequests.isEmpty)
}
@Test
func `router resolves CN balance default, explicit override, and nil for global`() {
#expect(
ZaiEndpointRouter.resolveBalanceURL(region: .bigmodelCN, environment: [:])
== ZaiAPIRegion.bigmodelCN.balanceURL)
#expect(ZaiEndpointRouter.resolveBalanceURL(region: .global, environment: [:]) == nil)
let overridden = ZaiEndpointRouter.resolveBalanceURL(
region: .bigmodelCN,
environment: [ZaiSettingsReader.balanceURLKey: "https://balance-proxy.test/report"])
#expect(overridden?.absoluteString == "https://balance-proxy.test/report")
}
// MARK: - Fixtures
private static func fetch(
region: String,
balanceBody: String,
balanceStatus: Int = 200,
recorder: BalanceRequestRecorder? = nil) async throws -> UsageSnapshot
{
let runtime = try ProviderPluginRuntime(
bundledPlugin: "zai",
transport: ProviderHTTPTransportHandler { request in
if let recorder {
await recorder.append(request)
}
let body = request.url?.host == "www.bigmodel.cn" ? balanceBody : Self.quotaFixture
let status = request.url?.host == "www.bigmodel.cn" ? balanceStatus : 200
return try Self.response(request: request, body: body, status: status)
})
return try await runtime.fetchUsage(
settings: [
"Z_AI_REGION": region,
"Z_AI_USAGE_SCOPE": "personal",
"Z_AI_QUOTA_ENDPOINT": "https://open.bigmodel.cn/api/monitor/usage/quota/limit",
"Z_AI_MODEL_USAGE_ENDPOINT": "https://open.bigmodel.cn/api/monitor/usage/model-usage",
],
secrets: [ZaiSettingsReader.apiTokenKey: "fixture-key"])
}
private static let quotaFixture = """
{
"code": 200,
"success": true,
"data": {
"limits": [
{ "type": "TOKENS_LIMIT", "unit": 5, "number": 300, "percentage": 42,
"usage": 1000, "remaining": 580, "nextResetTime": 1756000000 }
]
}
}
"""
private static func response(request: URLRequest, body: String, status: Int) throws -> (Data, HTTPURLResponse) {
let response = try #require(HTTPURLResponse(
url: request.url!,
statusCode: status,
httpVersion: nil,
headerFields: ["Content-Type": "application/json"]))
return (Data(body.utf8), response)
}
}
private actor BalanceRequestRecorder {
private(set) var requests: [URLRequest] = []
func append(_ request: URLRequest) {
self.requests.append(request)
}
}