@@ -538,7 +538,7 @@ test('snapshotAndroid keeps daemon-session helper alive for reuse until session
538538 ) ;
539539} ) ;
540540
541- test ( 'a borrowed capture releases a helper session it had to start' , async ( ) => {
541+ test ( 'a transient capture releases a helper session it had to start' , async ( ) => {
542542 const adbCalls : ( readonly string [ ] ) [ ] = [ ] ;
543543 const spawnArgs : ( readonly string [ ] ) [ ] = [ ] ;
544544 const processes : FakeAndroidProcess [ ] = [ ] ;
@@ -551,7 +551,7 @@ test('a borrowed capture releases a helper session it had to start', async () =>
551551 await snapshotAndroid ( device , {
552552 helperAdb : provider ,
553553 helperArtifact,
554- helperSessionScope : 'borrow' ,
554+ transient : openSettleWindow ( ) ,
555555 } ) ;
556556
557557 assert . equal ( spawnArgs . length , 1 ) ;
@@ -562,7 +562,7 @@ test('a borrowed capture releases a helper session it had to start', async () =>
562562 ) ;
563563} ) ;
564564
565- test ( 'a borrowed capture leaves a warm daemon-session helper running' , async ( ) => {
565+ test ( 'a transient capture leaves a warm daemon-session helper running' , async ( ) => {
566566 const adbCalls : ( readonly string [ ] ) [ ] = [ ] ;
567567 const spawnArgs : ( readonly string [ ] ) [ ] = [ ] ;
568568 const processes : FakeAndroidProcess [ ] = [ ] ;
@@ -580,7 +580,7 @@ test('a borrowed capture leaves a warm daemon-session helper running', async ()
580580 const borrowed = await snapshotAndroid ( device , {
581581 helperAdb : provider ,
582582 helperArtifact,
583- helperSessionScope : 'borrow' ,
583+ transient : openSettleWindow ( ) ,
584584 } ) ;
585585
586586 assert . equal ( borrowed . androidSnapshot . helperSessionReused , true ) ;
@@ -662,7 +662,7 @@ test('snapshotAndroid retires content-invalid daemon helper before the next requ
662662 ) ;
663663} ) ;
664664
665- test ( 'a borrowed capture reports its content verdict without retiring the warm helper' , async ( ) => {
665+ test ( 'a transient capture reports its content verdict without retiring the warm helper' , async ( ) => {
666666 const adbCalls : ( readonly string [ ] ) [ ] = [ ] ;
667667 const spawnArgs : ( readonly string [ ] ) [ ] = [ ] ;
668668 const processes : FakeAndroidProcess [ ] = [ ] ;
@@ -682,7 +682,7 @@ test('a borrowed capture reports its content verdict without retiring the warm h
682682 } ) ;
683683
684684 await assert . rejects (
685- snapshotAndroid ( device , { helperAdb : provider , helperArtifact, helperSessionScope : 'borrow' } ) ,
685+ snapshotAndroid ( device , { helperAdb : provider , helperArtifact, transient : openSettleWindow ( ) } ) ,
686686 ( error : unknown ) => isUnreadableCaptureContentError ( error ) ,
687687 ) ;
688688
@@ -691,6 +691,90 @@ test('a borrowed capture reports its content verdict without retiring the warm h
691691 assert . equal ( spawnArgs . length , 1 ) ;
692692} ) ;
693693
694+ test ( 'a settle window that passes during the helper start never cancels the start' , async ( ) => {
695+ const adbCalls : ( readonly string [ ] ) [ ] = [ ] ;
696+ const spawnArgs : ( readonly string [ ] ) [ ] = [ ] ;
697+ const processes : FakeAndroidProcess [ ] = [ ] ;
698+ const provider = createPersistentSnapshotHelperProvider ( {
699+ calls : adbCalls ,
700+ spawnArgs,
701+ processes,
702+ sessionXml : ( ) => androidSystemWindowOnlyXml ( ) ,
703+ sessionReadyDelayMs : 150 ,
704+ } ) ;
705+
706+ await assert . rejects (
707+ snapshotAndroid ( device , {
708+ helperAdb : provider ,
709+ helperArtifact,
710+ transient : { settleBy : Date . now ( ) + 30 } ,
711+ } ) ,
712+ ( error : unknown ) =>
713+ isUnreadableCaptureContentError ( error ) && ( error as AppError ) . details ?. attempts === 1 ,
714+ ) ;
715+
716+ assert . equal ( spawnArgs . length , 1 ) ;
717+ assert . equal ( processes [ 0 ] ?. killed , false , 'the started helper was not signalled' ) ;
718+ assert . equal ( processes [ 0 ] ?. exitCode , 0 , 'the started helper quit on its own' ) ;
719+ assert . equal ( adbCalls . some ( isHelperRuntimeReset ) , false ) ;
720+ } ) ;
721+
722+ test ( 'a settle window that passes during a warm session capture leaves the session running' , async ( ) => {
723+ const adbCalls : ( readonly string [ ] ) [ ] = [ ] ;
724+ const spawnArgs : ( readonly string [ ] ) [ ] = [ ] ;
725+ const processes : FakeAndroidProcess [ ] = [ ] ;
726+ const provider = createPersistentSnapshotHelperProvider ( {
727+ calls : adbCalls ,
728+ spawnArgs,
729+ processes,
730+ sessionXml : ( _sessionIndex , snapshotCount ) =>
731+ snapshotCount === 1
732+ ? '<hierarchy><node text="warm helper" bounds="[0,0][10,10]" /></hierarchy>'
733+ : androidSystemWindowOnlyXml ( ) ,
734+ captureResponseDelayMs : ( snapshotCount ) => ( snapshotCount === 1 ? 0 : 150 ) ,
735+ } ) ;
736+ await snapshotAndroid ( device , {
737+ helperAdb : provider ,
738+ helperArtifact,
739+ helperSessionScope : 'daemon-session' ,
740+ } ) ;
741+
742+ await assert . rejects (
743+ snapshotAndroid ( device , {
744+ helperAdb : provider ,
745+ helperArtifact,
746+ transient : { settleBy : Date . now ( ) + 30 } ,
747+ } ) ,
748+ ( error : unknown ) =>
749+ isUnreadableCaptureContentError ( error ) && ( error as AppError ) . details ?. attempts === 1 ,
750+ ) ;
751+
752+ assert . equal ( spawnArgs . length , 1 ) ;
753+ assert . equal ( processes [ 0 ] ?. killed , false ) ;
754+ assert . equal ( processes [ 0 ] ?. exitCode , null , 'the session helper is still running' ) ;
755+ assert . equal ( adbCalls . some ( isHelperRuntimeReset ) , false ) ;
756+ } ) ;
757+
758+ test ( 'a transient capture on a device without the current helper installs nothing' , async ( ) => {
759+ const adbCalls : ( readonly string [ ] ) [ ] = [ ] ;
760+ const helperAdb : AndroidAdbExecutor = async ( args ) => {
761+ adbCalls . push ( args ) ;
762+ if ( isHelperVersionProbe ( args ) ) return { exitCode : 1 , stdout : '' , stderr : 'not found' } ;
763+ return { exitCode : 0 , stdout : '' , stderr : '' } ;
764+ } ;
765+
766+ await assert . rejects (
767+ snapshotAndroid ( device , { helperAdb, helperArtifact, transient : openSettleWindow ( ) } ) ,
768+ ( error : unknown ) =>
769+ ( error as AppError ) . details ?. reason === 'android-snapshot-helper-not-current' ,
770+ ) ;
771+
772+ assert . equal (
773+ adbCalls . some ( ( args ) => args . includes ( 'install' ) || args . includes ( 'instrument' ) ) ,
774+ false ,
775+ ) ;
776+ } ) ;
777+
694778test ( 'content-invalid daemon helper retirement force-stops the helper runtime' , async ( ) => {
695779 // Retirement after a content failure is a recovery path, not a release: the helper answered with
696780 // output we could not trust, so the next capture must meet a runtime that was reset. A clean quit
@@ -1514,3 +1598,7 @@ test('buildUiHierarchySnapshot derives hidden content hints from can-scroll-* on
15141598 assert . equal ( scrollArea . hiddenContentAbove , true ) ;
15151599 assert . equal ( scrollArea . hiddenContentBelow , true ) ;
15161600} ) ;
1601+
1602+ function openSettleWindow ( ) : { settleBy : number } {
1603+ return { settleBy : Date . now ( ) + 60_000 } ;
1604+ }
0 commit comments