11import assert from 'node:assert/strict' ;
22import fs from 'node:fs' ;
3+ import http from 'node:http' ;
34import os from 'node:os' ;
45import path from 'node:path' ;
56import { test } from 'vitest' ;
@@ -228,7 +229,7 @@ async function runLeg(leg: ScenarioLeg, legName: string): Promise<DaemonResponse
228229}
229230
230231async function withProxiedWorld < T > (
231- run : ( context : { world : ParityWorld ; proxied : ScenarioLeg } ) => Promise < T > ,
232+ run : ( context : { world : ParityWorld ; proxied : ScenarioLeg ; upstream : http . Server } ) => Promise < T > ,
232233 options : ParityWorldOptions = { } ,
233234) : Promise < T > {
234235 const world = await createParityWorld ( options ) ;
@@ -259,7 +260,7 @@ async function withProxiedWorld<T>(
259260 return responseFromClientError ( error ) ;
260261 }
261262 } ;
262- return await run ( { world, proxied } ) ;
263+ return await run ( { world, proxied, upstream } ) ;
263264 } finally {
264265 await closeLoopbackServer ( proxy ) ;
265266 await closeLoopbackServer ( upstream ) ;
@@ -268,6 +269,39 @@ async function withProxiedWorld<T>(
268269 }
269270}
270271
272+ /**
273+ * Every request the proxy forwards upstream, in order. Registering a second `request`
274+ * listener records without displacing the server's own handler, so the daemon behaves
275+ * exactly as it does untraced.
276+ */
277+ function recordUpstreamRequests ( upstream : http . Server ) : { forwarded : ForwardedRequest [ ] } {
278+ const forwarded : ForwardedRequest [ ] = [ ] ;
279+ upstream . on ( 'request' , ( req , res ) => {
280+ const route = ( req . url ?? '' ) . split ( '?' , 1 ) [ 0 ] ?? '' ;
281+ const entry : ForwardedRequest = { method : req . method ?? '' , route, responseBytes : 0 } ;
282+ const end = res . end . bind ( res ) ;
283+ const write = res . write . bind ( res ) ;
284+ res . write = ( ( chunk : unknown , ...rest : unknown [ ] ) => {
285+ entry . responseBytes += byteLengthOf ( chunk ) ;
286+ return ( write as ( ...args : unknown [ ] ) => boolean ) ( chunk , ...rest ) ;
287+ } ) as typeof res . write ;
288+ res . end = ( ( chunk ?: unknown , ...rest : unknown [ ] ) => {
289+ entry . responseBytes += byteLengthOf ( chunk ) ;
290+ return ( end as ( ...args : unknown [ ] ) => http . ServerResponse ) ( chunk , ...rest ) ;
291+ } ) as typeof res . end ;
292+ forwarded . push ( entry ) ;
293+ } ) ;
294+ return { forwarded } ;
295+ }
296+
297+ type ForwardedRequest = { method : string ; route : string ; responseBytes : number } ;
298+
299+ function byteLengthOf ( chunk : unknown ) : number {
300+ if ( typeof chunk === 'string' ) return Buffer . byteLength ( chunk ) ;
301+ if ( chunk instanceof Uint8Array ) return chunk . byteLength ;
302+ return 0 ;
303+ }
304+
271305type LegRun = { responses : DaemonResponse [ ] ; sessionRoot : string } ;
272306
273307function assertPublishedParity ( direct : LegRun , proxied : LegRun ) : void {
@@ -403,6 +437,65 @@ test(
403437 PARALLEL_PROVIDER_SCENARIO_TIMEOUT_MS ,
404438) ;
405439
440+ /**
441+ * #2198: "the regular-snapshot trace must prove the bridge adds no network round trip and
442+ * transfers only the published response." That was previously read off the RTT benchmark —
443+ * identical response bytes and an unchanged wall-clock slope — which is inference, not proof.
444+ * This asserts it: one `snapshot -i` behind the proxy forwards exactly one upstream request,
445+ * it is the RPC, and the bytes crossing are the published response and nothing else.
446+ *
447+ * The regression it guards is a second forwarded call per snapshot — a bridge, helper or
448+ * admin round trip appearing on the wire, which #2198 forbids outright.
449+ */
450+ test (
451+ 'a proxied snapshot forwards one upstream request and only the published response' ,
452+ {
453+ timeout : PARALLEL_PROVIDER_SCENARIO_TIMEOUT_MS ,
454+ } ,
455+ async ( t ) => {
456+ if ( await skipWhenLoopbackUnavailable ( t ) ) return ;
457+ await withProxiedWorld ( async ( { proxied, upstream } ) => {
458+ const flags = { platform : 'ios' , udid : SIM . id } as const ;
459+ await proxied ( { session : 'default' , command : 'open' , positionals : [ APP ] , flags } ) ;
460+
461+ // Record only the snapshot: the open above is setup and makes its own calls.
462+ const { forwarded } = recordUpstreamRequests ( upstream ) ;
463+ const response = await proxied ( {
464+ session : 'default' ,
465+ command : 'snapshot' ,
466+ positionals : [ ] ,
467+ flags : { snapshotInteractiveOnly : true } ,
468+ } ) ;
469+ assert . equal ( response . ok , true ) ;
470+
471+ // The WHOLE wire conversation, not just the RPC, so any new call of any kind breaks
472+ // this. `GET /health` is the client's own ADR 0006 protocol-compatibility probe on the
473+ // remote path (`ensureRemoteDaemon`); it is not the bridge, and it predates the bridge.
474+ // The bridge itself contributes nothing: no helper, admin or acquisition route appears.
475+ assert . deepEqual (
476+ forwarded . map ( ( entry ) => `${ entry . method } ${ entry . route } ` ) ,
477+ [ 'GET /health' , 'POST /rpc' ] ,
478+ 'a proxied snapshot must cross the wire only as the compat probe and the RPC' ,
479+ ) ;
480+
481+ // Only the published response crosses: the proxy adds no envelope of its own, so the
482+ // bytes the daemon wrote are the bytes the client's response accounts for.
483+ const publishedBytes = Buffer . byteLength ( JSON . stringify ( response ) ) ;
484+ const forwardedBytes = forwarded . at ( - 1 ) ?. responseBytes ?? 0 ;
485+ assert . ok (
486+ forwardedBytes >= publishedBytes ,
487+ `upstream sent ${ forwardedBytes } bytes, less than the published response (${ publishedBytes } )` ,
488+ ) ;
489+ // The JSON-RPC envelope around the published result is small and fixed; anything that
490+ // carried a second payload (an acquired tree, lineage, a runner quality blob) would not fit.
491+ assert . ok (
492+ forwardedBytes < publishedBytes * 2 ,
493+ `upstream sent ${ forwardedBytes } bytes for a ${ publishedBytes } -byte response: more than the published response crossed` ,
494+ ) ;
495+ } ) ;
496+ } ,
497+ ) ;
498+
406499const LEASE_SCOPE = {
407500 tenantId : 'team-a' ,
408501 runId : 'run-a' ,
0 commit comments