@@ -257,3 +257,100 @@ test('prepareMetroRuntime fails fast on non-retryable bridge errors after compan
257257 fs . rmSync ( tempRoot , { recursive : true , force : true } ) ;
258258 }
259259} ) ;
260+
261+ test ( 'prepareMetroRuntime retries malformed retryable bridge responses after companion startup' , async ( ) => {
262+ const tempRoot = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , 'agent-device-metro-companion-html-' ) ) ;
263+ const projectRoot = path . join ( tempRoot , 'project' ) ;
264+ fs . mkdirSync ( path . join ( projectRoot , 'node_modules' ) , { recursive : true } ) ;
265+ fs . writeFileSync (
266+ path . join ( projectRoot , 'package.json' ) ,
267+ JSON . stringify ( {
268+ name : 'metro-auto-companion-html-test' ,
269+ private : true ,
270+ dependencies : {
271+ 'react-native' : '0.0.0-test' ,
272+ } ,
273+ } ) ,
274+ ) ;
275+
276+ vi . mocked ( ensureMetroCompanion ) . mockResolvedValue ( {
277+ pid : 123 ,
278+ spawned : true ,
279+ statePath : path . join ( projectRoot , '.agent-device' , 'metro-companion.json' ) ,
280+ logPath : path . join ( projectRoot , '.agent-device' , 'metro-companion.log' ) ,
281+ } ) ;
282+
283+ const fetchMock = vi . fn ( ) ;
284+ fetchMock . mockResolvedValueOnce ( {
285+ ok : true ,
286+ status : 200 ,
287+ text : async ( ) => 'packager-status:running' ,
288+ } ) ;
289+ fetchMock . mockResolvedValueOnce ( {
290+ ok : false ,
291+ status : 409 ,
292+ text : async ( ) => JSON . stringify ( { ok : false , error : 'Metro companion is not connected' } ) ,
293+ } ) ;
294+ fetchMock . mockResolvedValueOnce ( {
295+ ok : false ,
296+ status : 503 ,
297+ text : async ( ) => '<html>upstream unavailable</html>' ,
298+ } ) ;
299+ fetchMock . mockResolvedValueOnce ( {
300+ ok : true ,
301+ status : 200 ,
302+ text : async ( ) =>
303+ JSON . stringify ( {
304+ ok : true ,
305+ data : {
306+ enabled : true ,
307+ base_url : 'https://proxy.example.test' ,
308+ status_url : 'https://proxy.example.test/status' ,
309+ bundle_url : 'https://proxy.example.test/index.bundle?platform=ios' ,
310+ ios_runtime : {
311+ metro_bundle_url : 'https://proxy.example.test/index.bundle?platform=ios' ,
312+ } ,
313+ android_runtime : {
314+ metro_bundle_url : 'https://proxy.example.test/index.bundle?platform=android' ,
315+ } ,
316+ upstream : {
317+ bundle_url :
318+ 'https://public.example.test/index.bundle?platform=ios&dev=true&minify=false' ,
319+ } ,
320+ probe : {
321+ reachable : true ,
322+ status_code : 200 ,
323+ latency_ms : 5 ,
324+ detail : 'ok' ,
325+ } ,
326+ } ,
327+ } ) ,
328+ } ) ;
329+ vi . stubGlobal ( 'fetch' , fetchMock ) ;
330+ vi . useFakeTimers ( ) ;
331+
332+ try {
333+ const preparePromise = prepareMetroRuntime ( {
334+ projectRoot,
335+ publicBaseUrl : 'https://public.example.test' ,
336+ proxyBaseUrl : 'https://proxy.example.test' ,
337+ proxyBearerToken : 'shared-token' ,
338+ metroPort : 8081 ,
339+ reuseExisting : true ,
340+ installDependenciesIfNeeded : false ,
341+ probeTimeoutMs : 10 ,
342+ } ) ;
343+
344+ await vi . advanceTimersByTimeAsync ( 1_000 ) ;
345+ const result = await preparePromise ;
346+
347+ assert . equal ( result . bridge ?. enabled , true ) ;
348+ assert . equal (
349+ result . iosRuntime . bundleUrl ,
350+ 'https://proxy.example.test/index.bundle?platform=ios' ,
351+ ) ;
352+ assert . equal ( fetchMock . mock . calls . length , 4 ) ;
353+ } finally {
354+ fs . rmSync ( tempRoot , { recursive : true , force : true } ) ;
355+ }
356+ } ) ;
0 commit comments