@@ -404,6 +404,53 @@ describe("formatAssistantErrorText", () => {
404404 ) ;
405405 } ) ;
406406
407+ it ( "does not claim HTTP 401 for plain 403 errors that fall through to the generic auth reason (#77394 review)" , ( ) => {
408+ // `classifyFailoverClassificationFromHttpStatus` returns the same
409+ // `auth` reason for both `status === 401` AND `status === 403` after
410+ // the billing/`auth_permanent` precedence at line 661 of errors.ts.
411+ // A real 403 (key revoked, scope-missing) without an HTML body or
412+ // `permission_error` JSON falls through to that generic `auth` branch.
413+ // Before the 401-evidence gate, my new `auth_invalid_token` branch
414+ // would pick those up and tell the user "HTTP 401" when the provider
415+ // actually returned 403. The gate now requires `status === 401` or
416+ // (status unknown AND message embeds 401 but not 403); a plain 403
417+ // satisfies neither leg and falls through to the existing copy.
418+ // Pin both directions: the 403 must NOT get the new "HTTP 401" copy,
419+ // and it must still produce a non-empty user-facing message.
420+ const plain403 = makeAssistantError ( "403 Forbidden" ) ;
421+ const friendly = formatAssistantErrorText ( plain403 ) ;
422+ expect ( friendly ) . toBeDefined ( ) ;
423+ expect ( friendly ) . not . toContain ( "HTTP 401" ) ;
424+ expect ( friendly ) . not . toBe (
425+ "Authentication failed (provider returned HTTP 401). " +
426+ "Your provider token may have expired — try the request again in a moment. " +
427+ "If the failure persists, re-authenticate this provider." ,
428+ ) ;
429+ } ) ;
430+
431+ it ( "does not claim HTTP 401 for message-only auth errors with no HTTP status prefix (#77394 review)" , ( ) => {
432+ // `classifyFailoverSignal`'s message-only path at line 848 of errors.ts
433+ // returns the `auth` reason via `isAuthErrorMessage(raw)` for payloads
434+ // that have no leading HTTP status at all — for example a plain
435+ // `{"error":{"code":"invalid_api_key"}}` envelope. Before the 401-
436+ // evidence gate, the new `auth_invalid_token` branch would catch those
437+ // too and surface "HTTP 401" copy when no HTTP status was ever present.
438+ // `status` is `undefined` here and the message contains no `401`, so
439+ // the second leg of the gate (`messageMentions401 && !messageMentions403`)
440+ // is false and the gate falls through. Pin the negative behavior so a
441+ // future widening that drops the gate fails this test rather than
442+ // silently shipping the regression.
443+ const messageOnly = makeAssistantError ( '{"error":{"code":"invalid_api_key"}}' ) ;
444+ const friendly = formatAssistantErrorText ( messageOnly ) ;
445+ expect ( friendly ) . toBeDefined ( ) ;
446+ expect ( friendly ) . not . toContain ( "HTTP 401" ) ;
447+ expect ( friendly ) . not . toBe (
448+ "Authentication failed (provider returned HTTP 401). " +
449+ "Your provider token may have expired — try the request again in a moment. " +
450+ "If the failure persists, re-authenticate this provider." ,
451+ ) ;
452+ } ) ;
453+
407454 it ( "returns a proxy-specific message for proxy misroutes" , ( ) => {
408455 const msg = makeAssistantError ( "407 Proxy Authentication Required" ) ;
409456 expect ( formatAssistantErrorText ( msg ) ) . toBe (
0 commit comments