From 3f33ad80511731f818e76c2bc454f3648764d7e7 Mon Sep 17 00:00:00 2001 From: Sergey Zelenov Date: Tue, 20 Jan 2026 14:56:54 +0100 Subject: [PATCH 01/12] test(NODE-7345): custom vm.Context runner for mocha --- test/tools/runner/vm_runner.js | 207 +++++++++++++++++++++++++++++++++ 1 file changed, 207 insertions(+) create mode 100644 test/tools/runner/vm_runner.js diff --git a/test/tools/runner/vm_runner.js b/test/tools/runner/vm_runner.js new file mode 100644 index 0000000000..9421dc8bcf --- /dev/null +++ b/test/tools/runner/vm_runner.js @@ -0,0 +1,207 @@ +/* eslint-disable no-restricted-globals */ + +const fs = require('node:fs'); +const path = require('node:path'); +const vm = require('node:vm'); +const ts = require('typescript'); +const Mocha = require('mocha'); + +require('ts-node/register'); +require('source-map-support/register'); + +const mocha = new Mocha({ + extension: ['js', 'ts'], + ui: 'test/tools/runner/metadata_ui.js', + recursive: true, + timeout: 60000, + failZero: true, + reporter: 'test/tools/reporter/mongodb_reporter.js', + sort: true, + color: true, + ignore: [ + 'test/integration/node-specific/examples/handler.js', + 'test/integration/node-specific/examples/handler.test.js', + 'test/integration/node-specific/examples/aws_handler.js', + 'test/integration/node-specific/examples/aws_handler.test.js', + 'test/integration/node-specific/examples/setup.js', + 'test/integration/node-specific/examples/transactions.test.js', + 'test/integration/node-specific/examples/versioned_api.js' + ] +}); +mocha.suite.emit('pre-require', global, 'host-context', mocha); + +require('./throw_rejections.cjs'); +require('./chai_addons.ts'); +require('./ee_checker.ts'); + +for (const path of ['./hooks/leak_checker.ts', './hooks/configuration.ts']) { + const mod = require(path); + const hooks = mod.mochaHooks; + const register = (hookName, globalFn) => { + if (hooks[hookName]) { + const list = Array.isArray(hooks[hookName]) ? hooks[hookName] : [hooks[hookName]]; + list.forEach(fn => globalFn(fn)); + } + }; + + register('beforeAll', global.before); + register('afterAll', global.after); + register('beforeEach', global.beforeEach); + register('afterEach', global.afterEach); +} + +let compilerOptions = { module: ts.ModuleKind.CommonJS }; +const tsConfigPath = path.join(__dirname, '../../tsconfig.json'); +const configFile = ts.readConfigFile(tsConfigPath, ts.sys.readFile); +if (!configFile.error) { + const parsedConfig = ts.parseJsonConfigFileContent( + configFile.config, + ts.sys, + path.dirname(tsConfigPath) + ); + compilerOptions = { + ...parsedConfig.options, + module: ts.ModuleKind.CommonJS, + sourceMap: false, + inlineSourceMap: false + }; +} + +const moduleCache = new Map(); + +function createSandboxContext(filename) { + const exportsContainer = {}; + return { + console: console, + AbortController: AbortController, + AbortSignal: AbortSignal, + + context: global.context, + describe: global.describe, + xdescribe: global.xdescribe, + it: global.it, + xit: global.xit, + before: global.before, + after: global.after, + beforeEach: global.beforeEach, + afterEach: global.afterEach, + + exports: exportsContainer, + module: { exports: exportsContainer }, + __filename: filename, + __dirname: path.dirname(filename), + + // Buffer: Buffer, + queueMicrotask: queueMicrotask + }; +} + +function createProxiedRequire(parentPath) { + const parentDir = path.dirname(parentPath); + + return function sandboxRequire(moduleIdentifier) { + if (!moduleIdentifier.startsWith('.')) { + return require(moduleIdentifier); + } + + const absolutePath = path.resolve(parentDir, moduleIdentifier); + + let fullPath; + try { + fullPath = require.resolve(absolutePath); + } catch (e) { + if (e.code === 'MODULE_NOT_FOUND') { + const alternatives = [absolutePath + '.ts', path.join(absolutePath, 'index.ts')]; + + for (const alt of alternatives) { + try { + fullPath = require.resolve(alt); + break; + } catch {} + } + + if (!fullPath) { + return require(moduleIdentifier); + } + } else { + throw e; + } + } + + if (fullPath.includes('node_modules')) { + return require(fullPath); + } + + if (fullPath.endsWith('.ts') || fullPath.endsWith('.js')) { + return loadInSandbox(fullPath); + } + + return require(fullPath); + }; +} + +function loadInSandbox(filepath) { + const realPath = fs.realpathSync(filepath); + + if (moduleCache.has(realPath)) { + return moduleCache.get(realPath); + } + + const content = fs.readFileSync(realPath, 'utf8'); + + const transpiled = ts.transpileModule(content, { + compilerOptions: compilerOptions, + filename: realPath + }); + + const sandbox = createSandboxContext(realPath); + sandbox.require = createProxiedRequire(realPath); + + moduleCache.set(realPath, sandbox.module.exports); + + try { + const script = new vm.Script(transpiled.outputText, { filename: realPath }); + script.runInNewContext(sandbox); + } catch (err) { + console.error(`Error running ${realPath} in sandbox:`, err.message); + throw err; + } + + moduleCache.set(realPath, sandbox.module.exports); + return sandbox.module.exports; +} + +// use it similar to regular mocha: +// mocha --config test/mocha_mongodb.js test/integration +// node test/runner/vm_context.js test/integration +const userArgs = process.argv.slice(2); +const searchTargets = userArgs.length > 0 ? userArgs : ['test']; +const testFiles = searchTargets.flatMap(target => { + try { + const stats = fs.statSync(target); + if (stats.isDirectory()) { + const pattern = path.join(target, '**/*.test.{ts,js}').replace(/\\/g, '/'); + return fs.globSync(pattern); + } + if (stats.isFile()) { + return [target]; + } + } catch { + console.error(`Error: Could not find path "${target}"`); + } + return []; +}); + +if (testFiles.length === 0) { + console.log('No test files found.'); + process.exit(0); +} + +testFiles.forEach(file => { + loadInSandbox(path.resolve(file)); +}); + +console.log('Running Tests...'); +mocha.run(failures => { + process.exitCode = failures ? 1 : 0; +}); From e267e34360f17ac19f93efa411ed07d8a7d10e0a Mon Sep 17 00:00:00 2001 From: Sergey Zelenov Date: Fri, 23 Jan 2026 14:07:48 +0100 Subject: [PATCH 02/12] address review comments --- .evergreen/config.yml | 13 ++ .evergreen/generate_evergreen_tasks.js | 15 ++ .evergreen/run-runtime-independence-tests.sh | 28 +++ package.json | 1 + test/tools/runner/vm_runner.js | 207 ------------------ test/tools/runner/vm_runner.ts | 211 +++++++++++++++++++ 6 files changed, 268 insertions(+), 207 deletions(-) create mode 100644 .evergreen/run-runtime-independence-tests.sh delete mode 100644 test/tools/runner/vm_runner.js create mode 100644 test/tools/runner/vm_runner.ts diff --git a/.evergreen/config.yml b/.evergreen/config.yml index e5f5c0962e..5a98c38b1d 100644 --- a/.evergreen/config.yml +++ b/.evergreen/config.yml @@ -2021,6 +2021,19 @@ tasks: - func: bootstrap mongo-orchestration - func: assume secrets manager role - func: build and test alpine FLE + - name: test-runtime-independence + tags: [] + commands: + - command: expansions.update + type: setup + params: + updates: + - {key: NODE_LTS_VERSION, value: 20.19.0} + - {key: VERSION, value: '7.0'} + - {key: TOPOLOGY, value: replica_set} + - func: install dependencies + - func: bootstrap mongo-orchestration + - func: run tests - name: test-latest-server-noauth tags: - latest diff --git a/.evergreen/generate_evergreen_tasks.js b/.evergreen/generate_evergreen_tasks.js index ff977cdede..c0b2ceea92 100644 --- a/.evergreen/generate_evergreen_tasks.js +++ b/.evergreen/generate_evergreen_tasks.js @@ -649,6 +649,21 @@ SINGLETON_TASKS.push({ ] }); +SINGLETON_TASKS.push({ + name: 'test-runtime-independence', + tags: [], + commands: [ + updateExpansions({ + NODE_LTS_VERSION: LOWEST_LTS, + VERSION: '7.0', + TOPOLOGY: 'replica_set' + }), + { func: 'install dependencies' }, + { func: 'bootstrap mongo-orchestration' }, + { func: 'run tests' } + ] +}); + function addPerformanceTasks() { const makePerfTask = (name, MONGODB_CLIENT_OPTIONS) => ({ name, diff --git a/.evergreen/run-runtime-independence-tests.sh b/.evergreen/run-runtime-independence-tests.sh new file mode 100644 index 0000000000..5cfe2fa324 --- /dev/null +++ b/.evergreen/run-runtime-independence-tests.sh @@ -0,0 +1,28 @@ +#!/bin/bash +# set -o xtrace # Write all commands first to stderr +set -o errexit # Exit the script with error if any of the commands fail + +# Supported/used environment variables: +# AUTH Set to enable authentication. Defaults to "noauth" +# MONGODB_URI Set the suggested connection MONGODB_URI (including credentials and topology info) +# MARCH Machine Architecture. Defaults to lowercase uname -m +# SKIP_DEPS Skip installing dependencies + +AUTH=${AUTH:-noauth} +MONGODB_URI=${MONGODB_URI:-} +SKIP_DEPS=${SKIP_DEPS:-true} + +# run tests +echo "Running $AUTH tests, connecting to $MONGODB_URI" + +if [[ -z "${SKIP_DEPS}" ]]; then + source "${PROJECT_DIRECTORY}/.evergreen/install-dependencies.sh" +else + source $DRIVERS_TOOLS/.evergreen/init-node-and-npm-env.sh +fi + +export AUTH=$AUTH +export MONGODB_API_VERSION=${MONGODB_API_VERSION} +export MONGODB_URI=${MONGODB_URI} + +npx nyc npm run check:runtime-independence diff --git a/package.json b/package.json index 3f7c7f8366..c584b814ae 100644 --- a/package.json +++ b/package.json @@ -157,6 +157,7 @@ "check:csfle": "nyc mocha --config test/mocha_mongodb.js test/integration/client-side-encryption", "check:snappy": "nyc mocha test/unit/assorted/snappy.test.js", "check:x509": "nyc mocha test/manual/x509_auth.test.ts", + "check:runtime-independence": "ts-node test/tools/runner/vm_runner.ts test/integration/change-streams/change_stream.test.ts", "fix:eslint": "npm run check:eslint -- --fix", "prepare": "node etc/prepare.js", "preview:docs": "ts-node etc/docs/preview.ts", diff --git a/test/tools/runner/vm_runner.js b/test/tools/runner/vm_runner.js deleted file mode 100644 index 9421dc8bcf..0000000000 --- a/test/tools/runner/vm_runner.js +++ /dev/null @@ -1,207 +0,0 @@ -/* eslint-disable no-restricted-globals */ - -const fs = require('node:fs'); -const path = require('node:path'); -const vm = require('node:vm'); -const ts = require('typescript'); -const Mocha = require('mocha'); - -require('ts-node/register'); -require('source-map-support/register'); - -const mocha = new Mocha({ - extension: ['js', 'ts'], - ui: 'test/tools/runner/metadata_ui.js', - recursive: true, - timeout: 60000, - failZero: true, - reporter: 'test/tools/reporter/mongodb_reporter.js', - sort: true, - color: true, - ignore: [ - 'test/integration/node-specific/examples/handler.js', - 'test/integration/node-specific/examples/handler.test.js', - 'test/integration/node-specific/examples/aws_handler.js', - 'test/integration/node-specific/examples/aws_handler.test.js', - 'test/integration/node-specific/examples/setup.js', - 'test/integration/node-specific/examples/transactions.test.js', - 'test/integration/node-specific/examples/versioned_api.js' - ] -}); -mocha.suite.emit('pre-require', global, 'host-context', mocha); - -require('./throw_rejections.cjs'); -require('./chai_addons.ts'); -require('./ee_checker.ts'); - -for (const path of ['./hooks/leak_checker.ts', './hooks/configuration.ts']) { - const mod = require(path); - const hooks = mod.mochaHooks; - const register = (hookName, globalFn) => { - if (hooks[hookName]) { - const list = Array.isArray(hooks[hookName]) ? hooks[hookName] : [hooks[hookName]]; - list.forEach(fn => globalFn(fn)); - } - }; - - register('beforeAll', global.before); - register('afterAll', global.after); - register('beforeEach', global.beforeEach); - register('afterEach', global.afterEach); -} - -let compilerOptions = { module: ts.ModuleKind.CommonJS }; -const tsConfigPath = path.join(__dirname, '../../tsconfig.json'); -const configFile = ts.readConfigFile(tsConfigPath, ts.sys.readFile); -if (!configFile.error) { - const parsedConfig = ts.parseJsonConfigFileContent( - configFile.config, - ts.sys, - path.dirname(tsConfigPath) - ); - compilerOptions = { - ...parsedConfig.options, - module: ts.ModuleKind.CommonJS, - sourceMap: false, - inlineSourceMap: false - }; -} - -const moduleCache = new Map(); - -function createSandboxContext(filename) { - const exportsContainer = {}; - return { - console: console, - AbortController: AbortController, - AbortSignal: AbortSignal, - - context: global.context, - describe: global.describe, - xdescribe: global.xdescribe, - it: global.it, - xit: global.xit, - before: global.before, - after: global.after, - beforeEach: global.beforeEach, - afterEach: global.afterEach, - - exports: exportsContainer, - module: { exports: exportsContainer }, - __filename: filename, - __dirname: path.dirname(filename), - - // Buffer: Buffer, - queueMicrotask: queueMicrotask - }; -} - -function createProxiedRequire(parentPath) { - const parentDir = path.dirname(parentPath); - - return function sandboxRequire(moduleIdentifier) { - if (!moduleIdentifier.startsWith('.')) { - return require(moduleIdentifier); - } - - const absolutePath = path.resolve(parentDir, moduleIdentifier); - - let fullPath; - try { - fullPath = require.resolve(absolutePath); - } catch (e) { - if (e.code === 'MODULE_NOT_FOUND') { - const alternatives = [absolutePath + '.ts', path.join(absolutePath, 'index.ts')]; - - for (const alt of alternatives) { - try { - fullPath = require.resolve(alt); - break; - } catch {} - } - - if (!fullPath) { - return require(moduleIdentifier); - } - } else { - throw e; - } - } - - if (fullPath.includes('node_modules')) { - return require(fullPath); - } - - if (fullPath.endsWith('.ts') || fullPath.endsWith('.js')) { - return loadInSandbox(fullPath); - } - - return require(fullPath); - }; -} - -function loadInSandbox(filepath) { - const realPath = fs.realpathSync(filepath); - - if (moduleCache.has(realPath)) { - return moduleCache.get(realPath); - } - - const content = fs.readFileSync(realPath, 'utf8'); - - const transpiled = ts.transpileModule(content, { - compilerOptions: compilerOptions, - filename: realPath - }); - - const sandbox = createSandboxContext(realPath); - sandbox.require = createProxiedRequire(realPath); - - moduleCache.set(realPath, sandbox.module.exports); - - try { - const script = new vm.Script(transpiled.outputText, { filename: realPath }); - script.runInNewContext(sandbox); - } catch (err) { - console.error(`Error running ${realPath} in sandbox:`, err.message); - throw err; - } - - moduleCache.set(realPath, sandbox.module.exports); - return sandbox.module.exports; -} - -// use it similar to regular mocha: -// mocha --config test/mocha_mongodb.js test/integration -// node test/runner/vm_context.js test/integration -const userArgs = process.argv.slice(2); -const searchTargets = userArgs.length > 0 ? userArgs : ['test']; -const testFiles = searchTargets.flatMap(target => { - try { - const stats = fs.statSync(target); - if (stats.isDirectory()) { - const pattern = path.join(target, '**/*.test.{ts,js}').replace(/\\/g, '/'); - return fs.globSync(pattern); - } - if (stats.isFile()) { - return [target]; - } - } catch { - console.error(`Error: Could not find path "${target}"`); - } - return []; -}); - -if (testFiles.length === 0) { - console.log('No test files found.'); - process.exit(0); -} - -testFiles.forEach(file => { - loadInSandbox(path.resolve(file)); -}); - -console.log('Running Tests...'); -mocha.run(failures => { - process.exitCode = failures ? 1 : 0; -}); diff --git a/test/tools/runner/vm_runner.ts b/test/tools/runner/vm_runner.ts new file mode 100644 index 0000000000..4ff9e2fc09 --- /dev/null +++ b/test/tools/runner/vm_runner.ts @@ -0,0 +1,211 @@ +/* eslint-disable no-restricted-globals, @typescript-eslint/no-require-imports */ + +import * as fs from 'node:fs'; +import { isBuiltin } from 'node:module'; +import * as path from 'node:path'; +import * as vm from 'node:vm'; + +import * as Mocha from 'mocha'; +import * as ts from 'typescript'; + +import * as mochaConfiguration from '../../mocha_mongodb'; + +const mocha = new Mocha(mochaConfiguration); +mocha.suite.emit('pre-require', global, 'host-context', mocha); + +// mocha hooks and custom "require" modules needs to be loaded and injected separately +require('./throw_rejections.cjs'); +require('./chai_addons.ts'); +require('./ee_checker.ts'); +for (const path of ['./hooks/leak_checker.ts', './hooks/configuration.ts']) { + const mod = require(path); + const hooks = mod.mochaHooks; + const register = (hookName, globalFn) => { + if (hooks[hookName]) { + const list = Array.isArray(hooks[hookName]) ? hooks[hookName] : [hooks[hookName]]; + list.forEach(fn => globalFn(fn)); + } + }; + + register('beforeAll', global.before); + register('afterAll', global.after); + register('beforeEach', global.beforeEach); + register('afterEach', global.afterEach); +} + +let compilerOptions: ts.CompilerOptions = { module: ts.ModuleKind.CommonJS }; +const tsConfigPath = path.join(__dirname, '../../tsconfig.json'); +const configFile = ts.readConfigFile(tsConfigPath, ts.sys.readFile); +if (!configFile.error) { + const parsedConfig = ts.parseJsonConfigFileContent( + configFile.config, + ts.sys, + path.dirname(tsConfigPath) + ); + compilerOptions = { + ...parsedConfig.options, + module: ts.ModuleKind.CommonJS, + sourceMap: false, + // inline source map for stack traces + inlineSourceMap: true + }; +} + +const moduleCache = new Map(); + +const sandbox = vm.createContext({ + __proto__: null, + + console: console, + AbortController: AbortController, + AbortSignal: AbortSignal, + Date: global.Date, + Error: global.Error, + URL: global.URL, + URLSearchParams: global.URLSearchParams, + queueMicrotask: queueMicrotask, + + process: process, + Buffer: Buffer, + + context: global.context, + describe: global.describe, + xdescribe: global.xdescribe, + it: global.it, + xit: global.xit, + before: global.before, + after: global.after, + beforeEach: global.beforeEach, + afterEach: global.afterEach +}); + +function createProxiedRequire(parentPath: string) { + const parentDir = path.dirname(parentPath); + + return function sandboxRequire(moduleIdentifier: string) { + // allow all code modules be imported by the host environment + if (isBuiltin(moduleIdentifier)) { + return require(moduleIdentifier); + } + + // list of dependencies we want to import from within the sandbox + const sandboxedDependencies = ['bson']; + const isSandboxedDep = sandboxedDependencies.some( + dep => moduleIdentifier === dep || moduleIdentifier.startsWith(`${dep}/`) + ); + if (!moduleIdentifier.startsWith('.') && !isSandboxedDep) { + return require(moduleIdentifier); + } + + // require.resolve throws if module can't be loaded, let it bubble up + const fullPath = require.resolve(moduleIdentifier, { paths: [parentDir] }); + return loadInSandbox(fullPath); + }; +} + +function loadInSandbox(filepath: string) { + const realPath = fs.realpathSync(filepath); + + if (moduleCache.has(realPath)) { + return moduleCache.get(realPath); + } + + // clientmetadata requires package.json to fetch driver's version + if (realPath.endsWith('.json')) { + const jsonContent = JSON.parse(fs.readFileSync(realPath, 'utf8')); + moduleCache.set(realPath, jsonContent); + return jsonContent; + } + + const content = fs.readFileSync(realPath, 'utf8'); + let executableCode: string; + if (realPath.endsWith('.ts')) { + executableCode = ts.transpileModule(content, { + compilerOptions: compilerOptions, + fileName: realPath + }).outputText; + } else { + // .js or .cjs should work just fine + executableCode = content; + } + + const exportsContainer = {}; + const localModule = { exports: exportsContainer }; + const localRequire = createProxiedRequire(realPath); + const filename = realPath; + const dirname = path.dirname(realPath); + + // prevent recursion + moduleCache.set(realPath, localModule.exports); + + try { + const wrapper = `(function(exports, require, module, __filename, __dirname) { + ${executableCode} + })`; + const script = new vm.Script(wrapper, { filename: realPath }); + const fn = script.runInContext(sandbox); + + fn(localModule.exports, localRequire, localModule, filename, dirname); + + const result = localModule.exports; + if (realPath.includes('src/error.ts')) { + for (const [key, value] of Object.entries(result)) { + if (typeof value === 'function' && value.name?.startsWith('Mongo')) { + (sandbox as any)[key] = value; + + // force instanceof to work across contexts by defining custom `instanceof` function + Object.defineProperty(value, Symbol.hasInstance, { + value: (instance: any) => { + return ( + instance && (instance.constructor.name === value.name || instance instanceof value) + ); + } + }); + } + } + } + + moduleCache.set(realPath, result); + + return result; + } catch (err: any) { + moduleCache.delete(realPath); + console.error(`Error running ${realPath} in sandbox:`, err.stack); + throw err; + } +} + +// use it similar to regular mocha: +// mocha --config test/mocha_mongodb.js test/integration +// ts-node test/runner/vm_context.ts test/integration +const userArgs = process.argv.slice(2); +const searchTargets = userArgs.length > 0 ? userArgs : ['test']; +const testFiles = searchTargets.flatMap(target => { + try { + const stats = fs.statSync(target); + if (stats.isDirectory()) { + const pattern = path.join(target, '**/*.test.{ts,js}').replace(/\\/g, '/'); + return fs.globSync(pattern); + } + if (stats.isFile()) { + return [target]; + } + } catch { + console.error(`Error: Could not find path "${target}"`); + } + return []; +}); + +if (testFiles.length === 0) { + console.log('No test files found.'); + process.exit(0); +} + +testFiles.forEach(file => { + loadInSandbox(path.resolve(file)); +}); + +console.log('Running Tests...'); +mocha.run(failures => { + process.exitCode = failures ? 1 : 0; +}); From 8f98008e147008286a76aff3e6ee0d8ac9ebbcab Mon Sep 17 00:00:00 2001 From: Sergey Zelenov Date: Fri, 23 Jan 2026 14:16:16 +0100 Subject: [PATCH 03/12] throw if ts config is missing --- test/tools/runner/vm_runner.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/tools/runner/vm_runner.ts b/test/tools/runner/vm_runner.ts index 4ff9e2fc09..52c276ffe6 100644 --- a/test/tools/runner/vm_runner.ts +++ b/test/tools/runner/vm_runner.ts @@ -49,6 +49,8 @@ if (!configFile.error) { // inline source map for stack traces inlineSourceMap: true }; +} else { + throw new Error('tsconfig is missing'); } const moduleCache = new Map(); From a7b383a3e75b8a98ba37ddd997363406f4845b06 Mon Sep 17 00:00:00 2001 From: Sergey Zelenov Date: Fri, 23 Jan 2026 14:23:10 +0100 Subject: [PATCH 04/12] run tests on evegreen --- .evergreen/config.yml | 9 +++++---- .evergreen/generate_evergreen_tasks.js | 8 ++++---- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/.evergreen/config.yml b/.evergreen/config.yml index 5a98c38b1d..150119cd55 100644 --- a/.evergreen/config.yml +++ b/.evergreen/config.yml @@ -2022,18 +2022,19 @@ tasks: - func: assume secrets manager role - func: build and test alpine FLE - name: test-runtime-independence - tags: [] + tags: + - runtime-independency-tests commands: - command: expansions.update type: setup params: updates: - - {key: NODE_LTS_VERSION, value: 20.19.0} - - {key: VERSION, value: '7.0'} + - {key: NODE_LTS_VERSION, value: '24'} + - {key: VERSION, value: latest} - {key: TOPOLOGY, value: replica_set} - func: install dependencies - func: bootstrap mongo-orchestration - - func: run tests + - func: run runtime independence tests - name: test-latest-server-noauth tags: - latest diff --git a/.evergreen/generate_evergreen_tasks.js b/.evergreen/generate_evergreen_tasks.js index c0b2ceea92..47650b8ef2 100644 --- a/.evergreen/generate_evergreen_tasks.js +++ b/.evergreen/generate_evergreen_tasks.js @@ -651,16 +651,16 @@ SINGLETON_TASKS.push({ SINGLETON_TASKS.push({ name: 'test-runtime-independence', - tags: [], + tags: ['runtime-independency-tests'], commands: [ updateExpansions({ - NODE_LTS_VERSION: LOWEST_LTS, - VERSION: '7.0', + NODE_LTS_VERSION: LATEST_LTS, + VERSION: 'latest', TOPOLOGY: 'replica_set' }), { func: 'install dependencies' }, { func: 'bootstrap mongo-orchestration' }, - { func: 'run tests' } + { func: 'run runtime independence tests' } ] }); From 38ff5981ec7ed31ac2e7075ada15a8da8cb7259e Mon Sep 17 00:00:00 2001 From: Sergey Zelenov Date: Fri, 23 Jan 2026 14:40:09 +0100 Subject: [PATCH 05/12] make names consistent --- .evergreen/config.yml | 4 ++-- .evergreen/generate_evergreen_tasks.js | 4 ++-- ...ndependence-tests.sh => run-runtime-independency-tests.sh} | 2 +- package.json | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) rename .evergreen/{run-runtime-independence-tests.sh => run-runtime-independency-tests.sh} (95%) diff --git a/.evergreen/config.yml b/.evergreen/config.yml index 150119cd55..82c906e00a 100644 --- a/.evergreen/config.yml +++ b/.evergreen/config.yml @@ -2021,7 +2021,7 @@ tasks: - func: bootstrap mongo-orchestration - func: assume secrets manager role - func: build and test alpine FLE - - name: test-runtime-independence + - name: test-runtime-independency tags: - runtime-independency-tests commands: @@ -2034,7 +2034,7 @@ tasks: - {key: TOPOLOGY, value: replica_set} - func: install dependencies - func: bootstrap mongo-orchestration - - func: run runtime independence tests + - func: run runtime independency tests - name: test-latest-server-noauth tags: - latest diff --git a/.evergreen/generate_evergreen_tasks.js b/.evergreen/generate_evergreen_tasks.js index 47650b8ef2..d58dd90552 100644 --- a/.evergreen/generate_evergreen_tasks.js +++ b/.evergreen/generate_evergreen_tasks.js @@ -650,7 +650,7 @@ SINGLETON_TASKS.push({ }); SINGLETON_TASKS.push({ - name: 'test-runtime-independence', + name: 'test-runtime-independency', tags: ['runtime-independency-tests'], commands: [ updateExpansions({ @@ -660,7 +660,7 @@ SINGLETON_TASKS.push({ }), { func: 'install dependencies' }, { func: 'bootstrap mongo-orchestration' }, - { func: 'run runtime independence tests' } + { func: 'run runtime independency tests' } ] }); diff --git a/.evergreen/run-runtime-independence-tests.sh b/.evergreen/run-runtime-independency-tests.sh similarity index 95% rename from .evergreen/run-runtime-independence-tests.sh rename to .evergreen/run-runtime-independency-tests.sh index 5cfe2fa324..7301925188 100644 --- a/.evergreen/run-runtime-independence-tests.sh +++ b/.evergreen/run-runtime-independency-tests.sh @@ -25,4 +25,4 @@ export AUTH=$AUTH export MONGODB_API_VERSION=${MONGODB_API_VERSION} export MONGODB_URI=${MONGODB_URI} -npx nyc npm run check:runtime-independence +npm run check:runtime-independency diff --git a/package.json b/package.json index c584b814ae..04cd99c653 100644 --- a/package.json +++ b/package.json @@ -157,7 +157,7 @@ "check:csfle": "nyc mocha --config test/mocha_mongodb.js test/integration/client-side-encryption", "check:snappy": "nyc mocha test/unit/assorted/snappy.test.js", "check:x509": "nyc mocha test/manual/x509_auth.test.ts", - "check:runtime-independence": "ts-node test/tools/runner/vm_runner.ts test/integration/change-streams/change_stream.test.ts", + "check:runtime-independency": "ts-node test/tools/runner/vm_runner.ts test/integration/change-streams/change_stream.test.ts", "fix:eslint": "npm run check:eslint -- --fix", "prepare": "node etc/prepare.js", "preview:docs": "ts-node etc/docs/preview.ts", From 626e2a593eb5eedc215a0ca061b6f76ebc96d14b Mon Sep 17 00:00:00 2001 From: Sergey Zelenov Date: Fri, 23 Jan 2026 15:42:35 +0100 Subject: [PATCH 06/12] add test runner in evergreen --- .evergreen/config.in.yml | 12 ++++++++++++ .evergreen/config.yml | 13 +++++++++++++ test/tools/runner/vm_runner.ts | 1 + 3 files changed, 26 insertions(+) diff --git a/.evergreen/config.in.yml b/.evergreen/config.in.yml index 2b5fa1eada..dad0e8326f 100644 --- a/.evergreen/config.in.yml +++ b/.evergreen/config.in.yml @@ -113,6 +113,18 @@ functions: args: - .evergreen/run-tests.sh + "run runtime independency tests": + - <<: *assume_secrets_manager_role + - command: subprocess.exec + type: test + params: + add_expansions_to_env: true + working_dir: "src" + timeout_secs: 300 + binary: bash + args: + - .evergreen/run-runtime-independency-tests.sh + "perf send": - command: s3.put params: diff --git a/.evergreen/config.yml b/.evergreen/config.yml index 82c906e00a..c15b2e760c 100644 --- a/.evergreen/config.yml +++ b/.evergreen/config.yml @@ -88,6 +88,19 @@ functions: binary: bash args: - .evergreen/run-tests.sh + run runtime independency tests: + - command: ec2.assume_role + params: + role_arn: ${DRIVERS_SECRETS_ARN} + - command: subprocess.exec + type: test + params: + add_expansions_to_env: true + working_dir: src + timeout_secs: 300 + binary: bash + args: + - .evergreen/run-runtime-independency-tests.sh perf send: - command: s3.put params: diff --git a/test/tools/runner/vm_runner.ts b/test/tools/runner/vm_runner.ts index 52c276ffe6..0bdcd1e56c 100644 --- a/test/tools/runner/vm_runner.ts +++ b/test/tools/runner/vm_runner.ts @@ -66,6 +66,7 @@ const sandbox = vm.createContext({ URL: global.URL, URLSearchParams: global.URLSearchParams, queueMicrotask: queueMicrotask, + performance: global.performance, process: process, Buffer: Buffer, From 1c0af0cbf112f4db66991ef4bab42d49780336cc Mon Sep 17 00:00:00 2001 From: Sergey Zelenov Date: Fri, 23 Jan 2026 18:37:26 +0100 Subject: [PATCH 07/12] add buildvariant to run tests --- .evergreen/config.in.yml | 9 +++++++-- .evergreen/config.yml | 15 ++++++++++----- .evergreen/generate_evergreen_tasks.js | 6 +++--- ...tests.sh => run-runtime-independence-tests.sh} | 2 +- package.json | 2 +- 5 files changed, 22 insertions(+), 12 deletions(-) rename .evergreen/{run-runtime-independency-tests.sh => run-runtime-independence-tests.sh} (96%) diff --git a/.evergreen/config.in.yml b/.evergreen/config.in.yml index dad0e8326f..2eb4a215ff 100644 --- a/.evergreen/config.in.yml +++ b/.evergreen/config.in.yml @@ -113,7 +113,7 @@ functions: args: - .evergreen/run-tests.sh - "run runtime independency tests": + "run runtime independence tests": - <<: *assume_secrets_manager_role - command: subprocess.exec type: test @@ -123,7 +123,7 @@ functions: timeout_secs: 300 binary: bash args: - - .evergreen/run-runtime-independency-tests.sh + - .evergreen/run-runtime-independence-tests.sh "perf send": - command: s3.put @@ -1249,3 +1249,8 @@ buildvariants: run_on: ubuntu2204-small tasks: - .alpine-fle + - name: Runtime Independence Test + display_name: Runtime Independence Test + run_on: ubuntu2204-small + tasks: + - .alpine-fle diff --git a/.evergreen/config.yml b/.evergreen/config.yml index c15b2e760c..758f90fd3d 100644 --- a/.evergreen/config.yml +++ b/.evergreen/config.yml @@ -88,7 +88,7 @@ functions: binary: bash args: - .evergreen/run-tests.sh - run runtime independency tests: + run runtime independence tests: - command: ec2.assume_role params: role_arn: ${DRIVERS_SECRETS_ARN} @@ -100,7 +100,7 @@ functions: timeout_secs: 300 binary: bash args: - - .evergreen/run-runtime-independency-tests.sh + - .evergreen/run-runtime-independence-tests.sh perf send: - command: s3.put params: @@ -2034,9 +2034,9 @@ tasks: - func: bootstrap mongo-orchestration - func: assume secrets manager role - func: build and test alpine FLE - - name: test-runtime-independency + - name: test-runtime-independence tags: - - runtime-independency-tests + - runtime-independence-tests commands: - command: expansions.update type: setup @@ -2047,7 +2047,7 @@ tasks: - {key: TOPOLOGY, value: replica_set} - func: install dependencies - func: bootstrap mongo-orchestration - - func: run runtime independency tests + - func: run runtime independence tests - name: test-latest-server-noauth tags: - latest @@ -2885,6 +2885,11 @@ buildvariants: run_on: ubuntu2204-small tasks: - .alpine-fle + - name: Runtime Independence Test + display_name: Runtime Independence Test + run_on: ubuntu2204-small + tasks: + - .alpine-fle - name: rhel80-large-iron display_name: rhel8 Node20.19.0 run_on: rhel80-large diff --git a/.evergreen/generate_evergreen_tasks.js b/.evergreen/generate_evergreen_tasks.js index d58dd90552..fdd1dd9bd9 100644 --- a/.evergreen/generate_evergreen_tasks.js +++ b/.evergreen/generate_evergreen_tasks.js @@ -650,8 +650,8 @@ SINGLETON_TASKS.push({ }); SINGLETON_TASKS.push({ - name: 'test-runtime-independency', - tags: ['runtime-independency-tests'], + name: 'test-runtime-independence', + tags: ['runtime-independence-tests'], commands: [ updateExpansions({ NODE_LTS_VERSION: LATEST_LTS, @@ -660,7 +660,7 @@ SINGLETON_TASKS.push({ }), { func: 'install dependencies' }, { func: 'bootstrap mongo-orchestration' }, - { func: 'run runtime independency tests' } + { func: 'run runtime independence tests' } ] }); diff --git a/.evergreen/run-runtime-independency-tests.sh b/.evergreen/run-runtime-independence-tests.sh similarity index 96% rename from .evergreen/run-runtime-independency-tests.sh rename to .evergreen/run-runtime-independence-tests.sh index 7301925188..ff75aec814 100644 --- a/.evergreen/run-runtime-independency-tests.sh +++ b/.evergreen/run-runtime-independence-tests.sh @@ -25,4 +25,4 @@ export AUTH=$AUTH export MONGODB_API_VERSION=${MONGODB_API_VERSION} export MONGODB_URI=${MONGODB_URI} -npm run check:runtime-independency +npm run check:runtime-independence diff --git a/package.json b/package.json index 7c33325a54..8a8d7b38af 100644 --- a/package.json +++ b/package.json @@ -154,7 +154,7 @@ "check:csfle": "nyc mocha --config test/mocha_mongodb.js test/integration/client-side-encryption", "check:snappy": "nyc mocha test/unit/assorted/snappy.test.js", "check:x509": "nyc mocha test/manual/x509_auth.test.ts", - "check:runtime-independency": "ts-node test/tools/runner/vm_runner.ts test/integration/change-streams/change_stream.test.ts", + "check:runtime-independence": "ts-node test/tools/runner/vm_runner.ts test/integration/change-streams/change_stream.test.ts", "fix:eslint": "npm run check:eslint -- --fix", "prepare": "node etc/prepare.js", "preview:docs": "ts-node etc/docs/preview.ts", From b5d497ed8125795ca32adefc7ec01164a3334270 Mon Sep 17 00:00:00 2001 From: Sergey Zelenov Date: Fri, 23 Jan 2026 19:31:46 +0100 Subject: [PATCH 08/12] run runtime independence tests --- .evergreen/config.in.yml | 2 +- .evergreen/config.yml | 4 ++-- .evergreen/generate_evergreen_tasks.js | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.evergreen/config.in.yml b/.evergreen/config.in.yml index 2eb4a215ff..cb327275fb 100644 --- a/.evergreen/config.in.yml +++ b/.evergreen/config.in.yml @@ -1253,4 +1253,4 @@ buildvariants: display_name: Runtime Independence Test run_on: ubuntu2204-small tasks: - - .alpine-fle + - .runtime-independence diff --git a/.evergreen/config.yml b/.evergreen/config.yml index 758f90fd3d..f8cb5a6324 100644 --- a/.evergreen/config.yml +++ b/.evergreen/config.yml @@ -2036,7 +2036,7 @@ tasks: - func: build and test alpine FLE - name: test-runtime-independence tags: - - runtime-independence-tests + - runtime-independence commands: - command: expansions.update type: setup @@ -2889,7 +2889,7 @@ buildvariants: display_name: Runtime Independence Test run_on: ubuntu2204-small tasks: - - .alpine-fle + - .runtime-independence - name: rhel80-large-iron display_name: rhel8 Node20.19.0 run_on: rhel80-large diff --git a/.evergreen/generate_evergreen_tasks.js b/.evergreen/generate_evergreen_tasks.js index fdd1dd9bd9..b812611d1b 100644 --- a/.evergreen/generate_evergreen_tasks.js +++ b/.evergreen/generate_evergreen_tasks.js @@ -651,7 +651,7 @@ SINGLETON_TASKS.push({ SINGLETON_TASKS.push({ name: 'test-runtime-independence', - tags: ['runtime-independence-tests'], + tags: ['runtime-independence'], commands: [ updateExpansions({ NODE_LTS_VERSION: LATEST_LTS, From 72896ada924f45829564666dc89b0fd36eccd7d8 Mon Sep 17 00:00:00 2001 From: Sergey Zelenov Date: Mon, 26 Jan 2026 12:27:39 +0100 Subject: [PATCH 09/12] differentiate between src and test files, propagate bson Long --- .../change-streams/change_stream.test.ts | 2 +- test/tools/runner/vm_runner.ts | 48 +++++++++++++------ 2 files changed, 35 insertions(+), 15 deletions(-) diff --git a/test/integration/change-streams/change_stream.test.ts b/test/integration/change-streams/change_stream.test.ts index a7b8dec85c..ce6dd80eb5 100644 --- a/test/integration/change-streams/change_stream.test.ts +++ b/test/integration/change-streams/change_stream.test.ts @@ -1717,7 +1717,7 @@ describe('Change Streams', function () { }); }); - context('when set to false', () => { + context.only('when set to false', () => { it('converts Long values to native numbers', { metadata: { requires: { topology: '!single' } }, test: async function () { diff --git a/test/tools/runner/vm_runner.ts b/test/tools/runner/vm_runner.ts index 0bdcd1e56c..2e1a26a5ee 100644 --- a/test/tools/runner/vm_runner.ts +++ b/test/tools/runner/vm_runner.ts @@ -69,7 +69,6 @@ const sandbox = vm.createContext({ performance: global.performance, process: process, - Buffer: Buffer, context: global.context, describe: global.describe, @@ -114,12 +113,32 @@ function loadInSandbox(filepath: string) { } // clientmetadata requires package.json to fetch driver's version - if (realPath.endsWith('.json')) { + if (realPath.endsWith('package.json')) { const jsonContent = JSON.parse(fs.readFileSync(realPath, 'utf8')); moduleCache.set(realPath, jsonContent); return jsonContent; } + // js-bson is allowed to use Buffer, only ./src/ is not + const isSourceFile = realPath.includes('/src/') || !realPath.includes('node_modules'); + const isTestFile = realPath.includes('.test.ts') || realPath.includes('.test.js'); + + let localBuffer = Buffer; + if (isSourceFile && !isTestFile) { + localBuffer = new Proxy(Buffer, { + get() { + throw new Error( + `Forbidden: 'Buffer' usage is not allowed in source files. Use Uint8Array instead. File: ${realPath}` + ); + }, + construct() { + throw new Error( + `Forbidden: 'Buffer' usage is not allowed in source files. Use Uint8Array instead. File: ${realPath}` + ); + } + }) as any; + } + const content = fs.readFileSync(realPath, 'utf8'); let executableCode: string; if (realPath.endsWith('.ts')) { @@ -142,28 +161,29 @@ function loadInSandbox(filepath: string) { moduleCache.set(realPath, localModule.exports); try { - const wrapper = `(function(exports, require, module, __filename, __dirname) { + const wrapper = `(function(exports, require, module, __filename, __dirname, Buffer) { ${executableCode} })`; const script = new vm.Script(wrapper, { filename: realPath }); const fn = script.runInContext(sandbox); - fn(localModule.exports, localRequire, localModule, filename, dirname); + fn(localModule.exports, localRequire, localModule, filename, dirname, localBuffer); const result = localModule.exports; - if (realPath.includes('src/error.ts')) { - for (const [key, value] of Object.entries(result)) { - if (typeof value === 'function' && value.name?.startsWith('Mongo')) { - (sandbox as any)[key] = value; + const isBSON = realPath.includes('node_modules/bson'); + const isError = realPath.includes('src/error.ts'); + + if (isBSON || isError) { + for (const [key, value] of Object.entries(result)) { + if (typeof value === 'function' && value.name) { // force instanceof to work across contexts by defining custom `instanceof` function Object.defineProperty(value, Symbol.hasInstance, { - value: (instance: any) => { - return ( - instance && (instance.constructor.name === value.name || instance instanceof value) - ); - } + value: (i: any) => i && (i.constructor.name === value.name || i instanceof value) }); + + // also inject into global for easier access in tests + (sandbox as any)[key] = value; } } } @@ -173,7 +193,7 @@ function loadInSandbox(filepath: string) { return result; } catch (err: any) { moduleCache.delete(realPath); - console.error(`Error running ${realPath} in sandbox:`, err.stack); + console.error(`Error running ${realPath} in sandbox:`, err); throw err; } } From 1ea5d927984f5beb327bfa09294514874941c3e0 Mon Sep 17 00:00:00 2001 From: Sergey Zelenov Date: Mon, 26 Jan 2026 13:19:15 +0100 Subject: [PATCH 10/12] allow bson from node_modules to use Buffer --- test/tools/runner/vm_runner.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/tools/runner/vm_runner.ts b/test/tools/runner/vm_runner.ts index 2e1a26a5ee..12eeb4747b 100644 --- a/test/tools/runner/vm_runner.ts +++ b/test/tools/runner/vm_runner.ts @@ -120,7 +120,7 @@ function loadInSandbox(filepath: string) { } // js-bson is allowed to use Buffer, only ./src/ is not - const isSourceFile = realPath.includes('/src/') || !realPath.includes('node_modules'); + const isSourceFile = realPath.includes('/src/') && !realPath.includes('node_modules'); const isTestFile = realPath.includes('.test.ts') || realPath.includes('.test.js'); let localBuffer = Buffer; From 7393381ec9ca35e207c9c573183c11e60f5ce49b Mon Sep 17 00:00:00 2001 From: Sergey Zelenov Date: Mon, 26 Jan 2026 16:16:40 +0100 Subject: [PATCH 11/12] run all change stream tests --- test/integration/change-streams/change_stream.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/change-streams/change_stream.test.ts b/test/integration/change-streams/change_stream.test.ts index ce6dd80eb5..a7b8dec85c 100644 --- a/test/integration/change-streams/change_stream.test.ts +++ b/test/integration/change-streams/change_stream.test.ts @@ -1717,7 +1717,7 @@ describe('Change Streams', function () { }); }); - context.only('when set to false', () => { + context('when set to false', () => { it('converts Long values to native numbers', { metadata: { requires: { topology: '!single' } }, test: async function () { From dda52f2d1138d3777790550c1de4d127a22699c5 Mon Sep 17 00:00:00 2001 From: Sergey Zelenov Date: Tue, 27 Jan 2026 16:44:17 +0100 Subject: [PATCH 12/12] try to work with Buffer & instanceof across environments --- test/tools/runner/vm_runner.ts | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/test/tools/runner/vm_runner.ts b/test/tools/runner/vm_runner.ts index 12eeb4747b..3109ac8e1a 100644 --- a/test/tools/runner/vm_runner.ts +++ b/test/tools/runner/vm_runner.ts @@ -126,7 +126,14 @@ function loadInSandbox(filepath: string) { let localBuffer = Buffer; if (isSourceFile && !isTestFile) { localBuffer = new Proxy(Buffer, { - get() { + get(target, prop, receiver) { + if ( + typeof prop === 'symbol' || + ['prototype', 'constructor', 'name', 'inspect'].includes(prop as string) + ) { + return Reflect.get(target, prop, receiver); + } + throw new Error( `Forbidden: 'Buffer' usage is not allowed in source files. Use Uint8Array instead. File: ${realPath}` ); @@ -179,7 +186,15 @@ function loadInSandbox(filepath: string) { if (typeof value === 'function' && value.name) { // force instanceof to work across contexts by defining custom `instanceof` function Object.defineProperty(value, Symbol.hasInstance, { - value: (i: any) => i && (i.constructor.name === value.name || i instanceof value) + value: (i: any) => { + if (!i) return false; + // use isPrototypeOf to avoid triggering the 'instanceof' trap recursively + return ( + i.constructor.name === value.name || + Object.prototype.isPrototypeOf.call(value.prototype, i) + ); + }, + configurable: true }); // also inject into global for easier access in tests