@@ -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 { CliError , EXIT_CODE , UserAbortError } from "../../lib/errors.ts" ;
6+ import { CliError , EXIT_CODE , PlapiError , UserAbortError } from "../../lib/errors.ts" ;
77
88const mockIsAgent = mock ( ) ;
99let _modeOverride : string | undefined ;
@@ -40,6 +40,8 @@ type DeployApiMockOptions = {
4040 failCreateProductionInstance ?: boolean ;
4141 failDnsVerification ?: boolean ;
4242 failOAuthSave ?: boolean ;
43+ failValidateCloningUnsupportedFeatures ?: string [ ] ;
44+ failCreateProductionInstanceExists ?: boolean ;
4345} ;
4446
4547let mockDeployApiOptions : DeployApiMockOptions = { } ;
@@ -53,6 +55,18 @@ function simulatedDeployApiFailure(step: string): Error {
5355 return new Error ( `Simulated deploy failure: ${ step } .` ) ;
5456}
5557
58+ function simulatedSpecificFailure (
59+ status : number ,
60+ code : string ,
61+ message : string ,
62+ meta ?: Record < string , unknown > ,
63+ ) : PlapiError {
64+ const body = JSON . stringify ( {
65+ errors : [ { code, message, ...( meta ? { meta } : { } ) } ] ,
66+ } ) ;
67+ return PlapiError . fromBody ( status , body , "clerk deploy test mock" ) ;
68+ }
69+
5670mock . module ( "@inquirer/prompts" , ( ) => ( {
5771 ...promptsStubs ,
5872 select : ( ...args : unknown [ ] ) => mockSelect ( ...args ) ,
@@ -85,13 +99,28 @@ mock.module("../../lib/plapi.ts", () => ({
8599mock . module ( "./api.ts" , ( ) => ( {
86100 configureMockDeployApi,
87101 createProductionInstance : ( ...args : unknown [ ] ) => {
102+ if ( mockDeployApiOptions . failCreateProductionInstanceExists ) {
103+ throw simulatedSpecificFailure (
104+ 400 ,
105+ "production_instance_exists" ,
106+ "You can only have one production instance." ,
107+ ) ;
108+ }
88109 const result = mockCreateProductionInstance ( ...args ) ;
89110 if ( mockDeployApiOptions . failCreateProductionInstance ) {
90111 throw simulatedDeployApiFailure ( "production instance creation" ) ;
91112 }
92113 return result ;
93114 } ,
94115 validateCloning : ( ...args : unknown [ ] ) => {
116+ if ( mockDeployApiOptions . failValidateCloningUnsupportedFeatures ) {
117+ throw simulatedSpecificFailure (
118+ 402 ,
119+ "unsupported_subscription_plan_features" ,
120+ "Unsupported plan features" ,
121+ { unsupported_features : mockDeployApiOptions . failValidateCloningUnsupportedFeatures } ,
122+ ) ;
123+ }
95124 const result = mockValidateCloning ( ...args ) ;
96125 if ( mockDeployApiOptions . failValidateCloning ) {
97126 throw simulatedDeployApiFailure ( "cloning validation" ) ;
@@ -1446,5 +1475,66 @@ describe("deploy", () => {
14461475 expect ( err ) . toContain ( "facebook" ) ;
14471476 expect ( err ) . toContain ( "Configure them from the Clerk Dashboard before going live" ) ;
14481477 } ) ;
1478+
1479+ test ( "recovers from production_instance_exists by resuming reconcileExistingDeploy" , async ( ) => {
1480+ _modeOverride = "human" ;
1481+ await linkedProject ( {
1482+ appId : "app_test" ,
1483+ appName : "Test App" ,
1484+ instances : { development : "ins_dev" } ,
1485+ } ) ;
1486+ // First call (resolveDeployContext): no production instance yet.
1487+ // Second call (reloadProductionState after recovery): production instance exists.
1488+ mockFetchApplication
1489+ . mockResolvedValueOnce ( {
1490+ application_id : "app_test" ,
1491+ name : "Test App" ,
1492+ instances : [
1493+ { instance_id : "ins_dev" , environment_type : "development" , publishable_key : "pk_test" } ,
1494+ ] ,
1495+ } )
1496+ . mockResolvedValueOnce ( {
1497+ application_id : "app_test" ,
1498+ name : "Test App" ,
1499+ instances : [
1500+ { instance_id : "ins_dev" , environment_type : "development" , publishable_key : "pk_test" } ,
1501+ {
1502+ instance_id : "ins_prod_existing" ,
1503+ environment_type : "production" ,
1504+ publishable_key : "pk_live" ,
1505+ } ,
1506+ ] ,
1507+ } ) ;
1508+ mockListApplicationDomains . mockResolvedValue ( {
1509+ data : [
1510+ {
1511+ object : "domain" ,
1512+ id : "dmn_existing" ,
1513+ name : "example.com" ,
1514+ is_satellite : false ,
1515+ is_provider_domain : false ,
1516+ frontend_api_url : "https://clerk.example.com" ,
1517+ development_origin : "" ,
1518+ cname_targets : [ ] ,
1519+ created_at : "2026-05-12T00:00:00Z" ,
1520+ updated_at : "2026-05-12T00:00:00Z" ,
1521+ } ,
1522+ ] ,
1523+ total_count : 1 ,
1524+ } ) ;
1525+ mockGetDeployStatus . mockResolvedValue ( { status : "complete" } ) ;
1526+ mockFetchInstanceConfig . mockResolvedValue ( { } ) ;
1527+ mockConfirm . mockResolvedValue ( true ) ;
1528+ mockInput . mockResolvedValueOnce ( "example.com" ) ;
1529+
1530+ captured = captureLog ( ) ;
1531+ await runDeploy ( { testFailCreateProductionInstanceExists : true } ) ;
1532+
1533+ expect ( captured . err ) . toContain (
1534+ "A production instance already exists for this application. Resuming" ,
1535+ ) ;
1536+ // Confirm reconcile path ran (plan renders "Use production domain").
1537+ expect ( captured . err ) . toContain ( "Use production domain example.com" ) ;
1538+ } ) ;
14491539 } ) ;
14501540} ) ;
0 commit comments