test: lotus decorations, call caps, crypto, featureCheck, typing, markdown (+34)

Subagent batch (no bugs found) + markdown:
- lotus/avatarDecorations (8): decorationUrl, CDN shape, ALL_DECORATIONS
  flattening, data invariants (unique category ids + slugs, slug charset).
- plugins/call/utils (7): getCallCapabilities — static caps + room/user/device
  scoped state-keys.
- utils/matrix-crypto (3): verifiedDevice via a stubbed CryptoApi.
- utils/featureCheck (3): checkIndexedDBSupport success/error/throw paths.
- state/typingMembers (8): add/dedup-by-latest-ts/per-room-scope/delete reducer
  via a jotai store (enableMapSet, mirroring app startup).
- plugins/markdown/utils (5): inline + block escape/unescape round-trips.

Full suite now 231 tests, all passing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-30 14:32:53 -04:00
parent 9f4516c6a8
commit 6e59395fb8
6 changed files with 385 additions and 0 deletions
+91
View File
@@ -0,0 +1,91 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { createStore } from 'jotai';
import { enableMapSet } from 'immer';
import {
TYPING_TIMEOUT_MS,
roomIdToTypingMembersAtom,
type IRoomIdToTypingMembers,
} from './typingMembers';
// The add/remove/dedup/filter logic lives in the unexported putTypingMember /
// deleteTypingMember reducers, reachable only through `roomIdToTypingMembersAtom`.
// We exercise it via a jotai store (the pure reducer path). The PUT branch also
// schedules a real setTimeout for auto-expiry; that timer-driven cleanup and the
// React `useBindRoomIdToTypingMembersAtom` hook wiring are not covered here.
// The reducers `produce` over an immer-managed Map. The app turns this on once
// at startup (src/index.tsx calls enableMapSet()); we replicate that here.
enableMapSet();
const ROOM = '!room:server';
const A = '@alice:server';
const B = '@bob:server';
const members = (store: ReturnType<typeof createStore>): IRoomIdToTypingMembers =>
store.get(roomIdToTypingMembersAtom);
const userIds = (store: ReturnType<typeof createStore>, roomId: string): string[] =>
(members(store).get(roomId) ?? []).map((r) => r.userId);
test('TYPING_TIMEOUT_MS is 5 seconds', () => {
assert.equal(TYPING_TIMEOUT_MS, 5000);
});
test('starts empty', () => {
const store = createStore();
assert.equal(members(store).size, 0);
});
test('PUT adds a typing member to a room', () => {
const store = createStore();
store.set(roomIdToTypingMembersAtom, { type: 'PUT', roomId: ROOM, userId: A, ts: 1 });
assert.deepEqual(userIds(store, ROOM), [A]);
});
test('PUT dedups by userId, keeping the latest ts', () => {
const store = createStore();
store.set(roomIdToTypingMembersAtom, { type: 'PUT', roomId: ROOM, userId: A, ts: 1 });
store.set(roomIdToTypingMembersAtom, { type: 'PUT', roomId: ROOM, userId: A, ts: 99 });
const receipts = members(store).get(ROOM) ?? [];
assert.equal(receipts.length, 1);
assert.equal(receipts[0].userId, A);
assert.equal(receipts[0].ts, 99);
});
test('PUT keeps distinct users side by side', () => {
const store = createStore();
store.set(roomIdToTypingMembersAtom, { type: 'PUT', roomId: ROOM, userId: A, ts: 1 });
store.set(roomIdToTypingMembersAtom, { type: 'PUT', roomId: ROOM, userId: B, ts: 1 });
assert.deepEqual(userIds(store, ROOM).sort(), [A, B].sort());
});
test('typing members are scoped per room', () => {
const store = createStore();
store.set(roomIdToTypingMembersAtom, { type: 'PUT', roomId: '!r1:s', userId: A, ts: 1 });
store.set(roomIdToTypingMembersAtom, { type: 'PUT', roomId: '!r2:s', userId: B, ts: 1 });
assert.deepEqual(userIds(store, '!r1:s'), [A]);
assert.deepEqual(userIds(store, '!r2:s'), [B]);
});
test('DELETE removes a user and drops the room when it becomes empty', () => {
const store = createStore();
store.set(roomIdToTypingMembersAtom, { type: 'PUT', roomId: ROOM, userId: A, ts: 1 });
store.set(roomIdToTypingMembersAtom, { type: 'PUT', roomId: ROOM, userId: B, ts: 1 });
store.set(roomIdToTypingMembersAtom, { type: 'DELETE', roomId: ROOM, userId: A });
assert.deepEqual(userIds(store, ROOM), [B]);
store.set(roomIdToTypingMembersAtom, { type: 'DELETE', roomId: ROOM, userId: B });
assert.equal(members(store).has(ROOM), false);
});
test('DELETE of an absent user is a no-op (no empty room created)', () => {
const store = createStore();
store.set(roomIdToTypingMembersAtom, { type: 'DELETE', roomId: ROOM, userId: A });
assert.equal(members(store).has(ROOM), false);
store.set(roomIdToTypingMembersAtom, { type: 'PUT', roomId: ROOM, userId: A, ts: 1 });
store.set(roomIdToTypingMembersAtom, { type: 'DELETE', roomId: ROOM, userId: B });
assert.deepEqual(userIds(store, ROOM), [A]);
});