diskeyval 1.0.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.
@@ -0,0 +1,534 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+ import {
4
+ createRaftNode,
5
+ isNotLeaderError,
6
+ type AppendEntriesRequest,
7
+ type AppendEntriesResponse,
8
+ type RaftLogEntry,
9
+ type RaftStorage,
10
+ type RequestVoteRequest,
11
+ type RequestVoteResponse
12
+ } from '../../lib/raft/index.ts';
13
+
14
+ function createScriptedTransport(options?: {
15
+ requestVote?: (
16
+ fromNodeId: string,
17
+ toNodeId: string,
18
+ request: RequestVoteRequest
19
+ ) => Promise<RequestVoteResponse>;
20
+ appendEntries?: (
21
+ fromNodeId: string,
22
+ toNodeId: string,
23
+ request: AppendEntriesRequest<string>
24
+ ) => Promise<AppendEntriesResponse>;
25
+ }) {
26
+ return {
27
+ requestVote:
28
+ options?.requestVote ??
29
+ (async (_fromNodeId, _toNodeId, request) => ({
30
+ term: request.term,
31
+ voteGranted: true
32
+ })),
33
+ appendEntries:
34
+ options?.appendEntries ??
35
+ (async (_fromNodeId, _toNodeId, request) => ({
36
+ term: request.term,
37
+ success: true,
38
+ matchIndex: request.prevLogIndex + request.entries.length
39
+ }))
40
+ };
41
+ }
42
+
43
+ test('[unit/raft-coverage] stopped node paths and startElection no-op before start', async () => {
44
+ const raft = createRaftNode<string>({
45
+ nodeId: 'node-a',
46
+ peerIds: ['node-b'],
47
+ transport: createScriptedTransport(),
48
+ applyCommand: () => undefined,
49
+ electionTimeoutMs: 10_000
50
+ });
51
+
52
+ await raft.startElection();
53
+ assert.equal(raft.getState().currentTerm, 0);
54
+
55
+ await assert.rejects(
56
+ () =>
57
+ raft.onRequestVote('node-b', {
58
+ term: 1,
59
+ candidateId: 'node-b',
60
+ lastLogIndex: 0,
61
+ lastLogTerm: 0
62
+ }),
63
+ /is stopped/i
64
+ );
65
+ await assert.rejects(
66
+ () =>
67
+ raft.onAppendEntries('node-b', {
68
+ term: 1,
69
+ leaderId: 'node-b',
70
+ prevLogIndex: 0,
71
+ prevLogTerm: 0,
72
+ entries: [],
73
+ leaderCommit: 0
74
+ }),
75
+ /is stopped/i
76
+ );
77
+ });
78
+
79
+ test('[unit/raft-coverage] raft event bus on/off duplicate handler paths', async () => {
80
+ const raft = createRaftNode<string>({
81
+ nodeId: 'node-events',
82
+ peerIds: [],
83
+ transport: createScriptedTransport(),
84
+ applyCommand: () => undefined,
85
+ electionTimeoutMs: 10_000
86
+ });
87
+
88
+ let leaderEvents = 0;
89
+ const leaderHandler = (): void => {
90
+ leaderEvents += 1;
91
+ };
92
+
93
+ raft.off('leader', leaderHandler);
94
+ raft.on('leader', leaderHandler);
95
+ raft.on('leader', leaderHandler);
96
+
97
+ await raft.start();
98
+ await raft.startElection();
99
+
100
+ raft.off('leader', leaderHandler);
101
+ raft.off('leader', leaderHandler);
102
+
103
+ assert.ok(leaderEvents >= 1);
104
+ await raft.stop();
105
+ });
106
+
107
+ test('[unit/raft-coverage] storage load failures and malformed logs are handled', async () => {
108
+ const loadFailStorage: RaftStorage<string> = {
109
+ load: async () => {
110
+ throw new Error('load failure');
111
+ },
112
+ save: async () => undefined
113
+ };
114
+
115
+ const raftWithLoadFailure = createRaftNode<string>({
116
+ nodeId: 'node-load-fail',
117
+ peerIds: [],
118
+ transport: createScriptedTransport(),
119
+ storage: loadFailStorage,
120
+ applyCommand: () => undefined,
121
+ electionTimeoutMs: 10_000
122
+ });
123
+ await raftWithLoadFailure.start();
124
+ assert.equal(raftWithLoadFailure.getMetrics().persistenceLoadFailures, 1);
125
+ await raftWithLoadFailure.stop();
126
+
127
+ const emptyLogStorage: RaftStorage<string> = {
128
+ load: async () => ({
129
+ currentTerm: 3,
130
+ votedFor: null,
131
+ log: [],
132
+ commitIndex: 0
133
+ }),
134
+ save: async () => undefined
135
+ };
136
+
137
+ const raftWithEmptyLog = createRaftNode<string>({
138
+ nodeId: 'node-empty-log',
139
+ peerIds: [],
140
+ transport: createScriptedTransport(),
141
+ storage: emptyLogStorage,
142
+ applyCommand: () => undefined,
143
+ electionTimeoutMs: 10_000
144
+ });
145
+ await raftWithEmptyLog.start();
146
+ assert.equal(raftWithEmptyLog.getState().lastLogIndex, 0);
147
+ await raftWithEmptyLog.stop();
148
+
149
+ const applied: RaftLogEntry<string>[] = [];
150
+ const noSentinelStorage: RaftStorage<string> = {
151
+ load: async () => ({
152
+ currentTerm: 2,
153
+ votedFor: null,
154
+ log: [{ index: 1, term: 2, command: 'loaded' }],
155
+ commitIndex: 1
156
+ }),
157
+ save: async () => undefined
158
+ };
159
+
160
+ const raftWithNoSentinel = createRaftNode<string>({
161
+ nodeId: 'node-no-sentinel',
162
+ peerIds: [],
163
+ transport: createScriptedTransport(),
164
+ storage: noSentinelStorage,
165
+ applyCommand: (entry) => {
166
+ applied.push(entry);
167
+ },
168
+ electionTimeoutMs: 10_000
169
+ });
170
+ await raftWithNoSentinel.start();
171
+ assert.equal(raftWithNoSentinel.getState().lastLogIndex, 1);
172
+ assert.equal(applied.length, 1);
173
+ assert.equal(applied[0].command, 'loaded');
174
+ await raftWithNoSentinel.stop();
175
+ });
176
+
177
+ test('[unit/raft-coverage] start is idempotent and save failures increment metrics', async () => {
178
+ const failingSaveStorage: RaftStorage<string> = {
179
+ load: async () => null,
180
+ save: async () => {
181
+ throw new Error('save failed');
182
+ }
183
+ };
184
+
185
+ const raft = createRaftNode<string>({
186
+ nodeId: 'node-save-fail',
187
+ peerIds: [],
188
+ transport: createScriptedTransport(),
189
+ storage: failingSaveStorage,
190
+ applyCommand: () => undefined,
191
+ electionTimeoutMs: 10_000
192
+ });
193
+
194
+ await raft.start();
195
+ await raft.start();
196
+ await raft.startElection();
197
+ await new Promise((resolve) => setTimeout(resolve, 10));
198
+
199
+ const metrics = raft.getMetrics();
200
+ assert.ok(metrics.persistenceSaveFailures >= 1);
201
+ await raft.stop();
202
+ });
203
+
204
+ test('[unit/raft-coverage] election handles higher term responses and vote transport failures', async () => {
205
+ const raft = createRaftNode<string>({
206
+ nodeId: 'node-1',
207
+ peerIds: ['node-2', 'node-3'],
208
+ transport: createScriptedTransport({
209
+ requestVote: async (_fromNodeId, toNodeId, request) => {
210
+ if (toNodeId === 'node-2') {
211
+ throw new Error('network down');
212
+ }
213
+ return {
214
+ term: request.term + 1,
215
+ voteGranted: false
216
+ };
217
+ }
218
+ }),
219
+ applyCommand: () => undefined,
220
+ electionTimeoutMs: 10_000
221
+ });
222
+
223
+ await raft.start();
224
+ await raft.startElection();
225
+
226
+ const state = raft.getState();
227
+ assert.equal(state.role, 'follower');
228
+ assert.ok(state.currentTerm >= 2);
229
+ const metrics = raft.getMetrics();
230
+ assert.ok(metrics.requestVoteFailed >= 1);
231
+ await raft.stop();
232
+ });
233
+
234
+ test('[unit/raft-coverage] election handles non-granted votes and newer candidate logs', async () => {
235
+ const raft = createRaftNode<string>({
236
+ nodeId: 'node-1',
237
+ peerIds: ['node-2'],
238
+ transport: createScriptedTransport({
239
+ requestVote: async (_fromNodeId, _toNodeId, request) => ({
240
+ term: request.term,
241
+ voteGranted: false
242
+ })
243
+ }),
244
+ applyCommand: () => undefined,
245
+ electionTimeoutMs: 10_000
246
+ });
247
+
248
+ await raft.start();
249
+ try {
250
+ await raft.startElection();
251
+ assert.equal(raft.isLeader(), false);
252
+
253
+ const vote = await raft.onRequestVote('node-2', {
254
+ term: raft.getState().currentTerm + 1,
255
+ candidateId: 'node-2',
256
+ lastLogIndex: 99,
257
+ lastLogTerm: 99
258
+ });
259
+ assert.equal(vote.voteGranted, true);
260
+ } finally {
261
+ await raft.stop();
262
+ }
263
+ });
264
+
265
+ test('[unit/raft-coverage] append entries stale term and log mismatch responses', async () => {
266
+ const raft = createRaftNode<string>({
267
+ nodeId: 'node-1',
268
+ peerIds: [],
269
+ transport: createScriptedTransport(),
270
+ applyCommand: () => undefined,
271
+ electionTimeoutMs: 10_000
272
+ });
273
+ await raft.start();
274
+ await raft.startElection();
275
+ assert.equal(raft.isLeader(), true);
276
+
277
+ const stale = await raft.onAppendEntries('node-x', {
278
+ term: 0,
279
+ leaderId: 'node-x',
280
+ prevLogIndex: 0,
281
+ prevLogTerm: 0,
282
+ entries: [],
283
+ leaderCommit: 0
284
+ });
285
+ assert.equal(stale.success, false);
286
+
287
+ const mismatch = await raft.onAppendEntries('node-x', {
288
+ term: raft.getState().currentTerm,
289
+ leaderId: 'node-x',
290
+ prevLogIndex: 5,
291
+ prevLogTerm: 99,
292
+ entries: [],
293
+ leaderCommit: 0
294
+ });
295
+ assert.equal(mismatch.success, false);
296
+ await raft.stop();
297
+ });
298
+
299
+ test('[unit/raft-coverage] readBarrier demotes leader on higher term response', async () => {
300
+ let appendEntriesCalls = 0;
301
+ const raft = createRaftNode<string>({
302
+ nodeId: 'node-1',
303
+ peerIds: ['node-2'],
304
+ transport: createScriptedTransport({
305
+ appendEntries: async (_fromNodeId, _toNodeId, request) => {
306
+ appendEntriesCalls += 1;
307
+ if (appendEntriesCalls <= 2) {
308
+ return {
309
+ term: request.term,
310
+ success: true,
311
+ matchIndex: request.prevLogIndex + request.entries.length
312
+ };
313
+ }
314
+
315
+ return {
316
+ term: request.term + 1,
317
+ success: false,
318
+ matchIndex: 0
319
+ };
320
+ }
321
+ }),
322
+ applyCommand: () => undefined,
323
+ electionTimeoutMs: 10_000,
324
+ proposalTimeoutMs: 500
325
+ });
326
+
327
+ await raft.start();
328
+ try {
329
+ await raft.startElection();
330
+ assert.equal(raft.isLeader(), true);
331
+
332
+ await assert.rejects(
333
+ () => raft.readBarrier(),
334
+ (error: unknown) => isNotLeaderError(error)
335
+ );
336
+ } finally {
337
+ await raft.stop();
338
+ }
339
+ });
340
+
341
+ test('[unit/raft-coverage] readBarrier callback exits when leadership changes mid-flight', async () => {
342
+ let appendEntriesCalls = 0;
343
+ const raft = createRaftNode<string>({
344
+ nodeId: 'node-1',
345
+ peerIds: ['node-2', 'node-3'],
346
+ transport: createScriptedTransport({
347
+ appendEntries: async (_fromNodeId, toNodeId, request) => {
348
+ appendEntriesCalls += 1;
349
+ if (appendEntriesCalls <= 2) {
350
+ return {
351
+ term: request.term,
352
+ success: true,
353
+ matchIndex: request.prevLogIndex + request.entries.length
354
+ };
355
+ }
356
+
357
+ if (toNodeId === 'node-2') {
358
+ return {
359
+ term: request.term + 1,
360
+ success: false,
361
+ matchIndex: 0
362
+ };
363
+ }
364
+
365
+ await new Promise((resolve) => setTimeout(resolve, 20));
366
+ return {
367
+ term: request.term,
368
+ success: true,
369
+ matchIndex: request.prevLogIndex + request.entries.length
370
+ };
371
+ }
372
+ }),
373
+ applyCommand: () => undefined,
374
+ electionTimeoutMs: 10_000
375
+ });
376
+
377
+ await raft.start();
378
+ try {
379
+ await raft.startElection();
380
+ await assert.rejects(
381
+ () => raft.readBarrier(),
382
+ (error: unknown) => isNotLeaderError(error)
383
+ );
384
+ } finally {
385
+ await raft.stop();
386
+ }
387
+ });
388
+
389
+ test('[unit/raft-coverage] stop rejects pending proposal promises', async () => {
390
+ const raft = createRaftNode<string>({
391
+ nodeId: 'node-1',
392
+ peerIds: ['node-2'],
393
+ transport: createScriptedTransport({
394
+ appendEntries: async (fromNodeId, toNodeId, request) => {
395
+ await new Promise((resolve) => setTimeout(resolve, 200));
396
+ return {
397
+ term: request.term,
398
+ success: false,
399
+ matchIndex: 0
400
+ };
401
+ }
402
+ }),
403
+ applyCommand: () => undefined,
404
+ electionTimeoutMs: 10_000,
405
+ proposalTimeoutMs: 600
406
+ });
407
+
408
+ await raft.start();
409
+ await raft.startElection();
410
+ assert.equal(raft.isLeader(), true);
411
+
412
+ const pending = raft.propose('blocked');
413
+ await new Promise((resolve) => setTimeout(resolve, 20));
414
+ await raft.stop();
415
+
416
+ await assert.rejects(
417
+ () => pending,
418
+ /Node stopped before proposal committed/i
419
+ );
420
+ });
421
+
422
+ test('[unit/raft-coverage] propose fails when leadership changes before commit', async () => {
423
+ let raft: ReturnType<typeof createRaftNode<string>> | null = null;
424
+ let appendEntriesCalls = 0;
425
+ const transport = createScriptedTransport({
426
+ appendEntries: async (_fromNodeId, _toNodeId, request) => {
427
+ appendEntriesCalls += 1;
428
+ if (appendEntriesCalls <= 2) {
429
+ return {
430
+ term: request.term,
431
+ success: true,
432
+ matchIndex: request.prevLogIndex + request.entries.length
433
+ };
434
+ }
435
+
436
+ if (raft) {
437
+ await raft.onAppendEntries('node-x', {
438
+ term: request.term + 1,
439
+ leaderId: 'node-x',
440
+ prevLogIndex: 0,
441
+ prevLogTerm: 0,
442
+ entries: [],
443
+ leaderCommit: 0
444
+ });
445
+ }
446
+
447
+ return {
448
+ term: request.term,
449
+ success: false,
450
+ matchIndex: 0
451
+ };
452
+ }
453
+ });
454
+
455
+ raft = createRaftNode<string>({
456
+ nodeId: 'node-1',
457
+ peerIds: ['node-2'],
458
+ transport,
459
+ applyCommand: () => undefined,
460
+ electionTimeoutMs: 10_000,
461
+ proposalTimeoutMs: 300
462
+ });
463
+
464
+ await raft.start();
465
+ try {
466
+ await raft.startElection();
467
+ assert.equal(raft.isLeader(), true);
468
+ await assert.rejects(
469
+ () => raft.propose('will-step-down'),
470
+ (error: unknown) => isNotLeaderError(error)
471
+ );
472
+ } finally {
473
+ await raft.stop();
474
+ }
475
+ });
476
+
477
+ test('[unit/raft-coverage] replication stepdown handles higher-term append response path', async () => {
478
+ let appendEntriesCalls = 0;
479
+ const raft = createRaftNode<string>({
480
+ nodeId: 'node-1',
481
+ peerIds: ['node-2'],
482
+ transport: createScriptedTransport({
483
+ appendEntries: async (_fromNodeId, _toNodeId, request) => {
484
+ appendEntriesCalls += 1;
485
+ if (appendEntriesCalls <= 2) {
486
+ return {
487
+ term: request.term,
488
+ success: true,
489
+ matchIndex: request.prevLogIndex + request.entries.length
490
+ };
491
+ }
492
+
493
+ return {
494
+ term: request.term + 1,
495
+ success: false,
496
+ matchIndex: 0
497
+ };
498
+ }
499
+ }),
500
+ applyCommand: () => undefined,
501
+ electionTimeoutMs: 10_000,
502
+ heartbeatMs: 5
503
+ });
504
+
505
+ await raft.start();
506
+ try {
507
+ await raft.startElection();
508
+ assert.equal(raft.isLeader(), true);
509
+ await new Promise((resolve) => setTimeout(resolve, 60));
510
+ assert.equal(raft.isLeader(), false);
511
+ } finally {
512
+ await raft.stop();
513
+ }
514
+ });
515
+
516
+ test('[unit/raft-coverage] follower readBarrier is rejected immediately', async () => {
517
+ const raft = createRaftNode<string>({
518
+ nodeId: 'node-follower',
519
+ peerIds: ['node-2'],
520
+ transport: createScriptedTransport(),
521
+ applyCommand: () => undefined,
522
+ electionTimeoutMs: 10_000
523
+ });
524
+
525
+ await raft.start();
526
+ try {
527
+ await assert.rejects(
528
+ () => raft.readBarrier(),
529
+ (error: unknown) => isNotLeaderError(error)
530
+ );
531
+ } finally {
532
+ await raft.stop();
533
+ }
534
+ });
@@ -0,0 +1,101 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+ import { isNotLeaderError } from '../../lib/index.ts';
4
+ import { createTestCluster, forceLeader, sleep } from '../support/helpers.ts';
5
+
6
+ test('[unit/election] elects one leader in stable 3-node cluster', async () => {
7
+ const cluster = createTestCluster();
8
+ await cluster.start();
9
+ try {
10
+ await forceLeader(cluster.nodes['node-1']);
11
+
12
+ assert.equal(cluster.nodes['node-1'].isLeader(), true);
13
+ assert.equal(cluster.nodes['node-2'].isLeader(), false);
14
+ assert.equal(cluster.nodes['node-3'].isLeader(), false);
15
+
16
+ assert.equal(cluster.nodes['node-2'].leaderId(), 'node-1');
17
+ assert.equal(cluster.nodes['node-3'].leaderId(), 'node-1');
18
+ } finally {
19
+ await cluster.stop();
20
+ }
21
+ });
22
+
23
+ test('[unit/election] does not elect two leaders in same term', async () => {
24
+ const cluster = createTestCluster();
25
+ await cluster.start();
26
+ try {
27
+ await forceLeader(cluster.nodes['node-1']);
28
+ const originalLeaderTerm = cluster.nodes['node-1'].getRaftNode().getState().currentTerm;
29
+ await cluster.nodes['node-2'].forceElection();
30
+ await sleep(40);
31
+
32
+ const leaderCount = Object.values(cluster.nodes).filter((node) => node.isLeader()).length;
33
+ assert.equal(leaderCount, 1);
34
+
35
+ const leadersByTerm = new Map<number, number>();
36
+ for (const node of Object.values(cluster.nodes)) {
37
+ const state = node.getRaftNode().getState();
38
+ if (!node.isLeader()) {
39
+ continue;
40
+ }
41
+ leadersByTerm.set(state.currentTerm, (leadersByTerm.get(state.currentTerm) ?? 0) + 1);
42
+ }
43
+
44
+ for (const [, count] of leadersByTerm.entries()) {
45
+ assert.equal(count, 1);
46
+ }
47
+ assert.ok(cluster.nodes['node-1'].getRaftNode().getState().currentTerm >= originalLeaderTerm);
48
+ } finally {
49
+ await cluster.stop();
50
+ }
51
+ });
52
+
53
+ test('[unit/election] follower rejects vote for stale term', async () => {
54
+ const cluster = createTestCluster();
55
+ await cluster.start();
56
+ try {
57
+ await forceLeader(cluster.nodes['node-1']);
58
+ const follower = cluster.nodes['node-2'].getRaftNode();
59
+
60
+ const vote = await follower.onRequestVote('node-3', {
61
+ term: 0,
62
+ candidateId: 'node-3',
63
+ lastLogIndex: 0,
64
+ lastLogTerm: 0
65
+ });
66
+
67
+ assert.equal(vote.voteGranted, false);
68
+ assert.ok(vote.term >= 1);
69
+ } finally {
70
+ await cluster.stop();
71
+ }
72
+ });
73
+
74
+ test('[unit/election] leader steps down when receiving higher term append', async () => {
75
+ const cluster = createTestCluster();
76
+ await cluster.start();
77
+ try {
78
+ await forceLeader(cluster.nodes['node-1']);
79
+ const leaderRaft = cluster.nodes['node-1'].getRaftNode();
80
+
81
+ const response = await leaderRaft.onAppendEntries('node-2', {
82
+ term: leaderRaft.getState().currentTerm + 1,
83
+ leaderId: 'node-2',
84
+ prevLogIndex: 0,
85
+ prevLogTerm: 0,
86
+ entries: [],
87
+ leaderCommit: leaderRaft.getState().commitIndex
88
+ });
89
+
90
+ assert.equal(response.success, true);
91
+ assert.equal(cluster.nodes['node-1'].isLeader(), false);
92
+ assert.equal(cluster.nodes['node-1'].leaderId(), 'node-2');
93
+
94
+ await assert.rejects(
95
+ () => cluster.nodes['node-1'].set('k', 'v'),
96
+ (error: unknown) => isNotLeaderError(error)
97
+ );
98
+ } finally {
99
+ await cluster.stop();
100
+ }
101
+ });
@@ -0,0 +1,90 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+ import { mkdtemp, rm } from 'node:fs/promises';
4
+ import { tmpdir } from 'node:os';
5
+ import { join } from 'node:path';
6
+ import {
7
+ createInMemoryRaftNetwork,
8
+ diskeyval,
9
+ type SetCommand
10
+ } from '../../lib/index.ts';
11
+ import { forceLeader, waitForCondition } from '../support/helpers.ts';
12
+
13
+ test('[unit/phase5] persists committed state across node restart', async () => {
14
+ const dataDir = await mkdtemp(join(tmpdir(), 'diskeyval-persist-'));
15
+ const network = createInMemoryRaftNetwork<SetCommand>();
16
+
17
+ const createNode = () =>
18
+ diskeyval({
19
+ nodeId: 'node-1',
20
+ peers: [],
21
+ transport: network,
22
+ persistence: {
23
+ dir: dataDir,
24
+ compactEvery: 2
25
+ },
26
+ electionTimeoutMs: 10_000,
27
+ heartbeatMs: 20,
28
+ proposalTimeoutMs: 400
29
+ });
30
+
31
+ const firstNode = createNode();
32
+
33
+ try {
34
+ await firstNode.start();
35
+ await forceLeader(firstNode);
36
+
37
+ await firstNode.set('persisted-key', 'persisted-value');
38
+ await firstNode.end();
39
+
40
+ const restartedNode = createNode();
41
+ await restartedNode.start();
42
+
43
+ try {
44
+ assert.equal(restartedNode.state['persisted-key'], 'persisted-value');
45
+
46
+ await forceLeader(restartedNode);
47
+ const value = await restartedNode.get('persisted-key');
48
+ assert.equal(value, 'persisted-value');
49
+ } finally {
50
+ await restartedNode.end();
51
+ }
52
+ } finally {
53
+ await rm(dataDir, { recursive: true, force: true });
54
+ }
55
+ });
56
+
57
+ test('[unit/phase5] exposes raft metrics and increments critical counters', async () => {
58
+ const network = createInMemoryRaftNetwork<SetCommand>();
59
+ const node = diskeyval({
60
+ nodeId: 'metrics-node',
61
+ peers: [],
62
+ transport: network,
63
+ electionTimeoutMs: 10_000,
64
+ heartbeatMs: 20,
65
+ proposalTimeoutMs: 400
66
+ });
67
+
68
+ await node.start();
69
+
70
+ try {
71
+ await forceLeader(node);
72
+ await node.set('m1', 1);
73
+ await node.get('m1');
74
+
75
+ await waitForCondition(() => node.getMetrics().commitsApplied > 0, {
76
+ timeoutMs: 500,
77
+ message: 'Expected commit metrics to increment'
78
+ });
79
+
80
+ const metrics = node.getMetrics();
81
+ assert.ok(metrics.electionsStarted >= 1);
82
+ assert.ok(metrics.leadershipsWon >= 1);
83
+ assert.ok(metrics.proposalsReceived >= 1);
84
+ assert.ok(metrics.proposalsCommitted >= 1);
85
+ assert.ok(metrics.readBarriersRequested >= 1);
86
+ assert.ok(metrics.commitsApplied >= 1);
87
+ } finally {
88
+ await node.end();
89
+ }
90
+ });