atagen

malice

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