@@ -1331,6 +1331,167 @@ test('record stop force-kills Android screenrecord when SIGINT fails but process
13311331 ) . toBe ( true ) ;
13321332} ) ;
13331333
1334+ test ( 'record stop warns when Android screenrecord hit the 180s platform limit' , async ( ) => {
1335+ const sessionStore = makeSessionStore ( ) ;
1336+ const sessionName = 'android-screenrecord-limit' ;
1337+ sessionStore . set (
1338+ sessionName ,
1339+ makeSession ( sessionName , {
1340+ platform : 'android' ,
1341+ id : 'emulator-5554' ,
1342+ name : 'Android' ,
1343+ kind : 'device' ,
1344+ booted : true ,
1345+ } ) ,
1346+ ) ;
1347+
1348+ mockRunCmd . mockImplementation ( async ( _cmd , args ) => {
1349+ const command = args . join ( ' ' ) ;
1350+ if (
1351+ / ^ - s e m u l a t o r - 5 5 5 4 s h e l l s c r e e n r e c o r d \/ s d c a r d \/ a g e n t - d e v i c e - r e c o r d i n g - \d + \. m p 4 > \/ d e v \/ n u l l 2 > & 1 & e c h o \$ ! $ / . test (
1352+ command ,
1353+ )
1354+ ) {
1355+ return { stdout : '4321\n' , stderr : '' , exitCode : 0 } ;
1356+ }
1357+ if (
1358+ / ^ - s e m u l a t o r - 5 5 5 4 s h e l l s t a t - c % s \/ s d c a r d \/ a g e n t - d e v i c e - r e c o r d i n g - \d + \. m p 4 $ / . test ( command )
1359+ ) {
1360+ return { stdout : '1024\n' , stderr : '' , exitCode : 0 } ;
1361+ }
1362+ return { stdout : '' , stderr : '' , exitCode : 0 } ;
1363+ } ) ;
1364+
1365+ await runRecordCommand ( {
1366+ sessionStore,
1367+ sessionName,
1368+ positionals : [ 'start' , './android-limit.mp4' ] ,
1369+ } ) ;
1370+
1371+ const recording = sessionStore . get ( sessionName ) ?. recording ;
1372+ if ( recording ) {
1373+ recording . startedAt = Date . now ( ) - 181_000 ;
1374+ }
1375+
1376+ mockRunCmd . mockImplementation ( async ( _cmd , args ) => {
1377+ const command = args . join ( ' ' ) ;
1378+ if ( command === '-s emulator-5554 shell ps -o pid= -p 4321' ) {
1379+ return { stdout : '' , stderr : '' , exitCode : 1 } ;
1380+ }
1381+ if ( command === '-s emulator-5554 shell kill -2 4321' ) {
1382+ return { stdout : '' , stderr : 'No such process' , exitCode : 1 } ;
1383+ }
1384+ if (
1385+ / ^ - s e m u l a t o r - 5 5 5 4 s h e l l s t a t - c % s \/ s d c a r d \/ a g e n t - d e v i c e - r e c o r d i n g - \d + \. m p 4 $ / . test ( command )
1386+ ) {
1387+ return { stdout : '2048\n' , stderr : '' , exitCode : 0 } ;
1388+ }
1389+ return { stdout : '' , stderr : '' , exitCode : 0 } ;
1390+ } ) ;
1391+
1392+ const response = await runRecordCommand ( {
1393+ sessionStore,
1394+ sessionName,
1395+ positionals : [ 'stop' ] ,
1396+ } ) ;
1397+
1398+ expect ( response ?. ok ) . toBe ( true ) ;
1399+ expect ( ( response as any ) . data ?. warning ) . toMatch ( / 1 8 0 s p l a t f o r m l i m i t / ) ;
1400+ } ) ;
1401+
1402+ test ( 'record stop returns multiple Android recording chunks' , async ( ) => {
1403+ const sessionStore = makeSessionStore ( ) ;
1404+ const sessionName = 'android-screenrecord-chunks' ;
1405+ const session = makeSession ( sessionName , {
1406+ platform : 'android' ,
1407+ id : 'emulator-5554' ,
1408+ name : 'Android' ,
1409+ kind : 'device' ,
1410+ booted : true ,
1411+ } ) ;
1412+ session . recording = {
1413+ platform : 'android' ,
1414+ outPath : path . resolve ( './android-long.mp4' ) ,
1415+ startedAt : Date . now ( ) - 172_000 ,
1416+ showTouches : true ,
1417+ gestureEvents : [ { kind : 'tap' , tMs : 120 , x : 90 , y : 180 } ] ,
1418+ remotePath : '/sdcard/agent-device-recording-2.mp4' ,
1419+ remotePid : '4322' ,
1420+ warning :
1421+ 'Android adb screenrecord is capped at 180s, so this recording was split into multiple MP4 chunks.' ,
1422+ chunks : [
1423+ {
1424+ index : 1 ,
1425+ path : path . resolve ( './android-long.mp4' ) ,
1426+ remotePath : '/sdcard/agent-device-recording-1.mp4' ,
1427+ startedAt : Date . now ( ) - 172_000 ,
1428+ stoppedAt : Date . now ( ) - 2_000 ,
1429+ } ,
1430+ {
1431+ index : 2 ,
1432+ path : path . resolve ( './android-long.part-002.mp4' ) ,
1433+ remotePath : '/sdcard/agent-device-recording-2.mp4' ,
1434+ startedAt : Date . now ( ) - 2_000 ,
1435+ } ,
1436+ ] ,
1437+ } ;
1438+ sessionStore . set ( sessionName , session ) ;
1439+
1440+ const adbCommands : string [ ] = [ ] ;
1441+ mockRunCmd . mockImplementation ( async ( _cmd , args ) => {
1442+ const command = args . join ( ' ' ) ;
1443+ adbCommands . push ( command ) ;
1444+ if ( command === '-s emulator-5554 shell ps -o pid= -p 4322' ) {
1445+ return adbCommands . includes ( '-s emulator-5554 shell kill -2 4322' )
1446+ ? { stdout : '' , stderr : '' , exitCode : 1 }
1447+ : { stdout : '4322\n' , stderr : '' , exitCode : 0 } ;
1448+ }
1449+ if ( command === '-s emulator-5554 shell kill -2 4322' ) {
1450+ return { stdout : '' , stderr : '' , exitCode : 0 } ;
1451+ }
1452+ if (
1453+ / ^ - s e m u l a t o r - 5 5 5 4 s h e l l s t a t - c % s \/ s d c a r d \/ a g e n t - d e v i c e - r e c o r d i n g - \d + \. m p 4 $ / . test ( command )
1454+ ) {
1455+ return { stdout : '2048\n' , stderr : '' , exitCode : 0 } ;
1456+ }
1457+ return { stdout : '' , stderr : '' , exitCode : 0 } ;
1458+ } ) ;
1459+
1460+ const response = await runRecordCommand ( {
1461+ sessionStore,
1462+ sessionName,
1463+ positionals : [ 'stop' ] ,
1464+ } ) ;
1465+
1466+ expect ( response ?. ok ) . toBe ( true ) ;
1467+ if ( response ?. ok !== true ) {
1468+ throw new Error ( 'expected successful Android record stop response' ) ;
1469+ }
1470+ expect ( response . data ?. warning ) . toMatch ( / s p l i t i n t o m u l t i p l e M P 4 c h u n k s / ) ;
1471+ expect ( response . data ?. overlayWarning ) . toMatch ( / s k i p p e d f o r c h u n k e d A n d r o i d r e c o r d i n g s / ) ;
1472+ expect ( response . data ?. chunks ) . toEqual ( [
1473+ expect . objectContaining ( { index : 1 , path : path . resolve ( './android-long.mp4' ) } ) ,
1474+ expect . objectContaining ( { index : 2 , path : path . resolve ( './android-long.part-002.mp4' ) } ) ,
1475+ ] ) ;
1476+ expect ( response . data ?. artifacts ) . toEqual (
1477+ expect . arrayContaining ( [
1478+ expect . objectContaining ( { field : 'outPath' , path : path . resolve ( './android-long.mp4' ) } ) ,
1479+ expect . objectContaining ( {
1480+ field : 'chunkPath' ,
1481+ path : path . resolve ( './android-long.part-002.mp4' ) ,
1482+ } ) ,
1483+ ] ) ,
1484+ ) ;
1485+ expect ( adbCommands ) . toEqual (
1486+ expect . arrayContaining ( [
1487+ '-s emulator-5554 pull /sdcard/agent-device-recording-1.mp4 ' +
1488+ path . resolve ( './android-long.mp4' ) ,
1489+ '-s emulator-5554 pull /sdcard/agent-device-recording-2.mp4 ' +
1490+ path . resolve ( './android-long.part-002.mp4' ) ,
1491+ ] ) ,
1492+ ) ;
1493+ } ) ;
1494+
13341495test ( 'record stop keeps iOS simulator video when touch overlay recording was invalidated' , async ( ) => {
13351496 const sessionStore = makeSessionStore ( ) ;
13361497 const sessionName = 'ios-invalidated-recording' ;
0 commit comments