@@ -74,49 +74,87 @@ export function mockFetch(fn: FetchMockFn): typeof fetch {
7474 return fn as unknown as typeof fetch ;
7575}
7676
77+ /**
78+ * Module-level lock state for SENTRY_CONFIG_DIR.
79+ *
80+ * We replace process.env with a Proxy that intercepts reads, writes,
81+ * and deletes of SENTRY_CONFIG_DIR. When locked, the proxy returns the
82+ * locked value and silently ignores mutations. When unlocked, all
83+ * operations pass through to the real env object.
84+ *
85+ * A Proxy is used instead of Object.defineProperty because:
86+ * - configurable:true descriptors can be removed by `delete` (other
87+ * test files do `delete process.env[CONFIG_DIR_ENV_VAR]` in afterEach)
88+ * - configurable:false descriptors cause `delete` to throw in Bun,
89+ * breaking other test files
90+ *
91+ * The Proxy is installed once and persists for the process lifetime.
92+ * Lock/unlock just toggles the module-level state variables.
93+ */
94+ let configDirLocked = false ;
95+ let configDirLockedValue : string | undefined ;
96+ let envProxyInstalled = false ;
97+
98+ function installEnvProxy ( ) : void {
99+ if ( envProxyInstalled ) return ;
100+ const realEnv = process . env ;
101+ process . env = new Proxy ( realEnv , {
102+ get ( target , prop , receiver ) {
103+ if ( prop === "SENTRY_CONFIG_DIR" && configDirLocked ) {
104+ return configDirLockedValue ;
105+ }
106+ return Reflect . get ( target , prop , receiver ) ;
107+ } ,
108+ set ( target , prop , value ) {
109+ if ( prop === "SENTRY_CONFIG_DIR" && configDirLocked ) {
110+ return true ; // Silently ignore
111+ }
112+ return Reflect . set ( target , prop , value ) ;
113+ } ,
114+ deleteProperty ( target , prop ) {
115+ if ( prop === "SENTRY_CONFIG_DIR" && configDirLocked ) {
116+ return true ; // Silently ignore
117+ }
118+ return Reflect . deleteProperty ( target , prop ) ;
119+ } ,
120+ } ) ;
121+ envProxyInstalled = true ;
122+ }
123+
77124/**
78125 * Lock SENTRY_CONFIG_DIR so concurrent test files cannot change it.
79126 *
80127 * Bun runs test files concurrently in a single process, sharing
81- * `process.env` and `globalThis.fetch`. Between any `await` point
82- * inside our test, another file's beforeEach/afterEach can mutate
83- * these globals. The DB singleton auto-invalidates when the config
84- * dir changes, so even a momentary mutation causes getDatabase() to
85- * open the wrong DB and lose auth tokens / defaults.
128+ * `process.env`. Between any `await` point inside our test, another
129+ * file's beforeEach/afterEach can mutate the env var. The DB singleton
130+ * auto-invalidates when the config dir changes, so even a momentary
131+ * mutation causes getDatabase() to open the wrong DB and lose data.
86132 *
87- * Uses Object.defineProperty to make the env var return the locked
88- * value regardless of what other tests write. Call `unlockConfigDir`
89- * in afterEach so other tests can proceed normally.
133+ * Uses a Proxy on process. env that intercepts get/set/delete of the
134+ * config dir env var. When locked, returns the locked value and ignores
135+ * mutations. When unlocked, all operations pass through normally.
90136 *
91137 * @param configDir - The config directory path to lock
92138 * @returns Unlock function to call in afterEach
93139 */
94140export function lockConfigDir ( configDir : string ) : ( ) => void {
95- Object . defineProperty ( process . env , "SENTRY_CONFIG_DIR" , {
96- get ( ) {
97- return configDir ;
98- } ,
99- set ( ) {
100- // Silently ignore writes from other test files
101- } ,
102- configurable : true ,
103- enumerable : true ,
104- } ) ;
141+ installEnvProxy ( ) ;
142+ configDirLocked = true ;
143+ configDirLockedValue = configDir ;
105144
106145 return ( ) => {
107- // Restore normal property behavior
108- delete process . env . SENTRY_CONFIG_DIR ;
109- process . env . SENTRY_CONFIG_DIR = configDir ;
146+ configDirLocked = false ;
110147 } ;
111148}
112149
113150/**
114151 * Lock globalThis.fetch to a mock handler so concurrent test files
115152 * cannot replace it between async boundaries.
116153 *
117- * Uses Object.defineProperty to intercept writes to globalThis.fetch,
118- * similar to lockConfigDir. Call the returned unlock function in
119- * afterEach to restore normal behavior.
154+ * Uses configurable: true since concurrent test files use assignment
155+ * (not delete) for fetch, and other test files (e.g. api-client) need
156+ * to assign their own mock via globalThis.fetch = ... without going
157+ * through this lock.
120158 *
121159 * @param fn - The fetch mock implementation
122160 * @returns Unlock function to call in afterEach
0 commit comments