@@ -126,6 +126,59 @@ describe('SimulatorService', () => {
126126 expect ( backend . close ) . toHaveBeenCalledTimes ( 1 ) ;
127127 } ) ;
128128
129+ it ( 'reclaims a service-booted device whose session stops mid-boot' , async ( ) => {
130+ const backend = fakeBackend ( [ device ( 'A' , 'Shutdown' ) ] ) ;
131+ let resolveBoot ! : ( ) => void ;
132+ backend . boot . mockImplementation (
133+ ( ) =>
134+ new Promise < void > ( ( resolve ) => {
135+ resolveBoot = resolve ;
136+ } ) ,
137+ ) ;
138+ const service = new SimulatorService ( backend , { idleReclaimMs : 1000 } ) ;
139+
140+ const booting = service . boot ( S1 , 'A' ) ;
141+ // Let boot() claim the device and reach the (still-pending) backend.boot() call.
142+ await vi . advanceTimersByTimeAsync ( 0 ) ;
143+ // The owning session stops before the boot finishes, dropping the not-yet-service-booted claim.
144+ service . releaseSession ( S1 ) ;
145+ resolveBoot ( ) ;
146+ await booting ;
147+
148+ // The device we booted must not be left running untracked: it is re-tracked for reclaim, so it
149+ // is not shut down immediately but is once the idle window elapses (without the fix there is no
150+ // claim, so the booted device is never reclaimed).
151+ expect ( backend . shutdownDevice ) . not . toHaveBeenCalled ( ) ;
152+ await vi . advanceTimersByTimeAsync ( 1000 ) ;
153+ expect ( backend . shutdownDevice ) . toHaveBeenCalledWith ( 'A' ) ;
154+ expect ( service . ownerOf ( 'A' ) ) . toBeUndefined ( ) ;
155+ } ) ;
156+
157+ it ( 'holds the claim until the reclaim shutdown settles' , async ( ) => {
158+ const backend = fakeBackend ( [ device ( 'A' , 'Shutdown' ) ] ) ;
159+ let resolveShutdown ! : ( ) => void ;
160+ backend . shutdownDevice . mockImplementation (
161+ ( ) =>
162+ new Promise < void > ( ( resolve ) => {
163+ resolveShutdown = resolve ;
164+ } ) ,
165+ ) ;
166+ const service = new SimulatorService ( backend , { idleReclaimMs : 1000 } ) ;
167+
168+ await service . boot ( S1 , 'A' ) ;
169+ service . releaseSession ( S1 ) ;
170+ await vi . advanceTimersByTimeAsync ( 1000 ) ; // idle timer fires → shutdown in flight
171+ expect ( backend . shutdownDevice ) . toHaveBeenCalledWith ( 'A' ) ;
172+ // Another session must not grab the udid while its shutdown is still running.
173+ await expect ( service . openUrl ( S2 , 'A' , 'https://example.com' ) ) . rejects . toMatchObject ( {
174+ code : 'conflict' ,
175+ } ) ;
176+ // The device frees only once the shutdown settles.
177+ resolveShutdown ( ) ;
178+ await vi . advanceTimersByTimeAsync ( 0 ) ;
179+ expect ( service . ownerOf ( 'A' ) ) . toBeUndefined ( ) ;
180+ } ) ;
181+
129182 it ( 'frees a device on owner-driven shutdown' , async ( ) => {
130183 const backend = fakeBackend ( [ device ( 'A' , 'Shutdown' ) ] ) ;
131184 const service = new SimulatorService ( backend ) ;
0 commit comments