Skip to content

Commit aa9e9d4

Browse files
authored
Merge pull request #21 from imqueue/ai-coder-tests
Update deps and improve test coverage to 100%
2 parents 5d8b6f2 + 8a432c4 commit aa9e9d4

37 files changed

+1746
-779
lines changed

package-lock.json

Lines changed: 350 additions & 756 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@
1111
"json-message"
1212
],
1313
"scripts": {
14+
"benchmark": "node benchmark -c $(( $(nproc) - 2 )) -m 100000",
1415
"prepare": "./node_modules/.bin/tsc",
15-
"test": "./node_modules/.bin/tsc && ./node_modules/.bin/nyc mocha && ./node_modules/.bin/nyc report --reporter=text-lcov && npm run test-coverage",
16+
"test": "./node_modules/.bin/tsc && ./node_modules/.bin/nyc mocha && ./node_modules/.bin/nyc report --reporter=text-lcov",
1617
"test-fast": "./node_modules/.bin/tsc && ./node_modules/.bin/nyc mocha && /usr/bin/env node -e \"import('open').then(open => open.default('file://`pwd`/coverage/index.html', { wait: false }))\"",
1718
"test-local": "export COVERALLS_REPO_TOKEN=$IMQ_COVERALLS_TOKEN && npm test && /usr/bin/env node -e \"import('open').then(open => open.default('https://coveralls.io/github/imqueue/imq', { wait: false }))\"",
1819
"test-dev": "npm run test && npm run clean-js && npm run clean-typedefs && npm run clean-maps",
@@ -37,37 +38,36 @@
3738
"author": "imqueue.com <[email protected]> (https://imqueue.com)",
3839
"license": "GPL-3.0-only",
3940
"dependencies": {
40-
"ioredis": "^5.6.1"
41+
"ioredis": "^5.7.0"
4142
},
4243
"devDependencies": {
43-
"@eslint/js": "^9.30.0",
44+
"@eslint/js": "^9.33.0",
4445
"@types/chai": "^5.2.2",
4546
"@types/eslint__eslintrc": "^2.1.2",
4647
"@types/mocha": "^10.0.0",
4748
"@types/mock-require": "^3.0.0",
48-
"@types/node": "^24.0.8",
49+
"@types/node": "^24.2.1",
4950
"@types/sinon": "^17.0.4",
5051
"@types/yargs": "^17.0.33",
51-
"@typescript-eslint/eslint-plugin": "^8.35.1",
52-
"@typescript-eslint/parser": "^8.35.1",
53-
"@typescript-eslint/typescript-estree": "^8.35.1",
54-
"chai": "^5.2.0",
55-
"codeclimate-test-reporter": "^0.5.1",
56-
"coveralls-next": "^4.2.1",
57-
"eslint": "^9.30.0",
58-
"eslint-plugin-jsdoc": "^51.3.1",
52+
"@typescript-eslint/eslint-plugin": "^8.39.0",
53+
"@typescript-eslint/parser": "^8.39.0",
54+
"@typescript-eslint/typescript-estree": "^8.39.0",
55+
"chai": "^5.2.1",
56+
"coveralls-next": "^5.0.0",
57+
"eslint": "^9.33.0",
58+
"eslint-plugin-jsdoc": "^52.0.4",
5959
"mocha": "^11.7.1",
6060
"mocha-lcov-reporter": "^1.3.0",
6161
"mock-require": "^3.0.3",
6262
"nyc": "^17.1.0",
63-
"open": "^10.1.2",
63+
"open": "^10.2.0",
6464
"reflect-metadata": "^0.2.2",
6565
"sinon": "^21.0.0",
6666
"source-map-support": "^0.5.21",
6767
"ts-node": "^10.9.2",
68-
"typedoc": "^0.28.7",
69-
"typescript": "^5.8.3",
70-
"yargs": "^17.7.2"
68+
"typedoc": "^0.28.9",
69+
"typescript": "^5.9.2",
70+
"yargs": "^18.0.0"
7171
},
7272
"main": "index.js",
7373
"typescript": {

src/UDPClusterManager.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -368,7 +368,10 @@ export class UDPClusterManager extends ClusterManager {
368368
if (typeof socket.close === 'function') {
369369
socket.removeAllListeners();
370370
socket.close(() => {
371-
socket?.unref();
371+
// unref may be missing or not a function on mocked sockets
372+
if (socket && typeof (socket as any).unref === 'function') {
373+
socket.unref();
374+
}
372375

373376
if (
374377
socketKey

test/ClusterManager.spec.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*!
2+
* ClusterManager additional tests
3+
*
4+
* I'm Queue Software Project
5+
* Copyright (C) 2025 imqueue.com
6+
*/
7+
import './mocks';
8+
import { expect } from 'chai';
9+
import * as sinon from 'sinon';
10+
import { ClusterManager, InitializedCluster } from '../src/ClusterManager';
11+
12+
class TestClusterManager extends ClusterManager {
13+
public destroyed = false;
14+
public constructor() { super(); }
15+
public async destroy(): Promise<void> {
16+
this.destroyed = true;
17+
}
18+
}
19+
20+
describe('ClusterManager.remove()', () => {
21+
it('should call destroy when the last cluster is removed and destroy=true', async () => {
22+
const cm = new TestClusterManager();
23+
const cluster: InitializedCluster = cm.init({
24+
add: () => undefined,
25+
remove: () => undefined,
26+
find: () => undefined,
27+
});
28+
29+
// sanity: one cluster registered
30+
expect((cm as any).clusters.length).to.equal(1);
31+
const spy = sinon.spy(cm, 'destroy');
32+
33+
await cm.remove(cluster, true);
34+
35+
expect(spy.calledOnce).to.be.true;
36+
expect((cm as any).clusters.length).to.equal(0);
37+
expect(cm.destroyed).to.be.true;
38+
});
39+
40+
it('should not call destroy when destroy=false', async () => {
41+
const cm = new TestClusterManager();
42+
const cluster: InitializedCluster = cm.init({
43+
add: () => undefined,
44+
remove: () => undefined,
45+
find: () => undefined,
46+
});
47+
48+
const spy = sinon.spy(cm, 'destroy');
49+
await cm.remove(cluster.id, false);
50+
51+
expect(spy.called).to.be.false;
52+
expect((cm as any).clusters.length).to.equal(0);
53+
});
54+
});
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*!
2+
* ClusteredRedisQueue.addServerWithQueueInitializing default param branch
3+
*/
4+
import './mocks';
5+
import { expect } from 'chai';
6+
import { ClusteredRedisQueue } from '../src';
7+
8+
describe('ClusteredRedisQueue.addServerWithQueueInitializing() default param', () => {
9+
it('should use default initializeQueue=true when second param omitted', async () => {
10+
const cq: any = new ClusteredRedisQueue('CQ-Default', {
11+
logger: console,
12+
cluster: [{ host: '127.0.0.1', port: 6379 }],
13+
});
14+
// prevent any actual start/subscription side-effects
15+
(cq as any).state.started = false;
16+
(cq as any).state.subscription = null;
17+
18+
const server = { host: '192.168.0.1', port: 6380 };
19+
const initializedSpy = new Promise<void>((resolve) => {
20+
cq['clusterEmitter'].once('initialized', () => resolve());
21+
});
22+
23+
// Call without the second argument to hit default "true" branch
24+
(cq as any).addServerWithQueueInitializing(server);
25+
26+
await initializedSpy; // should emit initialized when default is true
27+
28+
// Ensure the server added and queue length updated
29+
expect((cq as any).servers.some((s: any) => s.host === server.host && s.port === server.port)).to.equal(true);
30+
expect((cq as any).queueLength).to.equal((cq as any).imqs.length);
31+
32+
await cq.destroy();
33+
});
34+
});
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*!
2+
* Cover ClusteredRedisQueue.addServerWithQueueInitializing with initializeQueue=false
3+
*/
4+
import './mocks';
5+
import { expect } from 'chai';
6+
import { ClusteredRedisQueue } from '../src';
7+
import { ClusterManager } from '../src/ClusterManager';
8+
9+
const server = { host: '127.0.0.1', port: 6380 };
10+
11+
describe('ClusteredRedisQueue.addServerWithQueueInitializing(false)', () => {
12+
it('should add server without initializing queue and not emit initialized', async () => {
13+
const manager = new (ClusterManager as any)();
14+
const cq: any = new ClusteredRedisQueue('NoInit', { clusterManagers: [manager] });
15+
16+
let initializedCalled = false;
17+
(cq as any).clusterEmitter.on('initialized', () => { initializedCalled = true; });
18+
19+
// call private method via any to cover branch
20+
(cq as any).addServerWithQueueInitializing(server, false);
21+
22+
// should have server and imq added
23+
expect(cq.servers.length).to.be.greaterThan(0);
24+
expect(cq.imqs.length).to.be.greaterThan(0);
25+
// queueLength updated
26+
expect(cq.queueLength).to.equal(cq.imqs.length);
27+
// initialized not emitted
28+
expect(initializedCalled).to.equal(false);
29+
30+
await cq.destroy();
31+
});
32+
});
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*!
2+
* Additional tests for ClusteredRedisQueue event emitter proxy methods
3+
*/
4+
import './mocks';
5+
import { expect } from 'chai';
6+
import * as sinon from 'sinon';
7+
import { ClusteredRedisQueue } from '../src';
8+
import { ClusterManager } from '../src/ClusterManager';
9+
10+
const clusterConfig = {
11+
cluster: [
12+
{ host: '127.0.0.1', port: 6379 },
13+
],
14+
};
15+
16+
describe('ClusteredRedisQueue - EventEmitter proxy methods', () => {
17+
it('should cover rawListeners/getMaxListeners/eventNames/listenerCount/emit', async () => {
18+
const clusterManager = new (ClusterManager as any)();
19+
const cq: any = new ClusteredRedisQueue('ProxyQueue', {
20+
clusterManagers: [clusterManager],
21+
});
22+
23+
// add underlying server and listener
24+
cq.addServer(clusterConfig.cluster[0]);
25+
const handler = sinon.spy();
26+
cq.imqs[0].on('test', handler);
27+
28+
// set max listeners across emitters and verify getMaxListeners uses templateEmitter
29+
cq.setMaxListeners(20);
30+
expect(cq.getMaxListeners()).to.equal(20);
31+
32+
// collect raw listeners
33+
const raw = cq.rawListeners('test');
34+
expect(raw.length).to.be.greaterThan(0);
35+
36+
// event names come from underlying imq
37+
const names = cq.eventNames();
38+
expect(names).to.be.an('array');
39+
expect(names.map(String)).to.include('test');
40+
41+
// listener count is aggregated via templateEmitter method applied on imq[0]
42+
expect(cq.listenerCount('test')).to.equal(1);
43+
44+
// emit should return true
45+
expect(cq.emit('test', 1, 2, 3)).to.equal(true);
46+
expect(handler.calledOnce).to.be.true;
47+
});
48+
});
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*!
2+
* Additional tests for ClusteredRedisQueue.initializeQueue branches
3+
*/
4+
import './mocks';
5+
import { expect } from 'chai';
6+
import * as sinon from 'sinon';
7+
import { ClusteredRedisQueue, RedisQueue } from '../src';
8+
import { ClusterManager } from '../src/ClusterManager';
9+
10+
describe('ClusteredRedisQueue.initializeQueue()', () => {
11+
it('should call imq.start when started and imq.subscribe when subscription is set', async () => {
12+
const startStub = sinon.stub(RedisQueue.prototype as any, 'start').resolves(undefined);
13+
const subscribeStub = sinon.stub(RedisQueue.prototype as any, 'subscribe').resolves();
14+
15+
const clusterManager = new (ClusterManager as any)();
16+
const cq: any = new ClusteredRedisQueue('InitCover', { clusterManagers: [clusterManager] });
17+
18+
// mark started and set subscription using public APIs
19+
await cq.start();
20+
const channel = 'X';
21+
const handler = () => undefined;
22+
await cq.subscribe(channel, handler);
23+
24+
// adding a server triggers initializeQueue which should call start and subscribe
25+
cq.addServer({ host: '127.0.0.1', port: 6453 });
26+
27+
// allow promises to resolve
28+
await new Promise(res => setTimeout(res, 0));
29+
30+
expect(startStub.called).to.be.true;
31+
expect(subscribeStub.called).to.be.true;
32+
33+
startStub.restore();
34+
subscribeStub.restore();
35+
await cq.destroy();
36+
});
37+
});
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*!
2+
* Tests for ClusteredRedisQueue.matchServers combinations
3+
*/
4+
import './mocks';
5+
import { expect } from 'chai';
6+
import { ClusteredRedisQueue } from '../src';
7+
8+
// Access private static via casting
9+
const match = (ClusteredRedisQueue as any).matchServers as (
10+
source: any, target: any, strict?: boolean
11+
) => boolean;
12+
13+
describe('ClusteredRedisQueue.matchServers()', () => {
14+
it('should return sameAddress when no ids provided', () => {
15+
expect(match({ host: 'h', port: 1 }, { host: 'h', port: 1 })).to.be.true;
16+
expect(match({ host: 'h', port: 1 }, { host: 'h', port: 2 })).to.be.false;
17+
});
18+
19+
it('should use strict logic when strict=true', () => {
20+
// same id and same address -> true
21+
expect(match({ id: 'a', host: 'h', port: 1 }, { id: 'a', host: 'h', port: 1 }, true)).to.be.true;
22+
// same id but different address -> false
23+
expect(match({ id: 'a', host: 'h', port: 1 }, { id: 'a', host: 'h', port: 2 }, true)).to.be.false;
24+
// different id but same address -> false
25+
expect(match({ id: 'a', host: 'h', port: 1 }, { id: 'b', host: 'h', port: 1 }, true)).to.be.false;
26+
});
27+
28+
it('should use relaxed logic when strict=false', () => {
29+
// id matches -> true even if address differs
30+
expect(match({ id: 'a', host: 'h', port: 1 }, { id: 'a', host: 'h', port: 2 }, false)).to.be.true;
31+
// address matches -> true even if id differs
32+
expect(match({ id: 'a', host: 'h', port: 1 }, { id: 'b', host: 'h', port: 1 }, false)).to.be.true;
33+
});
34+
});

test/ClusteredRedisQueue.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
* purchase a proprietary commercial license. Please contact us at
2222
* <[email protected]> to get commercial licensing options.
2323
*/
24-
import * as mocks from './mocks';
24+
import { logger } from './mocks';
2525
import { expect } from 'chai';
2626
import * as sinon from 'sinon';
2727
import { ClusteredRedisQueue } from '../src';
@@ -30,7 +30,7 @@ import { ClusterManager } from '../src/ClusterManager';
3030
process.setMaxListeners(100);
3131

3232
const clusterConfig = {
33-
logger: mocks.logger,
33+
logger,
3434
cluster: [{
3535
host: '127.0.0.1',
3636
port: 7777
@@ -163,14 +163,14 @@ describe('ClusteredRedisQueue', function() {
163163
'TestClusteredQueueOne',
164164
{
165165
clusterManagers: [clusterManager],
166-
logger: mocks.logger,
166+
logger,
167167
},
168168
);
169169
const cqTwo: any = new ClusteredRedisQueue(
170170
'TestClusteredQueueTwo',
171171
{
172172
clusterManagers: [clusterManager],
173-
logger: mocks.logger,
173+
logger,
174174
},
175175
);
176176
const message = { 'hello': 'world' };
@@ -238,7 +238,7 @@ describe('ClusteredRedisQueue', function() {
238238
'TestClusteredQueue',
239239
{
240240
clusterManagers: [clusterManager],
241-
logger: mocks.logger,
241+
logger,
242242
},
243243
);
244244
const channel = 'TestChannel';

0 commit comments

Comments
 (0)