From 011e26928cc58ea69bb43b1b588b7188c9c360c1 Mon Sep 17 00:00:00 2001 From: nirinchev Date: Wed, 3 Jun 2026 16:25:23 +0300 Subject: [PATCH] chore: add option to spawn mongod attached to parent process --- .../mongodb-runner/src/mongocluster.spec.ts | 37 +++++++++++++++++++ packages/mongodb-runner/src/mongocluster.ts | 1 + packages/mongodb-runner/src/mongoserver.ts | 16 +++++++- 3 files changed, 53 insertions(+), 1 deletion(-) diff --git a/packages/mongodb-runner/src/mongocluster.spec.ts b/packages/mongodb-runner/src/mongocluster.spec.ts index c82ef9c3..1e95da1f 100644 --- a/packages/mongodb-runner/src/mongocluster.spec.ts +++ b/packages/mongodb-runner/src/mongocluster.spec.ts @@ -5,6 +5,7 @@ import path from 'path'; import os from 'os'; import createDebug from 'debug'; import sinon from 'sinon'; +import childProcess from 'child_process'; import type { LogEntry } from './mongologreader'; import type { MongoClientOptions } from 'mongodb'; import { eventually } from './util'; @@ -81,6 +82,42 @@ describe('MongoCluster', function () { ); }); + it('forwards the detached option to the spawned server process', async function () { + // Stub spawn so we assert the option is plumbed through without actually + // starting a server. binDir is set so no binary download is attempted. + const spawnStub = sinon + .stub(childProcess, 'spawn') + .throws(new Error('stub: not actually spawning a server')); + const binDir = path.join(tmpDir, 'unused-bin'); + + for (const { detached, expected } of [ + { detached: undefined, expected: true }, // default preserves CLI behavior + { detached: false, expected: false }, + { detached: true, expected: true }, + ]) { + spawnStub.resetHistory(); + try { + await MongoCluster.start({ + topology: 'standalone', + tmpDir, + binDir, + ...(detached === undefined ? {} : { detached }), + }); + expect.fail('expected start to throw because spawn is stubbed'); + } catch (err) { + if ( + !String((err as Error).message).includes( + 'stub: not actually spawning', + ) + ) { + throw err; + } + } + expect(spawnStub).to.have.been.calledOnce; + expect(spawnStub.firstCall.args[2]).to.include({ detached: expected }); + } + }); + it('can spawn a 6.x standalone mongod', async function () { cluster = await MongoCluster.start({ version: '6.x', diff --git a/packages/mongodb-runner/src/mongocluster.ts b/packages/mongodb-runner/src/mongocluster.ts index d8d97388..c71e574e 100644 --- a/packages/mongodb-runner/src/mongocluster.ts +++ b/packages/mongodb-runner/src/mongocluster.ts @@ -150,6 +150,7 @@ export type MongoClusterOptions = Pick< | 'docker' | 'internalClientOptions' | 'host' + | 'detached' > & CommonOptions & RSOptions & diff --git a/packages/mongodb-runner/src/mongoserver.ts b/packages/mongodb-runner/src/mongoserver.ts index ec88dd13..04d05765 100644 --- a/packages/mongodb-runner/src/mongoserver.ts +++ b/packages/mongodb-runner/src/mongoserver.ts @@ -51,6 +51,20 @@ export interface MongoServerOptions { isConfigSvr?: boolean; /** Internal option -- if keyfile auth is used, this will be its contents */ keyFileContents?: string; + /** + * Whether to spawn the server process detached from the current Node.js + * process (default: `true`). + * + * When `true`, the server keeps running even if the parent process exits, + * which is what the CLI relies on for its persistent-server behavior. + * + * Library consumers that manage the server lifetime within a single process + * (e.g. test suites) can set this to `false` so the server is tied to the + * parent process and terminated together with it. In particular, this avoids + * orphaned `mongod` processes on Windows when the parent is killed without a + * chance to call `close()` (e.g. an out-of-memory test worker). + */ + detached?: boolean; } interface SerializedServerProperties { @@ -264,7 +278,7 @@ export class MongoServer extends EventEmitter { const proc = spawn(executable, args, { stdio: ['inherit', 'pipe', 'pipe'], cwd: options.tmpDir, - detached: true, + detached: options.detached ?? true, }); await once(proc, 'spawn'); srv.childProcess = proc;