OverviewFilesCommitsIssuesDocsReleasesPeers

keet-cli commits / c92b7e2

Add --debug flag and discovery integration tests
- --debug flag enables verbose discovery logging via debug.js module - test-discovery.js: 13 tests covering single-registry ops and critical close-rename-reopen pattern for multi-registry - All 13 tests pass — confirms RoomDiscovery class works correctly Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
c92b7e27db5229d08647d803b24f8195c497a668
by plankatron <fleeky@gmail.com> · 2026-02-20T00:33:21.000Z

Changed files (2) +373 -1

index.js +5 -1
……@@ -10,6 +10,7 @@ const rawArgs = Pear.config.args || []
1010
1111let customStorage = null
1212let customSocket = null
13let debugMode = false
1314const args = []
1415for (let i = 0; i < rawArgs.length; i++) {
1516 if (rawArgs[i] === '--storage' && rawArgs[i + 1]) {
……@@ -26,6 +27,8 @@ for (let i = 0; i < rawArgs.length; i++) {
2627 customSocket = rawArgs[++i]
2728 } else if (rawArgs[i].startsWith('--socket=')) {
2829 customSocket = rawArgs[i].split('=')[1]
30 } else if (rawArgs[i] === '--debug') {
31 debugMode = true
2932 } else {
3033 args.push(rawArgs[i])
3134 }
……@@ -46,6 +49,7 @@ Options:
4649 --profile <name> Use named profile (each profile = separate identity)
4750 --storage <path> Use custom storage path
4851 --socket <path> Daemon socket (default: /tmp/keet-daemon.sock)
52 --debug Enable verbose discovery debug logging
4953
5054Daemon Commands:
5155 daemon Start daemon (foreground, use tmux for background)
……@@ -158,7 +162,7 @@ if (command === 'daemon') {
158162
159163 // Start daemon (foreground)
160164 const { startDaemon } = await import('./daemon.js')
161 await startDaemon({ storage: STORAGE, socketPath: SOCKET })
165 await startDaemon({ storage: STORAGE, socketPath: SOCKET, debug: debugMode })
162166 // Keep alive
163167 await new Promise(() => {})
164168}
test-discovery.js +368 -0
……@@ -0,0 +1,368 @@
1/**
2 * Test runner for discovery integration tests.
3 * Run from keet-cli dir:
4 * node test-discovery.js
5 * bare test-discovery.js
6 */
7
8// Use relative path to keet-daemon package (not in npm, copied during CI)
9const DAEMON_PKG = '../packages/keet-daemon'
10
11const { RoomDiscovery } = await import(DAEMON_PKG + '/lib/room-discovery.js')
12const { setDebug, dbg } = await import(DAEMON_PKG + '/lib/debug.js')
13
14import fs from 'fs'
15import path from 'path'
16import os from 'os'
17
18// Enable debug for all tests
19setDebug(true)
20
21let passed = 0
22let failed = 0
23const tmpDirs = []
24
25function tmpDir (label) {
26 const dir = path.join(os.tmpdir(), 'keet-test-' + label + '-' + Date.now() + '-' + Math.random().toString(36).slice(2, 8))
27 fs.mkdirSync(dir, { recursive: true })
28 tmpDirs.push(dir)
29 return dir
30}
31
32function rmrf (dir) {
33 try {
34 fs.rmSync(dir, { recursive: true, force: true })
35 } catch {}
36}
37
38async function test (name, fn) {
39 try {
40 await fn()
41 passed++
42 console.log(' PASS:', name)
43 } catch (err) {
44 failed++
45 console.log(' FAIL:', name)
46 console.log(' ', err.message)
47 if (err.stack) console.log(' ', err.stack.split('\n').slice(1, 3).join('\n '))
48 }
49}
50
51function assert (cond, msg) {
52 if (!cond) throw new Error(msg || 'Assertion failed')
53}
54
55function assertEqual (a, b, msg) {
56 if (a !== b) throw new Error((msg || 'assertEqual') + ': ' + JSON.stringify(a) + ' !== ' + JSON.stringify(b))
57}
58
59// ============================================================
60// T1: Single Autobase (RoomDiscovery)
61// ============================================================
62
63console.log('\n--- T1: Single RoomDiscovery ---')
64
65await test('T1.1 Create registry and verify admin role', async () => {
66 const dir = tmpDir('t1-1')
67 const disc = new RoomDiscovery({ storagePath: dir, bootstrapKey: null })
68 await disc.ready()
69
70 assert(disc._base.writable, 'should be writable (indexer)')
71 assert(disc.localWriterKey, 'should have localWriterKey')
72
73 const roleEntry = await disc.getRole(disc.localWriterKey)
74 assert(roleEntry, 'should have role entry')
75 assertEqual(roleEntry.role, 'admin', 'creator should be admin')
76
77 await disc.close()
78})
79
80await test('T1.2 Register room and verify in view', async () => {
81 const dir = tmpDir('t1-2')
82 const disc = new RoomDiscovery({ storagePath: dir, bootstrapKey: null })
83 await disc.ready()
84
85 const roomId = 'test-room-' + Date.now()
86 await disc.register(roomId, 'Test Room', 'A test', ['chat'])
87
88 const entry = await disc.view.get('roomid:' + roomId)
89 assert(entry, 'roomid entry should exist')
90
91 const room = await disc.view.get('room:' + entry.value.titleLower)
92 assert(room, 'room entry should exist')
93 assertEqual(room.value.title, 'Test Room')
94 assertEqual(room.value.description, 'A test')
95
96 await disc.close()
97})
98
99await test('T1.3 Register duplicate title throws', async () => {
100 const dir = tmpDir('t1-3')
101 const disc = new RoomDiscovery({ storagePath: dir, bootstrapKey: null })
102 await disc.ready()
103
104 await disc.register('room-a', 'UniqueTitle', '', [])
105
106 let threw = false
107 try {
108 await disc.register('room-b', 'UniqueTitle', '', [])
109 } catch (err) {
110 threw = true
111 assert(err.message.includes('taken'), 'should say title is taken: ' + err.message)
112 }
113 assert(threw, 'should throw for duplicate title')
114
115 await disc.close()
116})
117
118await test('T1.4 Register duplicate roomId throws', async () => {
119 const dir = tmpDir('t1-4')
120 const disc = new RoomDiscovery({ storagePath: dir, bootstrapKey: null })
121 await disc.ready()
122
123 await disc.register('same-room-id', 'Title A', '', [])
124
125 let threw = false
126 try {
127 await disc.register('same-room-id', 'Title B', '', [])
128 } catch (err) {
129 threw = true
130 assert(err.message.includes('already registered'), 'should say already registered: ' + err.message)
131 }
132 assert(threw, 'should throw for duplicate roomId')
133
134 await disc.close()
135})
136
137await test('T1.5 Unregister room removes from view', async () => {
138 const dir = tmpDir('t1-5')
139 const disc = new RoomDiscovery({ storagePath: dir, bootstrapKey: null })
140 await disc.ready()
141
142 const roomId = 'del-room-' + Date.now()
143 await disc.register(roomId, 'Deletable', '', [])
144
145 const before = await disc.view.get('roomid:' + roomId)
146 assert(before, 'room should exist before unregister')
147
148 await disc.unregister(roomId)
149
150 const after = await disc.view.get('roomid:' + roomId)
151 assert(!after, 'room should be gone after unregister')
152
153 await disc.close()
154})
155
156await test('T1.6 Ban room prevents re-registration', async () => {
157 const dir = tmpDir('t1-6')
158 const disc = new RoomDiscovery({ storagePath: dir, bootstrapKey: null })
159 await disc.ready()
160
161 const roomId = 'ban-room-' + Date.now()
162 await disc.register(roomId, 'BanMe', '', [])
163 await disc.banRoom(roomId, 'spam')
164
165 const banned = await disc.isBanned(roomId)
166 assert(banned, 'room should be banned')
167
168 let threw = false
169 try {
170 await disc.register(roomId, 'BanMe Again', '', [])
171 } catch (err) {
172 threw = true
173 assert(err.message.includes('banned'), 'should say room is banned: ' + err.message)
174 }
175 assert(threw, 'should throw when registering banned room')
176
177 await disc.close()
178})
179
180await test('T1.7 List rooms returns registered rooms', async () => {
181 const dir = tmpDir('t1-7')
182 const disc = new RoomDiscovery({ storagePath: dir, bootstrapKey: null })
183 await disc.ready()
184
185 await disc.register('list-1', 'Alpha Room', '', ['tag1'])
186 await disc.register('list-2', 'Beta Room', '', ['tag2'])
187
188 const rooms = await disc.list()
189 assert(rooms.length >= 2, 'should have at least 2 rooms, got ' + rooms.length)
190
191 const titles = rooms.map(r => r.title)
192 assert(titles.includes('Alpha Room'), 'should include Alpha Room')
193 assert(titles.includes('Beta Room'), 'should include Beta Room')
194
195 await disc.close()
196})
197
198// ============================================================
199// T2: Multi-Registry (Create, Close-Rename-Reopen pattern)
200// ============================================================
201
202console.log('\n--- T2: Multi-Registry ---')
203
204await test('T2.1 New registry returns key and writable instance', async () => {
205 const dir = tmpDir('t2-1')
206 const disc = new RoomDiscovery({ storagePath: dir, bootstrapKey: null })
207 await disc.ready()
208
209 assert(disc._base.key, 'should have a key')
210 assert(disc._base.writable, 'should be writable')
211 assert(disc.localWriterKey, 'should have localWriterKey')
212
213 await disc.close()
214})
215
216await test('T2.2 CRITICAL: Close-rename-reopen preserves writer key', async () => {
217 const tempDir = tmpDir('t2-2-temp')
218 const disc1 = new RoomDiscovery({ storagePath: tempDir, bootstrapKey: null })
219 await disc1.ready()
220
221 const key = disc1._base.key.toString('hex')
222 const writerKey1 = disc1.localWriterKey
223 const writable1 = disc1._base.writable
224
225 console.log(' Phase 1: key=' + key.slice(0, 12) + ' writerKey=' + writerKey1.slice(0, 12) + ' writable=' + writable1)
226
227 await disc1.close()
228
229 const renamedDir = tmpDir('t2-2-renamed')
230 rmrf(renamedDir)
231 fs.renameSync(tempDir, renamedDir)
232
233 const disc2 = new RoomDiscovery({ storagePath: renamedDir, bootstrapKey: key })
234 await disc2.ready()
235
236 const writerKey2 = disc2.localWriterKey
237 const writable2 = disc2._base.writable
238
239 console.log(' Phase 2: writerKey=' + writerKey2.slice(0, 12) + ' writable=' + writable2)
240
241 assertEqual(writerKey1, writerKey2, 'Writer key MUST survive close-rename-reopen')
242 assertEqual(writable2, true, 'Must still be writable (indexer)')
243
244 await disc2.close()
245})
246
247await test('T2.3 CRITICAL: Close-rename-reopen preserves admin role', async () => {
248 const tempDir = tmpDir('t2-3-temp')
249 const disc1 = new RoomDiscovery({ storagePath: tempDir, bootstrapKey: null })
250 await disc1.ready()
251
252 const key = disc1._base.key.toString('hex')
253 const writerKey1 = disc1.localWriterKey
254
255 const roleEntry1 = await disc1.getRole(writerKey1)
256 assert(roleEntry1, 'should have role entry in phase 1')
257 assertEqual(roleEntry1.role, 'admin', 'Should be admin in phase 1')
258
259 await disc1.close()
260
261 const renamedDir = tmpDir('t2-3-renamed')
262 rmrf(renamedDir)
263 fs.renameSync(tempDir, renamedDir)
264
265 const disc2 = new RoomDiscovery({ storagePath: renamedDir, bootstrapKey: key })
266 await disc2.ready()
267
268 const writerKey2 = disc2.localWriterKey
269 const roleEntry2 = await disc2.getRole(writerKey2)
270 console.log(' Phase 2: writerKey=' + writerKey2.slice(0, 12) + ' role=' + (roleEntry2 ? roleEntry2.role : '(null)'))
271
272 assert(roleEntry2, 'should have role entry in phase 2')
273 assertEqual(roleEntry2.role, 'admin', 'Admin role MUST survive close-rename-reopen')
274
275 await disc2.close()
276})
277
278await test('T2.4 CRITICAL: Register room works after close-rename-reopen', async () => {
279 const tempDir = tmpDir('t2-4-temp')
280 const disc1 = new RoomDiscovery({ storagePath: tempDir, bootstrapKey: null })
281 await disc1.ready()
282
283 const key = disc1._base.key.toString('hex')
284 await disc1.close()
285
286 const renamedDir = tmpDir('t2-4-renamed')
287 rmrf(renamedDir)
288 fs.renameSync(tempDir, renamedDir)
289
290 const disc2 = new RoomDiscovery({ storagePath: renamedDir, bootstrapKey: key })
291 await disc2.ready()
292
293 const roomId = 'post-reopen-' + Date.now()
294 await disc2.register(roomId, 'Post Reopen Room', '', [])
295
296 const entry = await disc2.view.get('roomid:' + roomId)
297 assert(entry, 'Room should be registered after close-rename-reopen')
298
299 const rooms = await disc2.list()
300 const titles = rooms.map(r => r.title)
301 assert(titles.includes('Post Reopen Room'), 'Room should appear in list')
302
303 await disc2.close()
304})
305
306await test('T2.5 Two registries are isolated', async () => {
307 const dir1 = tmpDir('t2-5a')
308 const dir2 = tmpDir('t2-5b')
309
310 const disc1 = new RoomDiscovery({ storagePath: dir1, bootstrapKey: null })
311 const disc2 = new RoomDiscovery({ storagePath: dir2, bootstrapKey: null })
312 await disc1.ready()
313 await disc2.ready()
314
315 await disc1.register('room-in-1', 'Only In Registry 1', '', [])
316 await disc2.register('room-in-2', 'Only In Registry 2', '', [])
317
318 const list1 = await disc1.list()
319 const list2 = await disc2.list()
320
321 assert(list1.some(r => r.title === 'Only In Registry 1'), 'Registry 1 should have its room')
322 assert(!list1.some(r => r.title === 'Only In Registry 2'), 'Registry 1 should NOT have registry 2\'s room')
323 assert(list2.some(r => r.title === 'Only In Registry 2'), 'Registry 2 should have its room')
324 assert(!list2.some(r => r.title === 'Only In Registry 1'), 'Registry 2 should NOT have registry 1\'s room')
325
326 await disc1.close()
327 await disc2.close()
328})
329
330await test('T2.6 SetMeta + GetMeta work after close-rename-reopen', async () => {
331 const tempDir = tmpDir('t2-6-temp')
332 const disc1 = new RoomDiscovery({ storagePath: tempDir, bootstrapKey: null })
333 await disc1.ready()
334
335 const key = disc1._base.key.toString('hex')
336 await disc1.setMeta('name', 'Testy Group')
337 await disc1.close()
338
339 const renamedDir = tmpDir('t2-6-renamed')
340 rmrf(renamedDir)
341 fs.renameSync(tempDir, renamedDir)
342
343 const disc2 = new RoomDiscovery({ storagePath: renamedDir, bootstrapKey: key })
344 await disc2.ready()
345
346 const meta = await disc2.getMeta('name')
347 assertEqual(meta, 'Testy Group', 'Metadata should survive close-rename-reopen')
348
349 await disc2.close()
350})
351
352// ============================================================
353// Summary
354// ============================================================
355
356console.log('\n--- Results ---')
357console.log(` ${passed} passed, ${failed} failed`)
358
359// Cleanup
360for (const dir of tmpDirs) rmrf(dir)
361
362if (failed > 0) {
363 console.log('\nFAILED')
364 if (typeof process !== 'undefined') process.exitCode = 1
365 if (typeof Bare !== 'undefined') Bare.exit(1)
366} else {
367 console.log('\nALL PASS')
368}