1+ import { randomUUID } from 'node:crypto' ;
12import type { PlatformRuntimeHost } from '@agent-device/contracts/platform-runtime-operations' ;
23import type {
34 CaptureSnapshotInput ,
45 SnapshotResult ,
56 SnapshotRuntimeAcquiredResult ,
67} from '@agent-device/contracts/snapshot-runtime' ;
78import type {
9+ IosAcquisitionResidue ,
810 IosSnapshotComparisonIdentity ,
911 IosSnapshotLineage ,
1012} from '@agent-device/contracts/ios-snapshot' ;
@@ -55,13 +57,15 @@ export function createAppleSnapshotRoute(
5557 try {
5658 target = await resolveTarget ( device , input . options ! . appBundleId ! , signal ) ;
5759 } catch ( error ) {
60+ signal . throwIfAborted ( ) ;
5861 emitRouteDiagnostic ( 'target-resolution-failed' , device , undefined , error ) ;
5962 return await runFallback (
6063 input ,
6164 fallback ,
6265 { targetId : `${ device . id } :${ input . options ! . appBundleId ! } ` } ,
6366 requestFor ( input ) ,
6467 'target-resolution-failed' ,
68+ [ unknownGenerationResidue ( ) ] ,
6569 ) ;
6670 }
6771 rebaselineGeneration ( target , latestGeneration , disabledGenerations ) ;
@@ -84,10 +88,19 @@ export function createAppleSnapshotRoute(
8488 ...outcome . failure . details ,
8589 } ) ;
8690 }
91+ const fallbackIdentity = await resolveFailureFallbackIdentity (
92+ outcome . failure ,
93+ target ,
94+ device ,
95+ input . options ! . appBundleId ! ,
96+ signal ,
97+ resolveTarget ,
98+ ) ;
8799 return await fallbackAfterFailure (
88100 input ,
89101 fallback ,
90102 target ,
103+ fallbackIdentity ,
91104 request ,
92105 outcome . failure ,
93106 disabledGenerations ,
@@ -108,6 +121,7 @@ export function createAppleSnapshotRoute(
108121 input ,
109122 fallback ,
110123 target ,
124+ { lineage : target , residue : [ ] } ,
111125 request ,
112126 { kind : 'malformed-tree' , code : 'presentation-invariant' } ,
113127 disabledGenerations ,
@@ -132,15 +146,29 @@ function isEligible(device: DeviceInfo, input: CaptureSnapshotInput): boolean {
132146async function fallbackAfterFailure (
133147 input : CaptureSnapshotInput ,
134148 fallback : SnapshotFallback ,
135- target : SimulatorSnapshotTarget ,
149+ failedTarget : SimulatorSnapshotTarget ,
150+ identity : FallbackIdentity ,
136151 request : ReturnType < typeof createIosSnapshotRequest > ,
137152 failure : SnapshotSourceFailure ,
138153 disabledGenerations : Set < string > ,
139154 cause ?: unknown ,
140155) : Promise < SnapshotResult > {
141- disabledGenerations . add ( generationKey ( target ) ) ;
142- emitRouteDiagnostic ( failure . code , { id : target . udid } , target . generation , cause , failure . details ) ;
143- return await runFallback ( input , fallback , target , request , failure . code ) ;
156+ disabledGenerations . add ( generationKey ( failedTarget ) ) ;
157+ emitRouteDiagnostic (
158+ failure . code ,
159+ { id : failedTarget . udid } ,
160+ failedTarget . generation ,
161+ cause ,
162+ failure . details ,
163+ ) ;
164+ return await runFallback (
165+ input ,
166+ fallback ,
167+ identity . lineage ,
168+ request ,
169+ failure . code ,
170+ identity . residue ,
171+ ) ;
144172}
145173
146174async function runFallback (
@@ -149,23 +177,64 @@ async function runFallback(
149177 lineage : IosSnapshotLineage ,
150178 request : ReturnType < typeof createIosSnapshotRequest > ,
151179 reason : string ,
180+ residue : readonly IosAcquisitionResidue [ ] = [ ] ,
152181) : Promise < SnapshotResult > {
153182 const result = await fallback ( input ) ;
154183 const comparisonIdentity : IosSnapshotComparisonIdentity = Object . freeze ( {
155184 producer : 'apple-runner' ,
156185 intent : request . acquisitionIntent ,
157- lineage : Object . freeze ( { ...lineage } ) ,
186+ lineage : Object . freeze ( {
187+ ...( lineage . targetId ? { targetId : lineage . targetId } : { } ) ,
188+ ...( lineage . generation ? { generation : lineage . generation } : { } ) ,
189+ } ) ,
158190 presentationKey : buildIosSnapshotPresentationKey ( request ) ,
159- residue : Object . freeze ( [ { kind : 'fallback-source' , producer : 'apple-runner' } as const ] ) ,
191+ residue : Object . freeze ( [
192+ ...residue ,
193+ { kind : 'fallback-source' , producer : 'apple-runner' } as const ,
194+ ] ) ,
160195 } ) ;
161- const warning = `Simulator AX snapshot unavailable (${ reason } ); used XCTest for this app generation.` ;
196+ const generation = lineage . generation ? 'this app generation' : 'an unverified app generation' ;
197+ const warning = `Simulator AX snapshot unavailable (${ reason } ); used XCTest for ${ generation } .` ;
162198 return {
163199 ...result ,
164200 comparisonIdentity,
165201 warnings : [ ...( result . warnings ?? [ ] ) , warning ] ,
166202 } ;
167203}
168204
205+ type FallbackIdentity = Readonly < {
206+ lineage : IosSnapshotLineage ;
207+ residue : readonly IosAcquisitionResidue [ ] ;
208+ } > ;
209+
210+ async function resolveFailureFallbackIdentity (
211+ failure : SnapshotSourceFailure ,
212+ target : SimulatorSnapshotTarget ,
213+ device : DeviceInfo ,
214+ appBundleId : string ,
215+ signal : AbortSignal ,
216+ resolveTarget : typeof resolveSimulatorSnapshotTarget ,
217+ ) : Promise < FallbackIdentity > {
218+ if ( failure . kind !== 'stale-target' ) return { lineage : target , residue : [ ] } ;
219+ try {
220+ return {
221+ lineage : await resolveTarget ( device , appBundleId , signal ) ,
222+ residue : [ ] ,
223+ } ;
224+ } catch ( error ) {
225+ signal . throwIfAborted ( ) ;
226+ emitRouteDiagnostic ( 'fallback-target-resolution-failed' , device , undefined , error ) ;
227+ return {
228+ lineage : { targetId : target . targetId } ,
229+ residue : [ unknownGenerationResidue ( ) ] ,
230+ } ;
231+ }
232+ }
233+
234+ function unknownGenerationResidue ( ) : IosAcquisitionResidue {
235+ return { kind : 'unknown-generation' , captureId : randomUUID ( ) } ;
236+ }
237+
169238function requestFor ( input : CaptureSnapshotInput ) {
170239 return createIosSnapshotRequest ( {
171240 raw : input . options ?. raw ,
0 commit comments