11/**
2- * Plain-node verification harness for the HTTP/2 (undici ) transport.
2+ * Plain-node verification harness for the HTTP/2 (node:http2 ) transport.
33 *
44 * Runs OUTSIDE jest against the BUILT package — which is exactly how real
55 * clients consume the SDK, and the only place a `"type": "commonjs"` interop
66 * regression would surface. It proves three things a green smoke run cannot:
77 *
8- * 1. h2 is actually NEGOTIATED (not a silent HTTP/1.1 fallback) — asserted by
9- * reading the TLS socket's ALPN protocol via undici's diagnostics channel .
8+ * 1. h2 is actually NEGOTIATED — asserted by reading the TLS socket's ALPN
9+ * protocol on every `http2.connect` session the SDK opens .
1010 * 2. A success response body parses (exercises Response.json()).
1111 * 3. A non-2xx response REJECTS with a readable error and does NOT crash the
1212 * process — the exact failure mode of the old got adapter on a 401.
1313 *
1414 * Usage: RUNLOOP_API_KEY=... [RUNLOOP_BASE_URL=...] node tests/smoketests/scripts/verify-http2.mjs
1515 * Exit code 0 = all checks passed, 1 = a check failed, 2 = misconfigured.
1616 */
17- import diagnostics_channel from 'node:diagnostics_channel ' ;
17+ import http2 from 'node:http2 ' ;
1818import { createRequire } from 'node:module' ;
1919
20+ // Capture the negotiated ALPN protocol for every H2 session the transport opens.
21+ // `node:http2` is a singleton module shared between this ESM harness and the CJS
22+ // dist bundle, so wrapping `connect` here instruments the SDK's own sessions.
23+ // Must be installed BEFORE the SDK is required.
24+ const alpnSeen = [ ] ;
25+ let connectCount = 0 ;
26+ const realConnect = http2 . connect ;
27+ http2 . connect = function ( ...args ) {
28+ connectCount ++ ;
29+ const session = realConnect . apply ( this , args ) ;
30+ session . on ( 'connect' , ( ) => {
31+ const proto = session . socket ?. alpnProtocol ;
32+ if ( proto ) alpnSeen . push ( proto ) ;
33+ } ) ;
34+ return session ;
35+ } ;
36+
2037const require = createRequire ( import . meta. url ) ;
2138const distPath = new URL ( '../../../dist/index.js' , import . meta. url ) . pathname ;
2239const { Runloop } = require ( distPath ) ;
@@ -28,17 +45,6 @@ if (!apiKey) {
2845 process . exit ( 2 ) ;
2946}
3047
31- // Capture the negotiated ALPN protocol for every undici connection. Channels are
32- // keyed by name globally, so this catches the SDK's own undici regardless of
33- // which module instance created the connection.
34- const alpnSeen = [ ] ;
35- let connectCount = 0 ;
36- diagnostics_channel . subscribe ( 'undici:client:connected' , ( msg ) => {
37- connectCount ++ ;
38- const proto = msg ?. socket ?. alpnProtocol ;
39- if ( proto ) alpnSeen . push ( proto ) ;
40- } ) ;
41-
4248let failures = 0 ;
4349const check = ( cond , label ) => {
4450 console . log ( `${ cond ? 'PASS' : 'FAIL' } : ${ label } ` ) ;
8086
8187// ── Pass D: HTTP/2 multiplexing — many concurrent requests reuse few connections ──
8288// The whole point of the bounded H2 pool: N concurrent requests share a small number of
83- // TLS sessions instead of one connection per request. Default config (pipelining=1) or the
84- // pre-fix undici Agent would open ~N connections here.
89+ // TLS sessions instead of one connection per request. The pool eagerly opens
90+ // minConnections (4) sessions and only scales beyond that under stream saturation,
91+ // so a fresh client serving 25 concurrent requests should open exactly 4.
8592try {
8693 const N = 25 ;
8794 const before = connectCount ;
0 commit comments