@@ -2,6 +2,7 @@ import { describe, expect, test } from "bun:test";
22
33import { normalizeInferenceErrorForTerminal } from "./inference-gateway-error.js" ;
44import {
5+ CREDENTIAL_FAILURE_USER_MESSAGE ,
56 inferenceErrorMessage ,
67 terminalProviderFailureMessage ,
78} from "./inference-error-message.js" ;
@@ -200,6 +201,155 @@ describe("terminalProviderFailureMessage", () => {
200201 expect ( message . toLowerCase ( ) ) . not . toContain ( "retrying" ) ;
201202 } ) ;
202203
204+ test ( "terminal xAI OAuth 426 names the profile and spells the reconnect command" , ( ) => {
205+ const normalized = normalizeInferenceErrorForTerminal (
206+ {
207+ category : "fatal" ,
208+ message : "Upgrade Required" ,
209+ statusCode : 426 ,
210+ raw : { error : { code : "upgrade_required" } } ,
211+ } ,
212+ "xai/default-2" ,
213+ ) ;
214+ const message = terminalProviderFailureMessage ( "xai/default-2" , normalized ) ;
215+ expect ( message ) . toContain ( 'xAI profile "default-2"' ) ;
216+ expect ( message ) . toContain ( '"/connect xai default-2"' ) ;
217+ expect ( message ) . toContain ( 'reconnect profile "default-2"' ) ;
218+ } ) ;
219+
220+ test ( "terminal xAI 401 credential failure spells the reconnect command" , ( ) => {
221+ const message = terminalProviderFailureMessage ( "xai/default-2" , {
222+ category : "credential_failure" ,
223+ message : '{"error":{"code":401}}' ,
224+ statusCode : 401 ,
225+ providerId : "xai/default-2" ,
226+ } ) ;
227+ expect ( message ) . toContain ( "/connect" ) ;
228+ expect ( message ) . toContain ( '"/connect xai default-2"' ) ;
229+ expect ( message ) . toContain ( 'reconnect profile "default-2"' ) ;
230+ } ) ;
231+
232+ test ( "terminal Codex credential failure keeps branded wording plus the explicit command" , ( ) => {
233+ const normalized = normalizeInferenceErrorForTerminal (
234+ {
235+ category : "fatal" ,
236+ message : "Not Found" ,
237+ statusCode : 404 ,
238+ raw : {
239+ error : {
240+ code : "invalid_token" ,
241+ message : "Not authorized: the access token has been revoked" ,
242+ } ,
243+ } ,
244+ } ,
245+ "codex/work" ,
246+ ) ;
247+ const message = terminalProviderFailureMessage ( "codex/work" , normalized ) ;
248+ expect ( message ) . toContain ( 'Codex profile "work"' ) ;
249+ // Branded line keeps its wording (bare /connect); the explicit command is
250+ // additive, never a stutter of the generic line.
251+ expect ( message ) . toContain ( "/connect" ) ;
252+ expect ( message ) . toContain ( '"/connect codex work"' ) ;
253+ expect ( message ) . not . toContain ( "run /connect to reconnect" ) ;
254+ } ) ;
255+
256+ test . each ( [
257+ {
258+ name : "bare 404" ,
259+ providerId : "codex/work" ,
260+ error : {
261+ category : "fatal" as const ,
262+ message : "Not Found" ,
263+ statusCode : 404 ,
264+ } ,
265+ exact :
266+ 'codex/work Provider failed (fatal): Not Found. Try again or switch models with "/model".' ,
267+ } ,
268+ {
269+ name : "quota" ,
270+ providerId : "xai/alice" ,
271+ error : {
272+ category : "quota_exhausted" as const ,
273+ message : "Too Many Requests" ,
274+ statusCode : 429 ,
275+ } ,
276+ exact :
277+ "xai/alice Provider failed (retryable): Rate limited. Wait a moment and try again." ,
278+ } ,
279+ {
280+ name : "overflow" ,
281+ providerId : "xai/alice" ,
282+ error : {
283+ category : "context_overflow" as const ,
284+ message : "context length exceeded" ,
285+ } ,
286+ exact :
287+ "xai/alice Provider failed (context_overflow): context length exceeded. Try /clear to start fresh." ,
288+ } ,
289+ {
290+ name : "non-OAuth 426" ,
291+ providerId : "custom-provider" ,
292+ error : {
293+ category : "fatal" as const ,
294+ message : "Upgrade Required" ,
295+ statusCode : 426 ,
296+ } ,
297+ exact :
298+ 'custom-provider Provider failed (fatal): Upgrade Required. Try again or switch models with "/model".' ,
299+ } ,
300+ ] ) (
301+ "non-reconnect $name keeps byte-identical guidance" ,
302+ ( { providerId, error, exact } ) => {
303+ const normalized = normalizeInferenceErrorForTerminal ( error , providerId ) ;
304+ const message = terminalProviderFailureMessage ( providerId , normalized ) ;
305+ expect ( message ) . toBe ( exact ) ;
306+ expect ( message ) . not . toContain ( '"/connect ' ) ;
307+ } ,
308+ ) ;
309+
310+ test . each ( [
311+ {
312+ providerId : "xai/alice" ,
313+ command : '"/connect xai alice"' ,
314+ profile : "alice" ,
315+ } ,
316+ {
317+ providerId : "codex/alice" ,
318+ command : '"/connect codex alice"' ,
319+ profile : "alice" ,
320+ } ,
321+ ] ) (
322+ "reconnect-class $providerId spells the explicit reconnect command" ,
323+ ( { providerId, command, profile } ) => {
324+ const normalized = normalizeInferenceErrorForTerminal (
325+ {
326+ category : "credential_failure" ,
327+ message : "Unauthorized" ,
328+ statusCode : 401 ,
329+ } ,
330+ providerId ,
331+ ) ;
332+ const message = terminalProviderFailureMessage ( providerId , normalized ) ;
333+ expect ( message ) . toContain (
334+ `Run ${ command } to reconnect profile "${ profile } ".` ,
335+ ) ;
336+ } ,
337+ ) ;
338+
339+ test ( "bare kind-only id is not reconnect-class and keeps base guidance" , ( ) => {
340+ const normalized = normalizeInferenceErrorForTerminal (
341+ {
342+ category : "credential_failure" ,
343+ message : "Unauthorized" ,
344+ statusCode : 401 ,
345+ } ,
346+ "xai" ,
347+ ) ;
348+ const message = terminalProviderFailureMessage ( "xai" , normalized ) ;
349+ expect ( message ) . not . toContain ( '"/connect ' ) ;
350+ expect ( message ) . toContain ( CREDENTIAL_FAILURE_USER_MESSAGE ) ;
351+ } ) ;
352+
203353 test ( "retryable 429 guidance asks the operator to wait before trying again" , ( ) => {
204354 const message = terminalProviderFailureMessage ( "codex/default" , {
205355 category : "retryable" ,
0 commit comments