@@ -209,6 +209,137 @@ struct ClaudeBaselineCharacterizationTests {
209209 #expect( strategyIDs == [ " claude.web " , " claude.cli " ] )
210210 }
211211
212+ @Test
213+ func `CLI explicit source delegates authentication to the configured Claude executable`() async throws {
214+ let invocationLog = FileManager . default. temporaryDirectory
215+ . appendingPathComponent ( " claude-invocations- \( UUID ( ) . uuidString) .log " )
216+ let stubCLIPath = try self . makeStubClaudeCLI (
217+ authStatusScript: " printf '%s \\ n' 'not-json' " ,
218+ invocationLog: invocationLog)
219+ defer {
220+ try ? FileManager . default. removeItem ( atPath: stubCLIPath)
221+ try ? FileManager . default. removeItem ( at: invocationLog)
222+ }
223+ let settings = ProviderSettingsSnapshot . make ( claude: . init(
224+ usageDataSource: . cli,
225+ webExtrasEnabled: false ,
226+ cookieSource: . off,
227+ manualCookieHeader: nil ) )
228+ let env = [ " CLAUDE_CLI_PATH " : stubCLIPath]
229+ let fetchOverride : ClaudeStatusProbe . FetchOverride = { binary, _, _ in
230+ #expect( binary == stubCLIPath)
231+ return Self . makeUsageStatusSnapshot ( )
232+ }
233+
234+ let outcome = await ClaudeStatusProbe . $fetchOverride. withValue ( fetchOverride) {
235+ await self . fetchOutcome ( runtime: . cli, sourceMode: . cli, env: env, settings: settings)
236+ }
237+ let result = try outcome. result. get ( )
238+
239+ #expect( outcome. attempts. map ( \. strategyID) == [ " claude.cli " ] )
240+ #expect( outcome. attempts. map ( \. wasAvailable) == [ true ] )
241+ #expect( result. strategyID == " claude.cli " )
242+ #expect( result. usage. dataConfidence == . percentOnly)
243+ #expect( try String ( contentsOf: invocationLog, encoding: . utf8) == " auth status --json \n " )
244+ }
245+
246+ @Test
247+ func `CLI auto reaches Claude executable after web credentials are unavailable`() async throws {
248+ let invocationLog = FileManager . default. temporaryDirectory
249+ . appendingPathComponent ( " claude-invocations- \( UUID ( ) . uuidString) .log " )
250+ let stubCLIPath = try self . makeStubClaudeCLI (
251+ authStatusScript: " printf '%s \\ n' 'not-json' " ,
252+ invocationLog: invocationLog)
253+ defer {
254+ try ? FileManager . default. removeItem ( atPath: stubCLIPath)
255+ try ? FileManager . default. removeItem ( at: invocationLog)
256+ }
257+ let settings = ProviderSettingsSnapshot . make ( claude: . init(
258+ usageDataSource: . auto,
259+ webExtrasEnabled: false ,
260+ cookieSource: . auto,
261+ manualCookieHeader: nil ) )
262+ let env = [ " CLAUDE_CLI_PATH " : stubCLIPath]
263+ let webLoader : ClaudeWebFetchStrategy . UsageLoader = { _ in
264+ throw ClaudeWebAPIFetcher . FetchError. noSessionKeyFound
265+ }
266+ let fetchOverride : ClaudeStatusProbe . FetchOverride = { binary, _, _ in
267+ #expect( binary == stubCLIPath)
268+ return Self . makeUsageStatusSnapshot ( )
269+ }
270+
271+ let outcome = await ClaudeWebFetchStrategy . $usageLoaderOverrideForTesting. withValue ( webLoader) {
272+ await ClaudeStatusProbe . $fetchOverride. withValue ( fetchOverride) {
273+ await self . fetchOutcome ( runtime: . cli, sourceMode: . auto, env: env, settings: settings)
274+ }
275+ }
276+ let result = try outcome. result. get ( )
277+
278+ #expect( outcome. attempts. map ( \. strategyID) == [ " claude.web " , " claude.cli " ] )
279+ #expect( outcome. attempts. map ( \. wasAvailable) == [ true , true ] )
280+ #expect( result. strategyID == " claude.cli " )
281+ #expect( result. usage. dataConfidence == . percentOnly)
282+ #expect( try String ( contentsOf: invocationLog, encoding: . utf8) == " auth status --json \n " )
283+ }
284+
285+ @Test ( arguments: [
286+ " /definitely/missing/claude " ,
287+ " /etc/hosts " ,
288+ ] )
289+ func `CLI source rejects missing and non executable Claude paths`(path: String) async {
290+ let settings = ProviderSettingsSnapshot . make ( claude: . init(
291+ usageDataSource: . cli,
292+ webExtrasEnabled: false ,
293+ cookieSource: . off,
294+ manualCookieHeader: nil ) )
295+ let outcome = await ClaudeCLIResolver . withResolvedBinaryPathOverrideForTesting ( path) {
296+ await self . fetchOutcome (
297+ runtime: . cli,
298+ sourceMode: . cli,
299+ env: [ " CLAUDE_CLI_PATH " : path] ,
300+ settings: settings)
301+ }
302+
303+ #expect( outcome. attempts. map ( \. strategyID) == [ " claude.cli " ] )
304+ #expect( outcome. attempts. map ( \. wasAvailable) == [ false ] )
305+ switch outcome. result {
306+ case let . failure( error as ProviderFetchError ) :
307+ guard case . noAvailableStrategy( . claude) = error else {
308+ Issue . record ( " Unexpected provider fetch error: \( error) " )
309+ return
310+ }
311+ case let . failure( error) :
312+ Issue . record ( " Unexpected error: \( error) " )
313+ case let . success( result) :
314+ Issue . record ( " Unavailable Claude executable unexpectedly produced \( result. strategyID) " )
315+ }
316+ }
317+
318+ @Test
319+ func `CLI source keeps definitive logged out guard`() async throws {
320+ let invocationLog = FileManager . default. temporaryDirectory
321+ . appendingPathComponent ( " claude-invocations- \( UUID ( ) . uuidString) .log " )
322+ let stubCLIPath = try self . makeStubClaudeCLI ( loggedIn: false , invocationLog: invocationLog)
323+ defer {
324+ try ? FileManager . default. removeItem ( atPath: stubCLIPath)
325+ try ? FileManager . default. removeItem ( at: invocationLog)
326+ }
327+ let settings = ProviderSettingsSnapshot . make ( claude: . init(
328+ usageDataSource: . cli,
329+ webExtrasEnabled: false ,
330+ cookieSource: . off,
331+ manualCookieHeader: nil ) )
332+ let outcome = await self . fetchOutcome (
333+ runtime: . cli,
334+ sourceMode: . cli,
335+ env: [ " CLAUDE_CLI_PATH " : stubCLIPath] ,
336+ settings: settings)
337+
338+ #expect( outcome. attempts. map ( \. strategyID) == [ " claude.cli " ] )
339+ #expect( outcome. attempts. map ( \. wasAvailable) == [ false ] )
340+ #expect( try String ( contentsOf: invocationLog, encoding: . utf8) == " auth status --json \n " )
341+ }
342+
212343 @Test
213344 func `app explicit CLI remains available for interactive authentication without preflight`() async {
214345 let settings = ProviderSettingsSnapshot . make ( claude: . init(
@@ -699,4 +830,18 @@ struct ClaudeBaselineCharacterizationTests {
699830 #expect( !TokenAccountSupportCatalog. isClaudeOAuthToken ( " sessionKey=sk-ant-session " ) )
700831 #expect( !TokenAccountSupportCatalog. isClaudeOAuthToken ( " Cookie: sessionKey=sk-ant-session; foo=bar " ) )
701832 }
833+
834+ private static func makeUsageStatusSnapshot( ) -> ClaudeStatusSnapshot {
835+ ClaudeStatusSnapshot (
836+ sessionPercentLeft: 88 ,
837+ weeklyPercentLeft: 60 ,
838+ opusPercentLeft: 95 ,
839+ accountEmail: " user@example.com " ,
840+ accountOrganization: " Example Org " ,
841+ loginMethod: nil ,
842+ primaryResetDescription: " Resets 11am " ,
843+ secondaryResetDescription: " Resets Nov 21 " ,
844+ opusResetDescription: " Resets Nov 21 " ,
845+ rawText: " stub " )
846+ }
702847}
0 commit comments