| … | … | @@ -1,19 +1,16 @@ |
| 1 | 1 | /** |
| 2 | | * test-admin-channel.mjs — Admin channel replication + protection tests |
| 2 | * test-admin-channel.mjs — MetadataBase Autobase sync tests |
| 3 | 3 | * |
| 4 | 4 | * Tests: |
| 5 | | * 1. meta.set replicates to admin channel (_hs:room-meta posted) |
| 6 | | * 2. meta.get reads from local cache after set |
| 7 | | * 3. admin channel room exists and is writable |
| 8 | | * 4. send route blocks direct writes to admin channel |
| 9 | | * 5. room.create with roomType creates correct type |
| 10 | | * 6. room.create without roomType creates Default room |
| 11 | | * 7. meta.set with null field removes it (not replicated as null) |
| 12 | | * 8. rapid meta.set calls don't corrupt cache (last-write-wins) |
| 13 | | * 9. sync reads admin channel messages into cache |
| 14 | | * 10. admin channel messages with non-meta _hs types are ignored |
| 15 | | * 11. meta.set for multiple rooms posts separate messages |
| 16 | | * 12. meta.get for unknown roomId returns empty object |
| 5 | * 1. meta.key returns deterministic bootstrap key info |
| 6 | * 2. meta.get reads from Autobase after set |
| 7 | * 3. room.create with roomType creates correct type |
| 8 | * 4. room.create without roomType creates Default room |
| 9 | * 5. meta.set with null field removes it |
| 10 | * 6. rapid meta.set calls don't corrupt (last-write-wins) |
| 11 | * 7. meta.set for multiple rooms stores separately |
| 12 | * 8. meta.get for unknown roomId returns empty object |
| 13 | * 9. meta.get is consistent after multiple operations |
| 17 | 14 | * |
| 18 | 15 | * Requires: keet-daemon running on /tmp/keet-daemon.sock |
| 19 | 16 | * Usage: bare tests/test-admin-channel.mjs |
| … | … | @@ -24,7 +21,6 @@ import FramedStream from 'framed-stream' |
| 24 | 21 | import b4a from 'b4a' |
| 25 | 22 | |
| 26 | 23 | const SOCKET = '/tmp/keet-daemon.sock' |
| 27 | | const ADMIN_CHANNEL = '98q5xy39rsibzjaxt6bjau5y9qfcjzk6phird399tpbq377uzkio' |
| 28 | 24 | let rpcId = 0 |
| 29 | 25 | let passed = 0 |
| 30 | 26 | let failed = 0 |
| … | … | @@ -73,7 +69,7 @@ function assert (condition, label) { |
| 73 | 69 | } |
| 74 | 70 | |
| 75 | 71 | async function run () { |
| 76 | | console.log('=== Admin Channel Regression Tests ===\n') |
| 72 | console.log('=== MetadataBase Autobase Sync Tests ===\n') |
| 77 | 73 | const client = connect() |
| 78 | 74 | |
| 79 | 75 | // Find or create dedicated test room (never pollute real rooms) |
| … | … | @@ -91,54 +87,38 @@ async function run () { |
| 91 | 87 | Bare.exit(1) |
| 92 | 88 | } |
| 93 | 89 | const roomId = testRoom.roomId || testRoom.id |
| 94 | | // Second test room: pick any other non-admin room |
| 90 | // Second test room: pick any other room |
| 95 | 91 | const chatRooms = rooms.filter(r => { |
| 96 | 92 | const id = r.roomId || r.id |
| 97 | | return id !== roomId && id !== ADMIN_CHANNEL && id !== 'nbpfz7aw1k8uko7usjjzatfjbao8mudyd45kyj4a6aknwb45341o' |
| 93 | return id !== roomId |
| 98 | 94 | }) |
| 99 | 95 | const secondRoom = chatRooms.length > 0 ? chatRooms[0] : null |
| 100 | | console.log(`Test room: "${testRoom.title}" (${roomId.slice(0, 12)}...)`) |
| 101 | | console.log(`Admin channel: ${ADMIN_CHANNEL.slice(0, 12)}...\n`) |
| 96 | console.log(`Test room: "${testRoom.title}" (${roomId.slice(0, 12)}...)\n`) |
| 102 | 97 | |
| 103 | 98 | const marker = 'regression-' + Date.now() |
| 104 | 99 | |
| 105 | | // === T1: meta.set replicates to admin channel === |
| 106 | | console.log('--- T1: meta.set replicates to admin channel ---') |
| 100 | // === T1: meta.key returns deterministic bootstrap key === |
| 101 | console.log('--- T1: meta.key returns deterministic bootstrap key ---') |
| 107 | 102 | try { |
| 108 | | await client.call('meta.set', { |
| 109 | | roomId, |
| 110 | | metadata: { topics: [marker], discoverable: true, category: 'test' } |
| 111 | | }) |
| 112 | | passed++ |
| 113 | | console.log(' OK: meta.set returned') |
| 114 | | |
| 115 | | await new Promise(r => setTimeout(r, 2000)) |
| 116 | | |
| 117 | | const msgs = await client.call('read', { roomId: ADMIN_CHANNEL, limit: 20, reverse: true }) |
| 118 | | const messages = Array.isArray(msgs) ? msgs : (msgs?.messages || msgs?.data || []) |
| 119 | | let found = false |
| 120 | | for (const m of messages) { |
| 121 | | const text = m.chat?.text || m.text || m.message?.text || '' |
| 122 | | if (!text.startsWith('{"_hs":')) continue |
| 123 | | try { |
| 124 | | const parsed = JSON.parse(text) |
| 125 | | if (parsed._hs === 'room-meta' && parsed.roomId === roomId && parsed.topics?.includes(marker)) { |
| 126 | | found = true |
| 127 | | break |
| 128 | | } |
| 129 | | } catch {} |
| 130 | | } |
| 131 | | assert(found, 'admin channel contains _hs:room-meta with marker') |
| 103 | const keyResult = await client.call('meta.key') |
| 104 | assert(keyResult.key !== null, 'meta.key returns bootstrap key') |
| 105 | assert(keyResult.writable === true, 'local peer is writable') |
| 106 | assert(keyResult.localKey !== null, 'has local writer key') |
| 107 | assert(typeof keyResult.key === 'string' && keyResult.key.length === 64, 'key is 64-char hex') |
| 132 | 108 | } catch (err) { |
| 133 | 109 | console.log(` FAIL: ${err.message}`) |
| 134 | 110 | failed++ |
| 135 | 111 | } |
| 136 | 112 | |
| 137 | | // === T2: meta.get reads local cache === |
| 138 | | console.log('\n--- T2: meta.get reads local cache ---') |
| 113 | // === T2: meta.get reads Autobase after set === |
| 114 | console.log('\n--- T2: meta.get reads Autobase after set ---') |
| 139 | 115 | try { |
| 116 | await client.call('meta.set', { |
| 117 | roomId, |
| 118 | metadata: { topics: [marker], discoverable: true, category: 'test' } |
| 119 | }) |
| 140 | 120 | const meta = await client.call('meta.get', { roomId }) |
| 141 | | assert(meta.topics?.includes(marker), 'local cache has marker topic') |
| 121 | assert(meta.topics?.includes(marker), 'Autobase has marker topic') |
| 142 | 122 | assert(meta.discoverable === true, 'discoverable preserved') |
| 143 | 123 | assert(meta.category === 'test', 'category preserved') |
| 144 | 124 | } catch (err) { |
| … | … | @@ -146,36 +126,8 @@ async function run () { |
| 146 | 126 | failed++ |
| 147 | 127 | } |
| 148 | 128 | |
| 149 | | // === T3: admin channel room info === |
| 150 | | console.log('\n--- T3: admin channel room exists ---') |
| 151 | | try { |
| 152 | | const info = await client.call('room', { roomId: ADMIN_CHANNEL }) |
| 153 | | assert(info !== null, 'admin channel room exists') |
| 154 | | assert(info.status?.isWritable === true, 'admin channel is writable') |
| 155 | | // keet-core internal: digest.settings.roomType 1 = Default, 2 = Broadcast, 3 = DM |
| 156 | | // (config.roomType uses compat format: '0' = Default, '1' = Broadcast, '2' = DM) |
| 157 | | const roomType = info.digest?.settings?.roomType |
| 158 | | assert(roomType === 1, `admin channel is Default type (digest.roomType=${roomType})`) |
| 159 | | } catch (err) { |
| 160 | | console.log(` FAIL: ${err.message}`) |
| 161 | | failed++ |
| 162 | | } |
| 163 | | |
| 164 | | // === T4: send route blocks admin channel === |
| 165 | | console.log('\n--- T4: send blocks direct writes to admin channel ---') |
| 166 | | try { |
| 167 | | await client.call('send', { roomId: ADMIN_CHANNEL, text: 'should be blocked' }) |
| 168 | | console.log(' FAIL: should have thrown') |
| 169 | | failed++ |
| 170 | | } catch (err) { |
| 171 | | assert( |
| 172 | | err.message.includes('read-only') || err.message.includes('meta.set'), |
| 173 | | `correctly rejects: ${err.message}` |
| 174 | | ) |
| 175 | | } |
| 176 | | |
| 177 | | // === T5: room.create with roomType === |
| 178 | | console.log('\n--- T5: room.create with roomType=1 (Broadcast) ---') |
| 129 | // === T3: room.create with roomType === |
| 130 | console.log('\n--- T3: room.create with roomType=1 (Broadcast) ---') |
| 179 | 131 | let broadcastId = null |
| 180 | 132 | try { |
| 181 | 133 | const result = await client.call('room.create', { title: '_test-broadcast-' + Date.now(), roomType: '1' }) |
| … | … | @@ -191,8 +143,8 @@ async function run () { |
| 191 | 143 | failed++ |
| 192 | 144 | } |
| 193 | 145 | |
| 194 | | // === T6: room.create without roomType === |
| 195 | | console.log('\n--- T6: room.create without roomType (Default) ---') |
| 146 | // === T4: room.create without roomType === |
| 147 | console.log('\n--- T4: room.create without roomType (Default) ---') |
| 196 | 148 | let defaultId = null |
| 197 | 149 | try { |
| 198 | 150 | const result = await client.call('room.create', { title: '_test-default-' + Date.now() }) |
| … | … | @@ -208,20 +160,20 @@ async function run () { |
| 208 | 160 | failed++ |
| 209 | 161 | } |
| 210 | 162 | |
| 211 | | // === T7: meta.set null removes field === |
| 212 | | console.log('\n--- T7: meta.set null removes field ---') |
| 163 | // === T5: meta.set null removes field === |
| 164 | console.log('\n--- T5: meta.set null removes field ---') |
| 213 | 165 | try { |
| 214 | 166 | await client.call('meta.set', { roomId, metadata: { category: null } }) |
| 215 | 167 | const meta = await client.call('meta.get', { roomId }) |
| 216 | | assert(meta.category === undefined, 'null field removed from cache') |
| 168 | assert(meta.category === undefined, 'null field removed') |
| 217 | 169 | assert(meta.topics?.includes(marker), 'other fields preserved') |
| 218 | 170 | } catch (err) { |
| 219 | 171 | console.log(` FAIL: ${err.message}`) |
| 220 | 172 | failed++ |
| 221 | 173 | } |
| 222 | 174 | |
| 223 | | // === T8: rapid meta.set (last-write-wins) === |
| 224 | | console.log('\n--- T8: rapid meta.set last-write-wins ---') |
| 175 | // === T6: rapid meta.set (last-write-wins) === |
| 176 | console.log('\n--- T6: rapid meta.set last-write-wins ---') |
| 225 | 177 | try { |
| 226 | 178 | // Fire 3 rapid sets |
| 227 | 179 | await client.call('meta.set', { roomId, metadata: { topics: ['first'] } }) |
| … | … | @@ -229,65 +181,21 @@ async function run () { |
| 229 | 181 | await client.call('meta.set', { roomId, metadata: { topics: ['third'] } }) |
| 230 | 182 | |
| 231 | 183 | const meta = await client.call('meta.get', { roomId }) |
| 232 | | assert(meta.topics?.includes('third'), 'last write wins in cache') |
| 184 | assert(meta.topics?.includes('third'), 'last write wins') |
| 233 | 185 | assert(!meta.topics?.includes('first'), 'earlier writes overwritten') |
| 234 | 186 | } catch (err) { |
| 235 | 187 | console.log(` FAIL: ${err.message}`) |
| 236 | 188 | failed++ |
| 237 | 189 | } |
| 238 | 190 | |
| 239 | | // === T9: sync reads from admin channel === |
| 240 | | console.log('\n--- T9: admin channel has multiple room-meta messages ---') |
| 241 | | try { |
| 242 | | await new Promise(r => setTimeout(r, 1000)) |
| 243 | | const msgs = await client.call('read', { roomId: ADMIN_CHANNEL, limit: 50, reverse: false }) |
| 244 | | const messages = Array.isArray(msgs) ? msgs : (msgs?.messages || msgs?.data || []) |
| 245 | | let metaCount = 0 |
| 246 | | const seenRooms = new Set() |
| 247 | | for (const m of messages) { |
| 248 | | const text = m.chat?.text || m.text || m.message?.text || '' |
| 249 | | if (!text.startsWith('{"_hs":')) continue |
| 250 | | try { |
| 251 | | const parsed = JSON.parse(text) |
| 252 | | if (parsed._hs === 'room-meta' && parsed.roomId) { |
| 253 | | metaCount++ |
| 254 | | seenRooms.add(parsed.roomId) |
| 255 | | } |
| 256 | | } catch {} |
| 257 | | } |
| 258 | | assert(metaCount > 0, `found ${metaCount} _hs:room-meta messages in channel`) |
| 259 | | assert(seenRooms.has(roomId), 'our test room is in channel') |
| 260 | | } catch (err) { |
| 261 | | console.log(` FAIL: ${err.message}`) |
| 262 | | failed++ |
| 263 | | } |
| 264 | | |
| 265 | | // === T10: non-meta _hs messages in channel are ignored by sync === |
| 266 | | console.log('\n--- T10: non-meta messages don\'t affect cache ---') |
| 267 | | try { |
| 268 | | // Read current meta for our room |
| 269 | | const before = await client.call('meta.get', { roomId }) |
| 270 | | const topicsBefore = JSON.stringify(before.topics) |
| 271 | | |
| 272 | | // The sync should only process _hs:room-meta, not other types |
| 273 | | // We can't easily inject a non-meta message (send is blocked), |
| 274 | | // but we verify the cache is consistent after sync |
| 275 | | assert(before.topics !== undefined, 'cache has topics after sync') |
| 276 | | assert(typeof before === 'object', 'cache returns object not array') |
| 277 | | } catch (err) { |
| 278 | | console.log(` FAIL: ${err.message}`) |
| 279 | | failed++ |
| 280 | | } |
| 281 | | |
| 282 | | // === T11: meta.set for multiple rooms === |
| 283 | | console.log('\n--- T11: multiple rooms get separate messages ---') |
| 191 | // === T7: meta.set for multiple rooms === |
| 192 | console.log('\n--- T7: multiple rooms store separately ---') |
| 284 | 193 | if (secondRoom) { |
| 285 | 194 | const secondId = secondRoom.roomId || secondRoom.id |
| 286 | 195 | try { |
| 287 | 196 | const marker2 = 'multi-' + Date.now() |
| 288 | 197 | await client.call('meta.set', { roomId, metadata: { topics: [marker2] } }) |
| 289 | 198 | await client.call('meta.set', { roomId: secondId, metadata: { topics: [marker2 + '-b'] } }) |
| 290 | | await new Promise(r => setTimeout(r, 2000)) |
| 291 | 199 | |
| 292 | 200 | const meta1 = await client.call('meta.get', { roomId }) |
| 293 | 201 | const meta2 = await client.call('meta.get', { roomId: secondId }) |
| … | … | @@ -302,8 +210,8 @@ async function run () { |
| 302 | 210 | console.log(' SKIP: only 1 chat room available') |
| 303 | 211 | } |
| 304 | 212 | |
| 305 | | // === T12: meta.get for unknown roomId === |
| 306 | | console.log('\n--- T12: meta.get unknown roomId returns {} ---') |
| 213 | // === T8: meta.get for unknown roomId === |
| 214 | console.log('\n--- T8: meta.get unknown roomId returns {} ---') |
| 307 | 215 | try { |
| 308 | 216 | const meta = await client.call('meta.get', { roomId: 'nonexistent-room-id-12345' }) |
| 309 | 217 | assert(typeof meta === 'object', 'returns object') |
| … | … | @@ -313,6 +221,17 @@ async function run () { |
| 313 | 221 | failed++ |
| 314 | 222 | } |
| 315 | 223 | |
| 224 | // === T9: meta.get consistency === |
| 225 | console.log('\n--- T9: meta.get consistent after operations ---') |
| 226 | try { |
| 227 | const meta = await client.call('meta.get', { roomId }) |
| 228 | assert(meta.topics !== undefined, 'has topics after all operations') |
| 229 | assert(typeof meta === 'object', 'returns object not array') |
| 230 | } catch (err) { |
| 231 | console.log(` FAIL: ${err.message}`) |
| 232 | failed++ |
| 233 | } |
| 234 | |
| 316 | 235 | // === Cleanup === |
| 317 | 236 | console.log('\n--- Cleanup ---') |
| 318 | 237 | try { |