1- import { BailianError , ExitCode } from "bailian-cli-core" ;
1+ import { BailianError , ExitCode , mapApiError } from "bailian-cli-core" ;
22import { expect , test , vi } from "vite-plus/test" ;
3- import { handleError } from "../src/error-handler.ts" ;
3+ import { errorHint , handleError } from "../src/error-handler.ts" ;
44import { createOutputReporter } from "../src/output/reporter.ts" ;
55import { ConfirmationRequiredError } from "../src/confirm.ts" ;
66
7- function render ( error : unknown , output : "text" | "json" = "json" ) {
7+ function render (
8+ error : unknown ,
9+ output : "text" | "json" = "json" ,
10+ options : { commandPrefix ?: string ; supportsFreeTierRecovery ?: boolean } = { } ,
11+ ) {
812 let stderr = "" ;
913 const reporter = createOutputReporter ( {
1014 settings : { output, quiet : true , verbose : false } ,
11- commandPrefix : "kscli" ,
15+ commandPrefix : options . commandPrefix ?? "kscli" ,
16+ supportsFreeTierRecovery : options . supportsFreeTierRecovery ,
1217 stderr : {
1318 write : ( chunk ) => {
1419 stderr += chunk ;
@@ -26,6 +31,12 @@ function render(error: unknown, output: "text" | "json" = "json") {
2631 return stderr ;
2732}
2833
34+ const FREE_TIER_ONLY_API_CODE = "AllocationQuota.FreeTierOnly" ;
35+ const FREE_TIER_MESSAGE =
36+ 'The free tier of the model has been exhausted. If you wish to continue access the model on a paid basis, please disable the "use free tier only" mode in the management console.' ;
37+ const FREE_QUOTA_MESSAGE =
38+ 'Free quota exhausted. To continue accessing the model on a paid basis, please add funds or disable the "use free tier only" mode in the management console.' ;
39+
2940test ( "handleError: fetch failed JSON includes cause.code from errno" , ( ) => {
3041 const cause = Object . assign ( new Error ( "getaddrinfo ENOTFOUND example.invalid" ) , {
3142 code : "ENOTFOUND" ,
@@ -66,3 +77,77 @@ test("model-auth guidance uses the invocation binary and does not leak into cons
6677 ) ;
6778 expect ( JSON . parse ( render ( consoleError ) ) ) . toEqual ( consoleError . toJSON ( ) ) ;
6879} ) ;
80+
81+ test ( "free-tier hint appends for AllocationQuota.FreeTierOnly when product supports usage" , ( ) => {
82+ const error = new BailianError ( FREE_TIER_MESSAGE , ExitCode . GENERAL , undefined , {
83+ api : { httpStatus : 403 , apiCode : FREE_TIER_ONLY_API_CODE } ,
84+ } ) ;
85+ const payload = JSON . parse (
86+ render ( error , "json" , { commandPrefix : "bl" , supportsFreeTierRecovery : true } ) ,
87+ ) . error ;
88+ expect ( payload . code ) . toBe ( ExitCode . GENERAL ) ;
89+ expect ( payload . message ) . toBe ( FREE_TIER_MESSAGE ) ;
90+ expect ( payload . hint ) . toContain ( "bl usage free --model <model>" ) ;
91+ expect ( payload . hint ) . toContain ( "bl usage freetier --off --model <model> --yes" ) ;
92+ expect ( payload . hint ) . not . toContain ( "--all" ) ;
93+ const text = render ( error , "text" , { commandPrefix : "bl" , supportsFreeTierRecovery : true } ) ;
94+ expect ( text ) . toContain ( "Hint:" ) ;
95+ expect ( text ) . toContain ( "usage freetier --off" ) ;
96+ } ) ;
97+
98+ test ( "free-tier hint uses message fallback when type shadows AllocationQuota.FreeTierOnly code" , ( ) => {
99+ const error = mapApiError ( 400 , {
100+ error : {
101+ message : FREE_TIER_MESSAGE ,
102+ type : "invalid_request_error" ,
103+ code : FREE_TIER_ONLY_API_CODE ,
104+ } ,
105+ } ) ;
106+ expect ( error . api ?. apiCode ) . toBe ( "invalid_request_error" ) ;
107+ const hint = errorHint ( error , "bl" , ( text ) => ( typeof text === "string" ? text : text [ "en-US" ] ) , {
108+ supportsFreeTierRecovery : true ,
109+ } ) ;
110+ expect ( hint ) . toContain ( "bl usage freetier --off" ) ;
111+ } ) ;
112+
113+ test ( "OpenAI-compat Free quota exhausted copy matches via use free tier only phrase" , ( ) => {
114+ const error = new BailianError ( FREE_QUOTA_MESSAGE , ExitCode . GENERAL ) ;
115+ const payload = JSON . parse (
116+ render ( error , "json" , { commandPrefix : "bl" , supportsFreeTierRecovery : true } ) ,
117+ ) . error ;
118+ expect ( payload . hint ) . toContain ( "bl usage freetier --off --model <model> --yes" ) ;
119+ } ) ;
120+
121+ test ( "insufficient_quota TPS/TPM message does not get free-tier recovery" , ( ) => {
122+ const error = new BailianError (
123+ "You exceeded your current quota, please check your plan and billing details." ,
124+ ExitCode . GENERAL ,
125+ undefined ,
126+ { api : { apiCode : "insufficient_quota" } } ,
127+ ) ;
128+ const payload = JSON . parse (
129+ render ( error , "json" , { commandPrefix : "bl" , supportsFreeTierRecovery : true } ) ,
130+ ) . error ;
131+ expect ( payload . hint ) . toBeUndefined ( ) ;
132+ } ) ;
133+
134+ test ( "free-tier hint is skipped when the product does not expose usage commands" , ( ) => {
135+ const error = new BailianError ( FREE_TIER_MESSAGE , ExitCode . GENERAL , undefined , {
136+ api : { apiCode : FREE_TIER_ONLY_API_CODE } ,
137+ } ) ;
138+ const payload = JSON . parse (
139+ render ( error , "json" , { commandPrefix : "kscli" , supportsFreeTierRecovery : false } ) ,
140+ ) . error ;
141+ expect ( payload . hint ) . toBeUndefined ( ) ;
142+ expect ( payload . message ) . toBe ( FREE_TIER_MESSAGE ) ;
143+ } ) ;
144+
145+ test ( "free-tier hint appends after an existing hint without replacing it" , ( ) => {
146+ const existing = "Task id: task-demo" ;
147+ const error = new BailianError ( FREE_QUOTA_MESSAGE , ExitCode . GENERAL , existing ) ;
148+ const payload = JSON . parse (
149+ render ( error , "json" , { commandPrefix : "bl" , supportsFreeTierRecovery : true } ) ,
150+ ) . error ;
151+ expect ( payload . hint ?. startsWith ( existing ) ) . toBe ( true ) ;
152+ expect ( payload . hint ) . toContain ( "usage freetier --off" ) ;
153+ } ) ;
0 commit comments