resplite 1.0.0 → 1.0.4

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,107 @@
1
+ /**
2
+ * Integration tests for blocking list commands (BLPOP, BRPOP) — SPEC_E.
3
+ */
4
+
5
+ import { describe, it, before, after } from 'node:test';
6
+ import assert from 'node:assert/strict';
7
+ import { createTestServer } from '../helpers/server.js';
8
+ import { sendCommand, argv } from '../helpers/client.js';
9
+ import { tryParseValue } from '../../src/resp/parser.js';
10
+
11
+ function delay(ms) {
12
+ return new Promise((r) => setTimeout(r, ms));
13
+ }
14
+
15
+ describe('Blocking lists (BLPOP/BRPOP)', () => {
16
+ let s;
17
+ let port;
18
+
19
+ before(async () => {
20
+ s = await createTestServer();
21
+ port = s.port;
22
+ });
23
+
24
+ after(async () => {
25
+ await s.closeAsync();
26
+ });
27
+
28
+ it('BLPOP returns immediately when key has element', async () => {
29
+ await sendCommand(port, argv('RPUSH', 'bq1', 'a'));
30
+ const reply = await sendCommand(port, argv('BLPOP', 'bq1', '1'));
31
+ const parsed = tryParseValue(reply, 0);
32
+ assert.ok(Array.isArray(parsed.value));
33
+ assert.equal(parsed.value.length, 2);
34
+ assert.equal(parsed.value[0].toString('utf8'), 'bq1');
35
+ assert.equal(parsed.value[1].toString('utf8'), 'a');
36
+ });
37
+
38
+ it('BRPOP returns immediately when key has element', async () => {
39
+ await sendCommand(port, argv('RPUSH', 'bq2', 'x'));
40
+ const reply = await sendCommand(port, argv('BRPOP', 'bq2', '1'));
41
+ const parsed = tryParseValue(reply, 0);
42
+ assert.ok(Array.isArray(parsed.value));
43
+ assert.equal(parsed.value[0].toString('utf8'), 'bq2');
44
+ assert.equal(parsed.value[1].toString('utf8'), 'x');
45
+ });
46
+
47
+ it('BLPOP blocks then returns after RPUSH from another client', async () => {
48
+ const blockPromise = sendCommand(port, argv('BLPOP', 'bq3', '5'));
49
+ await delay(80);
50
+ await sendCommand(port, argv('RPUSH', 'bq3', 'woken'));
51
+ const reply = await blockPromise;
52
+ const parsed = tryParseValue(reply, 0);
53
+ assert.ok(Array.isArray(parsed.value));
54
+ assert.equal(parsed.value[0].toString('utf8'), 'bq3');
55
+ assert.equal(parsed.value[1].toString('utf8'), 'woken');
56
+ });
57
+
58
+ it('BRPOP blocks then returns after LPUSH from another client', async () => {
59
+ const blockPromise = sendCommand(port, argv('BRPOP', 'bq4', '5'));
60
+ await delay(80);
61
+ await sendCommand(port, argv('LPUSH', 'bq4', 'tail'));
62
+ const reply = await blockPromise;
63
+ const parsed = tryParseValue(reply, 0);
64
+ assert.ok(Array.isArray(parsed.value));
65
+ assert.equal(parsed.value[0].toString('utf8'), 'bq4');
66
+ assert.equal(parsed.value[1].toString('utf8'), 'tail');
67
+ });
68
+
69
+ it('BLPOP with timeout returns nil after timeout', async () => {
70
+ const reply = await sendCommand(port, argv('BLPOP', 'emptyq', '1'));
71
+ const parsed = tryParseValue(reply, 0);
72
+ assert.strictEqual(parsed.value, null);
73
+ });
74
+
75
+ it('BLPOP wrong number of arguments returns error', async () => {
76
+ const reply = await sendCommand(port, argv('BLPOP', 'k'));
77
+ const parsed = tryParseValue(reply, 0);
78
+ assert.ok(parsed.value && parsed.value.error);
79
+ assert.ok(parsed.value.error.includes('wrong number of arguments'));
80
+ });
81
+
82
+ it('BLPOP invalid timeout returns error', async () => {
83
+ const reply = await sendCommand(port, argv('BLPOP', 'k', 'x'));
84
+ const parsed = tryParseValue(reply, 0);
85
+ assert.ok(parsed.value && parsed.value.error);
86
+ assert.ok(parsed.value.error.includes('timeout'));
87
+ });
88
+
89
+ it('BLPOP on wrong type key returns WRONGTYPE', async () => {
90
+ await sendCommand(port, argv('SET', 'strkey', 'v'));
91
+ const reply = await sendCommand(port, argv('BLPOP', 'strkey', '0'));
92
+ const parsed = tryParseValue(reply, 0);
93
+ assert.ok(parsed.value && parsed.value.error);
94
+ assert.ok(parsed.value.error.includes('WRONGTYPE'));
95
+ });
96
+
97
+ it('BLPOP multi-key returns first available in key order', async () => {
98
+ const blockPromise = sendCommand(port, argv('BLPOP', 'k1', 'k2', '5'));
99
+ await delay(50);
100
+ await sendCommand(port, argv('RPUSH', 'k2', 'second'));
101
+ const reply = await blockPromise;
102
+ const parsed = tryParseValue(reply, 0);
103
+ assert.ok(Array.isArray(parsed.value));
104
+ assert.equal(parsed.value[0].toString('utf8'), 'k2');
105
+ assert.equal(parsed.value[1].toString('utf8'), 'second');
106
+ });
107
+ });
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Unit tests for migration registry (SPEC_F §F.5).
3
+ */
4
+
5
+ import { describe, it } from 'node:test';
6
+ import assert from 'node:assert/strict';
7
+ import { openDb } from '../../src/storage/sqlite/db.js';
8
+ import {
9
+ createRun,
10
+ getRun,
11
+ setRunStatus,
12
+ updateBulkProgress,
13
+ upsertDirtyKey,
14
+ getDirtyBatch,
15
+ markDirtyState,
16
+ getDirtyCounts,
17
+ logError,
18
+ RUN_STATUS,
19
+ } from '../../src/migration/registry.js';
20
+ import { tmpDbPath } from '../helpers/tmp.js';
21
+
22
+ describe('migration registry', () => {
23
+ it('createRun inserts a new run and is idempotent', () => {
24
+ const db = openDb(tmpDbPath(), { pragmaTemplate: 'minimal' });
25
+ const { run_id, created } = createRun(db, 'run_1', 'redis://localhost:6379', { scan_count_hint: 500 });
26
+ assert.equal(run_id, 'run_1');
27
+ assert.equal(created, true);
28
+
29
+ const { created: created2 } = createRun(db, 'run_1', 'redis://localhost:6379');
30
+ assert.equal(created2, false);
31
+ });
32
+
33
+ it('getRun returns run row', () => {
34
+ const db = openDb(tmpDbPath(), { pragmaTemplate: 'minimal' });
35
+ createRun(db, 'run_2', 'redis://x:6379');
36
+ const run = getRun(db, 'run_2');
37
+ assert.ok(run);
38
+ assert.equal(run.run_id, 'run_2');
39
+ assert.equal(run.source_uri, 'redis://x:6379');
40
+ assert.equal(run.status, RUN_STATUS.RUNNING);
41
+ assert.equal(run.scan_cursor, '0');
42
+ });
43
+
44
+ it('setRunStatus and updateBulkProgress update run', () => {
45
+ const db = openDb(tmpDbPath(), { pragmaTemplate: 'minimal' });
46
+ createRun(db, 'run_3', 'redis://x:6379');
47
+ setRunStatus(db, 'run_3', RUN_STATUS.PAUSED);
48
+ assert.equal(getRun(db, 'run_3').status, RUN_STATUS.PAUSED);
49
+
50
+ updateBulkProgress(db, 'run_3', {
51
+ scan_cursor: '42',
52
+ scanned_keys: 100,
53
+ migrated_keys: 98,
54
+ skipped_keys: 1,
55
+ error_keys: 1,
56
+ migrated_bytes: 5000,
57
+ });
58
+ const run = getRun(db, 'run_3');
59
+ assert.equal(run.scan_cursor, '42');
60
+ assert.equal(run.scanned_keys, 100);
61
+ assert.equal(run.migrated_keys, 98);
62
+ assert.equal(run.migrated_bytes, 5000);
63
+ });
64
+
65
+ it('upsertDirtyKey inserts and updates', () => {
66
+ const db = openDb(tmpDbPath(), { pragmaTemplate: 'minimal' });
67
+ createRun(db, 'run_4', 'redis://x:6379');
68
+ const key = Buffer.from('mykey', 'utf8');
69
+
70
+ upsertDirtyKey(db, 'run_4', key, 'set');
71
+ let batch = getDirtyBatch(db, 'run_4', 'dirty', 10);
72
+ assert.equal(batch.length, 1);
73
+ assert.equal(batch[0].key.toString('utf8'), 'mykey');
74
+
75
+ upsertDirtyKey(db, 'run_4', 'mykey', 'hset');
76
+ batch = getDirtyBatch(db, 'run_4', 'dirty', 10);
77
+ assert.equal(batch.length, 1);
78
+ const counts = getDirtyCounts(db, 'run_4');
79
+ assert.ok(counts.dirty >= 1);
80
+ });
81
+
82
+ it('upsertDirtyKey marks deleted then dirty again', () => {
83
+ const db = openDb(tmpDbPath(), { pragmaTemplate: 'minimal' });
84
+ createRun(db, 'run_5', 'redis://x:6379');
85
+ upsertDirtyKey(db, 'run_5', 'key1', 'del');
86
+ let batch = getDirtyBatch(db, 'run_5', 'deleted', 10);
87
+ assert.equal(batch.length, 1);
88
+ assert.equal(batch[0].key.toString('utf8'), 'key1');
89
+ const row = db.prepare('SELECT state FROM migration_dirty_keys WHERE run_id = ? AND key = ?').get('run_5', Buffer.from('key1', 'utf8'));
90
+ assert.equal(row.state, 'deleted');
91
+
92
+ upsertDirtyKey(db, 'run_5', 'key1', 'set');
93
+ const row2 = db.prepare('SELECT state FROM migration_dirty_keys WHERE run_id = ? AND key = ?').get('run_5', Buffer.from('key1', 'utf8'));
94
+ assert.equal(row2.state, 'dirty');
95
+ });
96
+
97
+ it('getDirtyBatch returns keys in state order', () => {
98
+ const db = openDb(tmpDbPath(), { pragmaTemplate: 'minimal' });
99
+ createRun(db, 'run_6', 'redis://x:6379');
100
+ upsertDirtyKey(db, 'run_6', 'a', 'set');
101
+ upsertDirtyKey(db, 'run_6', 'b', 'set');
102
+ const batch = getDirtyBatch(db, 'run_6', 'dirty', 1);
103
+ assert.equal(batch.length, 1);
104
+ assert.ok(['a', 'b'].includes(batch[0].key.toString('utf8')));
105
+ });
106
+
107
+ it('markDirtyState updates state and run counters', () => {
108
+ const db = openDb(tmpDbPath(), { pragmaTemplate: 'minimal' });
109
+ createRun(db, 'run_7', 'redis://x:6379');
110
+ upsertDirtyKey(db, 'run_7', 'k1', 'set');
111
+ markDirtyState(db, 'run_7', 'k1', 'applied');
112
+ assert.equal(getRun(db, 'run_7').dirty_keys_applied, 1);
113
+ upsertDirtyKey(db, 'run_7', 'k2', 'set');
114
+ markDirtyState(db, 'run_7', 'k2', 'deleted');
115
+ assert.equal(getRun(db, 'run_7').dirty_keys_deleted, 1);
116
+ });
117
+
118
+ it('logError inserts into migration_errors', () => {
119
+ const db = openDb(tmpDbPath(), { pragmaTemplate: 'minimal' });
120
+ createRun(db, 'run_8', 'redis://x:6379');
121
+ logError(db, 'run_8', 'bulk', 'test error', Buffer.from('key', 'utf8'));
122
+ const row = db.prepare('SELECT * FROM migration_errors WHERE run_id = ?').get('run_8');
123
+ assert.ok(row);
124
+ assert.equal(row.stage, 'bulk');
125
+ assert.equal(row.message, 'test error');
126
+ });
127
+ });