11import { describe , expect , test } from "bun:test" ;
2+ import { AsyncLocalStorage } from "node:async_hooks" ;
23import { Composio , telemetry } from "@composio/core" ;
34import {
45 BrokerAccountGoneError ,
@@ -6727,6 +6728,19 @@ describe("the key becoming a vendor, and which delete that vendor carries", () =
67276728 * is opened either way, and the assertions are lists of what the vendor TRIED — which is the fact
67286729 * worth pinning, since a flag flipped back would be a try that succeeded in production.
67296730 *
6731+ * AND THE RECORDERS RECORD ONLY THE VENDOR UNDER TEST, because both globals belong to the whole
6732+ * process and `bun test` runs every test file in that one process. Work an EARLIER file left
6733+ * running reaches the same `fetch`: the browser extension's tests leave its activity heartbeat,
6734+ * its connection check and its streamer's reconnect timers behind, and those keep posting to
6735+ * `http://127.0.0.1:…/api/recorder/…` minutes after their own files have finished. One of them
6736+ * landing inside the second-and-a-half the raw client spends retrying below was recorded as an
6737+ * attempt with no key, to a host that is not Composio's, and failed this suite on CI while the
6738+ * vendor itself was behaving. So the body runs inside an `AsyncLocalStorage` scope, which every
6739+ * microtask, timer and promise it starts inherits, and anything calling either global from
6740+ * outside that scope is passed straight through to the real one, untouched and unrecorded, as if
6741+ * this test were not running. The scope holds only while `body` waits the ordinary way, which is
6742+ * why the helper says how it must wait.
6743+ *
67306744 * THE HANDLER HALF WOULD HAVE BEEN A VACUOUS ASSERTION AND IS NOT, which is worth knowing before
67316745 * anybody simplifies it. `registerExitHandlers` guards on an `exitHandlersRegistered` flag held by
67326746 * a MODULE-LEVEL singleton (`src/telemetry/Telemetry.ts:46,315-321`), so it fires at most once per
@@ -6741,7 +6755,15 @@ describe("what constructing the vendor is allowed to do on boot", () => {
67416755 type Attempt = { url : string ; apiKey : string | null } ;
67426756
67436757 /**
6744- * Runs `body` with the network and the process's own listener registry replaced by recorders.
6758+ * Runs `body` with the network and the process's own listener registry replaced by recorders,
6759+ * for `body` and whatever it sets going, and for nothing else in the process.
6760+ *
6761+ * `body` MUST WAIT WITH A PLAIN `await`, NEVER WITH `expect(...).rejects` OR `.resolves`. Bun
6762+ * waits for the promise those are given by running the event loop inside the call, so every
6763+ * other file's timer that falls due meanwhile runs nested in `body`'s scope, inherits it, and is
6764+ * recorded as the vendor's own. That was observed with Bun 1.4.3: with the scope below in place
6765+ * and `expect(broker.listApps()).rejects` still in the test, the extension streamer's reconnect
6766+ * was recorded with no key, its stack running through `run` and this file's `body`.
67456767 *
67466768 * `NODE_ENV` is forced away from `test` for the duration because the SDK's own
67476769 * `shouldSendTelemetry()` short-circuits on `test` and `ci`
@@ -6760,7 +6782,18 @@ describe("what constructing the vendor is allowed to do on boot", () => {
67606782 const memo = telemetry as unknown as { exitHandlersRegistered : boolean } ;
67616783 const realMemo = memo . exitHandlersRegistered ;
67626784
6785+ /*
6786+ * WHOSE CALL IT IS, WHICH IS WHAT MAKES THE RECORDING THIS TEST'S. A fresh scope per run, so
6787+ * `getStore()` is true only in `body`'s own async descendants: the client it constructs, the
6788+ * request it builds, the raw client's retry timers, and the SDK's deferred telemetry. A caller
6789+ * outside it is some other file's leftover work (see the describe's header), and it gets the
6790+ * real global back as though nothing here were installed.
6791+ */
6792+ const vendorUnderTest = new AsyncLocalStorage < true > ( ) ;
6793+
67636794 globalThis . fetch = ( ( input : unknown , init ?: RequestInit ) => {
6795+ if ( ! vendorUnderTest . getStore ( ) )
6796+ return realFetch ( input as Parameters < typeof fetch > [ 0 ] , init ) ;
67646797 const request = input as { url ?: string ; headers ?: HeadersInit } ;
67656798 const headers = new Headers ( init ?. headers ?? request ?. headers ?? { } ) ;
67666799 attempts . push ( {
@@ -6772,8 +6805,10 @@ describe("what constructing the vendor is allowed to do on boot", () => {
67726805 return Promise . reject ( new Error ( "no network in this test" ) ) ;
67736806 } ) as typeof globalThis . fetch ;
67746807
6775- process . on = ( ( event : string ) => {
6776- events . push ( event ) ;
6808+ process . on = ( ( ...registration : Parameters < typeof process . on > ) => {
6809+ if ( ! vendorUnderTest . getStore ( ) )
6810+ return realOn . apply ( process , registration ) ;
6811+ events . push ( String ( registration [ 0 ] ) ) ;
67776812 return process ;
67786813 } ) as typeof process . on ;
67796814
@@ -6792,11 +6827,13 @@ describe("what constructing the vendor is allowed to do on boot", () => {
67926827 memo . exitHandlersRegistered = false ;
67936828
67946829 try {
6795- await body ( ) ;
6796- // One turn of the macrotask queue, because the SDK defers its initialisation metric through
6797- // `queueMicrotask` (`src/telemetry/Telemetry.ts:241-257`) — a `setTimeout` lands after every
6798- // microtask that defer could have queued, so the recorder sees the POST before it is undone.
6799- await new Promise ( ( resolve ) => setTimeout ( resolve , 0 ) ) ;
6830+ await vendorUnderTest . run ( true , async ( ) => {
6831+ await body ( ) ;
6832+ // One turn of the macrotask queue, because the SDK defers its initialisation metric through
6833+ // `queueMicrotask` (`src/telemetry/Telemetry.ts:241-257`) — a `setTimeout` lands after every
6834+ // microtask that defer could have queued, so the recorder sees the POST before it is undone.
6835+ await new Promise ( ( resolve ) => setTimeout ( resolve , 0 ) ) ;
6836+ } ) ;
68006837 } finally {
68016838 globalThis . fetch = realFetch ;
68026839 process . on = realOn ;
@@ -6837,12 +6874,18 @@ describe("what constructing the vendor is allowed to do on boot", () => {
68376874 } ) ;
68386875
68396876 test ( "the key it was given is the key the vendor sends, to Composio's own host" , async ( ) => {
6877+ let refusal : unknown ;
68406878 const { attempts } = await withRecordedTransport ( async ( ) => {
68416879 const { broker } = createComposioClient ( "never-dialled" ) ;
68426880 // It rejects because the transport above rejects; what is under test is the request that was
6843- // built before it did, not the refusal that comes back.
6844- await expect ( broker . listApps ( ) ) . rejects . toThrow ( ) ;
6881+ // built before it did, not the refusal that comes back. Caught with a plain `await` and not
6882+ // `expect(...).rejects` for the reason given on `withRecordedTransport`.
6883+ refusal = await broker . listApps ( ) . then (
6884+ ( ) => undefined ,
6885+ ( error : unknown ) => error ,
6886+ ) ;
68456887 } ) ;
6888+ expect ( refusal ) . toBeInstanceOf ( Error ) ;
68466889
68476890 /*
68486891 * EVERY attempt, not the first one, because the claim worth making is about all of them. The
0 commit comments