@@ -11,6 +11,12 @@ vi.mock('../client-metro-companion.ts', () => ({
1111import { ensureMetroCompanion } from '../client-metro-companion.ts' ;
1212import { prepareMetroRuntime } from '../client-metro.ts' ;
1313
14+ const TEST_BRIDGE_SCOPE = {
15+ tenantId : 'tenant-1' ,
16+ runId : 'run-1' ,
17+ leaseId : 'lease-1' ,
18+ } ;
19+
1420afterEach ( ( ) => {
1521 vi . useRealTimers ( ) ;
1622 vi . clearAllMocks ( ) ;
@@ -96,6 +102,7 @@ test('prepareMetroRuntime starts the local companion only after bridge setup nee
96102 publicBaseUrl : 'https://public.example.test' ,
97103 proxyBaseUrl : 'https://proxy.example.test' ,
98104 proxyBearerToken : 'shared-token' ,
105+ bridgeScope : TEST_BRIDGE_SCOPE ,
99106 metroPort : 8081 ,
100107 reuseExisting : true ,
101108 installDependenciesIfNeeded : false ,
@@ -117,6 +124,7 @@ test('prepareMetroRuntime starts the local companion only after bridge setup nee
117124 projectRoot,
118125 serverBaseUrl : 'https://proxy.example.test' ,
119126 bearerToken : 'shared-token' ,
127+ bridgeScope : TEST_BRIDGE_SCOPE ,
120128 localBaseUrl : 'http://127.0.0.1:8081' ,
121129 launchUrl : undefined ,
122130 profileKey : undefined ,
@@ -126,6 +134,26 @@ test('prepareMetroRuntime starts the local companion only after bridge setup nee
126134 assert . equal ( fetchMock . mock . calls . length , 3 ) ;
127135 assert . equal ( fetchMock . mock . calls [ 1 ] ?. [ 0 ] , 'https://proxy.example.test/api/metro/bridge' ) ;
128136 assert . equal ( fetchMock . mock . calls [ 2 ] ?. [ 0 ] , 'https://proxy.example.test/api/metro/bridge' ) ;
137+ assert . deepEqual ( JSON . parse ( String ( fetchMock . mock . calls [ 1 ] ?. [ 1 ] ?. body ) ) , {
138+ tenantId : 'tenant-1' ,
139+ runId : 'run-1' ,
140+ leaseId : 'lease-1' ,
141+ ios_runtime : {
142+ metro_bundle_url :
143+ 'https://public.example.test/index.bundle?platform=ios&dev=true&minify=false' ,
144+ } ,
145+ timeout_ms : 10000 ,
146+ } ) ;
147+ assert . deepEqual ( JSON . parse ( String ( fetchMock . mock . calls [ 2 ] ?. [ 1 ] ?. body ) ) , {
148+ tenantId : 'tenant-1' ,
149+ runId : 'run-1' ,
150+ leaseId : 'lease-1' ,
151+ ios_runtime : {
152+ metro_bundle_url :
153+ 'https://public.example.test/index.bundle?platform=ios&dev=true&minify=false' ,
154+ } ,
155+ timeout_ms : 10000 ,
156+ } ) ;
129157 } finally {
130158 fs . rmSync ( tempRoot , { recursive : true , force : true } ) ;
131159 }
@@ -165,6 +193,7 @@ test('prepareMetroRuntime preserves the initial bridge error if companion startu
165193 publicBaseUrl : 'https://public.example.test' ,
166194 proxyBaseUrl : 'https://proxy.example.test' ,
167195 proxyBearerToken : 'shared-token' ,
196+ bridgeScope : TEST_BRIDGE_SCOPE ,
168197 metroPort : 8081 ,
169198 reuseExisting : true ,
170199 installDependenciesIfNeeded : false ,
@@ -183,6 +212,57 @@ test('prepareMetroRuntime preserves the initial bridge error if companion startu
183212 }
184213} ) ;
185214
215+ test ( 'prepareMetroRuntime fails fast when initial bridge failure is non-retryable' , async ( ) => {
216+ const tempRoot = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , 'agent-device-metro-companion-401-' ) ) ;
217+ const projectRoot = path . join ( tempRoot , 'project' ) ;
218+ fs . mkdirSync ( path . join ( projectRoot , 'node_modules' ) , { recursive : true } ) ;
219+ fs . writeFileSync (
220+ path . join ( projectRoot , 'package.json' ) ,
221+ JSON . stringify ( {
222+ name : 'metro-initial-bridge-non-retryable-test' ,
223+ private : true ,
224+ dependencies : {
225+ 'react-native' : '0.0.0-test' ,
226+ } ,
227+ } ) ,
228+ ) ;
229+
230+ const fetchMock = vi . fn ( ) ;
231+ fetchMock . mockResolvedValueOnce ( {
232+ ok : true ,
233+ status : 200 ,
234+ text : async ( ) => 'packager-status:running' ,
235+ } ) ;
236+ fetchMock . mockResolvedValueOnce ( {
237+ ok : false ,
238+ status : 401 ,
239+ text : async ( ) => JSON . stringify ( { ok : false , error : 'invalid scope' } ) ,
240+ } ) ;
241+ vi . stubGlobal ( 'fetch' , fetchMock ) ;
242+
243+ try {
244+ await assert . rejects (
245+ ( ) =>
246+ prepareMetroRuntime ( {
247+ projectRoot,
248+ publicBaseUrl : 'https://public.example.test' ,
249+ proxyBaseUrl : 'https://proxy.example.test' ,
250+ proxyBearerToken : 'shared-token' ,
251+ bridgeScope : TEST_BRIDGE_SCOPE ,
252+ metroPort : 8081 ,
253+ reuseExisting : true ,
254+ installDependenciesIfNeeded : false ,
255+ probeTimeoutMs : 10 ,
256+ } ) ,
257+ / \/ a p i \/ m e t r o \/ b r i d g e f a i l e d \( 4 0 1 \) / ,
258+ ) ;
259+ assert . equal ( vi . mocked ( ensureMetroCompanion ) . mock . calls . length , 0 ) ;
260+ assert . equal ( fetchMock . mock . calls . length , 2 ) ;
261+ } finally {
262+ fs . rmSync ( tempRoot , { recursive : true , force : true } ) ;
263+ }
264+ } ) ;
265+
186266test ( 'prepareMetroRuntime fails fast on non-retryable bridge errors after companion startup' , async ( ) => {
187267 const tempRoot = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , 'agent-device-metro-companion-401-' ) ) ;
188268 const projectRoot = path . join ( tempRoot , 'project' ) ;
@@ -231,6 +311,7 @@ test('prepareMetroRuntime fails fast on non-retryable bridge errors after compan
231311 publicBaseUrl : 'https://public.example.test' ,
232312 proxyBaseUrl : 'https://proxy.example.test' ,
233313 proxyBearerToken : 'shared-token' ,
314+ bridgeScope : TEST_BRIDGE_SCOPE ,
234315 metroPort : 8081 ,
235316 reuseExisting : true ,
236317 installDependenciesIfNeeded : false ,
@@ -335,6 +416,7 @@ test('prepareMetroRuntime retries malformed retryable bridge responses after com
335416 publicBaseUrl : 'https://public.example.test' ,
336417 proxyBaseUrl : 'https://proxy.example.test' ,
337418 proxyBearerToken : 'shared-token' ,
419+ bridgeScope : TEST_BRIDGE_SCOPE ,
338420 metroPort : 8081 ,
339421 reuseExisting : true ,
340422 installDependenciesIfNeeded : false ,
0 commit comments