import CodexBarCore import Commander import Foundation import XCTest @testable import CodexBarCLI final class CLIEntryTests: XCTestCase { func test_effectiveArgvDefaultsToUsage() { XCTAssertEqual(CodexBarCLI.effectiveArgv([]), ["usage"]) XCTAssertEqual(CodexBarCLI.effectiveArgv(["--json"]), ["usage", "--json"]) XCTAssertEqual(CodexBarCLI.effectiveArgv(["usage", "--json"]), ["usage", "--json"]) } func test_decodesFormatFromOptionsAndFlags() { let jsonOption = ParsedValues(positional: [], options: ["format": ["json"]], flags: []) XCTAssertEqual(CodexBarCLI._decodeFormatForTesting(from: jsonOption), .json) let jsonFlag = ParsedValues(positional: [], options: [:], flags: ["json"]) XCTAssertEqual(CodexBarCLI._decodeFormatForTesting(from: jsonFlag), .json) let textDefault = ParsedValues(positional: [], options: [:], flags: []) XCTAssertEqual(CodexBarCLI._decodeFormatForTesting(from: textDefault), .text) } func test_providerSelectionPrefersOverride() { let selection = CodexBarCLI.providerSelection(rawOverride: "codex", enabled: [.claude, .gemini]) XCTAssertEqual(selection.asList, [.codex]) } func test_normalizeVersionExtractsNumeric() { XCTAssertEqual(CodexBarCLI.normalizeVersion(raw: "codex 1.2.3 (build 4)"), "1.2.3") XCTAssertEqual(CodexBarCLI.normalizeVersion(raw: " v2.0 "), "2.0") } func test_makeHeaderIncludesVersionWhenAvailable() { let header = CodexBarCLI.makeHeader(provider: .codex, version: "1.2.3", source: "cli") XCTAssertTrue(header.contains("Codex")) XCTAssertTrue(header.contains("1.2.3")) XCTAssertTrue(header.contains("cli")) } func test_cliVersionFallsBackToContainingAppBundle() throws { let root = FileManager.default.temporaryDirectory .appendingPathComponent("codexbar-cli-version-\(UUID().uuidString)", isDirectory: true) defer { try? FileManager.default.removeItem(at: root) } let appURL = root.appendingPathComponent("CodexBar.app", isDirectory: true) let contentsURL = appURL.appendingPathComponent("Contents", isDirectory: true) let helpersURL = contentsURL.appendingPathComponent("Helpers", isDirectory: true) try FileManager.default.createDirectory(at: helpersURL, withIntermediateDirectories: true) let infoURL = contentsURL.appendingPathComponent("Info.plist") let plist: [String: Any] = ["CFBundleShortVersionString": "9.8.7"] let data = try PropertyListSerialization.data(fromPropertyList: plist, format: .xml, options: 0) try data.write(to: infoURL) let helperURL = helpersURL.appendingPathComponent("CodexBarCLI") try Data().write(to: helperURL) XCTAssertEqual(CodexBarCLI.containingAppVersion(for: helperURL), "9.8.7") } func test_cliVersionFollowsSymlinkedHelper() throws { let root = FileManager.default.temporaryDirectory .appendingPathComponent("codexbar-cli-version-symlink-\(UUID().uuidString)", isDirectory: true) defer { try? FileManager.default.removeItem(at: root) } let appURL = root.appendingPathComponent("CodexBar.app", isDirectory: true) let contentsURL = appURL.appendingPathComponent("Contents", isDirectory: true) let helpersURL = contentsURL.appendingPathComponent("Helpers", isDirectory: true) let binURL = root.appendingPathComponent("bin", isDirectory: true) try FileManager.default.createDirectory(at: helpersURL, withIntermediateDirectories: true) try FileManager.default.createDirectory(at: binURL, withIntermediateDirectories: true) let infoURL = contentsURL.appendingPathComponent("Info.plist") let plist: [String: Any] = ["CFBundleShortVersionString": "2.4.6"] let data = try PropertyListSerialization.data(fromPropertyList: plist, format: .xml, options: 0) try data.write(to: infoURL) let helperURL = helpersURL.appendingPathComponent("CodexBarCLI") try Data().write(to: helperURL) let symlinkURL = binURL.appendingPathComponent("codexbar") try FileManager.default.createSymbolicLink(at: symlinkURL, withDestinationURL: helperURL) XCTAssertEqual(CodexBarCLI.currentVersion(bundleVersion: nil, executablePath: symlinkURL.path), "2.4.6") } func test_cliVersionFallsBackToAdjacentVersionFile() throws { try self.expectAdjacentVersionFile(raw: "v3.2.1\n", expected: "3.2.1") try self.expectAdjacentVersionFile(raw: "3.2.2\n", expected: "3.2.2") try self.expectAdjacentVersionFile(raw: "version-3.2.3\n", expected: "version-3.2.3") } func test_cliVersionPrefersAdjacentVersionOverStandaloneBundleName() throws { let root = FileManager.default.temporaryDirectory .appendingPathComponent("codexbar-cli-version-bundle-\(UUID().uuidString)", isDirectory: true) defer { try? FileManager.default.removeItem(at: root) } let binURL = root.appendingPathComponent("bin", isDirectory: true) try FileManager.default.createDirectory(at: binURL, withIntermediateDirectories: true) let helperURL = binURL.appendingPathComponent("CodexBarCLI") try Data().write(to: helperURL) try "4.5.6\n".write( to: binURL.appendingPathComponent("VERSION"), atomically: false, encoding: .utf8) XCTAssertEqual( CodexBarCLI.currentVersion(bundleVersion: "CodexBar", executablePath: helperURL.path), "4.5.6") } private func expectAdjacentVersionFile(raw: String, expected: String) throws { let root = FileManager.default.temporaryDirectory .appendingPathComponent("codexbar-cli-version-file-\(UUID().uuidString)", isDirectory: true) defer { try? FileManager.default.removeItem(at: root) } let binURL = root.appendingPathComponent("bin", isDirectory: true) try FileManager.default.createDirectory(at: binURL, withIntermediateDirectories: true) let helperURL = binURL.appendingPathComponent("CodexBarCLI") try Data().write(to: helperURL) try raw.write( to: binURL.appendingPathComponent("VERSION"), atomically: false, encoding: .utf8) XCTAssertEqual(CodexBarCLI.currentVersion(bundleVersion: nil, executablePath: helperURL.path), expected) } func test_renderOpenAIWebDashboardTextIncludesSummary() { let event = CreditEvent( date: Date(timeIntervalSince1970: 1_700_000_000), service: "codex", creditsUsed: 10) let snapshot = OpenAIDashboardSnapshot( signedInEmail: "user@example.com", codeReviewRemainingPercent: 45, codeReviewLimit: RateWindow( usedPercent: 55, windowMinutes: nil, resetsAt: Date().addingTimeInterval(3600), resetDescription: nil), creditEvents: [event], dailyBreakdown: [], usageBreakdown: [], creditsPurchaseURL: nil, updatedAt: Date()) let text = CodexBarCLI.renderOpenAIWebDashboardText(snapshot) XCTAssertTrue(text.contains("Web session: user@example.com")) XCTAssertTrue(text.contains("Code review: 45% remaining (Resets in ")) XCTAssertTrue(text.contains("Web history: 1 events")) } func test_mapsErrorsToExitCodes() { XCTAssertEqual(CodexBarCLI.mapError(CodexStatusProbeError.codexNotInstalled), ExitCode(2)) XCTAssertEqual(CodexBarCLI.mapError(CodexStatusProbeError.timedOut), ExitCode(4)) XCTAssertEqual(CodexBarCLI.mapError(ClaudeWebFetchStrategyError.timedOut(seconds: 1)), ExitCode(4)) XCTAssertEqual(CodexBarCLI.mapError(UsageError.noRateLimitsFound), ExitCode(3)) } func test_antigravityPlanDebugKeepsOneShotHelperAliveUntilDebugFetch() { XCTAssertTrue(CodexBarCLI.holdsAntigravityCLISessionForPlanDebug( provider: .antigravity, planDebugEnabled: true, jsonOnly: false, persistsCLISessions: false)) XCTAssertFalse(CodexBarCLI.holdsAntigravityCLISessionForPlanDebug( provider: .codex, planDebugEnabled: true, jsonOnly: false, persistsCLISessions: false)) XCTAssertFalse(CodexBarCLI.holdsAntigravityCLISessionForPlanDebug( provider: .antigravity, planDebugEnabled: true, jsonOnly: true, persistsCLISessions: false)) XCTAssertFalse(CodexBarCLI.holdsAntigravityCLISessionForPlanDebug( provider: .antigravity, planDebugEnabled: true, jsonOnly: false, persistsCLISessions: true)) } func test_missingCodexBinaryErrorPayloadUsesInstallGuidance() { let payload = CodexBarCLI.makeErrorPayload(CodexStatusProbeError.codexNotInstalled, kind: .provider) XCTAssertEqual(payload.code, ExitCode.binaryNotFound.rawValue) XCTAssertTrue(payload.message.contains("Codex CLI missing")) XCTAssertFalse(payload.message.contains("Codex not running")) } func test_providerSelectionFallsBackToBothForPrimaryPair() { let selection = CodexBarCLI.providerSelection(rawOverride: nil, enabled: [.codex, .claude]) switch selection { case .both: break default: XCTFail("Expected both selection") } } func test_providerSelectionFallsBackToCustomWhenNonPrimary() { let selection = CodexBarCLI.providerSelection(rawOverride: nil, enabled: [.codex, .gemini]) switch selection { case let .custom(providers): XCTAssertEqual(providers, [.codex, .gemini]) default: XCTFail("Expected custom selection") } } func test_providerSelectionHonorsEmptyEnabledSet() { let selection = CodexBarCLI.providerSelection(rawOverride: nil, enabled: []) switch selection { case let .custom(providers): XCTAssertEqual(providers, []) default: XCTFail("Expected empty custom selection") } } func test_decodesSourceAndTimeoutOptions() throws { let signature = CodexBarCLI._usageSignatureForTesting() let parser = CommandParser(signature: signature) let parsed = try parser.parse(arguments: ["--web-timeout", "45", "--source", "oauth"]) XCTAssertEqual(try CodexBarCLI._decodeWebTimeoutForTesting(from: parsed), 45) XCTAssertEqual(CodexBarCLI._decodeSourceModeForTesting(from: parsed), .oauth) let parsedWeb = try parser.parse(arguments: ["--web"]) XCTAssertEqual(CodexBarCLI._decodeSourceModeForTesting(from: parsedWeb), .web) } func test_rejectsUnsafeWebTimeoutOptions() throws { for value in ["-1", "nan", "inf", "1e300"] { let parsed = ParsedValues(positional: [], options: ["webTimeout": [value]], flags: []) XCTAssertThrowsError(try CodexBarCLI._decodeWebTimeoutForTesting(from: parsed)) } } func test_shouldUseColorRespectsFormatAndFlags() { XCTAssertFalse(CodexBarCLI.shouldUseColor(noColor: true, format: .text)) XCTAssertFalse(CodexBarCLI.shouldUseColor(noColor: false, format: .json)) } func test_kiloUsageTextNotesShowFallbackOnlyForAutoResolvedToCLI() { XCTAssertEqual(CodexBarCLI.usageTextNotes( provider: .kilo, sourceMode: .auto, resolvedSourceLabel: "cli"), ["Using CLI fallback"]) XCTAssertTrue(CodexBarCLI.usageTextNotes( provider: .kilo, sourceMode: .api, resolvedSourceLabel: "cli").isEmpty) XCTAssertTrue(CodexBarCLI.usageTextNotes( provider: .codex, sourceMode: .auto, resolvedSourceLabel: "cli").isEmpty) } func test_kiloAutoFallbackSummaryIncludesOrderedAttemptDetails() { let attempts = [ ProviderFetchAttempt( strategyID: "kilo.api", kind: .apiToken, wasAvailable: true, errorDescription: "Kilo authentication failed (401/403)."), ProviderFetchAttempt( strategyID: "kilo.cli", kind: .cli, wasAvailable: true, errorDescription: "Kilo CLI session not found."), ] let summary = CodexBarCLI.kiloAutoFallbackSummary( provider: .kilo, sourceMode: .auto, attempts: attempts) let expected = [ "Kilo auto fallback attempts: api: Kilo authentication failed (401/403).", " -> cli: Kilo CLI session not found.", ].joined() XCTAssertEqual(summary, expected) } func test_kiloAutoFallbackSummaryIsNilOutsideKiloAutoFailures() { let attempts = [ ProviderFetchAttempt( strategyID: "kilo.api", kind: .apiToken, wasAvailable: true, errorDescription: "example"), ] XCTAssertNil(CodexBarCLI.kiloAutoFallbackSummary( provider: .kilo, sourceMode: .api, attempts: attempts)) XCTAssertNil(CodexBarCLI.kiloAutoFallbackSummary( provider: .codex, sourceMode: .auto, attempts: attempts)) } func test_sourceModeRequiresWebSupportIsProviderAware() throws { let directory = FileManager.default.temporaryDirectory .appendingPathComponent("mimo-cli-source-mode-\(UUID().uuidString)", isDirectory: true) try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) defer { try? FileManager.default.removeItem(at: directory) } let validMiMoCache = directory.appendingPathComponent("valid.json") let invalidMiMoCache = directory.appendingPathComponent("invalid.json") let payload: [String: Any] = [ "sessions_scanned": 1, "windows": [ "today": [:], "week": [:], "all_time": [:], ], ] try JSONSerialization.data(withJSONObject: payload).write(to: validMiMoCache) try Data("{}".utf8).write(to: invalidMiMoCache) XCTAssertTrue(CodexBarCLI.sourceModeRequiresWebSupport(.web, provider: .kilo)) XCTAssertFalse(CodexBarCLI.sourceModeRequiresWebSupport(.auto, provider: .codex)) XCTAssertFalse(CodexBarCLI.sourceModeRequiresWebSupport(.auto, provider: .claude)) XCTAssertTrue(CodexBarCLI.sourceModeRequiresWebSupport(.web, provider: .claude)) XCTAssertFalse(CodexBarCLI.sourceModeRequiresWebSupport(.auto, provider: .kilo)) XCTAssertFalse(CodexBarCLI.sourceModeRequiresWebSupport(.auto, provider: .grok)) XCTAssertFalse(CodexBarCLI.sourceModeRequiresWebSupport(.web, provider: .grok)) XCTAssertFalse(CodexBarCLI.sourceModeRequiresWebSupport(.auto, provider: .amp)) XCTAssertFalse(CodexBarCLI.sourceModeRequiresWebSupport(.api, provider: .kilo)) XCTAssertFalse(CodexBarCLI.sourceModeRequiresWebSupport( .auto, provider: .opencodego, settings: ProviderSettingsSnapshot.make( opencodego: .init( cookieSource: .manual, manualCookieHeader: "auth=manual", workspaceID: nil)))) XCTAssertFalse(CodexBarCLI.sourceModeRequiresWebSupport( .web, provider: .opencodego, settings: ProviderSettingsSnapshot.make( opencodego: .init( cookieSource: .manual, manualCookieHeader: "auth=manual", workspaceID: nil)))) XCTAssertFalse(CodexBarCLI.sourceModeRequiresWebSupport( .auto, provider: .opencodego, settings: ProviderSettingsSnapshot.make( opencodego: .init( cookieSource: .auto, manualCookieHeader: nil, workspaceID: nil)))) XCTAssertTrue(CodexBarCLI.sourceModeRequiresWebSupport( .web, provider: .opencodego, settings: ProviderSettingsSnapshot.make( opencodego: .init( cookieSource: .auto, manualCookieHeader: nil, workspaceID: nil)))) XCTAssertFalse(CodexBarCLI.sourceModeRequiresWebSupport( .auto, provider: .commandcode, settings: ProviderSettingsSnapshot.make( commandcode: .init( cookieSource: .manual, manualCookieHeader: "session=manual")))) XCTAssertFalse(CodexBarCLI.sourceModeRequiresWebSupport( .web, provider: .commandcode, settings: ProviderSettingsSnapshot.make( commandcode: .init( cookieSource: .manual, manualCookieHeader: "session=manual")))) XCTAssertTrue(CodexBarCLI.sourceModeRequiresWebSupport( .auto, provider: .commandcode, settings: ProviderSettingsSnapshot.make( commandcode: .init( cookieSource: .auto, manualCookieHeader: nil)))) XCTAssertFalse(CodexBarCLI.sourceModeRequiresWebSupport( .auto, provider: .sakana, environment: ["SAKANA_COOKIE": "session=manual"])) XCTAssertFalse(CodexBarCLI.sourceModeRequiresWebSupport( .web, provider: .sakana, environment: ["SAKANA_COOKIE": "session=manual"])) XCTAssertTrue(CodexBarCLI.sourceModeRequiresWebSupport( .auto, provider: .sakana, environment: [:])) XCTAssertFalse(CodexBarCLI.sourceModeRequiresWebSupport( .web, provider: .qoder, settings: ProviderSettingsSnapshot.make( qoder: .init( cookieSource: .manual, manualCookieHeader: "sid=manual")))) XCTAssertTrue(CodexBarCLI.sourceModeRequiresWebSupport( .web, provider: .qoder, settings: ProviderSettingsSnapshot.make( qoder: .init( cookieSource: .auto, manualCookieHeader: nil)))) XCTAssertTrue(CodexBarCLI.sourceModeRequiresWebSupport( .auto, provider: .opencode, settings: ProviderSettingsSnapshot.make( opencode: .init( cookieSource: .manual, manualCookieHeader: "auth=manual", workspaceID: nil)))) XCTAssertFalse(CodexBarCLI.sourceModeRequiresWebSupport( .auto, provider: .ollama, environment: ["OLLAMA_API_KEY": "ollama-test"])) XCTAssertFalse(CodexBarCLI.sourceModeRequiresWebSupport( .auto, provider: .codex, environment: ["OLLAMA_API_KEY": "ollama-test"])) XCTAssertFalse(CodexBarCLI.sourceModeRequiresWebSupport( .auto, provider: .ollama, settings: ProviderSettingsSnapshot.make( ollama: .init(cookieSource: .off, manualCookieHeader: nil)))) XCTAssertFalse(CodexBarCLI.sourceModeRequiresWebSupport( .auto, provider: .kimi, environment: ["KIMI_CODE_API_KEY": "kimi-test"])) try self.assertKimiCodeCredentialSourceMode(in: directory) XCTAssertFalse(CodexBarCLI.sourceModeRequiresWebSupport( .auto, provider: .mimo, environment: ["MIMO_LOCAL_USAGE_PATH": validMiMoCache.path])) XCTAssertTrue(CodexBarCLI.sourceModeRequiresWebSupport( .web, provider: .mimo, environment: ["MIMO_LOCAL_USAGE_PATH": validMiMoCache.path])) XCTAssertFalse(CodexBarCLI.sourceModeRequiresWebSupport( .auto, provider: .mimo, environment: ["MIMO_LOCAL_USAGE_PATH": invalidMiMoCache.path])) XCTAssertTrue(CodexBarCLI.sourceModeRequiresWebSupport( .auto, provider: .mimo, environment: ["MIMO_LOCAL_USAGE_PATH": directory.appendingPathComponent("missing.json").path])) } private func assertKimiCodeCredentialSourceMode(in directory: URL) throws { let home = directory.appendingPathComponent("kimi-code", isDirectory: true) let credentials = home.appendingPathComponent("credentials", isDirectory: true) try FileManager.default.createDirectory(at: credentials, withIntermediateDirectories: true) let payload: [String: Any] = [ "access_token": "expired", "refresh_token": "refresh", "expires_at": Date().addingTimeInterval(-60).timeIntervalSince1970, ] try JSONSerialization.data(withJSONObject: payload) .write(to: credentials.appendingPathComponent("kimi-code.json")) XCTAssertFalse(CodexBarCLI.sourceModeRequiresWebSupport( .auto, provider: .kimi, environment: ["KIMI_CODE_HOME": home.path])) } func test_sourceModeRequiresWebSupportAllowsFactoryAPIKeyOnLinuxGate() { XCTAssertFalse(CodexBarCLI.sourceModeRequiresWebSupport( .auto, provider: .factory, environment: ["FACTORY_API_KEY": "fk-test"])) XCTAssertFalse(CodexBarCLI.sourceModeRequiresWebSupport( .cli, provider: .factory, environment: ["FACTORY_API_KEY": "fk-test"])) XCTAssertTrue(CodexBarCLI.sourceModeRequiresWebSupport( .auto, provider: .factory, environment: [:])) XCTAssertTrue(CodexBarCLI.sourceModeRequiresWebSupport( .web, provider: .factory, environment: ["FACTORY_API_KEY": "fk-test"])) XCTAssertFalse(CodexBarCLI.sourceModeRequiresWebSupport( .api, provider: .factory, environment: [:])) } }