00ca0561eced | atagen | 2026-10-26 05:50 UTC | 1 | import assert from 'node:assert/strict'; |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 2 | import { test } from 'node:test'; |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 3 | import { |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 4 | isRoomMuted, |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 5 | muteSpacePlan, |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 6 | mutingSpace, |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 7 | parseMutedSpaces, |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 8 | reconcilePlan, |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 9 | spaceMutedRoomIds, |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 10 | unmuteSpacePlan, |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 11 | withSpaceMutes, |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 12 | type MutedSpaces |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 13 | } from '../src/lib/mute-model.ts'; |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 14 | import { badgeTotal } from '../src/lib/notification-model.ts'; |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 15 | import type { RoomSummary } from '../src/lib/types.ts'; |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 16 | |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 17 | function room(id: string, extra: Partial<RoomSummary> = {}): RoomSummary { |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 18 | return { |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 19 | room_id: id, |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 20 | name: id, |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 21 | membership: 'Joined', |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 22 | space: false, |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 23 | direct: false, |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 24 | unread_messages: 0, |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 25 | unread_mentions: 0, |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 26 | notification_mode: 'AllMessages', |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 27 | notification_mode_custom: false, |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 28 | parent_spaces: [], |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 29 | children: [], |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 30 | ...extra |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 31 | } as RoomSummary; |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 32 | } |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 33 | |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 34 | const empty: MutedSpaces = { spaces: [], applied: {} }; |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 35 | |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 36 | const rooms = [ |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 37 | room('!top', { space: true, children: ['!a', '!sub'] }), |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 38 | room('!sub', { space: true, parent_spaces: ['!top'], children: ['!b'] }), |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 39 | room('!a', { parent_spaces: ['!top'], unread_messages: 3 }), |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 40 | room('!b', { parent_spaces: ['!sub'], unread_messages: 2, notification_mode: 'MentionsAndKeywordsOnly', notification_mode_custom: true }), |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 41 | room('!c', { notification_mode: 'Mute', notification_mode_custom: true, parent_spaces: ['!top'] }), |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 42 | room('!other', { unread_messages: 1 }) |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 43 | ]; |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 44 | |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 45 | test('space mute covers nested rooms but not rooms elsewhere', () => { |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 46 | assert.deepEqual([...spaceMutedRoomIds(rooms, ['!top'])].sort(), ['!a', '!b', '!c']); |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 47 | assert.equal(spaceMutedRoomIds(rooms, []).size, 0); |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 48 | }); |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 49 | |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 50 | test('space mute silences the badge total and leaves other rooms', () => { |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 51 | const muted = withSpaceMutes(rooms, spaceMutedRoomIds(rooms, ['!top'])); |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 52 | assert.equal(badgeTotal(muted), 1); |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 53 | assert.equal(badgeTotal(rooms), 4); |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 54 | assert.equal(withSpaceMutes(rooms, new Set()), rooms); |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 55 | }); |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 56 | |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 57 | test('isRoomMuted covers explicit and inherited mute', () => { |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 58 | const inherited = spaceMutedRoomIds(rooms, ['!top']); |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 59 | assert.equal(isRoomMuted(rooms[2], inherited), true); |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 60 | assert.equal(isRoomMuted(rooms[5], inherited), false); |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 61 | assert.equal(isRoomMuted(rooms[4], new Set()), true); |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 62 | }); |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 63 | |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 64 | test('muting a space claims unmuted children with their prior mode', () => { |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 65 | const plan = muteSpacePlan(empty, '!top', rooms); |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 66 | assert.deepEqual(plan.state.spaces, ['!top']); |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 67 | assert.deepEqual(plan.state.applied['!top'], { '!a': null, '!b': 'MentionsAndKeywordsOnly' }); |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 68 | assert.deepEqual(plan.changes.map((c) => c.roomId).sort(), ['!a', '!b']); |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 69 | }); |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 70 | |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 71 | test('unmuting restores prior modes and leaves individual mutes alone', () => { |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 72 | const muted = muteSpacePlan(empty, '!top', rooms).state; |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 73 | const after = rooms.map((r) => (r.room_id === '!a' || r.room_id === '!b' ? { ...r, notification_mode: 'Mute' as const } : r)); |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 74 | const plan = unmuteSpacePlan(muted, '!top', after); |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 75 | assert.deepEqual(plan.state, empty); |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 76 | assert.deepEqual(plan.changes, [ |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 77 | { roomId: '!a', mode: null }, |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 78 | { roomId: '!b', mode: 'MentionsAndKeywordsOnly' } |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 79 | ]); |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 80 | assert.equal(plan.changes.some((c) => c.roomId === '!c'), false); |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 81 | }); |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 82 | |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 83 | test('a room a user unmuted inside a muted space is not reclaimed', () => { |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 84 | const muted = muteSpacePlan(empty, '!top', rooms).state; |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 85 | const after = rooms.map((r) => (r.room_id === '!a' ? { ...r, notification_mode: 'AllMessages' as const } : r)); |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 86 | assert.deepEqual(reconcilePlan(muted, after).changes, []); |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 87 | }); |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 88 | |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 89 | test('reconcile mutes children that joined later', () => { |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 90 | const muted = muteSpacePlan(empty, '!top', rooms).state; |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 91 | const joined = [...rooms, room('!new', { parent_spaces: ['!top'] })]; |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 92 | const plan = reconcilePlan(muted, joined); |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 93 | assert.deepEqual(plan.changes, [{ roomId: '!new', mode: 'Mute' }]); |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 94 | assert.equal(reconcilePlan(plan.state, joined).changes.length, 0); |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 95 | }); |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 96 | |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 97 | test('reconcile ignores rooms whose mode has not loaded', () => { |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 98 | const muted: MutedSpaces = { spaces: ['!top'], applied: {} }; |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 99 | const pending = rooms.map((r) => (r.room_id === '!a' ? { ...r, notification_mode: null } : r)); |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 100 | assert.equal(reconcilePlan(muted, pending).changes.some((c) => c.roomId === '!a'), false); |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 101 | }); |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 102 | |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 103 | test('overlapping muted spaces hand the room to the remaining space on unmute', () => { |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 104 | const both = [...rooms, room('!shared', { parent_spaces: ['!top', '!x'] }), room('!x', { space: true, children: ['!shared'] })]; |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 105 | let state = muteSpacePlan(empty, '!top', both).state; |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 106 | state = muteSpacePlan(state, '!x', both).state; |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 107 | const after = both.map((r) => (r.notification_mode === 'AllMessages' ? { ...r, notification_mode: 'Mute' as const } : r)); |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 108 | const plan = unmuteSpacePlan(state, '!top', after); |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 109 | assert.equal(plan.changes.some((c) => c.roomId === '!shared'), false); |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 110 | assert.ok('!shared' in (plan.state.applied['!x'] ?? {})); |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 111 | assert.equal(mutingSpace(plan.state, after, '!shared'), '!x'); |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 112 | }); |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 113 | |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 114 | test('parseMutedSpaces tolerates junk', () => { |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 115 | assert.deepEqual(parseMutedSpaces(null), empty); |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 116 | assert.deepEqual(parseMutedSpaces({ spaces: ['!a', 3], applied: { '!a': { '!r': 'Mute', '!q': 'Bogus' }, '!gone': { '!r': null } } }), { |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 117 | spaces: ['!a'], |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 118 | applied: { '!a': { '!r': 'Mute' } } |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 119 | }); |
00ca0561eced | atagen | 2026-10-26 05:50 UTC | 120 | }); |