test: add suites for list, roomToParents, roomToUnread reducers (+43)

Via subagent, verified against real behavior (all use jotai store + enableMapSet):
- state/list (11): createListAtom PUT/DELETE/REPLACE (single + array, identity).
- state/room/roomToParents (10): INITIALIZE/PUT/DELETE incl. cycle-skip and
  orphan-cleanup pruning of zero-parent children.
- state/room/roomToUnread (22): unreadInfoToUnread, unreadEqual, and the
  roomToUnreadAtom reducer — leaf/overwrite/equal-guard, multi-level parent
  roll-up with `from` recording, RESET rebuild, DELETE subtract/prune.

No bugs (noted a latent never-hit string-spread in deleteUnreadInfo's `from ??
roomId` fallback; left as-is). Suite growing toward full pure-logic coverage.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-30 14:42:36 -04:00
parent acd355bb5a
commit 589d45e0a0
3 changed files with 502 additions and 0 deletions
+104
View File
@@ -0,0 +1,104 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { createStore } from 'jotai';
import { createListAtom } from './list';
// `createListAtom` produces a write atom whose reducer handles PUT (append a
// single item or an array of items), DELETE (identity-based removal of a single
// item or array of items) and REPLACE (identity match -> replacement). We drive
// the pure reducer through a real jotai store. The exported `TListAtom` type is
// a type alias only, nothing to test at runtime.
test('starts as an empty array', () => {
const store = createStore();
const listAtom = createListAtom<string>();
assert.deepEqual(store.get(listAtom), []);
});
test('PUT appends a single item', () => {
const store = createStore();
const listAtom = createListAtom<string>();
store.set(listAtom, { type: 'PUT', item: 'a' });
store.set(listAtom, { type: 'PUT', item: 'b' });
assert.deepEqual(store.get(listAtom), ['a', 'b']);
});
test('PUT appends an array of items in order', () => {
const store = createStore();
const listAtom = createListAtom<string>();
store.set(listAtom, { type: 'PUT', item: 'a' });
store.set(listAtom, { type: 'PUT', item: ['b', 'c'] });
assert.deepEqual(store.get(listAtom), ['a', 'b', 'c']);
});
test('PUT does not deduplicate', () => {
const store = createStore();
const listAtom = createListAtom<string>();
store.set(listAtom, { type: 'PUT', item: 'a' });
store.set(listAtom, { type: 'PUT', item: 'a' });
assert.deepEqual(store.get(listAtom), ['a', 'a']);
});
test('DELETE removes a single item by identity', () => {
const store = createStore();
const listAtom = createListAtom<string>();
store.set(listAtom, { type: 'PUT', item: ['a', 'b', 'c'] });
store.set(listAtom, { type: 'DELETE', item: 'b' });
assert.deepEqual(store.get(listAtom), ['a', 'c']);
});
test('DELETE removes an array of items', () => {
const store = createStore();
const listAtom = createListAtom<string>();
store.set(listAtom, { type: 'PUT', item: ['a', 'b', 'c', 'd'] });
store.set(listAtom, { type: 'DELETE', item: ['a', 'c'] });
assert.deepEqual(store.get(listAtom), ['b', 'd']);
});
test('DELETE of an absent item is a no-op', () => {
const store = createStore();
const listAtom = createListAtom<string>();
store.set(listAtom, { type: 'PUT', item: ['a', 'b'] });
store.set(listAtom, { type: 'DELETE', item: 'z' });
assert.deepEqual(store.get(listAtom), ['a', 'b']);
});
test('DELETE uses reference identity for object items', () => {
const store = createStore();
const listAtom = createListAtom<{ id: number }>();
const a = { id: 1 };
const b = { id: 2 };
store.set(listAtom, { type: 'PUT', item: [a, b] });
// A structurally-equal but distinct object is not removed.
store.set(listAtom, { type: 'DELETE', item: { id: 1 } });
assert.deepEqual(store.get(listAtom), [a, b]);
// The same reference is removed.
store.set(listAtom, { type: 'DELETE', item: a });
assert.deepEqual(store.get(listAtom), [b]);
});
test('REPLACE swaps the matching item, preserving position', () => {
const store = createStore();
const listAtom = createListAtom<string>();
store.set(listAtom, { type: 'PUT', item: ['a', 'b', 'c'] });
store.set(listAtom, { type: 'REPLACE', item: 'b', replacement: 'B' });
assert.deepEqual(store.get(listAtom), ['a', 'B', 'c']);
});
test('REPLACE matches by identity and replaces every match', () => {
const store = createStore();
const listAtom = createListAtom<string>();
store.set(listAtom, { type: 'PUT', item: ['a', 'b', 'a'] });
store.set(listAtom, { type: 'REPLACE', item: 'a', replacement: 'X' });
assert.deepEqual(store.get(listAtom), ['X', 'b', 'X']);
});
test('REPLACE with no match leaves the list unchanged', () => {
const store = createStore();
const listAtom = createListAtom<string>();
store.set(listAtom, { type: 'PUT', item: ['a', 'b'] });
store.set(listAtom, { type: 'REPLACE', item: 'z', replacement: 'Z' });
assert.deepEqual(store.get(listAtom), ['a', 'b']);
});
+118
View File
@@ -0,0 +1,118 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { createStore } from 'jotai';
import { enableMapSet } from 'immer';
import { roomToParentsAtom } from './roomToParents';
import { RoomToParents } from '../../../types/matrix/room';
// `roomToParentsAtom` maps childRoomId -> Set<parentRoomId>. The reducer handles
// INITIALIZE (replace the whole map), PUT (mapParentWithChildren: register a
// parent for each given child, skipping cycles) and DELETE (drop the room as a
// parent map entry, strip it from every child's parent-set, then prune children
// left with zero parents). The reducers `produce` over an immer-managed Map/Set,
// so we enable that the same way the app does at startup (src/index.tsx).
// The React `useBindRoomToParentsAtom` hook wiring is not covered here.
enableMapSet();
const get = (store: ReturnType<typeof createStore>): RoomToParents => store.get(roomToParentsAtom);
const parentsOf = (store: ReturnType<typeof createStore>, child: string): string[] =>
Array.from(get(store).get(child) ?? []).sort();
test('starts as an empty map', () => {
const store = createStore();
assert.equal(get(store).size, 0);
});
test('INITIALIZE replaces the whole map', () => {
const store = createStore();
const seed: RoomToParents = new Map([['!child:s', new Set(['!space:s'])]]);
store.set(roomToParentsAtom, { type: 'INITIALIZE', roomToParents: seed });
assert.equal(get(store), seed);
assert.deepEqual(parentsOf(store, '!child:s'), ['!space:s']);
});
test('PUT registers a parent for each child', () => {
const store = createStore();
store.set(roomToParentsAtom, {
type: 'PUT',
parent: '!space:s',
children: ['!c1:s', '!c2:s'],
});
assert.deepEqual(parentsOf(store, '!c1:s'), ['!space:s']);
assert.deepEqual(parentsOf(store, '!c2:s'), ['!space:s']);
});
test('PUT accumulates multiple parents for the same child', () => {
const store = createStore();
store.set(roomToParentsAtom, { type: 'PUT', parent: '!p1:s', children: ['!c:s'] });
store.set(roomToParentsAtom, { type: 'PUT', parent: '!p2:s', children: ['!c:s'] });
assert.deepEqual(parentsOf(store, '!c:s'), ['!p1:s', '!p2:s']);
});
test('PUT skips a child that would create a cycle', () => {
const store = createStore();
// !b is a child of !a.
store.set(roomToParentsAtom, { type: 'PUT', parent: '!a:s', children: ['!b:s'] });
// Now make !a a child of !b -> !a's parents include !b, but !b already has !a
// as an ancestor, so the cycle branch skips re-registering.
store.set(roomToParentsAtom, { type: 'PUT', parent: '!b:s', children: ['!a:s'] });
assert.deepEqual(parentsOf(store, '!b:s'), ['!a:s']);
// !a was not registered as a child of !b because that closes a cycle.
assert.equal(get(store).has('!a:s'), false);
});
test('DELETE removes the room as a parent entry', () => {
const store = createStore();
store.set(roomToParentsAtom, { type: 'PUT', parent: '!space:s', children: ['!c:s'] });
// Register !space itself as a child of a grandparent so it has its own entry.
store.set(roomToParentsAtom, { type: 'PUT', parent: '!grand:s', children: ['!space:s'] });
assert.equal(get(store).has('!space:s'), true);
store.set(roomToParentsAtom, { type: 'DELETE', roomId: '!space:s' });
// !space's own entry (as a child of !grand) is gone.
assert.equal(get(store).has('!space:s'), false);
});
test('DELETE strips the room from every child parent-set', () => {
const store = createStore();
store.set(roomToParentsAtom, { type: 'PUT', parent: '!p1:s', children: ['!c:s'] });
store.set(roomToParentsAtom, { type: 'PUT', parent: '!p2:s', children: ['!c:s'] });
store.set(roomToParentsAtom, { type: 'DELETE', roomId: '!p1:s' });
// !c keeps !p2 as a parent.
assert.deepEqual(parentsOf(store, '!c:s'), ['!p2:s']);
});
test('DELETE prunes a child left with zero parents (orphan cleanup)', () => {
const store = createStore();
store.set(roomToParentsAtom, { type: 'PUT', parent: '!space:s', children: ['!c:s'] });
assert.equal(get(store).has('!c:s'), true);
store.set(roomToParentsAtom, { type: 'DELETE', roomId: '!space:s' });
// !c had only !space as a parent; with that gone it is pruned entirely.
assert.equal(get(store).has('!c:s'), false);
assert.equal(get(store).size, 0);
});
test('DELETE removes the room as both a parent and a child', () => {
const store = createStore();
// !mid is a child of !top and a parent of !leaf.
store.set(roomToParentsAtom, { type: 'PUT', parent: '!top:s', children: ['!mid:s'] });
store.set(roomToParentsAtom, { type: 'PUT', parent: '!mid:s', children: ['!leaf:s'] });
store.set(roomToParentsAtom, { type: 'DELETE', roomId: '!mid:s' });
// !mid's own entry is gone (was a child of !top).
assert.equal(get(store).has('!mid:s'), false);
// !leaf lost its only parent !mid, so it is pruned.
assert.equal(get(store).has('!leaf:s'), false);
});
test('DELETE of an unknown room only prunes nothing', () => {
const store = createStore();
store.set(roomToParentsAtom, { type: 'PUT', parent: '!p:s', children: ['!c:s'] });
store.set(roomToParentsAtom, { type: 'DELETE', roomId: '!unknown:s' });
assert.deepEqual(parentsOf(store, '!c:s'), ['!p:s']);
assert.equal(get(store).size, 1);
});
+280
View File
@@ -0,0 +1,280 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { createStore } from 'jotai';
import { enableMapSet } from 'immer';
import { roomToUnreadAtom, unreadInfoToUnread, unreadEqual } from './roomToUnread';
import { roomToParentsAtom } from './roomToParents';
import { RoomToParents, Unread, UnreadInfo } from '../../../types/matrix/room';
// `roomToUnreadAtom` maps roomId -> Unread { total, highlight, from }. The
// reducer handles RESET (rebuild from a list of UnreadInfo), PUT (set one room's
// unread + roll the delta up to all ancestor parents, accumulating their `from`
// sets) and DELETE (remove a room's unread + roll the removal back up to
// parents, pruning a parent whose `from` set becomes empty). Parent aggregation
// reads `roomToParentsAtom`, which we seed directly in the store.
//
// The reducers `produce` over immer-managed Map/Set, so we enable that the same
// way the app does at startup (src/index.tsx). The React
// `useBindRoomToUnreadAtom` hook wiring (timeline/receipt/membership listeners,
// getUnreadInfos) is not covered here.
enableMapSet();
const get = (store: ReturnType<typeof createStore>) => store.get(roomToUnreadAtom);
const info = (roomId: string, total: number, highlight: number): UnreadInfo => ({
roomId,
total,
highlight,
});
const seedParents = (store: ReturnType<typeof createStore>, map: RoomToParents) =>
store.set(roomToParentsAtom, { type: 'INITIALIZE', roomToParents: map });
// ---------------------------------------------------------------------------
// unreadInfoToUnread
// ---------------------------------------------------------------------------
test('unreadInfoToUnread copies counts and nulls `from`', () => {
assert.deepEqual(unreadInfoToUnread(info('!r:s', 5, 2)), {
total: 5,
highlight: 2,
from: null,
});
});
// ---------------------------------------------------------------------------
// unreadEqual
// ---------------------------------------------------------------------------
const u = (total: number, highlight: number, from: Set<string> | null): Unread => ({
total,
highlight,
from,
});
test('unreadEqual: equal counts with both `from` null are equal', () => {
assert.equal(unreadEqual(u(1, 0, null), u(1, 0, null)), true);
});
test('unreadEqual: differing total is not equal', () => {
assert.equal(unreadEqual(u(1, 0, null), u(2, 0, null)), false);
});
test('unreadEqual: differing highlight is not equal', () => {
assert.equal(unreadEqual(u(1, 0, null), u(1, 1, null)), false);
});
test('unreadEqual: one `from` null and the other a set is not equal', () => {
assert.equal(unreadEqual(u(1, 0, null), u(1, 0, new Set(['!a:s']))), false);
assert.equal(unreadEqual(u(1, 0, new Set(['!a:s'])), u(1, 0, null)), false);
});
test('unreadEqual: `from` sets of different size are not equal', () => {
assert.equal(unreadEqual(u(1, 0, new Set(['!a:s'])), u(1, 0, new Set(['!a:s', '!b:s']))), false);
});
test('unreadEqual: same-size `from` sets with same members are equal', () => {
assert.equal(
unreadEqual(u(2, 1, new Set(['!a:s', '!b:s'])), u(2, 1, new Set(['!b:s', '!a:s']))),
true,
);
});
test('unreadEqual: same-size `from` sets with different members are not equal', () => {
assert.equal(unreadEqual(u(1, 0, new Set(['!a:s'])), u(1, 0, new Set(['!b:s']))), false);
});
// ---------------------------------------------------------------------------
// roomToUnreadAtom: PUT (no parents)
// ---------------------------------------------------------------------------
test('starts empty', () => {
const store = createStore();
assert.equal(get(store).size, 0);
});
test('PUT sets a room unread with null `from`', () => {
const store = createStore();
store.set(roomToUnreadAtom, { type: 'PUT', unreadInfo: info('!r:s', 3, 1) });
assert.deepEqual(get(store).get('!r:s'), { total: 3, highlight: 1, from: null });
});
test('PUT overwrites a room unread with the latest counts', () => {
const store = createStore();
store.set(roomToUnreadAtom, { type: 'PUT', unreadInfo: info('!r:s', 3, 1) });
store.set(roomToUnreadAtom, { type: 'PUT', unreadInfo: info('!r:s', 7, 2) });
assert.deepEqual(get(store).get('!r:s'), { total: 7, highlight: 2, from: null });
});
test('PUT with unchanged counts is skipped (same map reference)', () => {
const store = createStore();
store.set(roomToUnreadAtom, { type: 'PUT', unreadInfo: info('!r:s', 3, 1) });
const before = get(store);
store.set(roomToUnreadAtom, { type: 'PUT', unreadInfo: info('!r:s', 3, 1) });
const after = get(store);
// The "skip update if equal" guard returns without producing a new map.
assert.equal(before, after);
});
// ---------------------------------------------------------------------------
// roomToUnreadAtom: PUT with parent aggregation
// ---------------------------------------------------------------------------
test('PUT rolls counts up to a single parent and records `from`', () => {
const store = createStore();
seedParents(store, new Map([['!c:s', new Set(['!space:s'])]]));
store.set(roomToUnreadAtom, { type: 'PUT', unreadInfo: info('!c:s', 4, 2) });
assert.deepEqual(get(store).get('!c:s'), { total: 4, highlight: 2, from: null });
const parent = get(store).get('!space:s');
assert.equal(parent?.total, 4);
assert.equal(parent?.highlight, 2);
assert.deepEqual(Array.from(parent?.from ?? []), ['!c:s']);
});
test('PUT aggregates two children into the same parent', () => {
const store = createStore();
seedParents(
store,
new Map([
['!c1:s', new Set(['!space:s'])],
['!c2:s', new Set(['!space:s'])],
]),
);
store.set(roomToUnreadAtom, { type: 'PUT', unreadInfo: info('!c1:s', 4, 2) });
store.set(roomToUnreadAtom, { type: 'PUT', unreadInfo: info('!c2:s', 1, 1) });
const parent = get(store).get('!space:s');
assert.equal(parent?.total, 5);
assert.equal(parent?.highlight, 3);
assert.deepEqual(Array.from(parent?.from ?? []).sort(), ['!c1:s', '!c2:s']);
});
test('PUT applies only the delta to the parent on re-PUT of the same child', () => {
const store = createStore();
seedParents(store, new Map([['!c:s', new Set(['!space:s'])]]));
store.set(roomToUnreadAtom, { type: 'PUT', unreadInfo: info('!c:s', 4, 2) });
// Increase the child's counts; the parent should reflect the delta, not 4+6.
store.set(roomToUnreadAtom, { type: 'PUT', unreadInfo: info('!c:s', 6, 3) });
const parent = get(store).get('!space:s');
assert.equal(parent?.total, 6);
assert.equal(parent?.highlight, 3);
assert.deepEqual(Array.from(parent?.from ?? []), ['!c:s']);
});
test('PUT rolls up through a grandparent chain', () => {
const store = createStore();
seedParents(
store,
new Map([
['!c:s', new Set(['!mid:s'])],
['!mid:s', new Set(['!top:s'])],
]),
);
store.set(roomToUnreadAtom, { type: 'PUT', unreadInfo: info('!c:s', 2, 1) });
// Both !mid and !top are ancestors of !c.
for (const ancestor of ['!mid:s', '!top:s']) {
const a = get(store).get(ancestor);
assert.equal(a?.total, 2, ancestor);
assert.equal(a?.highlight, 1, ancestor);
assert.deepEqual(Array.from(a?.from ?? []), ['!c:s'], ancestor);
}
});
// ---------------------------------------------------------------------------
// roomToUnreadAtom: RESET
// ---------------------------------------------------------------------------
test('RESET rebuilds the map from scratch', () => {
const store = createStore();
store.set(roomToUnreadAtom, { type: 'PUT', unreadInfo: info('!old:s', 9, 9) });
store.set(roomToUnreadAtom, {
type: 'RESET',
unreadInfos: [info('!a:s', 2, 0), info('!b:s', 3, 1)],
});
assert.equal(get(store).has('!old:s'), false);
assert.deepEqual(get(store).get('!a:s'), { total: 2, highlight: 0, from: null });
assert.deepEqual(get(store).get('!b:s'), { total: 3, highlight: 1, from: null });
});
test('RESET aggregates parents from the seeded roomToParents', () => {
const store = createStore();
seedParents(
store,
new Map([
['!a:s', new Set(['!space:s'])],
['!b:s', new Set(['!space:s'])],
]),
);
store.set(roomToUnreadAtom, {
type: 'RESET',
unreadInfos: [info('!a:s', 2, 1), info('!b:s', 3, 0)],
});
const parent = get(store).get('!space:s');
assert.equal(parent?.total, 5);
assert.equal(parent?.highlight, 1);
assert.deepEqual(Array.from(parent?.from ?? []).sort(), ['!a:s', '!b:s']);
});
// ---------------------------------------------------------------------------
// roomToUnreadAtom: DELETE
// ---------------------------------------------------------------------------
test('DELETE removes a leaf room with no parents', () => {
const store = createStore();
store.set(roomToUnreadAtom, { type: 'PUT', unreadInfo: info('!r:s', 3, 1) });
store.set(roomToUnreadAtom, { type: 'DELETE', roomId: '!r:s' });
assert.equal(get(store).has('!r:s'), false);
});
test('DELETE of an absent room is a no-op (same map reference)', () => {
const store = createStore();
store.set(roomToUnreadAtom, { type: 'PUT', unreadInfo: info('!r:s', 3, 1) });
const before = get(store);
store.set(roomToUnreadAtom, { type: 'DELETE', roomId: '!absent:s' });
assert.equal(get(store), before);
});
test('DELETE subtracts the child counts from a parent that keeps other children', () => {
const store = createStore();
seedParents(
store,
new Map([
['!c1:s', new Set(['!space:s'])],
['!c2:s', new Set(['!space:s'])],
]),
);
store.set(roomToUnreadAtom, { type: 'PUT', unreadInfo: info('!c1:s', 4, 2) });
store.set(roomToUnreadAtom, { type: 'PUT', unreadInfo: info('!c2:s', 1, 1) });
store.set(roomToUnreadAtom, { type: 'DELETE', roomId: '!c1:s' });
assert.equal(get(store).has('!c1:s'), false);
const parent = get(store).get('!space:s');
assert.equal(parent?.total, 1);
assert.equal(parent?.highlight, 1);
assert.deepEqual(Array.from(parent?.from ?? []), ['!c2:s']);
});
test('DELETE prunes a parent whose `from` set becomes empty', () => {
const store = createStore();
seedParents(store, new Map([['!c:s', new Set(['!space:s'])]]));
store.set(roomToUnreadAtom, { type: 'PUT', unreadInfo: info('!c:s', 4, 2) });
store.set(roomToUnreadAtom, { type: 'DELETE', roomId: '!c:s' });
assert.equal(get(store).has('!c:s'), false);
// The parent had only !c contributing, so it is removed entirely.
assert.equal(get(store).has('!space:s'), false);
assert.equal(get(store).size, 0);
});