@@ -3,7 +3,7 @@ import { mkdtemp, rm } from "node:fs/promises";
33import { join , relative } from "node:path" ;
44import { tmpdir } from "node:os" ;
55import { captureLog , promptsStubs , listageStubs } from "../../test/lib/stubs.ts" ;
6- import { EXIT_CODE , UserAbortError , type CliError } from "../../lib/errors.ts" ;
6+ import { CliError , EXIT_CODE , UserAbortError } from "../../lib/errors.ts" ;
77
88const mockIsAgent = mock ( ) ;
99let _modeOverride : string | undefined ;
@@ -197,6 +197,20 @@ describe("deploy", () => {
197197 return captured . run ( ( ) => deploy ( options ) ) ;
198198 }
199199
200+ async function expectTestApiFailure ( promise : Promise < unknown > , message : string ) : Promise < Error > {
201+ let error : Error | undefined ;
202+ try {
203+ await promise ;
204+ } catch ( caught ) {
205+ error = caught as Error ;
206+ }
207+
208+ expect ( error ) . toBeInstanceOf ( Error ) ;
209+ expect ( error ) . not . toBeInstanceOf ( CliError ) ;
210+ expect ( error ?. message ) . toContain ( message ) ;
211+ return error ! ;
212+ }
213+
200214 async function linkedProject ( profile : Record < string , unknown > = { } ) {
201215 tempDir = await mkdtemp ( join ( tmpdir ( ) , "clerk-deploy-test-" ) ) ;
202216 _setConfigDir ( tempDir ) ;
@@ -857,14 +871,47 @@ describe("deploy", () => {
857871 await linkedProject ( ) ;
858872 mockIsAgent . mockReturnValue ( false ) ;
859873
860- await expect ( runDeploy ( { testFailProductionInstanceCheck : true } ) ) . rejects . toThrow (
874+ await expectTestApiFailure (
875+ runDeploy ( { testFailProductionInstanceCheck : true } ) ,
861876 "Simulated deploy failure: production instance check." ,
862877 ) ;
863878
864- expect ( mockFetchApplication ) . not . toHaveBeenCalled ( ) ;
879+ expect ( mockFetchApplication ) . toHaveBeenCalledWith ( "app_xyz789" ) ;
865880 expect ( mockFetchInstanceConfig ) . not . toHaveBeenCalled ( ) ;
866881 } ) ;
867882
883+ test ( "--test-fail-production-instance-check prints one Failed status in interactive output" , async ( ) => {
884+ await linkedProject ( ) ;
885+ mockIsAgent . mockReturnValue ( false ) ;
886+ stderrSpy = spyOn ( process . stderr , "write" ) . mockImplementation ( ( ) => true ) ;
887+ const originalCi = process . env . CI ;
888+ const originalIsTty = process . stderr . isTTY ;
889+ Object . defineProperty ( process . stderr , "isTTY" , { configurable : true , value : true } ) ;
890+ delete process . env . CI ;
891+
892+ try {
893+ await expectTestApiFailure (
894+ runDeploy ( { testFailProductionInstanceCheck : true } ) ,
895+ "Simulated deploy failure: production instance check." ,
896+ ) ;
897+ } finally {
898+ Object . defineProperty ( process . stderr , "isTTY" , {
899+ configurable : true ,
900+ value : originalIsTty ,
901+ } ) ;
902+ if ( originalCi === undefined ) {
903+ delete process . env . CI ;
904+ } else {
905+ process . env . CI = originalCi ;
906+ }
907+ }
908+
909+ const terminalOutput = stripAnsi (
910+ stderrSpy . mock . calls . map ( ( call : unknown [ ] ) => String ( call [ 0 ] ) ) . join ( "" ) ,
911+ ) ;
912+ expect ( terminalOutput . match ( / \b F a i l e d \b / g) ?? [ ] ) . toHaveLength ( 1 ) ;
913+ } ) ;
914+
868915 test ( "--test-fail-domain-lookup simulates production domain lookup failure" , async ( ) => {
869916 await linkedProject ( ) ;
870917 mockLiveProduction ( {
@@ -873,22 +920,26 @@ describe("deploy", () => {
873920 } ) ;
874921 mockIsAgent . mockReturnValue ( false ) ;
875922
876- await expect ( runDeploy ( { testFailDomainLookup : true } ) ) . rejects . toThrow (
923+ await expectTestApiFailure (
924+ runDeploy ( { testFailDomainLookup : true } ) ,
877925 "Simulated deploy failure: production domain lookup." ,
878926 ) ;
879927
880- expect ( mockListApplicationDomains ) . not . toHaveBeenCalled ( ) ;
928+ expect ( mockListApplicationDomains ) . toHaveBeenCalledWith ( "app_xyz789" ) ;
881929 } ) ;
882930
883931 test ( "--test-fail-validate-cloning simulates cloning validation failure" , async ( ) => {
884932 await linkedProject ( ) ;
885933 mockIsAgent . mockReturnValue ( false ) ;
886934
887- await expect ( runDeploy ( { testFailValidateCloning : true } ) ) . rejects . toThrow (
935+ await expectTestApiFailure (
936+ runDeploy ( { testFailValidateCloning : true } ) ,
888937 "Simulated deploy failure: cloning validation." ,
889938 ) ;
890939
891- expect ( mockValidateCloning ) . not . toHaveBeenCalled ( ) ;
940+ expect ( mockValidateCloning ) . toHaveBeenCalledWith ( "app_xyz789" , {
941+ clone_instance_id : "ins_dev_123" ,
942+ } ) ;
892943 expect ( mockCreateProductionInstance ) . not . toHaveBeenCalled ( ) ;
893944 } ) ;
894945
@@ -898,11 +949,15 @@ describe("deploy", () => {
898949 mockConfirm . mockResolvedValueOnce ( true ) . mockResolvedValueOnce ( true ) ;
899950 mockInput . mockResolvedValueOnce ( "example.com" ) ;
900951
901- await expect ( runDeploy ( { testFailCreateProductionInstance : true } ) ) . rejects . toThrow (
952+ await expectTestApiFailure (
953+ runDeploy ( { testFailCreateProductionInstance : true } ) ,
902954 "Simulated deploy failure: production instance creation." ,
903955 ) ;
904956
905- expect ( mockCreateProductionInstance ) . not . toHaveBeenCalled ( ) ;
957+ expect ( mockCreateProductionInstance ) . toHaveBeenCalledWith ( "app_xyz789" , {
958+ home_url : "example.com" ,
959+ clone_instance_id : "ins_dev_123" ,
960+ } ) ;
906961 } ) ;
907962
908963 test ( "--test-fail-dns-verification simulates DNS verification failure" , async ( ) => {
@@ -920,23 +975,13 @@ describe("deploy", () => {
920975 mockPassword . mockResolvedValueOnce ( "google-secret" ) ;
921976 mockPatchInstanceConfig . mockResolvedValueOnce ( { } ) ;
922977
923- await runDeploy ( { testFailDnsVerification : true } ) ;
924- const err = stripAnsi ( captured . err ) ;
978+ await expectTestApiFailure (
979+ runDeploy ( { testFailDnsVerification : true } ) ,
980+ "Simulated deploy failure: DNS verification." ,
981+ ) ;
925982
926- expect ( mockGetDeployStatus ) . not . toHaveBeenCalled ( ) ;
927- expect ( err ) . toContain ( "DNS propagation can take time" ) ;
928- expect ( err ) . toContain ( "Add the following records at your DNS provider:" ) ;
929- expect ( err ) . toContain ( "Host: clerk.example.com" ) ;
930- expect ( err ) . toContain ( "Value: frontend-api.clerk.services" ) ;
931- expect ( err ) . toContain ( "Skipping DNS verification for now." ) ;
932- expect ( err ) . toContain ( "Saved Google OAuth credentials" ) ;
933- expect ( mockPatchInstanceConfig ) . toHaveBeenCalledWith ( "app_xyz789" , "ins_prod_mock" , {
934- connection_oauth_google : {
935- enabled : true ,
936- client_id : "google-client-id.apps.googleusercontent.com" ,
937- client_secret : "google-secret" ,
938- } ,
939- } ) ;
983+ expect ( mockGetDeployStatus ) . toHaveBeenCalledWith ( "app_xyz789" , "ins_prod_mock" ) ;
984+ expect ( mockPatchInstanceConfig ) . not . toHaveBeenCalled ( ) ;
940985 } ) ;
941986
942987 test ( "--test-fail-oauth-save simulates OAuth credential save failure" , async ( ) => {
@@ -953,11 +998,18 @@ describe("deploy", () => {
953998 mockInput . mockResolvedValueOnce ( "google-client-id.apps.googleusercontent.com" ) ;
954999 mockPassword . mockResolvedValueOnce ( "google-secret" ) ;
9551000
956- await expect ( runDeploy ( { testFailOAuthSave : true } ) ) . rejects . toThrow (
1001+ await expectTestApiFailure (
1002+ runDeploy ( { testFailOAuthSave : true } ) ,
9571003 "Simulated deploy failure: OAuth credential save." ,
9581004 ) ;
9591005
960- expect ( mockPatchInstanceConfig ) . not . toHaveBeenCalled ( ) ;
1006+ expect ( mockPatchInstanceConfig ) . toHaveBeenCalledWith ( "app_xyz789" , "ins_prod_123" , {
1007+ connection_oauth_google : {
1008+ enabled : true ,
1009+ client_id : "google-client-id.apps.googleusercontent.com" ,
1010+ client_secret : "google-secret" ,
1011+ } ,
1012+ } ) ;
9611013 } ) ;
9621014
9631015 test ( "plain deploy resumes DNS verification from live API state" , async ( ) => {
0 commit comments