@@ -3,6 +3,7 @@ import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
33import { tmpdir } from "node:os" ;
44import { dirname , join } from "node:path" ;
55import { createProgram , formatApiBody } from "./cli-program.ts" ;
6+ import { ApiError } from "./lib/errors.ts" ;
67import { STANDARD_AGENT_DIRS , EXTRA_REL_PATHS } from "./lib/skill-detection.ts" ;
78
89test ( "registers users as a top-level command" , ( ) => {
@@ -140,7 +141,7 @@ describe("formatApiBody", () => {
140141 } ,
141142 ] ,
142143 } ) ;
143- const result = formatApiBody ( body , false ) ;
144+ const result = formatApiBody ( new ApiError ( 400 , body ) , false ) ;
144145 expect ( result ) . toContain ( "Your plan does not support these features" ) ;
145146 expect ( result ) . toContain ( "Unsupported features: saml, custom_roles" ) ;
146147 } ) ;
@@ -155,7 +156,7 @@ describe("formatApiBody", () => {
155156 } ,
156157 ] ,
157158 } ) ;
158- const result = formatApiBody ( body , false ) ;
159+ const result = formatApiBody ( new ApiError ( 400 , body ) , false ) ;
159160 expect ( result ) . toContain ( "Unknown config key: sesion" ) ;
160161 expect ( result ) . toContain ( "Did you mean: session" ) ;
161162 expect ( result ) . toContain ( "Parameter: sesion" ) ;
@@ -171,7 +172,7 @@ describe("formatApiBody", () => {
171172 } ,
172173 ] ,
173174 } ) ;
174- const result = formatApiBody ( body , false ) ;
175+ const result = formatApiBody ( new ApiError ( 400 , body ) , false ) ;
175176 expect ( result ) . toContain ( "This feature is not enabled on this instance" ) ;
176177 expect ( result ) . toContain ( "Feature: organizations" ) ;
177178 } ) ;
@@ -186,7 +187,7 @@ describe("formatApiBody", () => {
186187 } ,
187188 ] ,
188189 } ) ;
189- const result = formatApiBody ( body , false ) ;
190+ const result = formatApiBody ( new ApiError ( 400 , body ) , false ) ;
190191 expect ( result ) . toContain ( "Invalid value for session.lifetime" ) ;
191192 expect ( result ) . toContain ( "Parameter: session.lifetime" ) ;
192193 } ) ;
@@ -201,7 +202,7 @@ describe("formatApiBody", () => {
201202 } ,
202203 ] ,
203204 } ) ;
204- const result = formatApiBody ( body , false ) ;
205+ const result = formatApiBody ( new ApiError ( 400 , body ) , false ) ;
205206 expect ( result ) . toContain ( "Cannot clear this key" ) ;
206207 expect ( result ) . toContain ( "Parameter: sign_up.mode" ) ;
207208 } ) ;
@@ -216,14 +217,15 @@ describe("formatApiBody", () => {
216217 } ,
217218 ] ,
218219 } ) ;
219- const result = formatApiBody ( body , false ) ;
220+ const result = formatApiBody ( new ApiError ( 400 , body ) , false ) ;
220221 expect ( result ) . toContain ( "Value is not in the allowed set" ) ;
221222 expect ( result ) . toContain ( "Parameter: branding.logo_url" ) ;
222223 } ) ;
223224
224225 // --- Multiple errors ---
226+ // The structured path reads from the first parsed error only.
225227
226- test ( "formats multiple errors joined by newlines " , ( ) => {
228+ test ( "formats multiple errors: surfaces first error with its meta " , ( ) => {
227229 const body = JSON . stringify ( {
228230 errors : [
229231 {
@@ -238,13 +240,9 @@ describe("formatApiBody", () => {
238240 } ,
239241 ] ,
240242 } ) ;
241- const result = formatApiBody ( body , false ) ;
243+ const result = formatApiBody ( new ApiError ( 400 , body ) , false ) ;
242244 expect ( result ) . toContain ( "Invalid session lifetime" ) ;
243- expect ( result ) . toContain ( "Unknown key: bogus" ) ;
244- expect ( result ) . toContain ( "Did you mean: session" ) ;
245- // Two errors separated by newline
246- const lines = result . split ( "\n" ) ;
247- expect ( lines . length ) . toBeGreaterThanOrEqual ( 2 ) ;
245+ expect ( result ) . toContain ( "Parameter: session.lifetime" ) ;
248246 } ) ;
249247
250248 // --- Error without meta ---
@@ -253,32 +251,34 @@ describe("formatApiBody", () => {
253251 const body = JSON . stringify ( {
254252 errors : [ { code : "resource_not_found" , message : "Instance not found" } ] ,
255253 } ) ;
256- const result = formatApiBody ( body , false ) ;
254+ const result = formatApiBody ( new ApiError ( 400 , body ) , false ) ;
257255 expect ( result ) . toBe ( "Instance not found" ) ;
258256 } ) ;
259257
260- // --- Fallback paths ---
258+ // --- Bodies without a Clerk errors array ---
259+ // parseApiBody falls back to truncateBody(body) as the message when there
260+ // is no errors[0], so formatStructuredError returns the truncated body string.
261261
262- test ( "falls back to parsed.error when no errors array" , ( ) => {
262+ test ( "returns truncated body when no errors array (error field only) " , ( ) => {
263263 const body = JSON . stringify ( { error : "Something went wrong" } ) ;
264- const result = formatApiBody ( body , false ) ;
265- expect ( result ) . toBe ( "Something went wrong" ) ;
264+ const result = formatApiBody ( new ApiError ( 400 , body ) , false ) ;
265+ expect ( result ) . toBe ( body ) ;
266266 } ) ;
267267
268- test ( "falls back to parsed.message when no errors array or error field" , ( ) => {
268+ test ( "returns truncated body when no errors array (message field only) " , ( ) => {
269269 const body = JSON . stringify ( { message : "Bad request" } ) ;
270- const result = formatApiBody ( body , false ) ;
271- expect ( result ) . toBe ( "Bad request" ) ;
270+ const result = formatApiBody ( new ApiError ( 400 , body ) , false ) ;
271+ expect ( result ) . toBe ( body ) ;
272272 } ) ;
273273
274274 test ( "truncates non-JSON body over 200 chars" , ( ) => {
275275 const body = "x" . repeat ( 300 ) ;
276- const result = formatApiBody ( body , false ) ;
276+ const result = formatApiBody ( new ApiError ( 400 , body ) , false ) ;
277277 expect ( result ) . toBe ( "x" . repeat ( 200 ) + "..." ) ;
278278 } ) ;
279279
280280 test ( "returns short non-JSON body as-is" , ( ) => {
281- const result = formatApiBody ( "Bad Request" , false ) ;
281+ const result = formatApiBody ( new ApiError ( 400 , "Bad Request" ) , false ) ;
282282 expect ( result ) . toBe ( "Bad Request" ) ;
283283 } ) ;
284284
@@ -287,28 +287,29 @@ describe("formatApiBody", () => {
287287 test ( "verbose mode returns full pretty-printed JSON" , ( ) => {
288288 const obj = { errors : [ { code : "test" , message : "test msg" } ] } ;
289289 const body = JSON . stringify ( obj ) ;
290- const result = formatApiBody ( body , true ) ;
290+ const result = formatApiBody ( new ApiError ( 400 , body ) , true ) ;
291291 expect ( result ) . toBe ( "\n" + JSON . stringify ( obj , null , 2 ) ) ;
292292 } ) ;
293293
294294 test ( "verbose mode returns raw body for non-JSON" , ( ) => {
295- const result = formatApiBody ( "not json" , true ) ;
295+ const result = formatApiBody ( new ApiError ( 400 , "not json" ) , true ) ;
296296 expect ( result ) . toBe ( "\nnot json" ) ;
297297 } ) ;
298298
299299 // --- Edge cases ---
300300
301- test ( "handles empty errors array by falling through " , ( ) => {
301+ test ( "handles empty errors array by returning truncated body " , ( ) => {
302302 const body = JSON . stringify ( { errors : [ ] , message : "fallback" } ) ;
303- const result = formatApiBody ( body , false ) ;
304- expect ( result ) . toBe ( "fallback" ) ;
303+ const result = formatApiBody ( new ApiError ( 400 , body ) , false ) ;
304+ // No errors[0] so parseApiBody falls back to truncateBody(body)
305+ expect ( result ) . toBe ( body ) ;
305306 } ) ;
306307
307308 test ( "handles error with empty meta" , ( ) => {
308309 const body = JSON . stringify ( {
309310 errors : [ { code : "config_validation_error" , message : "Bad value" , meta : { } } ] ,
310311 } ) ;
311- const result = formatApiBody ( body , false ) ;
312+ const result = formatApiBody ( new ApiError ( 400 , body ) , false ) ;
312313 expect ( result ) . toBe ( "Bad value" ) ;
313314 } ) ;
314315
@@ -322,7 +323,7 @@ describe("formatApiBody", () => {
322323 } ,
323324 ] ,
324325 } ) ;
325- const result = formatApiBody ( body , false ) ;
326+ const result = formatApiBody ( new ApiError ( 400 , body ) , false ) ;
326327 expect ( result ) . toBe ( "Plan limitation" ) ;
327328 } ) ;
328329} ) ;
0 commit comments