@@ -27,6 +27,7 @@ import * as banner from "../../../src/lib/banner.js";
2727import * as auth from "../../../src/lib/db/auth.js" ;
2828// biome-ignore lint/performance/noNamespaceImport: spyOn requires object reference
2929import * as userDb from "../../../src/lib/db/user.js" ;
30+ import { WizardError } from "../../../src/lib/errors.js" ;
3031// biome-ignore lint/performance/noNamespaceImport: spyOn requires object reference
3132import * as fmt from "../../../src/lib/init/formatters.js" ;
3233// biome-ignore lint/performance/noNamespaceImport: spyOn requires object reference
@@ -259,7 +260,11 @@ afterEach(() => {
259260
260261// ── Tests ───────────────────────────────────────────────────────────────────
261262
262- describe ( "runWizard" , ( ) => {
263+ // Guard against tests that accidentally wait for interactive input.
264+ // If a test hangs for 10s it's almost certainly blocked on stdin, not slow I/O.
265+ const TEST_TIMEOUT_MS = 10_000 ;
266+
267+ describe ( "runWizard" , { timeout : TEST_TIMEOUT_MS } , ( ) => {
263268 describe ( "success path" , ( ) => {
264269 test ( "calls formatResult when workflow completes successfully" , async ( ) => {
265270 mockStartResult = { status : "success" , result : { platform : "React" } } ;
@@ -273,23 +278,21 @@ describe("runWizard", () => {
273278 } ) ;
274279
275280 describe ( "TTY check" , ( ) => {
276- test ( "writes error to stderr when not TTY and not --yes" , async ( ) => {
281+ test ( "throws WizardError when not TTY and not --yes" , async ( ) => {
277282 const origIsTTY = process . stdin . isTTY ;
278283 Object . defineProperty ( process . stdin , "isTTY" , {
279284 value : false ,
280285 configurable : true ,
281286 } ) ;
282287
283- await runWizard ( makeOptions ( { yes : false } ) ) ;
288+ await expect ( runWizard ( makeOptions ( { yes : false } ) ) ) . rejects . toThrow (
289+ WizardError
290+ ) ;
284291
285292 Object . defineProperty ( process . stdin , "isTTY" , {
286293 value : origIsTTY ,
287294 configurable : true ,
288295 } ) ;
289-
290- const written = stderrSpy . mock . calls . map ( ( c ) => String ( c [ 0 ] ) ) . join ( "" ) ;
291- expect ( written ) . toContain ( "Interactive mode requires a terminal" ) ;
292- expect ( process . exitCode ) . toBe ( 1 ) ;
293296 } ) ;
294297 } ) ;
295298
@@ -404,12 +407,11 @@ describe("runWizard", () => {
404407 // Advance past the timeout
405408 jest . advanceTimersByTime ( API_TIMEOUT_MS ) ;
406409
407- await promise ;
410+ await expect ( promise ) . rejects . toThrow ( WizardError ) ;
408411
409412 expect ( logErrorSpy ) . toHaveBeenCalled ( ) ;
410413 const errorMsg : string = logErrorSpy . mock . calls [ 0 ] [ 0 ] ;
411414 expect ( errorMsg ) . toContain ( "timed out" ) ;
412- expect ( process . exitCode ) . toBe ( 1 ) ;
413415
414416 jest . useRealTimers ( ) ;
415417 } ) ;
@@ -424,23 +426,21 @@ describe("runWizard", () => {
424426 } ;
425427 getWorkflowSpy . mockReturnValue ( mockWorkflow as any ) ;
426428
427- await runWizard ( makeOptions ( ) ) ;
429+ await expect ( runWizard ( makeOptions ( ) ) ) . rejects . toThrow ( WizardError ) ;
428430
429431 expect ( logErrorSpy ) . toHaveBeenCalledWith ( "Connection refused" ) ;
430432 expect ( cancelSpy ) . toHaveBeenCalledWith ( "Setup failed" ) ;
431- expect ( process . exitCode ) . toBe ( 1 ) ;
432433 } ) ;
433434 } ) ;
434435
435436 describe ( "workflow failure" , ( ) => {
436437 test ( "calls formatError when status is failed" , async ( ) => {
437438 mockStartResult = { status : "failed" , error : "workflow exploded" } ;
438439
439- await runWizard ( makeOptions ( ) ) ;
440+ await expect ( runWizard ( makeOptions ( ) ) ) . rejects . toThrow ( WizardError ) ;
440441
441442 expect ( formatErrorSpy ) . toHaveBeenCalled ( ) ;
442443 expect ( formatResultSpy ) . not . toHaveBeenCalled ( ) ;
443- expect ( process . exitCode ) . toBe ( 1 ) ;
444444 } ) ;
445445 } ) ;
446446
@@ -451,10 +451,9 @@ describe("runWizard", () => {
451451 result : { exitCode : 10 } ,
452452 } ;
453453
454- await runWizard ( makeOptions ( ) ) ;
454+ await expect ( runWizard ( makeOptions ( ) ) ) . rejects . toThrow ( WizardError ) ;
455455
456456 expect ( formatErrorSpy ) . toHaveBeenCalled ( ) ;
457- expect ( process . exitCode ) . toBe ( 1 ) ;
458457 } ) ;
459458 } ) ;
460459
@@ -734,12 +733,11 @@ describe("runWizard", () => {
734733 } ,
735734 } ;
736735
737- await runWizard ( makeOptions ( ) ) ;
736+ await expect ( runWizard ( makeOptions ( ) ) ) . rejects . toThrow ( WizardError ) ;
738737
739738 expect ( logErrorSpy ) . toHaveBeenCalled ( ) ;
740739 const errorMsg : string = logErrorSpy . mock . calls [ 0 ] [ 0 ] ;
741740 expect ( errorMsg ) . toContain ( "alien" ) ;
742- expect ( process . exitCode ) . toBe ( 1 ) ;
743741 } ) ;
744742
745743 test ( "handles missing suspend payload" , async ( ) => {
@@ -749,12 +747,11 @@ describe("runWizard", () => {
749747 steps : { } ,
750748 } ;
751749
752- await runWizard ( makeOptions ( ) ) ;
750+ await expect ( runWizard ( makeOptions ( ) ) ) . rejects . toThrow ( WizardError ) ;
753751
754752 expect ( logErrorSpy ) . toHaveBeenCalled ( ) ;
755753 const errorMsg : string = logErrorSpy . mock . calls [ 0 ] [ 0 ] ;
756754 expect ( errorMsg ) . toContain ( "No suspend payload" ) ;
757- expect ( process . exitCode ) . toBe ( 1 ) ;
758755 } ) ;
759756
760757 test ( "non-WizardCancelledError in catch triggers log.error + cancel" , async ( ) => {
@@ -775,11 +772,10 @@ describe("runWizard", () => {
775772 } ,
776773 } ;
777774
778- await runWizard ( makeOptions ( ) ) ;
775+ await expect ( runWizard ( makeOptions ( ) ) ) . rejects . toThrow ( WizardError ) ;
779776
780777 expect ( logErrorSpy ) . toHaveBeenCalledWith ( "string error" ) ;
781778 expect ( cancelSpy ) . toHaveBeenCalledWith ( "Setup failed" ) ;
782- expect ( process . exitCode ) . toBe ( 1 ) ;
783779 } ) ;
784780
785781 test ( "falls back to result.suspendPayload when step payload missing" , async ( ) => {
@@ -880,12 +876,11 @@ describe("runWizard", () => {
880876 } ;
881877 getWorkflowSpy . mockReturnValue ( badWorkflow as any ) ;
882878
883- await runWizard ( makeOptions ( ) ) ;
879+ await expect ( runWizard ( makeOptions ( ) ) ) . rejects . toThrow ( WizardError ) ;
884880
885881 expect ( logErrorSpy ) . toHaveBeenCalledWith (
886882 "Invalid workflow response: expected object"
887883 ) ;
888- expect ( process . exitCode ) . toBe ( 1 ) ;
889884 } ) ;
890885
891886 test ( "rejects response with invalid status" , async ( ) => {
@@ -900,12 +895,11 @@ describe("runWizard", () => {
900895 } ;
901896 getWorkflowSpy . mockReturnValue ( badWorkflow as any ) ;
902897
903- await runWizard ( makeOptions ( ) ) ;
898+ await expect ( runWizard ( makeOptions ( ) ) ) . rejects . toThrow ( WizardError ) ;
904899
905900 expect ( logErrorSpy ) . toHaveBeenCalledWith (
906901 "Unexpected workflow status: banana"
907902 ) ;
908- expect ( process . exitCode ) . toBe ( 1 ) ;
909903 } ) ;
910904
911905 test ( "rejects null response from startAsync" , async ( ) => {
@@ -918,12 +912,11 @@ describe("runWizard", () => {
918912 } ;
919913 getWorkflowSpy . mockReturnValue ( badWorkflow as any ) ;
920914
921- await runWizard ( makeOptions ( ) ) ;
915+ await expect ( runWizard ( makeOptions ( ) ) ) . rejects . toThrow ( WizardError ) ;
922916
923917 expect ( logErrorSpy ) . toHaveBeenCalledWith (
924918 "Invalid workflow response: expected object"
925919 ) ;
926- expect ( process . exitCode ) . toBe ( 1 ) ;
927920 } ) ;
928921 } ) ;
929922} ) ;
0 commit comments