atagen

malice

public
Star0
Watch0
d9be72fdb39386b8e5863ac7f41631066c55cb22
2
ssh://brewery@pls.lobotomise.me:33/atagen/malice.git

Blame: web/test/mute.test.ts

00ca0561ecedatagen2026-10-26 05:50 UTC1
import assert from 'node:assert/strict';
00ca0561ecedatagen2026-10-26 05:50 UTC2
import { test } from 'node:test';
00ca0561ecedatagen2026-10-26 05:50 UTC3
import {
00ca0561ecedatagen2026-10-26 05:50 UTC4
  isRoomMuted,
00ca0561ecedatagen2026-10-26 05:50 UTC5
  muteSpacePlan,
00ca0561ecedatagen2026-10-26 05:50 UTC6
  mutingSpace,
00ca0561ecedatagen2026-10-26 05:50 UTC7
  parseMutedSpaces,
00ca0561ecedatagen2026-10-26 05:50 UTC8
  reconcilePlan,
00ca0561ecedatagen2026-10-26 05:50 UTC9
  spaceMutedRoomIds,
00ca0561ecedatagen2026-10-26 05:50 UTC10
  unmuteSpacePlan,
00ca0561ecedatagen2026-10-26 05:50 UTC11
  withSpaceMutes,
00ca0561ecedatagen2026-10-26 05:50 UTC12
  type MutedSpaces
00ca0561ecedatagen2026-10-26 05:50 UTC13
} from '../src/lib/mute-model.ts';
00ca0561ecedatagen2026-10-26 05:50 UTC14
import { badgeTotal } from '../src/lib/notification-model.ts';
00ca0561ecedatagen2026-10-26 05:50 UTC15
import type { RoomSummary } from '../src/lib/types.ts';
00ca0561ecedatagen2026-10-26 05:50 UTC16
00ca0561ecedatagen2026-10-26 05:50 UTC17
function room(id: string, extra: Partial<RoomSummary> = {}): RoomSummary {
00ca0561ecedatagen2026-10-26 05:50 UTC18
  return {
00ca0561ecedatagen2026-10-26 05:50 UTC19
    room_id: id,
00ca0561ecedatagen2026-10-26 05:50 UTC20
    name: id,
00ca0561ecedatagen2026-10-26 05:50 UTC21
    membership: 'Joined',
00ca0561ecedatagen2026-10-26 05:50 UTC22
    space: false,
00ca0561ecedatagen2026-10-26 05:50 UTC23
    direct: false,
00ca0561ecedatagen2026-10-26 05:50 UTC24
    unread_messages: 0,
00ca0561ecedatagen2026-10-26 05:50 UTC25
    unread_mentions: 0,
00ca0561ecedatagen2026-10-26 05:50 UTC26
    notification_mode: 'AllMessages',
00ca0561ecedatagen2026-10-26 05:50 UTC27
    notification_mode_custom: false,
00ca0561ecedatagen2026-10-26 05:50 UTC28
    parent_spaces: [],
00ca0561ecedatagen2026-10-26 05:50 UTC29
    children: [],
00ca0561ecedatagen2026-10-26 05:50 UTC30
    ...extra
00ca0561ecedatagen2026-10-26 05:50 UTC31
  } as RoomSummary;
00ca0561ecedatagen2026-10-26 05:50 UTC32
}
00ca0561ecedatagen2026-10-26 05:50 UTC33
00ca0561ecedatagen2026-10-26 05:50 UTC34
const empty: MutedSpaces = { spaces: [], applied: {} };
00ca0561ecedatagen2026-10-26 05:50 UTC35
00ca0561ecedatagen2026-10-26 05:50 UTC36
const rooms = [
00ca0561ecedatagen2026-10-26 05:50 UTC37
  room('!top', { space: true, children: ['!a', '!sub'] }),
00ca0561ecedatagen2026-10-26 05:50 UTC38
  room('!sub', { space: true, parent_spaces: ['!top'], children: ['!b'] }),
00ca0561ecedatagen2026-10-26 05:50 UTC39
  room('!a', { parent_spaces: ['!top'], unread_messages: 3 }),
00ca0561ecedatagen2026-10-26 05:50 UTC40
  room('!b', { parent_spaces: ['!sub'], unread_messages: 2, notification_mode: 'MentionsAndKeywordsOnly', notification_mode_custom: true }),
00ca0561ecedatagen2026-10-26 05:50 UTC41
  room('!c', { notification_mode: 'Mute', notification_mode_custom: true, parent_spaces: ['!top'] }),
00ca0561ecedatagen2026-10-26 05:50 UTC42
  room('!other', { unread_messages: 1 })
00ca0561ecedatagen2026-10-26 05:50 UTC43
];
00ca0561ecedatagen2026-10-26 05:50 UTC44
00ca0561ecedatagen2026-10-26 05:50 UTC45
test('space mute covers nested rooms but not rooms elsewhere', () => {
00ca0561ecedatagen2026-10-26 05:50 UTC46
  assert.deepEqual([...spaceMutedRoomIds(rooms, ['!top'])].sort(), ['!a', '!b', '!c']);
00ca0561ecedatagen2026-10-26 05:50 UTC47
  assert.equal(spaceMutedRoomIds(rooms, []).size, 0);
00ca0561ecedatagen2026-10-26 05:50 UTC48
});
00ca0561ecedatagen2026-10-26 05:50 UTC49
00ca0561ecedatagen2026-10-26 05:50 UTC50
test('space mute silences the badge total and leaves other rooms', () => {
00ca0561ecedatagen2026-10-26 05:50 UTC51
  const muted = withSpaceMutes(rooms, spaceMutedRoomIds(rooms, ['!top']));
00ca0561ecedatagen2026-10-26 05:50 UTC52
  assert.equal(badgeTotal(muted), 1);
00ca0561ecedatagen2026-10-26 05:50 UTC53
  assert.equal(badgeTotal(rooms), 4);
00ca0561ecedatagen2026-10-26 05:50 UTC54
  assert.equal(withSpaceMutes(rooms, new Set()), rooms);
00ca0561ecedatagen2026-10-26 05:50 UTC55
});
00ca0561ecedatagen2026-10-26 05:50 UTC56
00ca0561ecedatagen2026-10-26 05:50 UTC57
test('isRoomMuted covers explicit and inherited mute', () => {
00ca0561ecedatagen2026-10-26 05:50 UTC58
  const inherited = spaceMutedRoomIds(rooms, ['!top']);
00ca0561ecedatagen2026-10-26 05:50 UTC59
  assert.equal(isRoomMuted(rooms[2], inherited), true);
00ca0561ecedatagen2026-10-26 05:50 UTC60
  assert.equal(isRoomMuted(rooms[5], inherited), false);
00ca0561ecedatagen2026-10-26 05:50 UTC61
  assert.equal(isRoomMuted(rooms[4], new Set()), true);
00ca0561ecedatagen2026-10-26 05:50 UTC62
});
00ca0561ecedatagen2026-10-26 05:50 UTC63
00ca0561ecedatagen2026-10-26 05:50 UTC64
test('muting a space claims unmuted children with their prior mode', () => {
00ca0561ecedatagen2026-10-26 05:50 UTC65
  const plan = muteSpacePlan(empty, '!top', rooms);
00ca0561ecedatagen2026-10-26 05:50 UTC66
  assert.deepEqual(plan.state.spaces, ['!top']);
00ca0561ecedatagen2026-10-26 05:50 UTC67
  assert.deepEqual(plan.state.applied['!top'], { '!a': null, '!b': 'MentionsAndKeywordsOnly' });
00ca0561ecedatagen2026-10-26 05:50 UTC68
  assert.deepEqual(plan.changes.map((c) => c.roomId).sort(), ['!a', '!b']);
00ca0561ecedatagen2026-10-26 05:50 UTC69
});
00ca0561ecedatagen2026-10-26 05:50 UTC70
00ca0561ecedatagen2026-10-26 05:50 UTC71
test('unmuting restores prior modes and leaves individual mutes alone', () => {
00ca0561ecedatagen2026-10-26 05:50 UTC72
  const muted = muteSpacePlan(empty, '!top', rooms).state;
00ca0561ecedatagen2026-10-26 05:50 UTC73
  const after = rooms.map((r) => (r.room_id === '!a' || r.room_id === '!b' ? { ...r, notification_mode: 'Mute' as const } : r));
00ca0561ecedatagen2026-10-26 05:50 UTC74
  const plan = unmuteSpacePlan(muted, '!top', after);
00ca0561ecedatagen2026-10-26 05:50 UTC75
  assert.deepEqual(plan.state, empty);
00ca0561ecedatagen2026-10-26 05:50 UTC76
  assert.deepEqual(plan.changes, [
00ca0561ecedatagen2026-10-26 05:50 UTC77
    { roomId: '!a', mode: null },
00ca0561ecedatagen2026-10-26 05:50 UTC78
    { roomId: '!b', mode: 'MentionsAndKeywordsOnly' }
00ca0561ecedatagen2026-10-26 05:50 UTC79
  ]);
00ca0561ecedatagen2026-10-26 05:50 UTC80
  assert.equal(plan.changes.some((c) => c.roomId === '!c'), false);
00ca0561ecedatagen2026-10-26 05:50 UTC81
});
00ca0561ecedatagen2026-10-26 05:50 UTC82
00ca0561ecedatagen2026-10-26 05:50 UTC83
test('a room a user unmuted inside a muted space is not reclaimed', () => {
00ca0561ecedatagen2026-10-26 05:50 UTC84
  const muted = muteSpacePlan(empty, '!top', rooms).state;
00ca0561ecedatagen2026-10-26 05:50 UTC85
  const after = rooms.map((r) => (r.room_id === '!a' ? { ...r, notification_mode: 'AllMessages' as const } : r));
00ca0561ecedatagen2026-10-26 05:50 UTC86
  assert.deepEqual(reconcilePlan(muted, after).changes, []);
00ca0561ecedatagen2026-10-26 05:50 UTC87
});
00ca0561ecedatagen2026-10-26 05:50 UTC88
00ca0561ecedatagen2026-10-26 05:50 UTC89
test('reconcile mutes children that joined later', () => {
00ca0561ecedatagen2026-10-26 05:50 UTC90
  const muted = muteSpacePlan(empty, '!top', rooms).state;
00ca0561ecedatagen2026-10-26 05:50 UTC91
  const joined = [...rooms, room('!new', { parent_spaces: ['!top'] })];
00ca0561ecedatagen2026-10-26 05:50 UTC92
  const plan = reconcilePlan(muted, joined);
00ca0561ecedatagen2026-10-26 05:50 UTC93
  assert.deepEqual(plan.changes, [{ roomId: '!new', mode: 'Mute' }]);
00ca0561ecedatagen2026-10-26 05:50 UTC94
  assert.equal(reconcilePlan(plan.state, joined).changes.length, 0);
00ca0561ecedatagen2026-10-26 05:50 UTC95
});
00ca0561ecedatagen2026-10-26 05:50 UTC96
00ca0561ecedatagen2026-10-26 05:50 UTC97
test('reconcile ignores rooms whose mode has not loaded', () => {
00ca0561ecedatagen2026-10-26 05:50 UTC98
  const muted: MutedSpaces = { spaces: ['!top'], applied: {} };
00ca0561ecedatagen2026-10-26 05:50 UTC99
  const pending = rooms.map((r) => (r.room_id === '!a' ? { ...r, notification_mode: null } : r));
00ca0561ecedatagen2026-10-26 05:50 UTC100
  assert.equal(reconcilePlan(muted, pending).changes.some((c) => c.roomId === '!a'), false);
00ca0561ecedatagen2026-10-26 05:50 UTC101
});
00ca0561ecedatagen2026-10-26 05:50 UTC102
00ca0561ecedatagen2026-10-26 05:50 UTC103
test('overlapping muted spaces hand the room to the remaining space on unmute', () => {
00ca0561ecedatagen2026-10-26 05:50 UTC104
  const both = [...rooms, room('!shared', { parent_spaces: ['!top', '!x'] }), room('!x', { space: true, children: ['!shared'] })];
00ca0561ecedatagen2026-10-26 05:50 UTC105
  let state = muteSpacePlan(empty, '!top', both).state;
00ca0561ecedatagen2026-10-26 05:50 UTC106
  state = muteSpacePlan(state, '!x', both).state;
00ca0561ecedatagen2026-10-26 05:50 UTC107
  const after = both.map((r) => (r.notification_mode === 'AllMessages' ? { ...r, notification_mode: 'Mute' as const } : r));
00ca0561ecedatagen2026-10-26 05:50 UTC108
  const plan = unmuteSpacePlan(state, '!top', after);
00ca0561ecedatagen2026-10-26 05:50 UTC109
  assert.equal(plan.changes.some((c) => c.roomId === '!shared'), false);
00ca0561ecedatagen2026-10-26 05:50 UTC110
  assert.ok('!shared' in (plan.state.applied['!x'] ?? {}));
00ca0561ecedatagen2026-10-26 05:50 UTC111
  assert.equal(mutingSpace(plan.state, after, '!shared'), '!x');
00ca0561ecedatagen2026-10-26 05:50 UTC112
});
00ca0561ecedatagen2026-10-26 05:50 UTC113
00ca0561ecedatagen2026-10-26 05:50 UTC114
test('parseMutedSpaces tolerates junk', () => {
00ca0561ecedatagen2026-10-26 05:50 UTC115
  assert.deepEqual(parseMutedSpaces(null), empty);
00ca0561ecedatagen2026-10-26 05:50 UTC116
  assert.deepEqual(parseMutedSpaces({ spaces: ['!a', 3], applied: { '!a': { '!r': 'Mute', '!q': 'Bogus' }, '!gone': { '!r': null } } }), {
00ca0561ecedatagen2026-10-26 05:50 UTC117
    spaces: ['!a'],
00ca0561ecedatagen2026-10-26 05:50 UTC118
    applied: { '!a': { '!r': 'Mute' } }
00ca0561ecedatagen2026-10-26 05:50 UTC119
  });
00ca0561ecedatagen2026-10-26 05:50 UTC120
});