@@ -3,7 +3,9 @@ import fs from 'node:fs';
33import os from 'node:os' ;
44import path from 'node:path' ;
55import { test } from 'vitest' ;
6+ import { DEFAULT_PROXY_LEASE_TTL_MS } from '../../../src/core/lease-scope.ts' ;
67import { sendToDaemon } from '../../../src/daemon/client/daemon-client.ts' ;
8+ import { LeaseRegistry } from '../../../src/daemon/lease-registry.ts' ;
79import { createDaemonHttpServer } from '../../../src/daemon/server/http-server.ts' ;
810import { resolveSessionRequestLogPath } from '../../../src/daemon/session-store.ts' ;
911import type { DaemonRequest , DaemonResponse } from '../../../src/daemon/types.ts' ;
@@ -91,7 +93,9 @@ function scriptedTree() {
9193 } ;
9294}
9395
94- async function createParityWorld ( ) : Promise < ParityWorld > {
96+ type ParityWorldOptions = { leaseRegistry ?: LeaseRegistry } ;
97+
98+ async function createParityWorld ( options : ParityWorldOptions = { } ) : Promise < ParityWorld > {
9599 const runnerTranscript = createProviderTranscript ( [
96100 {
97101 command : 'ios.runner.snapshot' ,
@@ -107,6 +111,7 @@ async function createParityWorld(): Promise<ParityWorld> {
107111 ] ) ,
108112 } ) ;
109113 const daemon = await createProviderScenarioHarness ( {
114+ ...( options . leaseRegistry ? { leaseRegistry : options . leaseRegistry } : { } ) ,
110115 platformRuntime : true ,
111116 appleRunnerProvider : ( ) =>
112117 createAppleRunnerProviderFromTranscript ( runnerTranscript , 'ios.runner' ) ,
@@ -224,8 +229,9 @@ async function runLeg(leg: ScenarioLeg, legName: string): Promise<DaemonResponse
224229
225230async function withProxiedWorld < T > (
226231 run : ( context : { world : ParityWorld ; proxied : ScenarioLeg } ) => Promise < T > ,
232+ options : ParityWorldOptions = { } ,
227233) : Promise < T > {
228- const world = await createParityWorld ( ) ;
234+ const world = await createParityWorld ( options ) ;
229235 const upstream = await createDaemonHttpServer ( {
230236 token : world . daemon . token ,
231237 handleRequest : world . daemon . handleRequest ,
@@ -396,3 +402,93 @@ test(
396402 } ,
397403 PARALLEL_PROVIDER_SCENARIO_TIMEOUT_MS ,
398404) ;
405+
406+ const LEASE_SCOPE = {
407+ tenantId : 'team-a' ,
408+ runId : 'run-a' ,
409+ clientId : 'client-a' ,
410+ deviceKey : SIM . id ,
411+ leaseBackend : 'ios-simulator' ,
412+ } as const ;
413+
414+ test (
415+ 'Provider-backed integration proxy lease expiry tears the session down and a reacquired lease starts with no comparison state' ,
416+ async ( t ) => {
417+ if ( await skipWhenLoopbackUnavailable ( t , 'daemon proxy parity coverage' ) ) return ;
418+
419+ let now = 1_000_000 ;
420+ const leaseRegistry = new LeaseRegistry ( { now : ( ) => now } ) ;
421+ await withProxiedWorld (
422+ async ( { world, proxied } ) => {
423+ const session = 'leased' ;
424+ const flags = { platform : 'ios' , udid : SIM . id } as const ;
425+ const allocate = async ( ) : Promise < string > => {
426+ const response = await proxied ( {
427+ session,
428+ command : 'lease_allocate' ,
429+ positionals : [ ] ,
430+ flags : { } ,
431+ meta : LEASE_SCOPE ,
432+ } ) ;
433+ assert . equal ( response . ok , true , JSON . stringify ( response ) ) ;
434+ const leaseId = ( response . ok ? response . data : { } ) ?. lease as { leaseId ?: string } ;
435+ assert . equal ( typeof leaseId ?. leaseId , 'string' ) ;
436+ return leaseId . leaseId ! ;
437+ } ;
438+ const run = async (
439+ command : string ,
440+ positionals : string [ ] ,
441+ leaseId : string ,
442+ stepFlags : DaemonRequest [ 'flags' ] = flags ,
443+ ) =>
444+ await proxied ( {
445+ session,
446+ command,
447+ positionals,
448+ flags : stepFlags ,
449+ meta : { ...LEASE_SCOPE , leaseId } ,
450+ } ) ;
451+
452+ const firstLease = await allocate ( ) ;
453+ assert . equal ( ( await run ( 'open' , [ APP ] , firstLease ) ) . ok , true ) ;
454+ assert . equal (
455+ ( await run ( 'snapshot' , [ ] , firstLease , { snapshotInteractiveOnly : true } ) ) . ok ,
456+ true ,
457+ ) ;
458+ assert . equal (
459+ baselineInitialized (
460+ await run ( 'diff' , [ 'snapshot' ] , firstLease , { snapshotInteractiveOnly : true } ) ,
461+ ) ,
462+ false ,
463+ 'the leased session holds comparison state before it expires' ,
464+ ) ;
465+
466+ // The lease lapses without a heartbeat; the next request through the proxy finds it expired.
467+ now += DEFAULT_PROXY_LEASE_TTL_MS + 1 ;
468+ const expired = await run ( 'diff' , [ 'snapshot' ] , firstLease , {
469+ snapshotInteractiveOnly : true ,
470+ } ) ;
471+ assert . equal ( expired . ok , false ) ;
472+ if ( expired . ok ) return ;
473+ assert . equal ( expired . error . code , 'UNAUTHORIZED' ) ;
474+ assert . equal ( expired . error . details ?. reason , 'LEASE_NOT_FOUND' ) ;
475+ assert . equal ( typeof expired . error . hint , 'string' ) ;
476+ assert . equal ( world . daemon . session ( session ) , undefined , 'expiry tears the session down' ) ;
477+
478+ const secondLease = await allocate ( ) ;
479+ assert . notEqual ( secondLease , firstLease ) ;
480+ assert . equal ( ( await run ( 'open' , [ APP ] , secondLease ) ) . ok , true ) ;
481+ assert . equal (
482+ baselineInitialized (
483+ await run ( 'diff' , [ 'snapshot' ] , secondLease , { snapshotInteractiveOnly : true } ) ,
484+ ) ,
485+ true ,
486+ 'a reacquired lease must not compare against the expired session tree' ,
487+ ) ;
488+ assert . equal ( ( await run ( 'close' , [ ] , secondLease , { } ) ) . ok , true ) ;
489+ } ,
490+ { leaseRegistry } ,
491+ ) ;
492+ } ,
493+ PARALLEL_PROVIDER_SCENARIO_TIMEOUT_MS ,
494+ ) ;
0 commit comments