From 6787a27a7ffa8ec2c36c3ccb2ae5187dd1a8939c Mon Sep 17 00:00:00 2001 From: Serhiy Morenko Date: Thu, 9 Jul 2026 20:21:13 +0200 Subject: [PATCH 1/2] fix: test mocks vs ESM @imqueue/core and Node >=24.17 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two independent issues broke the suite after bumping @imqueue/core to ^3.2.0 (now an ES module consumed from CJS via require(esm)): 1. The ioredis mock registered only a default export; core's ESM sources bind the named { Redis } import, so linking failed with 'does not provide an export named Redis'. The mock now registers both shapes. 2. node:test module mocks evaluate asynchronously, and the synchronous require(esm) path cannot wait for them — the mocked graph loads with export names present but values unbound (core_1.Redis undefined). The new test/warmup.mjs preload (wired via --import) registers the mocks and fully awaits the @imqueue/core graph before any spec runs, so CJS specs then require() the cached, bound namespace. Also switches moduleMockOptions to the documented defaultExport / namedExports pair with cache:false — the undocumented exports option and cache:true value binding both broke on the Node 24.17/24.18 loader changes (same fix as imqueue/core@5137ffa). Verified: 197/197 on Node 24.16.0 and in docker node:24 (24.18.0); includes the @imqueue/core ^3.2.0 dependency bump the fixes target. --- package-lock.json | 11 +++++++---- package.json | 8 ++++---- test/mocks/moduleMock.ts | 15 ++++++--------- test/mocks/redis.ts | 11 +++++++++-- test/warmup.mjs | 15 +++++++++++++++ 5 files changed, 41 insertions(+), 19 deletions(-) create mode 100644 test/warmup.mjs diff --git a/package-lock.json b/package-lock.json index 564c558..f42db9f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "3.1.0", "license": "GPL-3.0-only", "dependencies": { - "@imqueue/core": "^3.1.0", + "@imqueue/core": "^3.2.0", "acorn": "^8.17.0", "typescript": "^7.0.2" }, @@ -20,12 +20,15 @@ } }, "node_modules/@imqueue/core": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@imqueue/core/-/core-3.1.0.tgz", - "integrity": "sha512-lz8AQU9RgRW48AXfAKthYXO3KqU5nvhjhbqQwu34muY7TiJilIkIVGsBSQp58lI4HjOgk3wLUrnbH9rJNJI4bQ==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@imqueue/core/-/core-3.2.0.tgz", + "integrity": "sha512-JKVV6/bYjhOWdD6tGr66X0EQRfysKw+QHiFUGcGw2ysZw/GAn5VKhjjfWbmjBh1polCFdmpRRuyUXOcUa4h2LA==", "license": "GPL-3.0-only", "dependencies": { "ioredis": "^5.11.1" + }, + "engines": { + "node": ">=22.12.0" } }, "node_modules/@ioredis/commands": { diff --git a/package.json b/package.json index a8f08b4..d1f1938 100644 --- a/package.json +++ b/package.json @@ -20,9 +20,9 @@ "lint": "oxlint", "format": "oxfmt \"index.ts\" \"debug.ts\" \"src/**/*.ts\" \"test/**/*.ts\"", "format:check": "oxfmt --check \"index.ts\" \"debug.ts\" \"src/**/*.ts\" \"test/**/*.ts\"", - "test": "npm run build && node --experimental-test-module-mocks --test --test-timeout=15000 $(find test -name '*.spec.js')", - "test-coverage": "npm run build && node --experimental-test-module-mocks --enable-source-maps --test --experimental-test-coverage --test-timeout=15000 $(find test -name '*.spec.js')", - "test-lcov": "npm run build && mkdir -p coverage && node --experimental-test-module-mocks --enable-source-maps --test --experimental-test-coverage --test-reporter=lcov --test-reporter-destination=coverage/lcov.info --test-timeout=15000 $(find test -name '*.spec.js'); node scripts/strip-comment-coverage.mjs coverage/lcov.info", + "test": "npm run build && node --experimental-test-module-mocks --import ./test/warmup.mjs --test --test-timeout=15000 $(find test -name '*.spec.js')", + "test-coverage": "npm run build && node --experimental-test-module-mocks --enable-source-maps --import ./test/warmup.mjs --test --experimental-test-coverage --test-timeout=15000 $(find test -name '*.spec.js')", + "test-lcov": "npm run build && mkdir -p coverage && node --experimental-test-module-mocks --enable-source-maps --import ./test/warmup.mjs --test --experimental-test-coverage --test-reporter=lcov --test-reporter-destination=coverage/lcov.info --test-timeout=15000 $(find test -name '*.spec.js'); node scripts/strip-comment-coverage.mjs coverage/lcov.info", "test-coverage-html": "npm run test-lcov; genhtml coverage/lcov.info --output-directory coverage/html --ignore-errors inconsistent,corrupt,format,mismatch && echo \"Coverage report: file://$(pwd)/coverage/html/index.html\"", "test-dev": "npm run test && npm run clean-js && npm run clean-typedefs && npm run clean-maps", "clean-typedefs": "find . -name '*.d.ts' -not -wholename '*node_modules*' -not -wholename '*generator*' -type f -delete", @@ -43,7 +43,7 @@ "author": "imqueue.com (https://imqueue.com)", "license": "GPL-3.0-only", "dependencies": { - "@imqueue/core": "^3.1.0", + "@imqueue/core": "^3.2.0", "acorn": "^8.17.0", "typescript": "^7.0.2" }, diff --git a/test/mocks/moduleMock.ts b/test/mocks/moduleMock.ts index 3ef988f..e682065 100644 --- a/test/mocks/moduleMock.ts +++ b/test/mocks/moduleMock.ts @@ -31,9 +31,11 @@ import { mock, MockModuleOptions } from 'node:test'; * Node 22 only understands the earlier pair, so the same exports shape is * translated there. * - * `cache: true` keeps mock-require's semantics: every `require()` of the - * mocked specifier returns the same module instance, so a test may patch a - * property in place and the code under test observes the change. + * `cache: false` is required: as of the Node 24.17/24.18 module-loader + * changes, `cache: true` mock modules expose their export names with the + * values never bound. Instance caching is not load-bearing here — shared + * state lives on the exported references themselves (mock class statics), + * so each re-evaluation serves the same objects. * * @param {Record} exports - mock exports, `default` key being * the default export @@ -42,13 +44,8 @@ import { mock, MockModuleOptions } from 'node:test'; export function moduleMockOptions( exports: Record, ): MockModuleOptions { - if (+process.versions.node.split('.')[0] >= 24) { - // typings (@types/node 24.x) lag the runtime's `exports` option - return { cache: true, exports } as MockModuleOptions; - } - const { default: defaultExport, ...namedExports } = exports; - const options: MockModuleOptions = { cache: true }; + const options: MockModuleOptions = { cache: false }; if (defaultExport !== undefined) { options.defaultExport = defaultExport; diff --git a/test/mocks/redis.ts b/test/mocks/redis.ts index 58a5ea8..8a40ee6 100644 --- a/test/mocks/redis.ts +++ b/test/mocks/redis.ts @@ -21,7 +21,8 @@ * purchase a proprietary commercial license. Please contact us at * to get commercial licensing options. */ -import { mockModule } from './moduleMock'; +import { mock } from 'node:test'; +import { moduleMockOptions } from './moduleMock'; import { EventEmitter } from 'node:events'; import * as crypto from 'node:crypto'; @@ -322,7 +323,13 @@ const Redis = RedisClientMock; // exports onto a class default, so the whole ESM-shaped namespace object is // registered as the module's export (for CJS consumers it IS what // `require('ioredis')` returns). -mockModule('ioredis', { __esModule: true, Redis, default: Redis }); +mock.module( + 'ioredis', + moduleMockOptions({ + default: { __esModule: true, Redis, default: Redis }, + Redis, + }), +); // @ts-ignore export * from 'ioredis'; diff --git a/test/warmup.mjs b/test/warmup.mjs new file mode 100644 index 0000000..6c42199 --- /dev/null +++ b/test/warmup.mjs @@ -0,0 +1,15 @@ +/*! + * Test preload (wired via `node --import ./test/warmup.mjs`). + * + * Registers all module mocks (side effect of the mocks index) and then + * fully loads the ES module graph of @imqueue/core BEFORE any spec file + * runs. CommonJS specs may then require('@imqueue/core') synchronously: + * require(esm) of an already-evaluated module returns its bound namespace, + * whereas evaluating a graph that contains node:test module mocks through + * the synchronous require(esm) path leaves the mock exports unbound (the + * mocks evaluate asynchronously), which manifests as + * "TypeError: core_1.Redis is not a constructor". + */ +import './mocks/index.js'; + +await import('@imqueue/core'); From 9ca346c983f6ceeedfcd0b51b7789a1c6f007eef Mon Sep 17 00:00:00 2001 From: Serhiy Morenko Date: Thu, 9 Jul 2026 20:23:53 +0200 Subject: [PATCH 2/2] 3.2.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index f42db9f..39ee4e9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@imqueue/rpc", - "version": "3.1.0", + "version": "3.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@imqueue/rpc", - "version": "3.1.0", + "version": "3.2.0", "license": "GPL-3.0-only", "dependencies": { "@imqueue/core": "^3.2.0", diff --git a/package.json b/package.json index d1f1938..0dc8b6e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@imqueue/rpc", - "version": "3.1.0", + "version": "3.2.0", "description": "RPC-like client-service implementation over messaging queue", "keywords": [ "redis",