From c1a8d4a72b777e6f23292667e2a5b3e7951bd618 Mon Sep 17 00:00:00 2001 From: Yuxin Qiao <104957188+Yuxin-Qiao@users.noreply.github.com> Date: Sun, 24 May 2026 02:07:50 +0800 Subject: [PATCH 01/10] Add redacted MiniMax diagnostic export --- .../CodexBarCore/Logging/LogRedactor.swift | 12 +- .../MiniMax/MiniMaxDiagnosticExport.swift | 264 +++++++++ .../MiniMaxDiagnosticExportTests.swift | 538 ++++++++++++++++++ .../MiniMaxLogRedactorTests.swift | 105 ++++ docs/minimax.md | 23 + 5 files changed, 941 insertions(+), 1 deletion(-) create mode 100644 Sources/CodexBarCore/Providers/MiniMax/MiniMaxDiagnosticExport.swift create mode 100644 Tests/CodexBarTests/MiniMaxDiagnosticExportTests.swift create mode 100644 Tests/CodexBarTests/MiniMaxLogRedactorTests.swift diff --git a/Sources/CodexBarCore/Logging/LogRedactor.swift b/Sources/CodexBarCore/Logging/LogRedactor.swift index 02cf42c404..d664fd7f84 100644 --- a/Sources/CodexBarCore/Logging/LogRedactor.swift +++ b/Sources/CodexBarCore/Logging/LogRedactor.swift @@ -18,13 +18,23 @@ public enum LogRedactor { pattern: #"(?i)(authorization\s*:\s*)([^\r\n]+)"#) private static let bearerRegex = Self.makeRegex( pattern: #"(?i)\bbearer\s+[a-z0-9._\-]+=*\b"#) + private static let minimaxCodingPlanTokenRegex = Self.makeRegex( + pattern: #"sk-cp-[^\s"'`;,)>\]]+"#) + private static let minimaxApiTokenRegex = Self.makeRegex( + pattern: #"sk-api-[^\s"'`;,)>\]]+"#) public static func redact(_ text: String) -> String { var output = text + // Email is broad and safe first output = self.replace(self.emailRegex, in: output, with: "") + // MiniMax tokens before broader rules catch them + output = self.replace(self.minimaxCodingPlanTokenRegex, in: output, with: "") + output = self.replace(self.minimaxApiTokenRegex, in: output, with: "") + // Bearer catches "bearer " before authorization wraps it + output = self.replace(self.bearerRegex, in: output, with: "Bearer ") + // Authorization catches the rest (already-redacted content) output = self.replace(self.cookieHeaderRegex, in: output, with: "$1") output = self.replace(self.authorizationRegex, in: output, with: "$1") - output = self.replace(self.bearerRegex, in: output, with: "Bearer ") return output } diff --git a/Sources/CodexBarCore/Providers/MiniMax/MiniMaxDiagnosticExport.swift b/Sources/CodexBarCore/Providers/MiniMax/MiniMaxDiagnosticExport.swift new file mode 100644 index 0000000000..370d8f0c69 --- /dev/null +++ b/Sources/CodexBarCore/Providers/MiniMax/MiniMaxDiagnosticExport.swift @@ -0,0 +1,264 @@ +import Foundation + +public enum MiniMaxDiagnosticExportErrorCategory: String, Sendable, Codable { + case auth + case network + case parse + case timeout + case unknown +} + +public struct MiniMaxFetchAttemptSummary: Sendable, Codable { + public let strategyID: String + public let wasAvailable: Bool + public let errorCode: String? + public let errorCategory: MiniMaxDiagnosticExportErrorCategory + + public init( + strategyID: String, + wasAvailable: Bool, + errorCode: String?, + errorCategory: MiniMaxDiagnosticExportErrorCategory) + { + self.strategyID = strategyID + self.wasAvailable = wasAvailable + self.errorCode = errorCode + self.errorCategory = errorCategory + } +} + +public struct MiniMaxDiagnosticExport: Sendable, Codable { + public let schemaVersion: String + public let provider: UsageProvider + public let authMode: String + public let region: String? + public let sourceLabel: String? + public let strategyID: String? + public let fieldsPresent: Set + public let servicesCount: Int + public let billingSummaryPresent: Bool + public let fetchAttemptsSummary: [MiniMaxFetchAttemptSummary] + public let redactionPolicyVersion: String + public let exportedAt: Date + + public init( + schemaVersion: String = "1.0", + provider: UsageProvider = .minimax, + authMode: String, + region: String?, + sourceLabel: String?, + strategyID: String?, + fieldsPresent: Set, + servicesCount: Int, + billingSummaryPresent: Bool, + fetchAttemptsSummary: [MiniMaxFetchAttemptSummary], + redactionPolicyVersion: String = "1.0", + exportedAt: Date) + { + self.schemaVersion = schemaVersion + self.provider = provider + self.authMode = authMode + self.region = region + self.sourceLabel = sourceLabel + self.strategyID = strategyID + self.fieldsPresent = fieldsPresent + self.servicesCount = servicesCount + self.billingSummaryPresent = billingSummaryPresent + self.fetchAttemptsSummary = fetchAttemptsSummary + self.redactionPolicyVersion = redactionPolicyVersion + self.exportedAt = exportedAt + } +} + +public enum MiniMaxDiagnosticExportBuilder { + private static let allowlistedFields: Set = [ + "planName", + "availablePrompts", + "currentPrompts", + "remainingPrompts", + "windowMinutes", + "usedPercent", + "resetsAt", + "services", + "billingSummary", + ] + + private static let authErrorCodes: Set = ["401", "403"] + private static let boundedHTTPStatusCodeRegex: NSRegularExpression? = try? NSRegularExpression( + pattern: #"(? MiniMaxDiagnosticExport + { + let fieldsPresent = Self.allowlistedFields.filter { key in + Self.snapshotHasField(key, snapshot: snapshot) + } + + let servicesCount = snapshot?.services?.count ?? 0 + let billingSummaryPresent = snapshot?.billingSummary != nil + + var fetchAttemptsSummary: [MiniMaxFetchAttemptSummary] = [] + for attempt in outcome.attempts { + let summary = Self.makeAttemptSummary(attempt) + fetchAttemptsSummary.append(summary) + } + + let resultValue = outcome.result + var sourceLabel: String? + var strategyID: String? + if case let .success(result) = resultValue { + sourceLabel = result.sourceLabel + strategyID = result.strategyID + } + + return MiniMaxDiagnosticExport( + authMode: authMode, + region: region?.rawValue, + sourceLabel: sourceLabel, + strategyID: strategyID, + fieldsPresent: Set(fieldsPresent), + servicesCount: servicesCount, + billingSummaryPresent: billingSummaryPresent, + fetchAttemptsSummary: fetchAttemptsSummary, + exportedAt: now) + } + + private static func snapshotHasField(_ key: String, snapshot: MiniMaxUsageSnapshot?) -> Bool { + guard let snapshot else { return false } + switch key { + case "planName": + return snapshot.planName != nil + case "availablePrompts": + return snapshot.availablePrompts != nil + case "currentPrompts": + return snapshot.currentPrompts != nil + case "remainingPrompts": + return snapshot.remainingPrompts != nil + case "windowMinutes": + return snapshot.windowMinutes != nil + case "usedPercent": + return snapshot.usedPercent != nil + case "resetsAt": + return snapshot.resetsAt != nil + case "services": + return snapshot.services != nil && !(snapshot.services?.isEmpty ?? true) + case "billingSummary": + return snapshot.billingSummary != nil + default: + return false + } + } + + private static func makeAttemptSummary( + _ attempt: ProviderFetchAttempt) -> MiniMaxFetchAttemptSummary + { + var errorCode: String? + var errorCategory: MiniMaxDiagnosticExportErrorCategory = .unknown + + if let rawError = attempt.errorDescription, !rawError.isEmpty { + let redactedError = LogRedactor.redact(rawError) + errorCode = Self.extractErrorCode(from: redactedError) + errorCategory = Self.categorizeError(errorCode: errorCode, redactedError: redactedError) + } + + return MiniMaxFetchAttemptSummary( + strategyID: attempt.strategyID, + wasAvailable: attempt.wasAvailable, + errorCode: errorCode, + errorCategory: errorCategory) + } + + private static func extractErrorCode(from redactedError: String) -> String? { + if let statusCode = self.extractBoundedHTTPStatusCode(from: redactedError) { + return statusCode + } + + let lowercased = redactedError.lowercased() + if lowercased.contains("timeout") { + return "timeout" + } + if lowercased.contains("network") { + return "network" + } + if lowercased.contains("parse") { + return "parse" + } + return nil + } + + private static func categorizeError( + errorCode: String?, + redactedError: String) -> MiniMaxDiagnosticExportErrorCategory + { + if let code = errorCode { + if self.authErrorCodes.contains(code) { + return .auth + } + if code.lowercased().contains("timeout") { + return .timeout + } + if code.lowercased().contains("network") { + return .network + } + if code.lowercased().contains("parse") { + return .parse + } + } + + let lowercased = redactedError.lowercased() + if self.containsBoundedHTTPStatusCode("401", in: lowercased) + || self.containsBoundedHTTPStatusCode("403", in: lowercased) + || lowercased.contains("unauthorized") + || lowercased.contains("forbidden") + || lowercased.contains("auth") + { + return .auth + } + if lowercased.contains("timeout") || lowercased.contains("timed out") { + return .timeout + } + if lowercased.contains("network") || lowercased.contains("connection") { + return .network + } + if lowercased.contains("parse") || lowercased.contains("decode") || lowercased.contains("invalid") { + return .parse + } + + return .unknown + } + + private static func extractBoundedHTTPStatusCode(from text: String) -> String? { + guard let regex = self.boundedHTTPStatusCodeRegex else { + return nil + } + + let range = NSRange(text.startIndex.. Bool { + guard let regex = self.boundedHTTPStatusCodeRegex else { + return false + } + let range = NSRange(text.startIndex.. MiniMaxUsageSnapshot { + MiniMaxUsageSnapshot( + planName: nil, + availablePrompts: nil, + currentPrompts: nil, + remainingPrompts: nil, + windowMinutes: nil, + usedPercent: nil, + resetsAt: nil, + updatedAt: Date()) + } + + private static func makeSuccessOutcome(strategyID: String) -> ProviderFetchOutcome { + ProviderFetchOutcome( + result: .success(self.makeSuccessResult(strategyID: strategyID)), + attempts: [ + ProviderFetchAttempt( + strategyID: strategyID, + kind: .apiToken, + wasAvailable: true, + errorDescription: nil), + ]) + } + + private static func makeSuccessResult(strategyID: String) -> ProviderFetchResult { + let usage = UsageSnapshot( + primary: RateWindow(usedPercent: 0, windowMinutes: nil, resetsAt: nil, resetDescription: nil), + secondary: nil, + tertiary: nil, + providerCost: nil, + minimaxUsage: nil, + updatedAt: Date(), + identity: nil) + return ProviderFetchResult( + usage: usage, + credits: nil, + dashboard: nil, + sourceLabel: strategyID, + strategyID: strategyID, + strategyKind: .apiToken) + } +} diff --git a/Tests/CodexBarTests/MiniMaxLogRedactorTests.swift b/Tests/CodexBarTests/MiniMaxLogRedactorTests.swift new file mode 100644 index 0000000000..d37ed2251e --- /dev/null +++ b/Tests/CodexBarTests/MiniMaxLogRedactorTests.swift @@ -0,0 +1,105 @@ +import CodexBarCore +import Foundation +import Testing + +@Suite(.serialized) +struct MiniMaxLogRedactorTests { + private static var miniMaxCpPlaceholder: String { + ["sk", "cp", "placeholder"].joined(separator: "-") + } + + private static var miniMaxApiPlaceholder: String { + ["sk", "api", "placeholder"].joined(separator: "-") + } + + @Test + func `sk-cp token is redacted`() { + let input = Self.miniMaxCpPlaceholder + let redacted = LogRedactor.redact(input) + #expect(redacted.contains("sk-cp-") == false) + #expect(redacted.contains("")) + #expect(redacted.contains("placeholder") == false) + } + + @Test + func `sk-api token is redacted`() { + let input = Self.miniMaxApiPlaceholder + let redacted = LogRedactor.redact(input) + #expect(redacted.contains("sk-api-") == false) + #expect(redacted.contains("")) + #expect(redacted.contains("placeholder") == false) + } + + @Test + func `cookie header is redacted`() { + let input = "Cookie: session=cookie-session-placeholder; token=\(Self.miniMaxCpPlaceholder)" + let redacted = LogRedactor.redact(input) + #expect(redacted.contains("session=cookie-session-placeholder") == false) + #expect(redacted.contains(Self.miniMaxCpPlaceholder) == false) + #expect(redacted.contains("Cookie: ")) + } + + @Test + func `authorization header value is redacted`() { + // Short obvious placeholder, not JWT-like + let input = "Authorization: Bearer fake-bearer-token" + let redacted = LogRedactor.redact(input) + #expect(redacted.contains("fake-bearer-token") == false) + #expect(redacted.contains("Authorization:")) + } + + @Test + func `bearer token is not present in raw form`() { + let input = "Authorization: bearer \(Self.miniMaxApiPlaceholder)" + let redacted = LogRedactor.redact(input) + #expect(redacted.contains(Self.miniMaxApiPlaceholder) == false) + } + + @Test + func `email is redacted`() { + let input = "Contact: user@example.com" + let redacted = LogRedactor.redact(input) + #expect(redacted.contains("user@example.com") == false) + #expect(redacted.contains("")) + } + + @Test + func `minimax token in cookie is not present in raw form`() { + let input = "Cookie: session=session-placeholder; token=\(Self.miniMaxCpPlaceholder)" + let redacted = LogRedactor.redact(input) + #expect(redacted.contains("session=session-placeholder") == false) + #expect(redacted.contains(Self.miniMaxCpPlaceholder) == false) + } + + @Test + func `redacted text no longer matches original token pattern`() { + let originalToken = Self.miniMaxCpPlaceholder + let input = "Token: \(originalToken)" + let redacted = LogRedactor.redact(input) + + #expect(redacted.contains(originalToken) == false) + #expect(redacted.contains("")) + } + + @Test + func `minimax token with punctuation suffix is fully redacted`() { + let punctuatedToken = "\(Self.miniMaxApiPlaceholder).suffix-more" + let input = "Error: token=\(punctuatedToken)" + let redacted = LogRedactor.redact(input) + + #expect(redacted.contains("sk-api-") == false) + #expect(redacted.contains("suffix-more") == false) + #expect(redacted.contains("")) + } + + @Test + func `authorization header minimax token leaves no suffix fragment`() { + let punctuatedToken = "\(Self.miniMaxCpPlaceholder)-part.two" + let input = "Authorization: Bearer \(punctuatedToken)" + let redacted = LogRedactor.redact(input) + + #expect(redacted.contains("sk-cp-") == false) + #expect(redacted.contains("part.two") == false) + #expect(redacted.contains("Authorization: ")) + } +} diff --git a/docs/minimax.md b/docs/minimax.md index aa427bcd7e..9079931b0c 100644 --- a/docs/minimax.md +++ b/docs/minimax.md @@ -60,3 +60,26 @@ quota card and omits the chart instead of treating the whole provider as failed. - `Sources/CodexBarCore/Providers/MiniMax/MiniMaxUsageFetcher.swift` - `Sources/CodexBarCore/Providers/MiniMax/MiniMaxProviderDescriptor.swift` - `Sources/CodexBar/Providers/MiniMax/MiniMaxProviderImplementation.swift` + +## Safe diagnostic output + +MiniMax diagnostic export data is intentionally limited to structural metadata only — no raw tokens, cookies, +authorization headers, API responses, or personal data. + +The diagnostic export fields are: + +- `schemaVersion`, `provider`, `authMode`, `region` +- `sourceLabel` and `strategyID` +- `fieldsPresent` (allowlisted field names that were non-nil) +- `servicesCount` (integer count only) +- `billingSummaryPresent` (boolean only) +- `fetchAttemptsSummary` (strategy ID, availability, extracted error code, error category) +- `redactionPolicyVersion`, `exportedAt` + +The export intentionally excludes: raw API tokens (`sk-cp-*`, `sk-api-*`), cookies, authorization headers, +bearer tokens, raw API responses or HTML, email addresses, session IDs, account/organization IDs, and any +per-request billing record details. + +Error messages are pre-redacted via `LogRedactor` before code/category extraction. Only fixed error codes +(`401`, `403`, `timeout`, etc.) and category labels (`auth`, `network`, `parse`, `timeout`, `unknown`) appear +in the export. From 352fdc25f6d6e2b8162cfe373aa521c80b4b764c Mon Sep 17 00:00:00 2001 From: Yuxin Qiao <104957188+Yuxin-Qiao@users.noreply.github.com> Date: Sun, 24 May 2026 13:12:39 +0800 Subject: [PATCH 02/10] Add MiniMax diagnostic CLI export --- Sources/CodexBarCLI/CLIDiagnoseCommand.swift | 99 +++ Sources/CodexBarCLI/CLIEntry.swift | 8 + Sources/CodexBarCLI/DiagnoseOptions.swift | 20 + .../Providers/MiniMax/MiniMaxAuthMode.swift | 8 + .../MiniMax/MiniMaxDiagnosticExport.swift | 384 +++++------ .../MiniMaxDiagnosticExportBuilder.swift | 52 ++ .../MiniMaxDiagnosticExportTests.swift | 647 +++++------------- docs/minimax.md | 50 +- 8 files changed, 541 insertions(+), 727 deletions(-) create mode 100644 Sources/CodexBarCLI/CLIDiagnoseCommand.swift create mode 100644 Sources/CodexBarCLI/DiagnoseOptions.swift create mode 100644 Sources/CodexBarCore/Providers/MiniMax/MiniMaxDiagnosticExportBuilder.swift diff --git a/Sources/CodexBarCLI/CLIDiagnoseCommand.swift b/Sources/CodexBarCLI/CLIDiagnoseCommand.swift new file mode 100644 index 0000000000..30e7a9f767 --- /dev/null +++ b/Sources/CodexBarCLI/CLIDiagnoseCommand.swift @@ -0,0 +1,99 @@ +import CodexBarCore +import Commander +import Foundation + +extension CodexBarCLI { + static func runDiagnose(_ values: ParsedValues) async { + let output = CLIOutputPreferences.from(values: values) + let config = Self.loadConfig(output: output) + + let providerRaw = values.options["provider"]?.last ?? "minimax" + guard providerRaw.lowercased() == "minimax" else { + Self.exit( + code: .failure, + message: "Error: only 'minimax' provider is supported for diagnose", + output: output, + kind: .args) + } + + let format = Self.decodeFormat(from: values) + guard format == .json else { + Self.exit( + code: .failure, + message: "Error: only JSON format is supported for diagnose", + output: output, + kind: .args) + } + + let pretty = values.flags.contains("pretty") + let verbose = values.flags.contains("verbose") + let browserDetection = BrowserDetection() + let fetcher = UsageFetcher() + + let tokenSelection = TokenAccountCLISelection(label: nil, index: nil, allAccounts: false) + let tokenContext: TokenAccountCLIContext + do { + tokenContext = try TokenAccountCLIContext( + selection: tokenSelection, + config: config, + verbose: verbose) + } catch { + Self.exit(code: .failure, message: "Error: \(error.localizedDescription)", output: output, kind: .config) + } + + let env = ProcessInfo.processInfo.environment + let settings = tokenContext.settingsSnapshot( + for: .minimax, + account: nil, + codexActiveSourceOverride: nil) + let sourceMode = tokenContext.preferredSourceMode(for: .minimax) + + let apiToken = ProviderTokenResolver.minimaxToken(environment: env) + let cookieHeader = ProviderTokenResolver.minimaxCookie(environment: env) + let authMode = MiniMaxAuthMode.resolve(apiToken: apiToken, cookieHeader: cookieHeader) + + let fetchContext = ProviderFetchContext( + runtime: .cli, + sourceMode: sourceMode, + includeCredits: true, + includeOptionalUsage: true, + webTimeout: 60, + webDebugDumpHTML: false, + verbose: verbose, + env: env, + settings: settings, + fetcher: fetcher, + claudeFetcher: ClaudeUsageFetcher(browserDetection: browserDetection), + browserDetection: browserDetection) + + let outcome = await Self.fetchProviderUsage(provider: .minimax, context: fetchContext) + + let diagnostic = MiniMaxDiagnosticExportBuilder.build( + outcome: outcome, + settings: settings, + authMode: authMode) + + let encoder = JSONEncoder() + encoder.dateEncodingStrategy = .iso8601 + if pretty { + encoder.outputFormatting = [.prettyPrinted, .sortedKeys] + } else { + encoder.outputFormatting = .sortedKeys + } + + do { + let data = try encoder.encode(diagnostic) + var jsonString = String(data: data, encoding: .utf8) ?? "{}" + jsonString = LogRedactor.redact(jsonString) + print(jsonString) + } catch { + Self.exit( + code: .failure, + message: "Error encoding diagnostic: \(error.localizedDescription)", + output: output, + kind: .runtime) + } + + Self.exit(code: .success, output: output, kind: .runtime) + } +} diff --git a/Sources/CodexBarCLI/CLIEntry.swift b/Sources/CodexBarCLI/CLIEntry.swift index f6b7aa061a..c81af77801 100644 --- a/Sources/CodexBarCLI/CLIEntry.swift +++ b/Sources/CodexBarCLI/CLIEntry.swift @@ -52,6 +52,8 @@ enum CodexBarCLI { self.runConfigSetAPIKey(invocation.parsedValues) case ["cache", "clear"]: self.runCacheClear(invocation.parsedValues) + case ["diagnose"]: + await self.runDiagnose(invocation.parsedValues) default: Self.exit( code: .failure, @@ -74,6 +76,7 @@ enum CodexBarCLI { let configProviderToggleSignature = CommandSignature.describe(ConfigProviderToggleOptions()) let configSetAPIKeySignature = CommandSignature.describe(ConfigSetAPIKeyOptions()) let cacheSignature = CommandSignature.describe(CacheOptions()) + let diagnoseSignature = CommandSignature.describe(DiagnoseOptions()) return [ CommandDescriptor( @@ -142,6 +145,11 @@ enum CodexBarCLI { signature: cacheSignature), ], defaultSubcommandName: "clear"), + CommandDescriptor( + name: "diagnose", + abstract: "Run provider diagnostic and emit safe JSON export", + discussion: nil, + signature: diagnoseSignature), ] } diff --git a/Sources/CodexBarCLI/DiagnoseOptions.swift b/Sources/CodexBarCLI/DiagnoseOptions.swift new file mode 100644 index 0000000000..9f2038602c --- /dev/null +++ b/Sources/CodexBarCLI/DiagnoseOptions.swift @@ -0,0 +1,20 @@ +import CodexBarCore +import Commander +import Foundation + +struct DiagnoseOptions: CommanderParsable { + @Flag(names: [.short("v"), .long("verbose")], help: "Enable verbose logging") + var verbose: Bool = false + + @Option(name: .long("log-level"), help: "Set log level (trace|verbose|debug|info|warning|error|critical)") + var logLevel: String? + + @Option(name: .long("provider"), help: "Provider to diagnose: minimax") + var provider: String? + + @Option(name: .long("format"), help: "Output format: json") + var format: String? + + @Flag(name: .long("pretty"), help: "Pretty-print JSON output") + var pretty: Bool = false +} diff --git a/Sources/CodexBarCore/Providers/MiniMax/MiniMaxAuthMode.swift b/Sources/CodexBarCore/Providers/MiniMax/MiniMaxAuthMode.swift index 89f4c9c81d..ea0fd87a68 100644 --- a/Sources/CodexBarCore/Providers/MiniMax/MiniMaxAuthMode.swift +++ b/Sources/CodexBarCore/Providers/MiniMax/MiniMaxAuthMode.swift @@ -29,6 +29,14 @@ public enum MiniMaxAuthMode: Sendable { self != .apiToken } + public var description: String { + switch self { + case .apiToken: "apiToken" + case .cookie: "cookie" + case .none: "none" + } + } + private static func cleaned(_ raw: String?) -> String? { guard let value = raw?.trimmingCharacters(in: .whitespacesAndNewlines), !value.isEmpty diff --git a/Sources/CodexBarCore/Providers/MiniMax/MiniMaxDiagnosticExport.swift b/Sources/CodexBarCore/Providers/MiniMax/MiniMaxDiagnosticExport.swift index 370d8f0c69..dbc446457c 100644 --- a/Sources/CodexBarCore/Providers/MiniMax/MiniMaxDiagnosticExport.swift +++ b/Sources/CodexBarCore/Providers/MiniMax/MiniMaxDiagnosticExport.swift @@ -1,264 +1,200 @@ import Foundation -public enum MiniMaxDiagnosticExportErrorCategory: String, Sendable, Codable { - case auth - case network - case parse - case timeout - case unknown -} - -public struct MiniMaxFetchAttemptSummary: Sendable, Codable { - public let strategyID: String - public let wasAvailable: Bool - public let errorCode: String? - public let errorCategory: MiniMaxDiagnosticExportErrorCategory - - public init( - strategyID: String, - wasAvailable: Bool, - errorCode: String?, - errorCategory: MiniMaxDiagnosticExportErrorCategory) - { - self.strategyID = strategyID - self.wasAvailable = wasAvailable - self.errorCode = errorCode - self.errorCategory = errorCategory - } -} - -public struct MiniMaxDiagnosticExport: Sendable, Codable { - public let schemaVersion: String - public let provider: UsageProvider +public struct MiniMaxDiagnosticExport: Codable, Sendable { + public let timestamp: Date + public let provider: String + public let source: String public let authMode: String - public let region: String? - public let sourceLabel: String? - public let strategyID: String? - public let fieldsPresent: Set - public let servicesCount: Int - public let billingSummaryPresent: Bool - public let fetchAttemptsSummary: [MiniMaxFetchAttemptSummary] - public let redactionPolicyVersion: String - public let exportedAt: Date + public let authConfigured: Bool + public let usage: MiniMaxDiagnosticUsage? + public let fetchAttempts: [MiniMaxDiagnosticFetchAttempt] + public let error: MiniMaxDiagnosticError? + public let settingsSummary: MiniMaxSettingsSummary public init( - schemaVersion: String = "1.0", - provider: UsageProvider = .minimax, + timestamp: Date, + provider: String, + source: String, authMode: String, - region: String?, - sourceLabel: String?, - strategyID: String?, - fieldsPresent: Set, - servicesCount: Int, - billingSummaryPresent: Bool, - fetchAttemptsSummary: [MiniMaxFetchAttemptSummary], - redactionPolicyVersion: String = "1.0", - exportedAt: Date) + authConfigured: Bool, + usage: MiniMaxDiagnosticUsage?, + fetchAttempts: [MiniMaxDiagnosticFetchAttempt], + error: MiniMaxDiagnosticError?, + settingsSummary: MiniMaxSettingsSummary) { - self.schemaVersion = schemaVersion + self.timestamp = timestamp self.provider = provider + self.source = source self.authMode = authMode - self.region = region - self.sourceLabel = sourceLabel - self.strategyID = strategyID - self.fieldsPresent = fieldsPresent - self.servicesCount = servicesCount - self.billingSummaryPresent = billingSummaryPresent - self.fetchAttemptsSummary = fetchAttemptsSummary - self.redactionPolicyVersion = redactionPolicyVersion - self.exportedAt = exportedAt + self.authConfigured = authConfigured + self.usage = usage + self.fetchAttempts = fetchAttempts + self.error = error + self.settingsSummary = settingsSummary } } -public enum MiniMaxDiagnosticExportBuilder { - private static let allowlistedFields: Set = [ - "planName", - "availablePrompts", - "currentPrompts", - "remainingPrompts", - "windowMinutes", - "usedPercent", - "resetsAt", - "services", - "billingSummary", - ] +public struct MiniMaxDiagnosticUsage: Codable, Sendable { + public let planName: String? + public let availablePrompts: Int? + public let currentPrompts: Int? + public let remainingPrompts: Int? + public let windowMinutes: Int? + public let usedPercent: Double? + public let resetsAt: Date? + public let services: [MiniMaxDiagnosticServiceUsage]? - private static let authErrorCodes: Set = ["401", "403"] - private static let boundedHTTPStatusCodeRegex: NSRegularExpression? = try? NSRegularExpression( - pattern: #"(? MiniMaxDiagnosticExport + public init( + planName: String?, + availablePrompts: Int?, + currentPrompts: Int?, + remainingPrompts: Int?, + windowMinutes: Int?, + usedPercent: Double?, + resetsAt: Date?, + services: [MiniMaxDiagnosticServiceUsage]?) { - let fieldsPresent = Self.allowlistedFields.filter { key in - Self.snapshotHasField(key, snapshot: snapshot) - } + self.planName = planName + self.availablePrompts = availablePrompts + self.currentPrompts = currentPrompts + self.remainingPrompts = remainingPrompts + self.windowMinutes = windowMinutes + self.usedPercent = usedPercent + self.resetsAt = resetsAt + self.services = services + } - let servicesCount = snapshot?.services?.count ?? 0 - let billingSummaryPresent = snapshot?.billingSummary != nil + public init(from snapshot: MiniMaxUsageSnapshot) { + self.planName = snapshot.planName + self.availablePrompts = snapshot.availablePrompts + self.currentPrompts = snapshot.currentPrompts + self.remainingPrompts = snapshot.remainingPrompts + self.windowMinutes = snapshot.windowMinutes + self.usedPercent = snapshot.usedPercent + self.resetsAt = snapshot.resetsAt + self.services = snapshot.services?.map { MiniMaxDiagnosticServiceUsage(from: $0) } + } +} - var fetchAttemptsSummary: [MiniMaxFetchAttemptSummary] = [] - for attempt in outcome.attempts { - let summary = Self.makeAttemptSummary(attempt) - fetchAttemptsSummary.append(summary) - } +public struct MiniMaxDiagnosticServiceUsage: Codable, Sendable { + public let displayName: String + public let percent: Double + public let windowType: String + public let resetsAt: Date? + public let resetDescription: String? + + public init(from service: MiniMaxServiceUsage) { + self.displayName = service.displayName + self.percent = service.percent + self.windowType = service.windowType + self.resetsAt = service.resetsAt + self.resetDescription = service.resetDescription + } +} - let resultValue = outcome.result - var sourceLabel: String? - var strategyID: String? - if case let .success(result) = resultValue { - sourceLabel = result.sourceLabel - strategyID = result.strategyID - } +public struct MiniMaxDiagnosticFetchAttempt: Codable, Sendable { + public let strategyID: String + public let kind: String + public let wasAvailable: Bool + public let errorMessage: String? - return MiniMaxDiagnosticExport( - authMode: authMode, - region: region?.rawValue, - sourceLabel: sourceLabel, - strategyID: strategyID, - fieldsPresent: Set(fieldsPresent), - servicesCount: servicesCount, - billingSummaryPresent: billingSummaryPresent, - fetchAttemptsSummary: fetchAttemptsSummary, - exportedAt: now) + public init( + strategyID: String, + kind: String, + wasAvailable: Bool, + errorMessage: String?) + { + self.strategyID = strategyID + self.kind = kind + self.wasAvailable = wasAvailable + self.errorMessage = errorMessage } - private static func snapshotHasField(_ key: String, snapshot: MiniMaxUsageSnapshot?) -> Bool { - guard let snapshot else { return false } - switch key { - case "planName": - return snapshot.planName != nil - case "availablePrompts": - return snapshot.availablePrompts != nil - case "currentPrompts": - return snapshot.currentPrompts != nil - case "remainingPrompts": - return snapshot.remainingPrompts != nil - case "windowMinutes": - return snapshot.windowMinutes != nil - case "usedPercent": - return snapshot.usedPercent != nil - case "resetsAt": - return snapshot.resetsAt != nil - case "services": - return snapshot.services != nil && !(snapshot.services?.isEmpty ?? true) - case "billingSummary": - return snapshot.billingSummary != nil - default: - return false - } + public init(from attempt: ProviderFetchAttempt) { + self.strategyID = attempt.strategyID + self.kind = Self.kindLabel(attempt.kind) + self.wasAvailable = attempt.wasAvailable + self.errorMessage = attempt.errorDescription } - private static func makeAttemptSummary( - _ attempt: ProviderFetchAttempt) -> MiniMaxFetchAttemptSummary - { - var errorCode: String? - var errorCategory: MiniMaxDiagnosticExportErrorCategory = .unknown - - if let rawError = attempt.errorDescription, !rawError.isEmpty { - let redactedError = LogRedactor.redact(rawError) - errorCode = Self.extractErrorCode(from: redactedError) - errorCategory = Self.categorizeError(errorCode: errorCode, redactedError: redactedError) + private static func kindLabel(_ kind: ProviderFetchKind) -> String { + switch kind { + case .cli: "cli" + case .web: "web" + case .oauth: "oauth" + case .apiToken: "api" + case .localProbe: "local" + case .webDashboard: "web" } - - return MiniMaxFetchAttemptSummary( - strategyID: attempt.strategyID, - wasAvailable: attempt.wasAvailable, - errorCode: errorCode, - errorCategory: errorCategory) } +} - private static func extractErrorCode(from redactedError: String) -> String? { - if let statusCode = self.extractBoundedHTTPStatusCode(from: redactedError) { - return statusCode - } +public struct MiniMaxDiagnosticError: Codable, Sendable { + public let category: String + public let safeDescription: String - let lowercased = redactedError.lowercased() - if lowercased.contains("timeout") { - return "timeout" - } - if lowercased.contains("network") { - return "network" - } - if lowercased.contains("parse") { - return "parse" - } - return nil + public init(category: String, safeDescription: String) { + self.category = category + self.safeDescription = safeDescription } - private static func categorizeError( - errorCode: String?, - redactedError: String) -> MiniMaxDiagnosticExportErrorCategory - { - if let code = errorCode { - if self.authErrorCodes.contains(code) { - return .auth - } - if code.lowercased().contains("timeout") { - return .timeout - } - if code.lowercased().contains("network") { - return .network + public init(from error: Error) { + self.category = Self.errorCategory(error) + self.safeDescription = Self.safeDescription(for: error) + } + + private static func errorCategory(_ error: Error) -> String { + if let minimaxError = error as? MiniMaxUsageError { + switch minimaxError { + case .networkError: return "network" + case .invalidCredentials: return "auth" + case .apiError: return "api" + case .parseFailed: return "parse" } - if code.lowercased().contains("parse") { - return .parse + } + if let settingsError = error as? MiniMaxSettingsError { + switch settingsError { + case .missingCookie: return "auth" } } + if error is MiniMaxAPISettingsError { return "auth" } + return "unknown" + } - let lowercased = redactedError.lowercased() - if self.containsBoundedHTTPStatusCode("401", in: lowercased) - || self.containsBoundedHTTPStatusCode("403", in: lowercased) - || lowercased.contains("unauthorized") - || lowercased.contains("forbidden") - || lowercased.contains("auth") - { - return .auth - } - if lowercased.contains("timeout") || lowercased.contains("timed out") { - return .timeout + private static func safeDescription(for error: Error) -> String { + if let minimaxError = error as? MiniMaxUsageError { + switch minimaxError { + case .networkError: + return "Network error - check your connection" + case .invalidCredentials: + return "Invalid credentials - please re-authenticate" + case .apiError: + return "API error - service returned an unexpected response" + case .parseFailed: + return "Parse error - unexpected response format" + } } - if lowercased.contains("network") || lowercased.contains("connection") { - return .network + if let settingsError = error as? MiniMaxSettingsError { + switch settingsError { + case .missingCookie: + return "Cookie not configured - import from browser or provide manually" + } } - if lowercased.contains("parse") || lowercased.contains("decode") || lowercased.contains("invalid") { - return .parse + if error is MiniMaxAPISettingsError { + return "API settings error - check your token configuration" } - - return .unknown + return "An unexpected error occurred" } +} - private static func extractBoundedHTTPStatusCode(from text: String) -> String? { - guard let regex = self.boundedHTTPStatusCodeRegex else { - return nil - } - - let range = NSRange(text.startIndex.. Bool { - guard let regex = self.boundedHTTPStatusCodeRegex else { - return false - } - let range = NSRange(text.startIndex.. MiniMaxDiagnosticExport + { + let sourceLabel = outcome.sourceLabel + let authConfigured = outcome.authConfigured(authMode: authMode) + let usage = outcome.usageSnapshot.map { MiniMaxDiagnosticUsage(from: $0) } + let error = outcome.failureError.map { MiniMaxDiagnosticError(from: $0) } + + let settingsSummary = MiniMaxSettingsSummary( + cookieSource: settings?.minimax?.cookieSource.rawValue ?? "auto", + apiRegion: settings?.minimax?.apiRegion.rawValue ?? "global", + authMode: authMode.description) + + return MiniMaxDiagnosticExport( + timestamp: Date(), + provider: "minimax", + source: sourceLabel, + authMode: authMode.description, + authConfigured: authConfigured, + usage: usage, + fetchAttempts: outcome.attempts.map { MiniMaxDiagnosticFetchAttempt(from: $0) }, + error: error, + settingsSummary: settingsSummary) + } +} + +extension ProviderFetchOutcome { + fileprivate var sourceLabel: String { + guard case let .success(result) = result else { return "failed" } + return result.sourceLabel + } + + fileprivate func authConfigured(authMode: MiniMaxAuthMode) -> Bool { + guard case .success = result else { return authMode.usesAPIToken || authMode.usesCookie } + return true + } + + fileprivate var usageSnapshot: MiniMaxUsageSnapshot? { + guard case let .success(result) = result else { return nil } + return result.usage.minimaxUsage + } + + fileprivate var failureError: Error? { + guard case let .failure(error) = result else { return nil } + return error + } +} diff --git a/Tests/CodexBarTests/MiniMaxDiagnosticExportTests.swift b/Tests/CodexBarTests/MiniMaxDiagnosticExportTests.swift index 751b0cc87e..dd1b075870 100644 --- a/Tests/CodexBarTests/MiniMaxDiagnosticExportTests.swift +++ b/Tests/CodexBarTests/MiniMaxDiagnosticExportTests.swift @@ -2,537 +2,222 @@ import Foundation import Testing @testable import CodexBarCore -@Suite(.serialized) struct MiniMaxDiagnosticExportTests { - // MARK: - Auth mode and region - - @Test - func `export captures auth mode apiToken`() { - let outcome = Self.makeSuccessOutcome(strategyID: "minimax.api") - let snapshot = MiniMaxUsageSnapshot( - planName: "Max", - availablePrompts: 1000, - currentPrompts: 250, - remainingPrompts: 750, - windowMinutes: 300, - usedPercent: 25, - resetsAt: nil, - updatedAt: Date()) - - let export = MiniMaxDiagnosticExportBuilder.build( - from: outcome, - region: .global, - authMode: "apiToken", - snapshot: snapshot) - - #expect(export.authMode == "apiToken") - } - - @Test - func `export captures auth mode webSession`() { - let outcome = Self.makeSuccessOutcome(strategyID: "minimax.web") - let snapshot = MiniMaxUsageSnapshot( - planName: nil, - availablePrompts: nil, - currentPrompts: nil, - remainingPrompts: nil, - windowMinutes: nil, - usedPercent: nil, - resetsAt: nil, - updatedAt: Date()) - - let export = MiniMaxDiagnosticExportBuilder.build( - from: outcome, - region: .chinaMainland, - authMode: "webSession", - snapshot: snapshot) - - #expect(export.authMode == "webSession") - #expect(export.region == "cn") - } - - @Test - func `export captures region global`() { - let outcome = Self.makeSuccessOutcome(strategyID: "minimax.api") - let snapshot = Self.makeEmptySnapshot() - - let export = MiniMaxDiagnosticExportBuilder.build( - from: outcome, - region: .global, - authMode: "apiToken", - snapshot: snapshot) - - #expect(export.region == "global") - } - - @Test - func `export captures region chinaMainland`() { - let outcome = Self.makeSuccessOutcome(strategyID: "minimax.api") - let snapshot = Self.makeEmptySnapshot() - - let export = MiniMaxDiagnosticExportBuilder.build( - from: outcome, - region: .chinaMainland, - authMode: "apiToken", - snapshot: snapshot) - - #expect(export.region == "cn") - } - - @Test - func `export captures nil region when unknown`() { - let outcome = Self.makeSuccessOutcome(strategyID: "minimax.api") - let snapshot = Self.makeEmptySnapshot() - - let export = MiniMaxDiagnosticExportBuilder.build( - from: outcome, - region: nil, - authMode: "apiToken", - snapshot: snapshot) - - #expect(export.region == nil) - } - - // MARK: - Field presence - - @Test - func `fieldsPresent includes planName when non-nil`() { - let outcome = Self.makeSuccessOutcome(strategyID: "minimax.api") - let snapshot = MiniMaxUsageSnapshot( - planName: "Pro", - availablePrompts: nil, - currentPrompts: nil, - remainingPrompts: nil, - windowMinutes: nil, - usedPercent: nil, - resetsAt: nil, - updatedAt: Date()) - - let export = MiniMaxDiagnosticExportBuilder.build( - from: outcome, - region: nil, - authMode: "apiToken", - snapshot: snapshot) - - #expect(export.fieldsPresent.contains("planName")) - } - - @Test - func `fieldsPresent excludes planName when nil`() { - let outcome = Self.makeSuccessOutcome(strategyID: "minimax.api") - let snapshot = Self.makeEmptySnapshot() - - let export = MiniMaxDiagnosticExportBuilder.build( - from: outcome, - region: nil, - authMode: "apiToken", - snapshot: snapshot) - - #expect(export.fieldsPresent.contains("planName") == false) - } - - @Test - func `fieldsPresent includes services when non-nil`() { - let outcome = Self.makeSuccessOutcome(strategyID: "minimax.api") - let service = MiniMaxServiceUsage( - serviceType: "text-generation", - windowType: "5 hours", - timeRange: "10:00-15:00(UTC+8)", - usage: 100, - limit: 1000, - percent: 10, - resetsAt: nil, - resetDescription: "Resets in 5 hours") - let snapshot = MiniMaxUsageSnapshot( - planName: nil, - availablePrompts: nil, - currentPrompts: nil, - remainingPrompts: nil, - windowMinutes: nil, - usedPercent: nil, - resetsAt: nil, - updatedAt: Date(), - services: [service]) - - let export = MiniMaxDiagnosticExportBuilder.build( - from: outcome, - region: nil, - authMode: "apiToken", - snapshot: snapshot) - - #expect(export.fieldsPresent.contains("services")) - } - @Test - func `servicesCount reflects services length`() { - let outcome = Self.makeSuccessOutcome(strategyID: "minimax.api") - let service1 = MiniMaxServiceUsage( - serviceType: "text-generation", - windowType: "5 hours", - timeRange: "10:00-15:00", - usage: 100, - limit: 1000, - percent: 10, - resetsAt: nil, - resetDescription: "Resets in 5 hours") - let service2 = MiniMaxServiceUsage( - serviceType: "image", - windowType: "Today", - timeRange: "00:00-23:59", - usage: 50, - limit: 500, - percent: 10, - resetsAt: nil, - resetDescription: "Resets at midnight") - let snapshot = MiniMaxUsageSnapshot( - planName: nil, - availablePrompts: nil, - currentPrompts: nil, - remainingPrompts: nil, - windowMinutes: nil, - usedPercent: nil, - resetsAt: nil, - updatedAt: Date(), - services: [service1, service2]) - - let export = MiniMaxDiagnosticExportBuilder.build( - from: outcome, - region: nil, - authMode: "apiToken", - snapshot: snapshot) - - #expect(export.servicesCount == 2) - } - - @Test - func `billingSummaryPresent true when billingSummary non-nil`() { - let outcome = Self.makeSuccessOutcome(strategyID: "minimax.api") - let billing = MiniMaxBillingSummary( - todayTokens: 1000, - last30DaysTokens: 50000, - todayCash: 0.05, - last30DaysCash: 2.50, - daily: [], - topMethods: [], - topModels: [], - updatedAt: Date()) - let snapshot = MiniMaxUsageSnapshot( - planName: nil, - availablePrompts: nil, - currentPrompts: nil, - remainingPrompts: nil, - windowMinutes: nil, - usedPercent: nil, - resetsAt: nil, - updatedAt: Date(), - billingSummary: billing) - - let export = MiniMaxDiagnosticExportBuilder.build( - from: outcome, - region: nil, - authMode: "apiToken", - snapshot: snapshot) - - #expect(export.billingSummaryPresent == true) - } - - @Test - func `billingSummaryPresent false when billingSummary nil`() { - let outcome = Self.makeSuccessOutcome(strategyID: "minimax.api") - let snapshot = Self.makeEmptySnapshot() - - let export = MiniMaxDiagnosticExportBuilder.build( - from: outcome, - region: nil, - authMode: "apiToken", - snapshot: snapshot) - - #expect(export.billingSummaryPresent == false) - } - - // MARK: - Fetch attempt categorization - - @Test - func `fetch attempt 401 maps to auth`() { - let attempt = ProviderFetchAttempt( - strategyID: "minimax.api", - kind: .apiToken, - wasAvailable: true, - errorDescription: "HTTP 401 Unauthorized: invalid token sk-cp-secret") - let outcome = ProviderFetchOutcome(result: .failure(MiniMaxUsageError.invalidCredentials), attempts: [attempt]) - - let export = MiniMaxDiagnosticExportBuilder.build( - from: outcome, - region: nil, - authMode: "apiToken", - snapshot: nil) - - #expect(export.fetchAttemptsSummary.count == 1) - #expect(export.fetchAttemptsSummary[0].errorCategory == .auth) - } - - @Test - func `fetch attempt 403 maps to auth`() { - let attempt = ProviderFetchAttempt( - strategyID: "minimax.api", - kind: .apiToken, - wasAvailable: true, - errorDescription: "HTTP/1.1 403 Forbidden - access denied") - let outcome = ProviderFetchOutcome(result: .failure(MiniMaxUsageError.invalidCredentials), attempts: [attempt]) + func `diagnostic export encodes to JSON with all safe fields`() throws { + let now = Date(timeIntervalSince1970: 1_700_000_000) + let export = MiniMaxDiagnosticExport( + timestamp: now, + provider: "minimax", + source: "api", + authMode: "apiToken", + authConfigured: true, + usage: MiniMaxDiagnosticUsage( + planName: "Max", + availablePrompts: 1000, + currentPrompts: 250, + remainingPrompts: 750, + windowMinutes: 300, + usedPercent: 25, + resetsAt: now.addingTimeInterval(18000), + services: nil), + fetchAttempts: [ + MiniMaxDiagnosticFetchAttempt( + strategyID: "minimax.api", + kind: "api", + wasAvailable: true, + errorMessage: nil), + ], + error: nil, + settingsSummary: MiniMaxSettingsSummary( + cookieSource: "auto", + apiRegion: "global", + authMode: "apiToken")) - let export = MiniMaxDiagnosticExportBuilder.build( - from: outcome, - region: nil, - authMode: "apiToken", - snapshot: nil) + let encoder = JSONEncoder() + encoder.dateEncodingStrategy = .iso8601 + encoder.outputFormatting = .prettyPrinted + let data = try encoder.encode(export) + let json = String(data: data, encoding: .utf8) ?? "" - #expect(export.fetchAttemptsSummary[0].errorCategory == .auth) + #expect(json.contains("\"provider\"")) + #expect(json.contains("\"minimax\"")) + #expect(json.contains("\"authConfigured\"")) + #expect(!json.contains("sk-cp-")) + #expect(!json.contains("sk-api-")) + #expect(!json.contains("Bearer")) } @Test - func `bounded 401 maps to auth`() { - let attempt = ProviderFetchAttempt( - strategyID: "minimax.api", - kind: .apiToken, - wasAvailable: true, - errorDescription: "response status 401 and request failed") - let outcome = ProviderFetchOutcome(result: .failure(MiniMaxUsageError.invalidCredentials), attempts: [attempt]) + func `diagnostic error maps MiniMaxUsageError categories safely`() { + let networkError = MiniMaxUsageError.networkError("connection refused") + let invalidCreds = MiniMaxUsageError.invalidCredentials + let apiError = MiniMaxUsageError.apiError("HTTP 404") + let parseError = MiniMaxUsageError.parseFailed("unexpected") - let export = MiniMaxDiagnosticExportBuilder.build( - from: outcome, - region: nil, - authMode: "apiToken", - snapshot: nil) - - #expect(export.fetchAttemptsSummary[0].errorCategory == .auth) - #expect(export.fetchAttemptsSummary[0].errorCode == "401") - } + let diagNetwork = MiniMaxDiagnosticError(from: networkError) + #expect(diagNetwork.category == "network") + #expect(!diagNetwork.safeDescription.contains("connection refused")) - @Test - func `embedded 401 digits do not map to auth`() { - let attempt = ProviderFetchAttempt( - strategyID: "minimax.api", - kind: .apiToken, - wasAvailable: true, - errorDescription: "service status_code=1401 and retrying") - let outcome = ProviderFetchOutcome( - result: .failure(MiniMaxUsageError.parseFailed("bad response")), - attempts: [attempt]) + let diagCreds = MiniMaxDiagnosticError(from: invalidCreds) + #expect(diagCreds.category == "auth") - let export = MiniMaxDiagnosticExportBuilder.build( - from: outcome, - region: nil, - authMode: "apiToken", - snapshot: nil) + let diagAPI = MiniMaxDiagnosticError(from: apiError) + #expect(diagAPI.category == "api") - #expect(export.fetchAttemptsSummary[0].errorCategory == .unknown) - #expect(export.fetchAttemptsSummary[0].errorCode == nil) + let diagParse = MiniMaxDiagnosticError(from: parseError) + #expect(diagParse.category == "parse") } @Test - func `overlong status code does not map to auth`() { - let attempt = ProviderFetchAttempt( - strategyID: "minimax.api", - kind: .apiToken, - wasAvailable: true, - errorDescription: "HTTP 2000 from proxy") - let outcome = ProviderFetchOutcome( - result: .failure(MiniMaxUsageError.parseFailed("bad response")), - attempts: [attempt]) - - let export = MiniMaxDiagnosticExportBuilder.build( - from: outcome, - region: nil, - authMode: "apiToken", - snapshot: nil) - - #expect(export.fetchAttemptsSummary[0].errorCategory == .unknown) - #expect(export.fetchAttemptsSummary[0].errorCode == nil) + func `diagnostic error maps MiniMaxSettingsError categories safely`() { + let missingCookie = MiniMaxSettingsError.missingCookie + let diag = MiniMaxDiagnosticError(from: missingCookie) + #expect(diag.category == "auth") + #expect(diag.safeDescription.contains("Cookie")) } @Test - func `timeout maps to timeout`() { - let attempt = ProviderFetchAttempt( + func `diagnostic fetch attempt serializes kind correctly`() { + let webAttempt = ProviderFetchAttempt( strategyID: "minimax.web", kind: .web, wasAvailable: true, - errorDescription: "Request timed out after 60 seconds") - let outcome = ProviderFetchOutcome( - result: .failure(MiniMaxUsageError.networkError("timed out")), - attempts: [attempt]) - - let export = MiniMaxDiagnosticExportBuilder.build( - from: outcome, - region: nil, - authMode: "webSession", - snapshot: nil) - - #expect(export.fetchAttemptsSummary[0].errorCategory == .timeout) - } + errorDescription: nil) + let diagAttempt = MiniMaxDiagnosticFetchAttempt(from: webAttempt) + #expect(diagAttempt.kind == "web") + #expect(diagAttempt.strategyID == "minimax.web") - @Test - func `unknown error maps to unknown`() { - let attempt = ProviderFetchAttempt( + let apiAttempt = ProviderFetchAttempt( strategyID: "minimax.api", kind: .apiToken, - wasAvailable: true, - errorDescription: "Something went wrong") - let outcome = ProviderFetchOutcome( - result: .failure(MiniMaxUsageError.parseFailed("unknown")), - attempts: [attempt]) - - let export = MiniMaxDiagnosticExportBuilder.build( - from: outcome, - region: nil, - authMode: "apiToken", - snapshot: nil) - - #expect(export.fetchAttemptsSummary[0].errorCategory == .unknown) + wasAvailable: false, + errorDescription: "token missing") + let diagApiAttempt = MiniMaxDiagnosticFetchAttempt(from: apiAttempt) + #expect(diagApiAttempt.kind == "api") + #expect(diagApiAttempt.wasAvailable == false) + #expect(diagApiAttempt.errorMessage == "token missing") } @Test - func `fetch attempt wasAvailable true has no error`() { - let attempt = ProviderFetchAttempt( - strategyID: "minimax.api", - kind: .apiToken, - wasAvailable: true, - errorDescription: nil) - let outcome = ProviderFetchOutcome( - result: .success(Self.makeSuccessResult(strategyID: "minimax.api")), - attempts: [attempt]) - - let export = MiniMaxDiagnosticExportBuilder.build( - from: outcome, - region: nil, - authMode: "apiToken", - snapshot: nil) + func `usage maps from MiniMaxUsageSnapshot correctly`() { + let now = Date(timeIntervalSince1970: 1_700_000_000) + let snapshot = MiniMaxUsageSnapshot( + planName: "Max", + availablePrompts: 1000, + currentPrompts: 250, + remainingPrompts: 750, + windowMinutes: 300, + usedPercent: 25, + resetsAt: now.addingTimeInterval(18000), + updatedAt: now, + services: nil) - #expect(export.fetchAttemptsSummary[0].wasAvailable == true) - #expect(export.fetchAttemptsSummary[0].errorCode == nil) - #expect(export.fetchAttemptsSummary[0].errorCategory == .unknown) + let diagUsage = MiniMaxDiagnosticUsage(from: snapshot) + #expect(diagUsage.planName == "Max") + #expect(diagUsage.availablePrompts == 1000) + #expect(diagUsage.currentPrompts == 250) + #expect(diagUsage.remainingPrompts == 750) + #expect(diagUsage.windowMinutes == 300) + #expect(diagUsage.usedPercent == 25) } @Test - func `empty fetch attempts produces valid export`() { - let attempt = ProviderFetchAttempt( - strategyID: "minimax.api", - kind: .apiToken, - wasAvailable: false, - errorDescription: nil) - let outcome = ProviderFetchOutcome(result: .failure(MiniMaxUsageError.invalidCredentials), attempts: [attempt]) - - let export = MiniMaxDiagnosticExportBuilder.build( - from: outcome, - region: nil, - authMode: "apiToken", - snapshot: nil) + func `service usage maps from MiniMaxServiceUsage correctly`() { + let now = Date(timeIntervalSince1970: 1_700_000_000) + let service = MiniMaxServiceUsage( + serviceType: "Text Generation", + windowType: "5 hours", + timeRange: "10:00-15:00(UTC+8)", + usage: 750, + limit: 1000, + percent: 75, + resetsAt: now.addingTimeInterval(18000), + resetDescription: "5 hours") - #expect(export.schemaVersion == "1.0") - #expect(export.provider == .minimax) - #expect(export.fetchAttemptsSummary.count == 1) + let diagService = MiniMaxDiagnosticServiceUsage(from: service) + #expect(diagService.displayName == "Text Generation") + #expect(diagService.percent == 75) + #expect(diagService.windowType == "5 hours") } - // MARK: - JSON serialization - @Test - func `serialized diagnostic JSON contains no raw token strings`() throws { - let attempt = ProviderFetchAttempt( - strategyID: "minimax.api", - kind: .apiToken, - wasAvailable: true, - errorDescription: "401 Unauthorized: Bearer sk-cp-faketoken and user@test.com") + func `builder creates safe diagnostic with error on failure`() { + let error = MiniMaxUsageError.networkError("timeout") let outcome = ProviderFetchOutcome( - result: .success(Self.makeSuccessResult(strategyID: "minimax.api")), - attempts: [attempt]) - - let export = MiniMaxDiagnosticExportBuilder.build( - from: outcome, - region: nil, - authMode: "apiToken", - snapshot: nil) + result: .failure(error), + attempts: [ + ProviderFetchAttempt( + strategyID: "minimax.api", + kind: .apiToken, + wasAvailable: true, + errorDescription: "timeout"), + ]) - let jsonData = try JSONEncoder().encode(export) - let jsonString = String(data: jsonData, encoding: .utf8) ?? "" + let diag = MiniMaxDiagnosticExportBuilder.build( + outcome: outcome, + settings: nil, + authMode: .apiToken) - #expect(jsonString.contains("sk-cp-") == false) - #expect(jsonString.contains("faketoken") == false) - #expect(jsonString.contains("user@test.com") == false) - #expect(jsonString.contains("401")) + #expect(diag.provider == "minimax") + #expect(diag.source == "failed") + #expect(diag.authConfigured == true) + #expect(diag.usage == nil) + #expect(diag.error != nil) + #expect(diag.error?.category == "network") + #expect(diag.fetchAttempts.count == 1) } @Test - func `json roundtrip preserves all fields`() throws { - let outcome = Self.makeSuccessOutcome(strategyID: "minimax.api") + func `builder creates safe diagnostic with usage on success`() { + let now = Date(timeIntervalSince1970: 1_700_000_000) let snapshot = MiniMaxUsageSnapshot( - planName: "Pro", + planName: "Max", availablePrompts: 1000, - currentPrompts: nil, - remainingPrompts: nil, + currentPrompts: 250, + remainingPrompts: 750, windowMinutes: 300, - usedPercent: 10, - resetsAt: nil, - updatedAt: Date()) - - let export = MiniMaxDiagnosticExportBuilder.build( - from: outcome, - region: .global, - authMode: "apiToken", - snapshot: snapshot) - - let jsonData = try JSONEncoder().encode(export) - let decoded = try JSONDecoder().decode(MiniMaxDiagnosticExport.self, from: jsonData) - - #expect(decoded.schemaVersion == export.schemaVersion) - #expect(decoded.provider == export.provider) - #expect(decoded.authMode == export.authMode) - #expect(decoded.region == export.region) - #expect(decoded.fieldsPresent == export.fieldsPresent) - #expect(decoded.servicesCount == export.servicesCount) - #expect(decoded.billingSummaryPresent == export.billingSummaryPresent) - #expect(decoded.fetchAttemptsSummary.count == export.fetchAttemptsSummary.count) - } - - // MARK: - Helpers - - private static func makeEmptySnapshot() -> MiniMaxUsageSnapshot { - MiniMaxUsageSnapshot( - planName: nil, - availablePrompts: nil, - currentPrompts: nil, - remainingPrompts: nil, - windowMinutes: nil, - usedPercent: nil, - resetsAt: nil, - updatedAt: Date()) - } + usedPercent: 25, + resetsAt: now.addingTimeInterval(18000), + updatedAt: now) + + let result = ProviderFetchResult( + usage: UsageSnapshot( + primary: RateWindow( + usedPercent: 25, + windowMinutes: 300, + resetsAt: now.addingTimeInterval(18000), + resetDescription: nil), + secondary: nil, + tertiary: nil, + minimaxUsage: snapshot, + updatedAt: now), + credits: nil, + dashboard: nil, + sourceLabel: "api", + strategyID: "minimax.api", + strategyKind: .apiToken) - private static func makeSuccessOutcome(strategyID: String) -> ProviderFetchOutcome { - ProviderFetchOutcome( - result: .success(self.makeSuccessResult(strategyID: strategyID)), + let outcome = ProviderFetchOutcome( + result: .success(result), attempts: [ ProviderFetchAttempt( - strategyID: strategyID, + strategyID: "minimax.api", kind: .apiToken, wasAvailable: true, errorDescription: nil), ]) - } - private static func makeSuccessResult(strategyID: String) -> ProviderFetchResult { - let usage = UsageSnapshot( - primary: RateWindow(usedPercent: 0, windowMinutes: nil, resetsAt: nil, resetDescription: nil), - secondary: nil, - tertiary: nil, - providerCost: nil, - minimaxUsage: nil, - updatedAt: Date(), - identity: nil) - return ProviderFetchResult( - usage: usage, - credits: nil, - dashboard: nil, - sourceLabel: strategyID, - strategyID: strategyID, - strategyKind: .apiToken) + let diag = MiniMaxDiagnosticExportBuilder.build( + outcome: outcome, + settings: nil, + authMode: .apiToken) + + #expect(diag.provider == "minimax") + #expect(diag.source == "api") + #expect(diag.authConfigured == true) + #expect(diag.usage != nil) + #expect(diag.usage?.planName == "Max") + #expect(diag.error == nil) } } diff --git a/docs/minimax.md b/docs/minimax.md index 9079931b0c..c70f77a603 100644 --- a/docs/minimax.md +++ b/docs/minimax.md @@ -61,25 +61,31 @@ quota card and omits the chart instead of treating the whole provider as failed. - `Sources/CodexBarCore/Providers/MiniMax/MiniMaxProviderDescriptor.swift` - `Sources/CodexBar/Providers/MiniMax/MiniMaxProviderImplementation.swift` -## Safe diagnostic output - -MiniMax diagnostic export data is intentionally limited to structural metadata only — no raw tokens, cookies, -authorization headers, API responses, or personal data. - -The diagnostic export fields are: - -- `schemaVersion`, `provider`, `authMode`, `region` -- `sourceLabel` and `strategyID` -- `fieldsPresent` (allowlisted field names that were non-nil) -- `servicesCount` (integer count only) -- `billingSummaryPresent` (boolean only) -- `fetchAttemptsSummary` (strategy ID, availability, extracted error code, error category) -- `redactionPolicyVersion`, `exportedAt` - -The export intentionally excludes: raw API tokens (`sk-cp-*`, `sk-api-*`), cookies, authorization headers, -bearer tokens, raw API responses or HTML, email addresses, session IDs, account/organization IDs, and any -per-request billing record details. - -Error messages are pre-redacted via `LogRedactor` before code/category extraction. Only fixed error codes -(`401`, `403`, `timeout`, etc.) and category labels (`auth`, `network`, `parse`, `timeout`, `unknown`) appear -in the export. +## CLI diagnose command + +The `diagnose` command performs a real MiniMax diagnostic invocation and emits a safe, redacted JSON export +for issue reporting and verification. + +### Usage +``` +codexbar diagnose --provider minimax --format json --pretty +``` + +### Output +- Structural diagnostic JSON with provider, source, auth mode, usage snapshot, fetch attempts, and error categories. +- All sensitive fields (API tokens, cookies, emails, auth headers) are redacted via `LogRedactor`. +- Errors are mapped to safe categories (`network`, `auth`, `api`, `parse`) with user-friendly descriptions. +- No raw API responses, raw error messages, tokens, cookies, emails, account IDs, org IDs, or billing history. + +### What is excluded from output +- Raw API tokens (`sk-cp-*`, `sk-api-*`) and authorization headers +- Cookie header values +- Email addresses +- Account IDs, org IDs +- Raw error messages (replaced with safe category-based descriptions) +- Raw HTTP responses or request bodies +- Billing history details + +### Exit codes +- `0`: Diagnostic completed successfully (even if MiniMax auth is not configured) +- `1`: Unknown error or invalid arguments From 7819edf8431bda5bc972f9cb5ac1411f6b7ed0b7 Mon Sep 17 00:00:00 2001 From: Yuxin Qiao <104957188+Yuxin-Qiao@users.noreply.github.com> Date: Sun, 24 May 2026 17:34:20 +0800 Subject: [PATCH 03/10] Harden MiniMax diagnostic CLI export --- Sources/CodexBarCLI/CLIDiagnoseCommand.swift | 6 +- .../MiniMax/MiniMaxDiagnosticExport.swift | 25 +++++- .../MiniMaxDiagnosticExportTests.swift | 86 +++++++++++++++---- ...erLabelMetadataCharacterizationTests.swift | 67 +++++++++++++++ 4 files changed, 162 insertions(+), 22 deletions(-) create mode 100644 Tests/CodexBarTests/ProviderLabelMetadataCharacterizationTests.swift diff --git a/Sources/CodexBarCLI/CLIDiagnoseCommand.swift b/Sources/CodexBarCLI/CLIDiagnoseCommand.swift index 30e7a9f767..e722250f92 100644 --- a/Sources/CodexBarCLI/CLIDiagnoseCommand.swift +++ b/Sources/CodexBarCLI/CLIDiagnoseCommand.swift @@ -41,7 +41,11 @@ extension CodexBarCLI { Self.exit(code: .failure, message: "Error: \(error.localizedDescription)", output: output, kind: .config) } - let env = ProcessInfo.processInfo.environment + let env = tokenContext.environment( + base: ProcessInfo.processInfo.environment, + provider: .minimax, + account: nil, + codexActiveSourceOverride: nil) let settings = tokenContext.settingsSnapshot( for: .minimax, account: nil, diff --git a/Sources/CodexBarCore/Providers/MiniMax/MiniMaxDiagnosticExport.swift b/Sources/CodexBarCore/Providers/MiniMax/MiniMaxDiagnosticExport.swift index dbc446457c..5c3d5e8716 100644 --- a/Sources/CodexBarCore/Providers/MiniMax/MiniMaxDiagnosticExport.swift +++ b/Sources/CodexBarCore/Providers/MiniMax/MiniMaxDiagnosticExport.swift @@ -96,25 +96,25 @@ public struct MiniMaxDiagnosticFetchAttempt: Codable, Sendable { public let strategyID: String public let kind: String public let wasAvailable: Bool - public let errorMessage: String? + public let errorCategory: String? public init( strategyID: String, kind: String, wasAvailable: Bool, - errorMessage: String?) + errorCategory: String?) { self.strategyID = strategyID self.kind = kind self.wasAvailable = wasAvailable - self.errorMessage = errorMessage + self.errorCategory = errorCategory } public init(from attempt: ProviderFetchAttempt) { self.strategyID = attempt.strategyID self.kind = Self.kindLabel(attempt.kind) self.wasAvailable = attempt.wasAvailable - self.errorMessage = attempt.errorDescription + self.errorCategory = attempt.errorDescription.map { Self.errorCategoryLabel($0) } } private static func kindLabel(_ kind: ProviderFetchKind) -> String { @@ -127,6 +127,23 @@ public struct MiniMaxDiagnosticFetchAttempt: Codable, Sendable { case .webDashboard: "web" } } + + private static func errorCategoryLabel(_ description: String?) -> String { + guard let desc = description?.lowercased() else { return "unknown" } + if desc.contains("network") || desc.contains("timeout") || desc.contains("connection") { + return "network" + } + if desc.contains("auth") || desc.contains("credential") || desc.contains("token") || desc.contains("cookie") { + return "auth" + } + if desc.contains("api") || desc.contains("http") || desc.contains("404") || desc.contains("403") { + return "api" + } + if desc.contains("parse") || desc.contains("format") || desc.contains("decode") { + return "parse" + } + return "unknown" + } } public struct MiniMaxDiagnosticError: Codable, Sendable { diff --git a/Tests/CodexBarTests/MiniMaxDiagnosticExportTests.swift b/Tests/CodexBarTests/MiniMaxDiagnosticExportTests.swift index dd1b075870..1b585bbc10 100644 --- a/Tests/CodexBarTests/MiniMaxDiagnosticExportTests.swift +++ b/Tests/CodexBarTests/MiniMaxDiagnosticExportTests.swift @@ -26,7 +26,7 @@ struct MiniMaxDiagnosticExportTests { strategyID: "minimax.api", kind: "api", wasAvailable: true, - errorMessage: nil), + errorCategory: nil), ], error: nil, settingsSummary: MiniMaxSettingsSummary( @@ -46,6 +46,50 @@ struct MiniMaxDiagnosticExportTests { #expect(!json.contains("sk-cp-")) #expect(!json.contains("sk-api-")) #expect(!json.contains("Bearer")) + #expect(!json.contains("errorMessage")) + #expect(!json.contains("localizedDescription")) + } + + @Test + func `raw error text never appears in encoded JSON`() throws { + let now = Date(timeIntervalSince1970: 1_700_000_000) + let export = MiniMaxDiagnosticExport( + timestamp: now, + provider: "minimax", + source: "failed", + authMode: "apiToken", + authConfigured: true, + usage: nil, + fetchAttempts: [ + MiniMaxDiagnosticFetchAttempt( + strategyID: "minimax.api", + kind: "api", + wasAvailable: true, + errorCategory: "network"), + ], + error: MiniMaxDiagnosticError( + category: "network", + safeDescription: "Network error - check your connection"), + settingsSummary: MiniMaxSettingsSummary( + cookieSource: "auto", + apiRegion: "global", + authMode: "apiToken")) + + let encoder = JSONEncoder() + encoder.dateEncodingStrategy = .iso8601 + encoder.outputFormatting = .prettyPrinted + let data = try encoder.encode(export) + let json = String(data: data, encoding: .utf8) ?? "" + + #expect(!json.contains("connection refused")) + #expect(!json.contains("network probe")) + #expect(!json.contains("2024-01-01")) + #expect(!json.contains("not safe to expose")) + #expect(!json.contains("localizedDescription")) + #expect(!json.contains("raw")) + #expect(!json.contains("errorMessage")) + #expect(json.contains("errorCategory")) + #expect(json.contains("\"network\"")) } @Test @@ -78,25 +122,32 @@ struct MiniMaxDiagnosticExportTests { } @Test - func `diagnostic fetch attempt serializes kind correctly`() { - let webAttempt = ProviderFetchAttempt( - strategyID: "minimax.web", - kind: .web, - wasAvailable: true, - errorDescription: nil) - let diagAttempt = MiniMaxDiagnosticFetchAttempt(from: webAttempt) - #expect(diagAttempt.kind == "web") - #expect(diagAttempt.strategyID == "minimax.web") - - let apiAttempt = ProviderFetchAttempt( + func `fetch attempt error maps to safe category, never raw text`() { + let attemptWithRawError = ProviderFetchAttempt( strategyID: "minimax.api", kind: .apiToken, + wasAvailable: true, + errorDescription: "MiniMax API timeout after 30 seconds - connection refused for host platform.minimax.io") + let diagAttempt = MiniMaxDiagnosticFetchAttempt(from: attemptWithRawError) + #expect(diagAttempt.kind == "api") + #expect(diagAttempt.strategyID == "minimax.api") + #expect(diagAttempt.wasAvailable == true) + let errorCategoryOne = diagAttempt.errorCategory + #expect(errorCategoryOne == "network") + #expect(!errorCategoryOne!.contains("timeout")) + #expect(!errorCategoryOne!.contains("connection refused")) + #expect(!errorCategoryOne!.contains("platform.minimax.io")) + + let attemptWithAuthError = ProviderFetchAttempt( + strategyID: "minimax.web", + kind: .web, wasAvailable: false, - errorDescription: "token missing") - let diagApiAttempt = MiniMaxDiagnosticFetchAttempt(from: apiAttempt) - #expect(diagApiAttempt.kind == "api") - #expect(diagApiAttempt.wasAvailable == false) - #expect(diagApiAttempt.errorMessage == "token missing") + errorDescription: "invalid auth token cookie HERTZ-SESSION=abc123") + let diagAuthAttempt = MiniMaxDiagnosticFetchAttempt(from: attemptWithAuthError) + #expect(diagAuthAttempt.wasAvailable == false) + let errorCategoryTwo = diagAuthAttempt.errorCategory + #expect(errorCategoryTwo == "auth") + #expect(!errorCategoryTwo!.contains("HERTZ-SESSION")) } @Test @@ -166,6 +217,7 @@ struct MiniMaxDiagnosticExportTests { #expect(diag.error != nil) #expect(diag.error?.category == "network") #expect(diag.fetchAttempts.count == 1) + #expect(diag.fetchAttempts[0].errorCategory == "network") } @Test diff --git a/Tests/CodexBarTests/ProviderLabelMetadataCharacterizationTests.swift b/Tests/CodexBarTests/ProviderLabelMetadataCharacterizationTests.swift new file mode 100644 index 0000000000..76ea76eb88 --- /dev/null +++ b/Tests/CodexBarTests/ProviderLabelMetadataCharacterizationTests.swift @@ -0,0 +1,67 @@ +import CodexBarCore +import Testing + +struct ProviderLabelMetadataCharacterizationTests { + // MARK: - Label non-empty constraints + + @Test + func `displayName is non-empty for all providers`() { + for descriptor in ProviderDescriptorRegistry.all { + #expect( + !descriptor.metadata.displayName.isEmpty, + "Provider \(descriptor.id.rawValue) has empty displayName.") + } + } + + @Test + func `sessionLabel is non-empty for all providers`() { + for descriptor in ProviderDescriptorRegistry.all { + #expect( + !descriptor.metadata.sessionLabel.isEmpty, + "Provider \(descriptor.id.rawValue) has empty sessionLabel.") + } + } + + // MARK: - Known empty weeklyLabel exceptions + + @Test + func `weeklyLabel empty providers are explicitly characterized`() { + // Allowlist of providers known to have empty weeklyLabel on current main. + // If a new provider is added with empty weeklyLabel, this test fails and + // requires a deliberate decision to add it here — preventing silent regressions. + let knownEmptyWeeklyLabelProviders: Set = [.mistral] + for descriptor in ProviderDescriptorRegistry.all { + if descriptor.metadata.weeklyLabel.isEmpty { + #expect( + knownEmptyWeeklyLabelProviders.contains(descriptor.id), + "Provider \(descriptor.id.rawValue) has empty weeklyLabel and is not in the known exception list.") + } + } + } + + // MARK: - Invariant: supportsOpus implies opusLabel + + @Test + func `supportsOpus providers declare non-empty opusLabel`() { + for descriptor in ProviderDescriptorRegistry.all { + if descriptor.metadata.supportsOpus { + #expect( + descriptor.metadata.opusLabel != nil && !descriptor.metadata.opusLabel!.isEmpty, + "Provider \(descriptor.id.rawValue) has supportsOpus=true but opusLabel is nil or empty.") + } + } + } + + // MARK: - opusLabel structural constraint + + @Test + func `opusLabel is nil or non-empty`() { + for descriptor in ProviderDescriptorRegistry.all { + if let opusLabel = descriptor.metadata.opusLabel { + #expect( + !opusLabel.isEmpty, + "Provider \(descriptor.id.rawValue) has empty opusLabel string instead of nil.") + } + } + } +} From b1bcce5bb37ef7cca0b8a55ed8ccf779ffb680d9 Mon Sep 17 00:00:00 2001 From: Yuxin Qiao <104957188+Yuxin-Qiao@users.noreply.github.com> Date: Sun, 24 May 2026 17:58:21 +0800 Subject: [PATCH 04/10] Remove unrelated provider label test from MiniMax diagnostics PR --- .../MiniMaxDiagnosticExportTests.swift | 10 +-- ...erLabelMetadataCharacterizationTests.swift | 67 ------------------- 2 files changed, 5 insertions(+), 72 deletions(-) delete mode 100644 Tests/CodexBarTests/ProviderLabelMetadataCharacterizationTests.swift diff --git a/Tests/CodexBarTests/MiniMaxDiagnosticExportTests.swift b/Tests/CodexBarTests/MiniMaxDiagnosticExportTests.swift index 1b585bbc10..0312a74c3a 100644 --- a/Tests/CodexBarTests/MiniMaxDiagnosticExportTests.swift +++ b/Tests/CodexBarTests/MiniMaxDiagnosticExportTests.swift @@ -122,7 +122,7 @@ struct MiniMaxDiagnosticExportTests { } @Test - func `fetch attempt error maps to safe category, never raw text`() { + func `fetch attempt error maps to safe category, never raw text`() throws { let attemptWithRawError = ProviderFetchAttempt( strategyID: "minimax.api", kind: .apiToken, @@ -134,9 +134,9 @@ struct MiniMaxDiagnosticExportTests { #expect(diagAttempt.wasAvailable == true) let errorCategoryOne = diagAttempt.errorCategory #expect(errorCategoryOne == "network") - #expect(!errorCategoryOne!.contains("timeout")) - #expect(!errorCategoryOne!.contains("connection refused")) - #expect(!errorCategoryOne!.contains("platform.minimax.io")) + #expect(try !(#require(errorCategoryOne?.contains("timeout")))) + #expect(try !(#require(errorCategoryOne?.contains("connection refused")))) + #expect(try !(#require(errorCategoryOne?.contains("platform.minimax.io")))) let attemptWithAuthError = ProviderFetchAttempt( strategyID: "minimax.web", @@ -147,7 +147,7 @@ struct MiniMaxDiagnosticExportTests { #expect(diagAuthAttempt.wasAvailable == false) let errorCategoryTwo = diagAuthAttempt.errorCategory #expect(errorCategoryTwo == "auth") - #expect(!errorCategoryTwo!.contains("HERTZ-SESSION")) + #expect(try !(#require(errorCategoryTwo?.contains("HERTZ-SESSION")))) } @Test diff --git a/Tests/CodexBarTests/ProviderLabelMetadataCharacterizationTests.swift b/Tests/CodexBarTests/ProviderLabelMetadataCharacterizationTests.swift deleted file mode 100644 index 76ea76eb88..0000000000 --- a/Tests/CodexBarTests/ProviderLabelMetadataCharacterizationTests.swift +++ /dev/null @@ -1,67 +0,0 @@ -import CodexBarCore -import Testing - -struct ProviderLabelMetadataCharacterizationTests { - // MARK: - Label non-empty constraints - - @Test - func `displayName is non-empty for all providers`() { - for descriptor in ProviderDescriptorRegistry.all { - #expect( - !descriptor.metadata.displayName.isEmpty, - "Provider \(descriptor.id.rawValue) has empty displayName.") - } - } - - @Test - func `sessionLabel is non-empty for all providers`() { - for descriptor in ProviderDescriptorRegistry.all { - #expect( - !descriptor.metadata.sessionLabel.isEmpty, - "Provider \(descriptor.id.rawValue) has empty sessionLabel.") - } - } - - // MARK: - Known empty weeklyLabel exceptions - - @Test - func `weeklyLabel empty providers are explicitly characterized`() { - // Allowlist of providers known to have empty weeklyLabel on current main. - // If a new provider is added with empty weeklyLabel, this test fails and - // requires a deliberate decision to add it here — preventing silent regressions. - let knownEmptyWeeklyLabelProviders: Set = [.mistral] - for descriptor in ProviderDescriptorRegistry.all { - if descriptor.metadata.weeklyLabel.isEmpty { - #expect( - knownEmptyWeeklyLabelProviders.contains(descriptor.id), - "Provider \(descriptor.id.rawValue) has empty weeklyLabel and is not in the known exception list.") - } - } - } - - // MARK: - Invariant: supportsOpus implies opusLabel - - @Test - func `supportsOpus providers declare non-empty opusLabel`() { - for descriptor in ProviderDescriptorRegistry.all { - if descriptor.metadata.supportsOpus { - #expect( - descriptor.metadata.opusLabel != nil && !descriptor.metadata.opusLabel!.isEmpty, - "Provider \(descriptor.id.rawValue) has supportsOpus=true but opusLabel is nil or empty.") - } - } - } - - // MARK: - opusLabel structural constraint - - @Test - func `opusLabel is nil or non-empty`() { - for descriptor in ProviderDescriptorRegistry.all { - if let opusLabel = descriptor.metadata.opusLabel { - #expect( - !opusLabel.isEmpty, - "Provider \(descriptor.id.rawValue) has empty opusLabel string instead of nil.") - } - } - } -} From 71c7b033c737514fbb0cd1031d4986a5cc1b3396 Mon Sep 17 00:00:00 2001 From: Yuxin Qiao <104957188+Yuxin-Qiao@users.noreply.github.com> Date: Sun, 24 May 2026 18:28:35 +0800 Subject: [PATCH 05/10] Fix MiniMax diagnose token account routing --- Sources/CodexBarCLI/CLIDiagnoseCommand.swift | 8 ++++++-- .../CodexBarTests/MiniMaxDiagnosticExportTests.swift | 12 +++++++----- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/Sources/CodexBarCLI/CLIDiagnoseCommand.swift b/Sources/CodexBarCLI/CLIDiagnoseCommand.swift index e722250f92..af837a78a7 100644 --- a/Sources/CodexBarCLI/CLIDiagnoseCommand.swift +++ b/Sources/CodexBarCLI/CLIDiagnoseCommand.swift @@ -41,14 +41,18 @@ extension CodexBarCLI { Self.exit(code: .failure, message: "Error: \(error.localizedDescription)", output: output, kind: .config) } + let activeMiniMaxAccount: ProviderTokenAccount? = { + let accounts = (try? tokenContext.resolvedAccounts(for: .minimax)) ?? [] + return accounts.first + }() let env = tokenContext.environment( base: ProcessInfo.processInfo.environment, provider: .minimax, - account: nil, + account: activeMiniMaxAccount, codexActiveSourceOverride: nil) let settings = tokenContext.settingsSnapshot( for: .minimax, - account: nil, + account: activeMiniMaxAccount, codexActiveSourceOverride: nil) let sourceMode = tokenContext.preferredSourceMode(for: .minimax) diff --git a/Tests/CodexBarTests/MiniMaxDiagnosticExportTests.swift b/Tests/CodexBarTests/MiniMaxDiagnosticExportTests.swift index 0312a74c3a..884dd5b0b3 100644 --- a/Tests/CodexBarTests/MiniMaxDiagnosticExportTests.swift +++ b/Tests/CodexBarTests/MiniMaxDiagnosticExportTests.swift @@ -122,7 +122,7 @@ struct MiniMaxDiagnosticExportTests { } @Test - func `fetch attempt error maps to safe category, never raw text`() throws { + func `fetch attempt error maps to safe category, never raw text`() { let attemptWithRawError = ProviderFetchAttempt( strategyID: "minimax.api", kind: .apiToken, @@ -134,9 +134,10 @@ struct MiniMaxDiagnosticExportTests { #expect(diagAttempt.wasAvailable == true) let errorCategoryOne = diagAttempt.errorCategory #expect(errorCategoryOne == "network") - #expect(try !(#require(errorCategoryOne?.contains("timeout")))) - #expect(try !(#require(errorCategoryOne?.contains("connection refused")))) - #expect(try !(#require(errorCategoryOne?.contains("platform.minimax.io")))) + let cat1 = errorCategoryOne ?? "" + #expect(!cat1.contains("timeout")) + #expect(!cat1.contains("connection refused")) + #expect(!cat1.contains("platform.minimax.io")) let attemptWithAuthError = ProviderFetchAttempt( strategyID: "minimax.web", @@ -147,7 +148,8 @@ struct MiniMaxDiagnosticExportTests { #expect(diagAuthAttempt.wasAvailable == false) let errorCategoryTwo = diagAuthAttempt.errorCategory #expect(errorCategoryTwo == "auth") - #expect(try !(#require(errorCategoryTwo?.contains("HERTZ-SESSION")))) + let cat2 = errorCategoryTwo ?? "" + #expect(!cat2.contains("HERTZ-SESSION")) } @Test From 809de7c14172a5e72c008e66d7b7f81d65465027 Mon Sep 17 00:00:00 2001 From: Yuxin Qiao <104957188+Yuxin-Qiao@users.noreply.github.com> Date: Sun, 24 May 2026 20:02:39 +0800 Subject: [PATCH 06/10] Fix MiniMax diagnose auth summary for token-account cookies --- Sources/CodexBarCLI/CLIDiagnoseCommand.swift | 28 ++++++++-- .../CLIDiagnoseCommandTests.swift | 52 +++++++++++++++++++ 2 files changed, 77 insertions(+), 3 deletions(-) create mode 100644 Tests/CodexBarTests/CLIDiagnoseCommandTests.swift diff --git a/Sources/CodexBarCLI/CLIDiagnoseCommand.swift b/Sources/CodexBarCLI/CLIDiagnoseCommand.swift index af837a78a7..8ddeec2d6f 100644 --- a/Sources/CodexBarCLI/CLIDiagnoseCommand.swift +++ b/Sources/CodexBarCLI/CLIDiagnoseCommand.swift @@ -56,9 +56,7 @@ extension CodexBarCLI { codexActiveSourceOverride: nil) let sourceMode = tokenContext.preferredSourceMode(for: .minimax) - let apiToken = ProviderTokenResolver.minimaxToken(environment: env) - let cookieHeader = ProviderTokenResolver.minimaxCookie(environment: env) - let authMode = MiniMaxAuthMode.resolve(apiToken: apiToken, cookieHeader: cookieHeader) + let authMode = Self.resolveMiniMaxAuthMode(environment: env, settings: settings) let fetchContext = ProviderFetchContext( runtime: .cli, @@ -105,3 +103,27 @@ extension CodexBarCLI { Self.exit(code: .success, output: output, kind: .runtime) } } + +extension CodexBarCLI { + static func resolveMiniMaxAuthMode( + environment: [String: String], + settings: ProviderSettingsSnapshot?) -> MiniMaxAuthMode + { + let apiToken = ProviderTokenResolver.minimaxToken(environment: environment) + let envCookieHeader = ProviderTokenResolver.minimaxCookie(environment: environment) + let settingsCookieHeader = CookieHeaderNormalizer.normalize(settings?.minimax?.manualCookieHeader) + let cookieHeader = envCookieHeader ?? settingsCookieHeader + return MiniMaxAuthMode.resolve(apiToken: apiToken, cookieHeader: cookieHeader) + } +} + +#if DEBUG +extension CodexBarCLI { + static func _resolveMiniMaxAuthModeForTesting( + environment: [String: String], + settings: ProviderSettingsSnapshot?) -> MiniMaxAuthMode + { + self.resolveMiniMaxAuthMode(environment: environment, settings: settings) + } +} +#endif diff --git a/Tests/CodexBarTests/CLIDiagnoseCommandTests.swift b/Tests/CodexBarTests/CLIDiagnoseCommandTests.swift new file mode 100644 index 0000000000..a4f45cb055 --- /dev/null +++ b/Tests/CodexBarTests/CLIDiagnoseCommandTests.swift @@ -0,0 +1,52 @@ +import CodexBarCore +import Testing +@testable import CodexBarCLI + +struct CLIDiagnoseCommandTests { + private func makeSettingsWithMiniMaxCookie(_ manualCookieHeader: String) -> ProviderSettingsSnapshot { + ProviderSettingsSnapshot( + debugMenuEnabled: false, + debugKeepCLISessionsAlive: false, + codex: nil, + claude: nil, + cursor: nil, + opencode: nil, + opencodego: nil, + alibaba: nil, + factory: nil, + minimax: ProviderSettingsSnapshot.MiniMaxProviderSettings( + cookieSource: .manual, + manualCookieHeader: manualCookieHeader, + apiRegion: .global), + manus: nil, + zai: nil, + copilot: nil, + kilo: nil, + kimi: nil, + augment: nil, + amp: nil, + ollama: nil) + } + + @Test + func `diagnose auth mode uses settings-backed MiniMax manual cookie when env token is absent`() { + let settings = self.makeSettingsWithMiniMaxCookie("Cookie: session_id=demo-cookie") + + let authMode = CodexBarCLI._resolveMiniMaxAuthModeForTesting( + environment: [:], + settings: settings) + + #expect(authMode == .cookie) + } + + @Test + func `diagnose auth mode keeps apiToken precedence over settings cookie`() { + let settings = self.makeSettingsWithMiniMaxCookie("Cookie: session_id=demo-cookie") + + let authMode = CodexBarCLI._resolveMiniMaxAuthModeForTesting( + environment: [MiniMaxAPISettingsReader.apiTokenKey: "sk-api-demo-token"], + settings: settings) + + #expect(authMode == .apiToken) + } +} From 2b85df79b6551eef44a2ba3b44e95d7855b4c1bb Mon Sep 17 00:00:00 2001 From: Yuxin Qiao <104957188+Yuxin-Qiao@users.noreply.github.com> Date: Sun, 24 May 2026 20:52:42 +0800 Subject: [PATCH 07/10] Narrow MiniMax diagnostic export privacy surface --- .../Providers/MiniMax/MiniMaxDiagnosticExport.swift | 8 +------- .../MiniMax/MiniMaxDiagnosticExportBuilder.swift | 1 - Tests/CodexBarTests/MiniMaxDiagnosticExportTests.swift | 5 ----- 3 files changed, 1 insertion(+), 13 deletions(-) diff --git a/Sources/CodexBarCore/Providers/MiniMax/MiniMaxDiagnosticExport.swift b/Sources/CodexBarCore/Providers/MiniMax/MiniMaxDiagnosticExport.swift index 5c3d5e8716..e2b6253be5 100644 --- a/Sources/CodexBarCore/Providers/MiniMax/MiniMaxDiagnosticExport.swift +++ b/Sources/CodexBarCore/Providers/MiniMax/MiniMaxDiagnosticExport.swift @@ -93,25 +93,21 @@ public struct MiniMaxDiagnosticServiceUsage: Codable, Sendable { } public struct MiniMaxDiagnosticFetchAttempt: Codable, Sendable { - public let strategyID: String public let kind: String public let wasAvailable: Bool public let errorCategory: String? public init( - strategyID: String, kind: String, wasAvailable: Bool, errorCategory: String?) { - self.strategyID = strategyID self.kind = kind self.wasAvailable = wasAvailable self.errorCategory = errorCategory } public init(from attempt: ProviderFetchAttempt) { - self.strategyID = attempt.strategyID self.kind = Self.kindLabel(attempt.kind) self.wasAvailable = attempt.wasAvailable self.errorCategory = attempt.errorDescription.map { Self.errorCategoryLabel($0) } @@ -205,12 +201,10 @@ public struct MiniMaxDiagnosticError: Codable, Sendable { } public struct MiniMaxSettingsSummary: Codable, Sendable { - public let cookieSource: String public let apiRegion: String public let authMode: String - public init(cookieSource: String, apiRegion: String, authMode: String) { - self.cookieSource = cookieSource + public init(apiRegion: String, authMode: String) { self.apiRegion = apiRegion self.authMode = authMode } diff --git a/Sources/CodexBarCore/Providers/MiniMax/MiniMaxDiagnosticExportBuilder.swift b/Sources/CodexBarCore/Providers/MiniMax/MiniMaxDiagnosticExportBuilder.swift index 5457e52bb5..b6d9891d6f 100644 --- a/Sources/CodexBarCore/Providers/MiniMax/MiniMaxDiagnosticExportBuilder.swift +++ b/Sources/CodexBarCore/Providers/MiniMax/MiniMaxDiagnosticExportBuilder.swift @@ -12,7 +12,6 @@ public enum MiniMaxDiagnosticExportBuilder { let error = outcome.failureError.map { MiniMaxDiagnosticError(from: $0) } let settingsSummary = MiniMaxSettingsSummary( - cookieSource: settings?.minimax?.cookieSource.rawValue ?? "auto", apiRegion: settings?.minimax?.apiRegion.rawValue ?? "global", authMode: authMode.description) diff --git a/Tests/CodexBarTests/MiniMaxDiagnosticExportTests.swift b/Tests/CodexBarTests/MiniMaxDiagnosticExportTests.swift index 884dd5b0b3..1a13d55cf5 100644 --- a/Tests/CodexBarTests/MiniMaxDiagnosticExportTests.swift +++ b/Tests/CodexBarTests/MiniMaxDiagnosticExportTests.swift @@ -23,14 +23,12 @@ struct MiniMaxDiagnosticExportTests { services: nil), fetchAttempts: [ MiniMaxDiagnosticFetchAttempt( - strategyID: "minimax.api", kind: "api", wasAvailable: true, errorCategory: nil), ], error: nil, settingsSummary: MiniMaxSettingsSummary( - cookieSource: "auto", apiRegion: "global", authMode: "apiToken")) @@ -62,7 +60,6 @@ struct MiniMaxDiagnosticExportTests { usage: nil, fetchAttempts: [ MiniMaxDiagnosticFetchAttempt( - strategyID: "minimax.api", kind: "api", wasAvailable: true, errorCategory: "network"), @@ -71,7 +68,6 @@ struct MiniMaxDiagnosticExportTests { category: "network", safeDescription: "Network error - check your connection"), settingsSummary: MiniMaxSettingsSummary( - cookieSource: "auto", apiRegion: "global", authMode: "apiToken")) @@ -130,7 +126,6 @@ struct MiniMaxDiagnosticExportTests { errorDescription: "MiniMax API timeout after 30 seconds - connection refused for host platform.minimax.io") let diagAttempt = MiniMaxDiagnosticFetchAttempt(from: attemptWithRawError) #expect(diagAttempt.kind == "api") - #expect(diagAttempt.strategyID == "minimax.api") #expect(diagAttempt.wasAvailable == true) let errorCategoryOne = diagAttempt.errorCategory #expect(errorCategoryOne == "network") From a1fd47aaf4cfa92ba4f875c893f7c4f35153fee7 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 27 May 2026 03:16:54 +0100 Subject: [PATCH 08/10] fix: document MiniMax diagnose help --- Sources/CodexBarCLI/CLIHelp.swift | 22 +++++++++++++++++++ Sources/CodexBarCLI/CLIIO.swift | 2 ++ .../CLIDiagnoseCommandTests.swift | 9 ++++++++ 3 files changed, 33 insertions(+) diff --git a/Sources/CodexBarCLI/CLIHelp.swift b/Sources/CodexBarCLI/CLIHelp.swift index 0832594874..b46362d256 100644 --- a/Sources/CodexBarCLI/CLIHelp.swift +++ b/Sources/CodexBarCLI/CLIHelp.swift @@ -168,6 +168,26 @@ extension CodexBarCLI { """ } + static func diagnoseHelp(version: String) -> String { + """ + CodexBar \(version) + + Usage: + codexbar diagnose --provider minimax --format json + [--json-output] [--log-level ] + [-v|--verbose] + [--pretty] + + Description: + Run a MiniMax diagnostic fetch and print a safe JSON export for issue reporting. + The export is redacted and omits raw API tokens, cookies, auth headers, emails, + account IDs, org IDs, raw responses, and billing-history records. + + Examples: + codexbar diagnose --provider minimax --format json --pretty + """ + } + static func rootHelp(version: String) -> String { """ CodexBar \(version) @@ -198,6 +218,7 @@ extension CodexBarCLI { codexbar config disable --provider codexbar config set-api-key --provider (--api-key |--stdin) codexbar cache clear <--cookies|--cost|--all> [--provider ] + codexbar diagnose --provider minimax --format json [--pretty] Global flags: -h, --help Show help @@ -218,6 +239,7 @@ extension CodexBarCLI { codexbar config enable --provider grok codexbar config set-api-key --provider elevenlabs --stdin codexbar cache clear --cookies + codexbar diagnose --provider minimax --format json --pretty """ } } diff --git a/Sources/CodexBarCLI/CLIIO.swift b/Sources/CodexBarCLI/CLIIO.swift index 49242dabf1..1e371fd1b2 100644 --- a/Sources/CodexBarCLI/CLIIO.swift +++ b/Sources/CodexBarCLI/CLIIO.swift @@ -33,6 +33,8 @@ extension CodexBarCLI { print(Self.configHelp(version: version)) case "cache", "clear": print(Self.cacheHelp(version: version)) + case "diagnose": + print(Self.diagnoseHelp(version: version)) default: print(Self.rootHelp(version: version)) } diff --git a/Tests/CodexBarTests/CLIDiagnoseCommandTests.swift b/Tests/CodexBarTests/CLIDiagnoseCommandTests.swift index a4f45cb055..ecea06c63a 100644 --- a/Tests/CodexBarTests/CLIDiagnoseCommandTests.swift +++ b/Tests/CodexBarTests/CLIDiagnoseCommandTests.swift @@ -3,6 +3,15 @@ import Testing @testable import CodexBarCLI struct CLIDiagnoseCommandTests { + @Test + func `diagnose help describes minimax JSON export`() { + let help = CodexBarCLI.diagnoseHelp(version: "0.0.0") + + #expect(help.contains("codexbar diagnose --provider minimax --format json")) + #expect(help.contains("safe JSON export")) + #expect(help.contains("raw API tokens")) + } + private func makeSettingsWithMiniMaxCookie(_ manualCookieHeader: String) -> ProviderSettingsSnapshot { ProviderSettingsSnapshot( debugMenuEnabled: false, From 0ce922741fb24720260e16f96185df7b7b67bfb5 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 27 May 2026 03:17:20 +0100 Subject: [PATCH 09/10] docs: credit MiniMax diagnostic export --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a1a84d109..598215f85e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## 0.29.2 — Unreleased ### Added +- MiniMax: add a redacted diagnostic CLI export for safe issue reports (#1128). Thanks @Yuxin-Qiao! - Antigravity: show the complete per-model quota breakdown alongside the existing summary lanes (#1139). Thanks @guhyun9454! - Widget: show tertiary usage rows for providers that expose a third quota lane (#1160). Thanks @LeoLin990405! - DeepSeek: show optional web-session usage and cost summaries alongside the balance card (#1166). Thanks @Yuxin-Qiao! From 64901f939686a5c4afc355fb77c8f6269575bc86 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 27 May 2026 03:31:39 +0100 Subject: [PATCH 10/10] fix: keep MiniMax diagnose output quiet --- Sources/CodexBarCLI/CLIEntry.swift | 11 +++++++--- Sources/CodexBarCLI/CLIHelpers.swift | 4 ++++ Sources/CodexBarCLI/DiagnoseOptions.swift | 3 +++ .../CodexBarCore/Logging/CodexBarLog.swift | 15 ++++++++++++++ .../CLIArgumentParsingTests.swift | 20 +++++++++++++++++++ 5 files changed, 50 insertions(+), 3 deletions(-) diff --git a/Sources/CodexBarCLI/CLIEntry.swift b/Sources/CodexBarCLI/CLIEntry.swift index c81af77801..35eb55b2be 100644 --- a/Sources/CodexBarCLI/CLIEntry.swift +++ b/Sources/CodexBarCLI/CLIEntry.swift @@ -30,7 +30,7 @@ enum CodexBarCLI { do { let invocation = try program.resolve(argv: argv) - Self.bootstrapLogging(values: invocation.parsedValues) + Self.bootstrapLogging(path: invocation.path, values: invocation.parsedValues) switch invocation.path { case ["usage"]: await self.runUsage(invocation.parsedValues) @@ -155,12 +155,17 @@ enum CodexBarCLI { // MARK: - Helpers - private static func bootstrapLogging(values: ParsedValues) { + private static func bootstrapLogging(path: [String], values: ParsedValues) { + CodexBarLog.bootstrapIfNeeded(self.loggingConfiguration(path: path, values: values)) + } + + static func loggingConfiguration(path: [String], values: ParsedValues) -> CodexBarLog.Configuration { let isJSON = values.flags.contains("jsonOutput") || values.flags.contains("jsonOnly") let verbose = values.flags.contains("verbose") let rawLevel = values.options["logLevel"]?.last let level = Self.resolvedLogLevel(verbose: verbose, rawLevel: rawLevel) - CodexBarLog.bootstrapIfNeeded(.init(destination: .stderr, level: level, json: isJSON)) + let destination: CodexBarLog.Destination = path == ["diagnose"] ? .discard : .stderr + return .init(destination: destination, level: level, json: isJSON) } static func resolvedLogLevel(verbose: Bool, rawLevel: String?) -> CodexBarLog.Level { diff --git a/Sources/CodexBarCLI/CLIHelpers.swift b/Sources/CodexBarCLI/CLIHelpers.swift index c4444d8c9e..3ec5f3e9e5 100644 --- a/Sources/CodexBarCLI/CLIHelpers.swift +++ b/Sources/CodexBarCLI/CLIHelpers.swift @@ -366,6 +366,10 @@ extension CodexBarCLI { CommandSignature.describe(CacheOptions()) } + static func _diagnoseSignatureForTesting() -> CommandSignature { + CommandSignature.describe(DiagnoseOptions()) + } + static func _configSetAPIKeySignatureForTesting() -> CommandSignature { CommandSignature.describe(ConfigSetAPIKeyOptions()) } diff --git a/Sources/CodexBarCLI/DiagnoseOptions.swift b/Sources/CodexBarCLI/DiagnoseOptions.swift index 9f2038602c..b8dfd08edd 100644 --- a/Sources/CodexBarCLI/DiagnoseOptions.swift +++ b/Sources/CodexBarCLI/DiagnoseOptions.swift @@ -6,6 +6,9 @@ struct DiagnoseOptions: CommanderParsable { @Flag(names: [.short("v"), .long("verbose")], help: "Enable verbose logging") var verbose: Bool = false + @Flag(name: .long("json-output"), help: "Emit machine-readable logs") + var jsonOutput: Bool = false + @Option(name: .long("log-level"), help: "Set log level (trace|verbose|debug|info|warning|error|critical)") var logLevel: String? diff --git a/Sources/CodexBarCore/Logging/CodexBarLog.swift b/Sources/CodexBarCore/Logging/CodexBarLog.swift index 2f9db069c4..7a64eb328a 100644 --- a/Sources/CodexBarCore/Logging/CodexBarLog.swift +++ b/Sources/CodexBarCore/Logging/CodexBarLog.swift @@ -4,6 +4,7 @@ import Logging public enum CodexBarLog { public enum Destination: Sendable { case stderr + case discard case oslog(subsystem: String) } @@ -85,6 +86,8 @@ public enum CodexBarLog { case .stderr: if config.json { return JSONStderrLogHandler(label: label) } return StreamLogHandler.standardError(label: label) + case .discard: + return DiscardLogHandler() case let .oslog(subsystem): #if canImport(os) return OSLogLogHandler(label: label, subsystem: subsystem) @@ -154,6 +157,18 @@ public enum CodexBarLog { } } +private struct DiscardLogHandler: LogHandler { + var metadata: Logger.Metadata = [:] + var logLevel: Logger.Level = .critical + + subscript(metadataKey metadataKey: String) -> Logger.Metadata.Value? { + get { self.metadata[metadataKey] } + set { self.metadata[metadataKey] = newValue } + } + + func log(event _: LogEvent) {} +} + public struct CodexBarLogger: Sendable { private let logFn: @Sendable (CodexBarLog.Level, String, [String: String]?) -> Void diff --git a/Tests/CodexBarTests/CLIArgumentParsingTests.swift b/Tests/CodexBarTests/CLIArgumentParsingTests.swift index 13bc0453f2..86377e8a0a 100644 --- a/Tests/CodexBarTests/CLIArgumentParsingTests.swift +++ b/Tests/CodexBarTests/CLIArgumentParsingTests.swift @@ -64,4 +64,24 @@ struct CLIArgumentParsingTests { #expect(!parsed.flags.contains("jsonOutput")) #expect(CodexBarCLI._decodeFormatForTesting(from: parsed) == .json) } + + @Test + func `diagnose accepts json output flag but discards provider logs`() throws { + let signature = CodexBarCLI._diagnoseSignatureForTesting() + let parser = CommandParser(signature: signature) + let parsed = try parser.parse(arguments: [ + "--provider", "minimax", + "--format", "json", + "--json-output", + ]) + + #expect(parsed.flags.contains("jsonOutput")) + let config = CodexBarCLI.loggingConfiguration(path: ["diagnose"], values: parsed) + switch config.destination { + case .discard: + break + case .stderr, .oslog: + Issue.record("diagnose should not emit provider logs beside the safe JSON export") + } + } }