11import assert from 'node:assert/strict' ;
22import { test } from 'vitest' ;
3+ import { AppError } from '../../kernel/errors.ts' ;
34import {
45 isIosFamily ,
56 isMacOs ,
@@ -9,6 +10,7 @@ import {
910 type DeviceKind ,
1011 type DeviceTarget ,
1112} from '../../kernel/device.ts' ;
13+ import type { AndroidAdbExecutor } from '../../platforms/android/adb-executor.ts' ;
1214import {
1315 ANDROID_EMULATOR ,
1416 ANDROID_TV_DEVICE ,
@@ -161,3 +163,58 @@ test('buildPerfResponseData routes the support gate through the perf facet', asy
161163 ) ;
162164 }
163165} ) ;
166+
167+ // Shipped-path routing proof for the sampling body (issue #1188). The support-gate test
168+ // above never reaches sampler selection — it uses no `appBundleId`, so execution returns
169+ // through `applyMissingAppPerfMetrics` before `resolvePerfMetricsSampler`. WITH an app
170+ // bundle, execution clears that guard and hits the facet-owned sampler selection. The
171+ // Android sampler is the ONLY arm that threads `options.androidAdb`, so a scripted adb
172+ // executor is invoked exactly when the Android sampler was selected AND run through the
173+ // shipped code path. Breaking the facet lookup (Android returning a non-`android` tag, or
174+ // the resolver yielding no sampler) skips the Android sampler and fails this test — so the
175+ // suite now covers the dispatch line itself, not only registry parity.
176+ const SAMPLED_ADB_REASON = 'scripted adb unavailable' ;
177+ function makeThrowingAdb ( ) : { adb : AndroidAdbExecutor ; calls : ( ) => number } {
178+ let calls = 0 ;
179+ const adb : AndroidAdbExecutor = async ( ) => {
180+ calls += 1 ;
181+ throw new AppError ( 'COMMAND_FAILED' , SAMPLED_ADB_REASON ) ;
182+ } ;
183+ return { adb, calls : ( ) => calls } ;
184+ }
185+
186+ test ( 'buildPerfResponseData dispatches the Android sampler selected by the facet' , async ( ) => {
187+ for ( const device of SAMPLE_DEVICES . filter ( ( d ) => d . platform === 'android' ) ) {
188+ const { adb, calls } = makeThrowingAdb ( ) ;
189+ const session = makeSession ( `perf-routed-${ device . id } ` , {
190+ device,
191+ appBundleId : 'com.example.app' ,
192+ } ) ;
193+ const data = await buildPerfResponseData ( session , { androidAdb : adb } ) ;
194+
195+ assert . ok ( calls ( ) > 0 , `Android sampler reached through the shipped path for ${ device . id } ` ) ;
196+ for ( const metric of [ 'memory' , 'cpu' , 'fps' ] as const ) {
197+ const entry = data . metrics [ metric ] as { available ?: boolean ; reason ?: string } ;
198+ assert . equal (
199+ entry . available ,
200+ false ,
201+ `${ metric } was sampled (not the base response) for ${ device . id } ` ,
202+ ) ;
203+ assert . equal (
204+ entry . reason ,
205+ SAMPLED_ADB_REASON ,
206+ `${ metric } carries the sampler failure for ${ device . id } ` ,
207+ ) ;
208+ }
209+ }
210+ } ) ;
211+
212+ // The missing-app guard precedes sampler selection, so without an `appBundleId` the facet
213+ // sampler is never consulted and the scripted adb stays untouched.
214+ test ( 'buildPerfResponseData consults the sampler only past the missing-app guard' , async ( ) => {
215+ const { adb, calls } = makeThrowingAdb ( ) ;
216+ await buildPerfResponseData ( makeSession ( 'perf-no-bundle' , { device : ANDROID_EMULATOR } ) , {
217+ androidAdb : adb ,
218+ } ) ;
219+ assert . equal ( calls ( ) , 0 , 'sampler not consulted without an app bundle' ) ;
220+ } ) ;
0 commit comments