import Foundation import Testing @testable import CodeBurnMenubar @Suite("DataClient process") struct DataClientProcessTests { @Test("status argv local omits scope") func statusSubcommandLocalOmitsScope() { let args = DataClient.statusSubcommand( period: .today, provider: .claude, includeOptimize: false, scope: .local ) #expect(!args.contains("--scope")) #expect(value(after: "--provider", in: args) == "claude") #expect(args.contains("--no-optimize")) } @Test("status argv combined adds scope and forces provider all") func statusSubcommandCombinedAddsScopeAndForcesAllProvider() { let args = DataClient.statusSubcommand( period: .today, provider: .codex, includeOptimize: true, scope: .combined ) #expect(value(after: "--scope", in: args) == "combined") #expect(value(after: "--provider", in: args) == "all") #expect(!args.contains("--no-optimize")) } @Test("status argv combined multi-day coerces to local") func statusSubcommandCombinedMultiDayCoercesToLocal() { let args = DataClient.statusSubcommand( period: .today, days: ["2026-06-01", "2026-06-02"], provider: .codex, includeOptimize: false, scope: .combined ) #expect(!args.contains("--scope")) #expect(value(after: "--provider", in: args) == "codex") #expect(value(after: "--days", in: args) == "2026-06-01,2026-06-02") } @Test("status argv local includes Claude config source") func statusSubcommandLocalIncludesClaudeConfigSource() { let args = DataClient.statusSubcommand( period: .today, provider: .all, includeOptimize: false, scope: .local, claudeConfigSourceId: "claude-config:work" ) #expect(value(after: "--claude-config-source", in: args) == "claude-config:work") } @Test("status argv combined omits Claude config source") func statusSubcommandCombinedOmitsClaudeConfigSource() { let args = DataClient.statusSubcommand( period: .today, provider: .all, includeOptimize: false, scope: .combined, claudeConfigSourceId: "claude-config:work" ) #expect(value(after: "--scope", in: args) == "combined") #expect(!args.contains("--claude-config-source")) } @Test("status argv supports LingTai TUI provider") func statusSubcommandSupportsLingTaiTUI() { let args = DataClient.statusSubcommand( period: .month, provider: .lingtaiTui, includeOptimize: false, scope: .local ) #expect(value(after: "--provider", in: args) == "lingtai-tui") #expect(value(after: "--period", in: args) == "month") } /// Concurrency + timeout smoke test: launch more hung subprocesses than /// there are cooperative threads, all at once, with a short timeout, and /// assert every call returns once the timeout kills its sleep. /// /// NOTE: this does NOT reproduce the production permanent deadlock (16/16 /// cooperative threads parked in waitUntilExit). In a short-lived unit-test /// process libdispatch spins up replacement threads for blocked workers, so /// even the old blocking-on-the-pool code completes here. The real deadlock /// built up over ~2 days under the @MainActor refresh loop and is confirmed /// by the live `sample`, not by this test. Kept as a guard that the /// off-pool wait + timeout path stays correct under concurrency. @Test("concurrent timed-out processes all complete") func concurrentTimedOutProcessesAllComplete() { let count = ProcessInfo.processInfo.activeProcessorCount * 2 + 4 let done = DispatchSemaphore(value: 0) Task { await withTaskGroup(of: Void.self) { group in for _ in 0..")) } /// A normally-exiting process returns its real output and exit code through /// the off-pool wait path. @Test("process returns output and exit code") func processReturnsOutputAndExitCode() async throws { let process = Process() process.executableURL = URL(fileURLWithPath: "/bin/echo") process.arguments = ["hello"] let result = try await DataClient.runProcess(process, timeoutSeconds: 5, label: "echo hello") #expect(result.exitCode == 0) #expect(String(data: result.stdout, encoding: .utf8) == "hello\n") } /// Many NORMALLY-exiting processes, all at once, must every one complete /// through the terminationHandler wait path. Guards against the wait path /// leaking or wedging under concurrency (the production bug was the wait and /// its timeout sharing one queue that saturated under sustained load). @Test("many normal processes all complete") func manyNormalProcessesAllComplete() async { let count = 50 let codes = await withTaskGroup(of: Int32?.self) { group -> [Int32?] in for _ in 0.. 0) } } private func value(after flag: String, in args: [String]) -> String? { guard let index = args.firstIndex(of: flag), args.indices.contains(index + 1) else { return nil } return args[index + 1] } private actor PeakCounter { private var current = 0 private(set) var peak = 0 func enter() { current += 1; peak = max(peak, current) } func leave() { current -= 1 } }