node-red-contrib-aedes 0.15.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,6 +4,8 @@ const helper = require('node-red-node-test-helper');
4
4
  const aedesNode = require('../aedes.js');
5
5
  const mqttNode = require('../node_modules/node-red/node_modules/@node-red/nodes/core/network/10-mqtt.js');
6
6
  const mqtt = require('mqtt');
7
+ const should = require('should');
8
+ const { logError } = require('./test-utils');
7
9
 
8
10
  const credentialsOK = { n1: { username: 'test', password: 'test' }, b1: { user: 'test', password: 'test' } };
9
11
  const credentialsMissing = { n1: { username: 'test', password: 'test' }, b1: { user: 'test' } };
@@ -68,10 +70,13 @@ describe('Aedes Broker TCP tests', function () {
68
70
  }
69
71
  ],
70
72
  function () {
71
- const n2 = helper.getNode('n2');
72
- n2.on('input', function (msg) {
73
- msg.should.have.property('topic', 'clientReady');
74
- done();
73
+ const n1 = helper.getNode('n1');
74
+ n1._initPromise.then(function () {
75
+ const n2 = helper.getNode('n2');
76
+ n2.on('input', function (msg) {
77
+ msg.should.have.property('topic', 'clientReady');
78
+ done();
79
+ });
75
80
  });
76
81
  });
77
82
  });
@@ -108,10 +113,13 @@ describe('Aedes Broker TCP tests', function () {
108
113
 
109
114
  helper.load([aedesNode, mqttNode], flow, credentialsMissing,
110
115
  function () {
111
- const n2 = helper.getNode('n2');
112
- n2.on('input', function (msg) {
113
- msg.should.have.property('topic', 'clientError');
114
- done();
116
+ const n1 = helper.getNode('n1');
117
+ n1._initPromise.then(function () {
118
+ const n2 = helper.getNode('n2');
119
+ n2.on('input', function (msg) {
120
+ msg.should.have.property('topic', 'clientError');
121
+ done();
122
+ });
115
123
  });
116
124
  });
117
125
  });
@@ -148,10 +156,13 @@ describe('Aedes Broker TCP tests', function () {
148
156
 
149
157
  helper.load([aedesNode, mqttNode], flow, credentialsOK,
150
158
  function () {
151
- const n2 = helper.getNode('n2');
152
- n2.on('input', function (msg) {
153
- msg.should.have.property('topic', 'clientReady');
154
- done();
159
+ const n1 = helper.getNode('n1');
160
+ n1._initPromise.then(function () {
161
+ const n2 = helper.getNode('n2');
162
+ n2.on('input', function (msg) {
163
+ msg.should.have.property('topic', 'clientReady');
164
+ done();
165
+ });
155
166
  });
156
167
  });
157
168
  });
@@ -198,17 +209,20 @@ describe('Aedes Broker TCP tests', function () {
198
209
  }
199
210
  ],
200
211
  function () {
201
- const n2 = helper.getNode('n2');
202
- const n3 = helper.getNode('n3');
203
- const n5 = helper.getNode('n5');
204
- n2.on('input', function (msg) {
205
- if (msg.topic === 'subscribe') {
206
- n3.receive({ payload: 'test' });
207
- }
208
- });
209
- n5.on('input', function (msg) {
210
- msg.should.have.property('topic', 'test1883');
211
- done();
212
+ const n1 = helper.getNode('n1');
213
+ n1._initPromise.then(function () {
214
+ const n2 = helper.getNode('n2');
215
+ const n3 = helper.getNode('n3');
216
+ const n5 = helper.getNode('n5');
217
+ n2.on('input', function (msg) {
218
+ if (msg.topic === 'subscribe') {
219
+ n3.receive({ payload: 'test' });
220
+ }
221
+ });
222
+ n5.on('input', function (msg) {
223
+ msg.should.have.property('topic', 'test1883');
224
+ done();
225
+ });
212
226
  });
213
227
  });
214
228
  });
@@ -267,22 +281,26 @@ describe('Aedes Broker TCP tests', function () {
267
281
  }
268
282
  ],
269
283
  function () {
270
- let i = 0;
271
- const n2 = helper.getNode('n2');
272
- n2.on('input', function (msg) {
273
- msg.should.have.property('topic', 'clientReady');
274
- i++;
275
- if (i === 2) {
276
- done();
277
- }
278
- });
279
- const n12 = helper.getNode('n12');
280
- n12.on('input', function (msg) {
281
- msg.should.have.property('topic', 'clientReady');
282
- i++;
283
- if (i === 2) {
284
- done();
285
- }
284
+ const n1 = helper.getNode('n1');
285
+ const n11 = helper.getNode('n11');
286
+ Promise.all([n1._initPromise, n11._initPromise]).then(function () {
287
+ let i = 0;
288
+ const n2 = helper.getNode('n2');
289
+ n2.on('input', function (msg) {
290
+ msg.should.have.property('topic', 'clientReady');
291
+ i++;
292
+ if (i === 2) {
293
+ done();
294
+ }
295
+ });
296
+ const n12 = helper.getNode('n12');
297
+ n12.on('input', function (msg) {
298
+ msg.should.have.property('topic', 'clientReady');
299
+ i++;
300
+ if (i === 2) {
301
+ done();
302
+ }
303
+ });
286
304
  });
287
305
  });
288
306
  });
@@ -334,18 +352,19 @@ describe('Aedes Broker TCP tests', function () {
334
352
  type: 'helper'
335
353
  }];
336
354
  helper.load(aedesNode, flow, function () {
337
- const client = mqtt.connect('mqtt://localhost:1883', { clientId: 'client', resubscribe: false, reconnectPeriod: -1 });
338
- client.on('error', function (err) {
339
- console.error('Error: ', err.toString());
340
- });
341
- client.on('connect', function () {
342
- // console.log('External client connected');
343
- });
344
- const n2 = helper.getNode('n2');
345
- n2.on('input', function (msg) {
346
- msg.should.have.property('topic', 'clientReady');
347
- client.end(function () {
348
- done();
355
+ const n1 = helper.getNode('n1');
356
+ n1._initPromise.then(function () {
357
+ const client = mqtt.connect('mqtt://localhost:1883', { clientId: 'client', resubscribe: false, reconnectPeriod: -1 });
358
+ client.on('error', logError);
359
+ client.on('connect', function () {
360
+ // console.log('External client connected');
361
+ });
362
+ const n2 = helper.getNode('n2');
363
+ n2.on('input', function (msg) {
364
+ msg.should.have.property('topic', 'clientReady');
365
+ client.end(function () {
366
+ done();
367
+ });
349
368
  });
350
369
  });
351
370
  });
@@ -387,27 +406,198 @@ describe('Aedes Broker TCP tests', function () {
387
406
  }
388
407
  ];
389
408
  helper.load([aedesNode, mqttNode], flow, function () {
390
- const client = mqtt.connect('mqtt://localhost:1883', { clientId: 'client', resubscribe: false, reconnectPeriod: -1 });
391
- client.on('error', function (err) {
392
- console.error('Error: ', err.toString());
409
+ const n1 = helper.getNode('n1');
410
+ n1._initPromise.then(function () {
411
+ const client = mqtt.connect('mqtt://localhost:1883', { clientId: 'client', resubscribe: false, reconnectPeriod: -1 });
412
+ client.on('error', logError);
413
+ client.on('connect', function () {
414
+ // console.log('External client connected');
415
+ });
416
+ const n2 = helper.getNode('n2');
417
+ const n5 = helper.getNode('n5');
418
+ n2.on('input', function (msg) {
419
+ if (msg.topic === 'subscribe') {
420
+ client.publish('test1883', 'test');
421
+ }
422
+ });
423
+ n5.on('input', function (msg) {
424
+ // console.log(msg);
425
+ msg.should.have.property('topic', 'test1883');
426
+ client.end(function () {
427
+ done();
428
+ });
429
+ });
430
+ });
431
+ });
432
+ });
433
+
434
+ it('should handle initialization failure gracefully', function (done) {
435
+ this.timeout(10000);
436
+ const net = require('net');
437
+ // Occupy port 1887 to force an EADDRINUSE
438
+ const blocker = net.createServer();
439
+ blocker.listen(1887, function () {
440
+ const flow = [{
441
+ id: 'n1',
442
+ type: 'aedes broker',
443
+ mqtt_port: '1887',
444
+ name: 'Aedes 1887',
445
+ wires: [[], []]
446
+ }];
447
+ helper.load(aedesNode, flow, function () {
448
+ const n1 = helper.getNode('n1');
449
+ n1._initPromise.then(function () {
450
+ // Init succeeded but server.listen should have fired an error
451
+ blocker.close(function () {
452
+ done();
453
+ });
454
+ }).catch(function () {
455
+ // Expected to fail
456
+ blocker.close(function () {
457
+ done();
458
+ });
459
+ });
393
460
  });
394
- client.on('connect', function () {
395
- // console.log('External client connected');
461
+ });
462
+ });
463
+
464
+ it('should emit clientDisconnect when a client disconnects', function (done) {
465
+ this.timeout(10000);
466
+ const flow = [
467
+ {
468
+ id: 'n1',
469
+ type: 'aedes broker',
470
+ mqtt_port: '1883',
471
+ name: 'Aedes 1883',
472
+ wires: [['n2'], []]
473
+ },
474
+ {
475
+ id: 'n2',
476
+ type: 'helper'
477
+ }
478
+ ];
479
+ helper.load(aedesNode, flow, function () {
480
+ const n1 = helper.getNode('n1');
481
+ n1._initPromise.then(function () {
482
+ const client = mqtt.connect('mqtt://localhost:1883', {
483
+ clientId: 'client-disconnect-test',
484
+ resubscribe: false,
485
+ reconnectPeriod: -1
486
+ });
487
+ const n2 = helper.getNode('n2');
488
+ n2.on('input', function (msg) {
489
+ if (msg.topic === 'clientReady') {
490
+ client.end();
491
+ } else if (msg.topic === 'clientDisconnect') {
492
+ should(msg.payload.client.id).equal('client-disconnect-test');
493
+ done();
494
+ }
495
+ });
396
496
  });
397
- const n2 = helper.getNode('n2');
398
- const n5 = helper.getNode('n5');
399
- n2.on('input', function (msg) {
400
- if (msg.topic === 'subscribe') {
401
- client.publish('test1883', 'test');
402
- }
497
+ });
498
+ });
499
+
500
+ it('should emit keepaliveTimeout when a client stops responding', function (done) {
501
+ this.timeout(10000);
502
+ const flow = [
503
+ {
504
+ id: 'n1',
505
+ type: 'aedes broker',
506
+ mqtt_port: '1883',
507
+ name: 'Aedes 1883',
508
+ wires: [['n2'], []]
509
+ },
510
+ {
511
+ id: 'n2',
512
+ type: 'helper'
513
+ }
514
+ ];
515
+ helper.load(aedesNode, flow, function () {
516
+ const n1 = helper.getNode('n1');
517
+ n1._initPromise.then(function () {
518
+ const net = require('net');
519
+ const socket = net.createConnection({ port: 1883 }, function () {
520
+ // Send a minimal MQTT CONNECT packet with keepalive=1 second
521
+ const connectPacket = Buffer.from([
522
+ 0x10, // CONNECT packet type
523
+ 0x11, // Remaining length: 17
524
+ 0x00, 0x04, // Protocol name length
525
+ 0x4D, 0x51, 0x54, 0x54, // "MQTT"
526
+ 0x04, // Protocol level 4 (MQTT 3.1.1)
527
+ 0x02, // Connect flags (clean session)
528
+ 0x00, 0x01, // Keep alive: 1 second
529
+ 0x00, 0x05, // Client ID length: 5
530
+ 0x6B, 0x61, 0x74, 0x65, 0x73 // "kates"
531
+ ]);
532
+ socket.write(connectPacket);
533
+ // Do not send any more data — keepalive will expire
534
+ });
535
+ const n2 = helper.getNode('n2');
536
+ n2.on('input', function (msg) {
537
+ if (msg.topic === 'keepaliveTimeout') {
538
+ should(msg.payload.client.id).equal('kates');
539
+ socket.destroy();
540
+ done();
541
+ }
542
+ });
403
543
  });
404
- n5.on('input', function (msg) {
405
- // console.log(msg);
406
- msg.should.have.property('topic', 'test1883');
407
- client.end(function () {
408
- done();
544
+ });
545
+ });
546
+
547
+ it('should emit published messages on the 2nd output', function (done) {
548
+ this.timeout(10000);
549
+ const flow = [
550
+ {
551
+ id: 'n1',
552
+ type: 'aedes broker',
553
+ mqtt_port: '1883',
554
+ name: 'Aedes 1883',
555
+ wires: [[], ['n2']]
556
+ },
557
+ {
558
+ id: 'n2',
559
+ type: 'helper'
560
+ }
561
+ ];
562
+ helper.load(aedesNode, flow, function () {
563
+ const n1 = helper.getNode('n1');
564
+ n1._initPromise.then(function () {
565
+ const client = mqtt.connect('mqtt://localhost:1883', {
566
+ clientId: 'publish-test',
567
+ resubscribe: false,
568
+ reconnectPeriod: -1
569
+ });
570
+ client.on('connect', function () {
571
+ client.publish('testTopic', 'testPayload');
572
+ });
573
+ const n2 = helper.getNode('n2');
574
+ n2.on('input', function (msg) {
575
+ if (msg.payload.packet && msg.payload.packet.topic === 'testTopic') {
576
+ should(msg.topic).equal('publish');
577
+ should(msg.payload.packet.payload.toString()).equal('testPayload');
578
+ client.end(function () {
579
+ done();
580
+ });
581
+ }
409
582
  });
410
583
  });
411
584
  });
412
585
  });
586
+
587
+ it('should handle close during initialization', function (done) {
588
+ this.timeout(10000);
589
+ const flow = [{
590
+ id: 'n1',
591
+ type: 'aedes broker',
592
+ mqtt_port: '1888',
593
+ name: 'Aedes 1888',
594
+ wires: [[], []]
595
+ }];
596
+ helper.load(aedesNode, flow, function () {
597
+ // Immediately unload before init completes
598
+ helper.unload().then(function () {
599
+ done();
600
+ });
601
+ });
602
+ });
413
603
  });
@@ -4,6 +4,7 @@ const helper = require('node-red-node-test-helper');
4
4
  const aedesNode = require('../aedes.js');
5
5
  const mqttNode = require('../node_modules/node-red/node_modules/@node-red/nodes/core/network/10-mqtt.js');
6
6
  const mqtt = require('mqtt');
7
+ const { logError } = require('./test-utils');
7
8
 
8
9
  helper.init(require.resolve('node-red'));
9
10
 
@@ -17,6 +18,43 @@ describe('Aedes Broker Websocket tests', function () {
17
18
  });
18
19
  });
19
20
 
21
+ it('should connect an external ws mqtt client', function (done) {
22
+ this.timeout(10000);
23
+ const flow = [
24
+ {
25
+ id: 'n1',
26
+ type: 'aedes broker',
27
+ mqtt_port: '1883',
28
+ mqtt_ws_port: '8080',
29
+ name: 'Aedes 1883',
30
+ wires: [['n2'], []]
31
+ },
32
+ {
33
+ id: 'n2',
34
+ type: 'helper'
35
+ }
36
+ ];
37
+ helper.load(aedesNode, flow, function () {
38
+ const n1 = helper.getNode('n1');
39
+ n1._initPromise.then(function () {
40
+ const client = mqtt.connect('ws://localhost:8080', {
41
+ clientId: 'ws-client',
42
+ resubscribe: false,
43
+ reconnectPeriod: -1
44
+ });
45
+ client.on('error', logError);
46
+ const n2 = helper.getNode('n2');
47
+ n2.on('input', function (msg) {
48
+ if (msg.topic === 'clientReady') {
49
+ client.end(function () {
50
+ done();
51
+ });
52
+ }
53
+ });
54
+ });
55
+ });
56
+ });
57
+
20
58
  it('should not throw an exception with 2 servers on the same ws port', function (done) {
21
59
  this.timeout(10000); // have to wait for the inject with delay of two seconds
22
60
 
@@ -50,6 +88,35 @@ describe('Aedes Broker Websocket tests', function () {
50
88
  });
51
89
  });
52
90
 
91
+ it('should set error status when ws port is already in use', function (done) {
92
+ this.timeout(10000);
93
+ const net = require('net');
94
+ const blocker = net.createServer();
95
+ blocker.listen(8081, function () {
96
+ const flow = [{
97
+ id: 'n1',
98
+ type: 'aedes broker',
99
+ mqtt_port: '1883',
100
+ mqtt_ws_port: '8081',
101
+ name: 'Aedes WS Error',
102
+ wires: [[], []]
103
+ }];
104
+ helper.load(aedesNode, flow, function () {
105
+ const n1 = helper.getNode('n1');
106
+ n1.on('call:status', function (call) {
107
+ if (call.args[0].fill === 'red') {
108
+ call.args[0].should.have.property('fill', 'red');
109
+ call.args[0].should.have.property('shape', 'ring');
110
+ call.args[0].should.have.property('text', 'aedes-mqtt-broker.status.error');
111
+ blocker.close(function () {
112
+ done();
113
+ });
114
+ }
115
+ });
116
+ });
117
+ });
118
+ });
119
+
53
120
  it('a subscriber should receive a message from an external ws publisher', function (done) {
54
121
  this.timeout(10000); // have to wait for the inject with delay of 10 seconds
55
122
  const flow = [
@@ -87,25 +154,26 @@ describe('Aedes Broker Websocket tests', function () {
87
154
  }
88
155
  ];
89
156
  helper.load([aedesNode, mqttNode], flow, function () {
90
- const client = mqtt.connect('ws://localhost:8080', { clientId: 'client', resubscribe: false, reconnectPeriod: -1 });
91
- client.on('error', function (err) {
92
- console.error('Error: ', err.toString());
93
- });
94
- client.on('connect', function () {
95
- // console.log('External client connected');
96
- });
97
- const n2 = helper.getNode('n2');
98
- const n5 = helper.getNode('n5');
99
- n2.on('input', function (msg) {
100
- if (msg.topic === 'subscribe') {
101
- client.publish('test1883', 'test');
102
- }
103
- });
104
- n5.on('input', function (msg) {
105
- // console.log(msg);
106
- msg.should.have.property('topic', 'test1883');
107
- client.end(function () {
108
- done();
157
+ const n1 = helper.getNode('n1');
158
+ n1._initPromise.then(function () {
159
+ const client = mqtt.connect('ws://localhost:8080', { clientId: 'client', resubscribe: false, reconnectPeriod: -1 });
160
+ client.on('error', logError);
161
+ client.on('connect', function () {
162
+ // console.log('External client connected');
163
+ });
164
+ const n2 = helper.getNode('n2');
165
+ const n5 = helper.getNode('n5');
166
+ n2.on('input', function (msg) {
167
+ if (msg.topic === 'subscribe') {
168
+ client.publish('test1883', 'test');
169
+ }
170
+ });
171
+ n5.on('input', function (msg) {
172
+ // console.log(msg);
173
+ msg.should.have.property('topic', 'test1883');
174
+ client.end(function () {
175
+ done();
176
+ });
109
177
  });
110
178
  });
111
179
  });
@@ -150,25 +218,26 @@ describe('Aedes Broker Websocket tests', function () {
150
218
  ];
151
219
 
152
220
  helper.load([aedesNode, mqttNode], flow, function () {
153
- const client = mqtt.connect(helper.url().replace(/http/, 'ws') + '/mqtt', { clientId: 'client', resubscribe: false, reconnectPeriod: -1 });
154
- client.on('error', function (err) {
155
- console.error('Client on error: ', err.toString());
156
- });
157
- client.on('connect', function () {
158
- // console.log('External client connected');
159
- });
160
- const n2 = helper.getNode('n2');
161
- const n5 = helper.getNode('n5');
162
- n2.on('input', function (msg) {
163
- if (msg.topic === 'subscribe') {
164
- client.publish('test1883', 'test');
165
- }
166
- });
167
- n5.on('input', function (msg) {
168
- // console.log(msg);
169
- msg.should.have.property('topic', 'test1883');
170
- client.end(function () {
171
- done();
221
+ const n1 = helper.getNode('n1');
222
+ n1._initPromise.then(function () {
223
+ const client = mqtt.connect(helper.url().replace(/http/, 'ws') + '/mqtt', { clientId: 'client', resubscribe: false, reconnectPeriod: -1 });
224
+ client.on('error', logError);
225
+ client.on('connect', function () {
226
+ // console.log('External client connected');
227
+ });
228
+ const n2 = helper.getNode('n2');
229
+ const n5 = helper.getNode('n5');
230
+ n2.on('input', function (msg) {
231
+ if (msg.topic === 'subscribe') {
232
+ client.publish('test1883', 'test');
233
+ }
234
+ });
235
+ n5.on('input', function (msg) {
236
+ // console.log(msg);
237
+ msg.should.have.property('topic', 'test1883');
238
+ client.end(function () {
239
+ done();
240
+ });
172
241
  });
173
242
  });
174
243
  });
@@ -0,0 +1,17 @@
1
+ /* eslint no-console: ["error", { allow: ["log", "warn", "error"] }] */
2
+
3
+ /**
4
+ * Logs detailed error information including AggregateError support.
5
+ * Useful for debugging Node-RED and MQTT client errors in tests.
6
+ */
7
+ function logError (err) {
8
+ if (err instanceof AggregateError) {
9
+ console.error('AggregateError:', err.message);
10
+ console.error('Name:', err.name);
11
+ console.error('Errors:', err.errors);
12
+ } else {
13
+ console.error('Error:', err.message || err.toString());
14
+ }
15
+ }
16
+
17
+ module.exports = { logError };
package/docs/DEV-SETUP.md DELETED
@@ -1,86 +0,0 @@
1
- # Developer Setup Note
2
-
3
- After every `npm install` or `npm update`, re-link Node-RED:
4
-
5
- ```bash
6
- sudo npm link node-red
7
- ```
8
-
9
- `npm install` resets the `node_modules` directory and removes the symlink created by `npm link`. Without re-linking, the test helper cannot find Node-RED and tests will fail.
10
-
11
- ---
12
-
13
- ## Backporting a fix to a legacy branch
14
-
15
- ```bash
16
- # 1. Stash any WIP on current branch
17
- git stash push -m "WIP before backport"
18
-
19
- # 2. Switch to the legacy branch
20
- git checkout v0.15.x # or: git checkout v11
21
-
22
- # 3. Cherry-pick the fix commit from the source branch
23
- git cherry-pick <commit-hash> # resolve conflicts if any
24
-
25
- # 4. Run tests
26
- npm test
27
-
28
- # 5. Bump patch version and commit
29
- npm run patch # bumps version in package.json (no git tag)
30
- git add -A
31
- git commit -m "Bump patch version"
32
-
33
- # 6. Publish to npm (optional, with appropriate tag)
34
- npm publish --tag <tag> # e.g. --tag legacy or --tag v11
35
-
36
- # 7. Return to working branch and restore WIP
37
- git checkout <working-branch>
38
- git stash pop
39
- ```
40
-
41
- Notes:
42
- - Find the commit hash with `git log --oneline <source-branch>`.
43
- - If the cherry-pick has conflicts, resolve them, then `git cherry-pick --continue`.
44
- - `npm run patch` runs `npm --no-git-tag-version version patch` — it bumps the patch version in `package.json` without creating a git tag.
45
- - Use `--tag <tag>` with `npm publish` to avoid overwriting the `latest` tag on npm. Choose a tag that matches the branch (e.g. `legacy`, `v11`).
46
-
47
- ### Example: Backport Tasks 6 and 7.1
48
-
49
- Each fix you want to backport needs its own separate commit on the source branch.
50
-
51
- ```bash
52
- # 1. Commit each fix separately on the source branch
53
- git add aedes.js
54
- git commit -m "Fix: add removed parameter to close handler" # Task 7.1
55
-
56
- git add aedes.js
57
- git commit -m "Fix: iterate subscribe/unsubscribe arrays" # Task 6
58
-
59
- # 2. Find the commit hashes
60
- git log --oneline migrate-aedes-v1
61
-
62
- # 3. Backport to v0.15.x
63
- git stash push -m "WIP before backport"
64
- git checkout v0.15.x
65
- git cherry-pick <Task-7.1-commit> # close handler: add removed parameter
66
- git cherry-pick <Task-6-commit> # fix subscribe/unsubscribe event handlers
67
- npm test
68
- npm run patch
69
- git add -A && git commit -m "Bump patch version"
70
- npm publish --tag v0.15.x
71
- git push origin v0.15.x
72
-
73
- # 4. Backport to v11
74
- git checkout v11
75
- git cherry-pick <Task-7.1-commit> # close handler: add removed parameter
76
- git cherry-pick <Task-6-commit> # fix subscribe/unsubscribe event handlers
77
- npm test
78
- npm run patch
79
- git add -A && git commit -m "Bump patch version"
80
- npm publish --tag v11
81
- git push origin v11
82
-
83
- # 5. Return to working branch
84
- git checkout migrate-aedes-v1
85
- git stash pop
86
- ```