test: add suites for utils/room (40) + plugins/matrix-to (7)

- utils/room (40, via subagent): 28 helpers — state-event accessors, m.direct
  parsing, space/room classification, parent/child graph (incl. cycle safety),
  mute-rule + notification logic, unread info, reply trimming, member display/
  avatar/search, reaction/edit/mention extraction, room-icon branches. SDK/
  crypto-heavy helpers skipped. No bugs found.
- plugins/matrix-to (7): matrix.to permalink build + parse for user/room/event
  including via-server round-trips and negative cases.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-30 14:28:32 -04:00
parent d37fa1584c
commit 0bd2273bee
2 changed files with 659 additions and 0 deletions
+77
View File
@@ -0,0 +1,77 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import {
getMatrixToUser,
getMatrixToRoom,
getMatrixToRoomEvent,
testMatrixTo,
parseMatrixToUser,
parseMatrixToRoom,
parseMatrixToRoomEvent,
} from './matrix-to';
test('getMatrixToUser builds a user permalink', () => {
assert.equal(getMatrixToUser('@alice:example.org'), 'https://matrix.to/#/@alice:example.org');
});
test('getMatrixToRoom builds room links with optional via servers', () => {
assert.equal(getMatrixToRoom('#room:example.org'), 'https://matrix.to/#/#room:example.org');
assert.equal(
getMatrixToRoom('!abc:example.org', ['a.org', 'b.org']),
'https://matrix.to/#/!abc:example.org?via=a.org&via=b.org',
);
// empty via array → no query string
assert.equal(getMatrixToRoom('!abc:example.org', []), 'https://matrix.to/#/!abc:example.org');
});
test('getMatrixToRoomEvent builds event links', () => {
assert.equal(
getMatrixToRoomEvent('!abc:example.org', '$evt', ['a.org']),
'https://matrix.to/#/!abc:example.org/$evt?via=a.org',
);
assert.equal(
getMatrixToRoomEvent('!abc:example.org', '$evt'),
'https://matrix.to/#/!abc:example.org/$evt',
);
});
test('testMatrixTo recognizes matrix.to hrefs', () => {
assert.equal(testMatrixTo('https://matrix.to/#/@a:b.org'), true);
assert.equal(testMatrixTo('http://matrix.to/#/!r:b.org'), true);
assert.equal(testMatrixTo('https://example.org/#/@a:b.org'), false);
});
test('parseMatrixToUser round-trips and rejects non-user links', () => {
assert.equal(parseMatrixToUser(getMatrixToUser('@a:b.org')), '@a:b.org');
assert.equal(parseMatrixToUser('https://matrix.to/#/@a:b.org/'), '@a:b.org'); // trailing slash ok
assert.equal(parseMatrixToUser('https://matrix.to/#/#room:b.org'), undefined);
assert.equal(parseMatrixToUser('https://example.org'), undefined);
});
test('parseMatrixToRoom extracts alias/id and via servers', () => {
assert.deepEqual(parseMatrixToRoom('https://matrix.to/#/#room:b.org'), {
roomIdOrAlias: '#room:b.org',
viaServers: undefined,
});
assert.deepEqual(parseMatrixToRoom(getMatrixToRoom('!abc:b.org', ['a.org', 'c.org'])), {
roomIdOrAlias: '!abc:b.org',
viaServers: ['a.org', 'c.org'],
});
// a user link is not a room link
assert.equal(parseMatrixToRoom('https://matrix.to/#/@a:b.org'), undefined);
});
test('parseMatrixToRoomEvent extracts room, event, and via servers', () => {
assert.deepEqual(parseMatrixToRoomEvent(getMatrixToRoomEvent('!abc:b.org', '$e', ['a.org'])), {
roomIdOrAlias: '!abc:b.org',
eventId: '$e',
viaServers: ['a.org'],
});
assert.deepEqual(parseMatrixToRoomEvent('https://matrix.to/#/!abc:b.org/$e'), {
roomIdOrAlias: '!abc:b.org',
eventId: '$e',
viaServers: undefined,
});
// a room-only link has no event
assert.equal(parseMatrixToRoomEvent('https://matrix.to/#/!abc:b.org'), undefined);
});
+582
View File
@@ -0,0 +1,582 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import {
EventTimeline,
JoinRule,
MatrixEvent,
NotificationCountType,
Room,
RoomMember,
} from 'matrix-js-sdk';
import {
getStateEvent,
getStateEvents,
getMDirects,
isDirectInvite,
isSpace,
isRoom,
isUnsupportedRoom,
isValidChild,
getAllParents,
getSpaceChildren,
mapParentWithChildren,
getOrphanParents,
isMutedRule,
findMutedRule,
isNotificationEvent,
roomHaveNotification,
getUnreadInfo,
getRoomIconSrc,
trimReplyFromBody,
trimReplyFromFormattedBody,
parseReplyBody,
parseReplyFormattedBody,
getMemberDisplayName,
getMemberSearchStr,
getMemberAvatarMxc,
isMembershipChanged,
getReactionContent,
getLatestEdit,
reactionOrEditEvent,
getMentionContent,
getAllVersionsRoomCreator,
} from './room';
import { RoomType, StateEvent, RoomToParents } from '../../types/matrix/room';
// --- Mock helpers ---------------------------------------------------------
const mockEvent = (overrides: Partial<Record<string, unknown>> = {}): MatrixEvent => {
const base: Record<string, () => unknown> = {
getType: () => undefined,
getContent: () => ({}),
getPrevContent: () => ({}),
getStateKey: () => '',
getSender: () => undefined,
getTs: () => 0,
getId: () => undefined,
getRelation: () => null,
isRedacted: () => false,
};
return { ...base, ...overrides } as unknown as MatrixEvent;
};
// Build a Room whose live-timeline forward state returns the given state events.
const mockRoomWithState = (
stateEventsByType: Record<string, MatrixEvent[]>,
rest: Record<string, unknown> = {},
): Room => {
const state = {
getStateEvents: (eventType: string, stateKey?: string) => {
const events = stateEventsByType[eventType] ?? [];
if (stateKey !== undefined) {
return events[0]; // single-event form
}
return events; // array form
},
};
return {
getLiveTimeline: () => ({
getState: () => state,
}),
...rest,
} as unknown as Room;
};
// --- getStateEvent / getStateEvents --------------------------------------
test('getStateEvent returns the matching state event or undefined', () => {
const ev = mockEvent({ getType: () => StateEvent.RoomName });
const room = mockRoomWithState({ [StateEvent.RoomName]: [ev] });
assert.equal(getStateEvent(room, StateEvent.RoomName), ev);
const empty = mockRoomWithState({});
assert.equal(getStateEvent(empty, StateEvent.RoomName), undefined);
});
test('getStateEvent returns undefined when there is no forward state', () => {
const room = {
getLiveTimeline: () => ({ getState: () => undefined }),
} as unknown as Room;
assert.equal(getStateEvent(room, StateEvent.RoomName), undefined);
});
test('getStateEvents returns an array, empty when none', () => {
const ev = mockEvent();
const room = mockRoomWithState({ [StateEvent.SpaceChild]: [ev] });
assert.deepEqual(getStateEvents(room, StateEvent.SpaceChild), [ev]);
const empty = mockRoomWithState({});
assert.deepEqual(getStateEvents(empty, StateEvent.SpaceChild), []);
});
// --- getMDirects ----------------------------------------------------------
test('getMDirects collects all direct room ids across users', () => {
const ev = mockEvent({
getContent: () => ({
'@a:x': ['!room1', '!room2'],
'@b:x': ['!room3'],
}),
});
const result = getMDirects(ev);
assert.deepEqual([...result].sort(), ['!room1', '!room2', '!room3']);
});
test('getMDirects ignores non-array and non-string entries', () => {
const ev = mockEvent({
getContent: () => ({
'@a:x': 'not-an-array',
'@b:x': ['!ok', 42, null],
}),
});
const result = getMDirects(ev);
assert.deepEqual([...result], ['!ok']);
});
test('getMDirects returns empty set when content is undefined', () => {
const ev = mockEvent({ getContent: () => undefined });
assert.equal(getMDirects(ev).size, 0);
});
// --- isDirectInvite -------------------------------------------------------
test('isDirectInvite returns false for null room or null user', () => {
assert.equal(isDirectInvite(null, '@me:x'), false);
assert.equal(isDirectInvite({} as unknown as Room, null), false);
});
test('isDirectInvite reads is_direct from the member event content', () => {
const room = {
getMember: () => ({
events: { member: { getContent: () => ({ is_direct: true }) } },
}),
} as unknown as Room;
assert.equal(isDirectInvite(room, '@me:x'), true);
const roomNotDirect = {
getMember: () => ({
events: { member: { getContent: () => ({}) } },
}),
} as unknown as Room;
assert.equal(isDirectInvite(roomNotDirect, '@me:x'), false);
});
test('isDirectInvite returns false when member is missing', () => {
const room = { getMember: () => null } as unknown as Room;
assert.equal(isDirectInvite(room, '@me:x'), false);
});
// --- isSpace / isRoom / isUnsupportedRoom --------------------------------
test('isSpace detects space rooms via create event type', () => {
const spaceEv = mockEvent({ getContent: () => ({ type: RoomType.Space }) });
assert.equal(isSpace(mockRoomWithState({ [StateEvent.RoomCreate]: [spaceEv] })), true);
const normalEv = mockEvent({ getContent: () => ({}) });
assert.equal(isSpace(mockRoomWithState({ [StateEvent.RoomCreate]: [normalEv] })), false);
assert.equal(isSpace(null), false);
assert.equal(isSpace(mockRoomWithState({})), false); // no create event
});
test('isRoom is true for normal rooms and rooms with no create event', () => {
const spaceEv = mockEvent({ getContent: () => ({ type: RoomType.Space }) });
assert.equal(isRoom(mockRoomWithState({ [StateEvent.RoomCreate]: [spaceEv] })), false);
const normalEv = mockEvent({ getContent: () => ({}) });
assert.equal(isRoom(mockRoomWithState({ [StateEvent.RoomCreate]: [normalEv] })), true);
assert.equal(isRoom(mockRoomWithState({})), true); // no create event -> treated as room
assert.equal(isRoom(null), false);
});
test('isUnsupportedRoom is true when type is set and not a space', () => {
const callEv = mockEvent({ getContent: () => ({ type: RoomType.Call }) });
assert.equal(isUnsupportedRoom(mockRoomWithState({ [StateEvent.RoomCreate]: [callEv] })), true);
const spaceEv = mockEvent({ getContent: () => ({ type: RoomType.Space }) });
assert.equal(isUnsupportedRoom(mockRoomWithState({ [StateEvent.RoomCreate]: [spaceEv] })), false);
const normalEv = mockEvent({ getContent: () => ({}) });
assert.equal(
isUnsupportedRoom(mockRoomWithState({ [StateEvent.RoomCreate]: [normalEv] })),
false,
);
assert.equal(isUnsupportedRoom(mockRoomWithState({})), true); // missing create event
assert.equal(isUnsupportedRoom(null), false);
});
// --- isValidChild / getSpaceChildren -------------------------------------
test('isValidChild requires SpaceChild type with via array', () => {
const valid = mockEvent({
getType: () => StateEvent.SpaceChild,
getContent: () => ({ via: ['server'] }),
});
assert.equal(isValidChild(valid), true);
const noVia = mockEvent({
getType: () => StateEvent.SpaceChild,
getContent: () => ({}),
});
assert.equal(isValidChild(noVia), false);
const wrongType = mockEvent({
getType: () => StateEvent.RoomName,
getContent: () => ({ via: ['server'] }),
});
assert.equal(isValidChild(wrongType), false);
});
test('getSpaceChildren returns state keys of valid child events', () => {
const valid = mockEvent({
getType: () => StateEvent.SpaceChild,
getStateKey: () => '!child:x',
getContent: () => ({ via: ['s'] }),
});
const invalid = mockEvent({
getType: () => StateEvent.SpaceChild,
getStateKey: () => '!bad:x',
getContent: () => ({}),
});
const noKey = mockEvent({
getType: () => StateEvent.SpaceChild,
getStateKey: () => '',
getContent: () => ({ via: ['s'] }),
});
const room = mockRoomWithState({
[StateEvent.SpaceChild]: [valid, invalid, noKey],
});
assert.deepEqual(getSpaceChildren(room), ['!child:x']);
});
// --- getAllParents / mapParentWithChildren / getOrphanParents ------------
test('getAllParents walks the parent graph excluding the start room', () => {
const map: RoomToParents = new Map([
['child', new Set(['mid'])],
['mid', new Set(['root'])],
]);
assert.deepEqual([...getAllParents(map, 'child')].sort(), ['mid', 'root']);
assert.deepEqual([...getAllParents(map, 'root')], []);
});
test('getAllParents handles cycles without infinite recursion', () => {
const map: RoomToParents = new Map([
['a', new Set(['b'])],
['b', new Set(['a'])],
]);
assert.deepEqual([...getAllParents(map, 'a')], ['b']);
});
test('mapParentWithChildren registers parent for each child', () => {
const map: RoomToParents = new Map();
mapParentWithChildren(map, 'space1', ['c1', 'c2']);
assert.deepEqual([...(map.get('c1') ?? [])], ['space1']);
assert.deepEqual([...(map.get('c2') ?? [])], ['space1']);
});
test('mapParentWithChildren skips children that would create a cycle', () => {
const map: RoomToParents = new Map([['space1', new Set(['ancestor'])]]);
mapParentWithChildren(map, 'space1', ['ancestor', 'newchild']);
// ancestor is an existing parent -> cycle -> skipped
assert.equal(map.has('ancestor'), false);
assert.deepEqual([...(map.get('newchild') ?? [])], ['space1']);
});
test('getOrphanParents returns parents not present as keys in the map', () => {
const map: RoomToParents = new Map([
['child', new Set(['mid'])],
['mid', new Set(['root'])],
]);
// root is a parent but has no entry of its own -> orphan
assert.deepEqual(getOrphanParents(map, 'child'), ['root']);
});
// --- isMutedRule / findMutedRule -----------------------------------------
test('isMutedRule matches empty actions or dont_notify with event_match', () => {
assert.equal(
isMutedRule({
actions: [],
conditions: [{ kind: 'event_match' }],
} as never),
true,
);
assert.equal(
isMutedRule({
actions: ['dont_notify'],
conditions: [{ kind: 'event_match' }],
} as never),
true,
);
assert.equal(
isMutedRule({
actions: ['notify'],
conditions: [{ kind: 'event_match' }],
} as never),
false,
);
assert.ok(
!isMutedRule({
actions: [],
conditions: [{ kind: 'other' }],
} as never),
);
});
test('findMutedRule finds rule by id that is also muted', () => {
const rules = [
{ rule_id: '!a', actions: [], conditions: [{ kind: 'event_match' }] },
{ rule_id: '!b', actions: ['notify'], conditions: [{ kind: 'event_match' }] },
] as never[];
assert.equal(findMutedRule(rules, '!a'), rules[0]);
assert.equal(findMutedRule(rules, '!b'), undefined);
assert.equal(findMutedRule(rules, '!missing'), undefined);
});
// --- isNotificationEvent --------------------------------------------------
test('isNotificationEvent accepts message/sticker but rejects member, redacted, edits', () => {
assert.equal(isNotificationEvent(mockEvent({ getType: () => 'm.room.message' })), true);
assert.equal(isNotificationEvent(mockEvent({ getType: () => 'm.sticker' })), true);
assert.equal(isNotificationEvent(mockEvent({ getType: () => 'm.room.member' })), false);
assert.equal(isNotificationEvent(mockEvent({ getType: () => 'm.room.topic' })), false);
assert.equal(
isNotificationEvent(mockEvent({ getType: () => 'm.room.message', isRedacted: () => true })),
false,
);
assert.equal(
isNotificationEvent(
mockEvent({
getType: () => 'm.room.message',
getRelation: () => ({ rel_type: 'm.replace' }),
}),
),
false,
);
});
// --- roomHaveNotification / getUnreadInfo --------------------------------
test('roomHaveNotification true when total or highlight count > 0', () => {
const make = (total: number, highlight: number) =>
({
getUnreadNotificationCount: (type: NotificationCountType) =>
type === NotificationCountType.Total ? total : highlight,
}) as unknown as Room;
assert.equal(roomHaveNotification(make(0, 0)), false);
assert.equal(roomHaveNotification(make(3, 0)), true);
assert.equal(roomHaveNotification(make(0, 1)), true);
});
test('getUnreadInfo uses highlight when it exceeds total', () => {
const room = {
roomId: '!r:x',
getUnreadNotificationCount: (type: NotificationCountType) =>
type === NotificationCountType.Total ? 2 : 5,
} as unknown as Room;
assert.deepEqual(getUnreadInfo(room), { roomId: '!r:x', highlight: 5, total: 5 });
const room2 = {
roomId: '!r:y',
getUnreadNotificationCount: (type: NotificationCountType) =>
type === NotificationCountType.Total ? 7 : 1,
} as unknown as Room;
assert.deepEqual(getUnreadInfo(room2), { roomId: '!r:y', highlight: 1, total: 7 });
});
// --- getRoomIconSrc -------------------------------------------------------
test('getRoomIconSrc selects icon by room type and join rule', () => {
const icons = {
Warning: 'warning',
SpaceGlobe: 'space-globe',
SpaceLock: 'space-lock',
Space: 'space',
VolumeHighGlobe: 'vol-globe',
VolumeHighLock: 'vol-lock',
VolumeHigh: 'vol',
HashGlobe: 'hash-globe',
HashLock: 'hash-lock',
Hash: 'hash',
} as never;
assert.equal(getRoomIconSrc(icons, 'm.server_notice'), 'warning');
assert.equal(getRoomIconSrc(icons, RoomType.Space, JoinRule.Public), 'space-globe');
assert.equal(getRoomIconSrc(icons, RoomType.Space, JoinRule.Invite), 'space-lock');
assert.equal(getRoomIconSrc(icons, RoomType.Space), 'space');
assert.equal(getRoomIconSrc(icons, RoomType.Call, JoinRule.Public), 'vol-globe');
assert.equal(getRoomIconSrc(icons, RoomType.Call, JoinRule.Knock), 'vol-lock');
assert.equal(getRoomIconSrc(icons, RoomType.Call), 'vol');
assert.equal(getRoomIconSrc(icons, undefined, JoinRule.Public), 'hash-globe');
assert.equal(getRoomIconSrc(icons, undefined, JoinRule.Invite), 'hash-lock');
assert.equal(getRoomIconSrc(icons), 'hash');
});
// --- reply parsing helpers -----------------------------------------------
test('trimReplyFromBody strips a matrix reply fallback prefix', () => {
const body = '> <@a:x> hello\n> more\n\nactual message';
assert.equal(trimReplyFromBody(body), 'actual message');
// no fallback -> unchanged
assert.equal(trimReplyFromBody('plain text'), 'plain text');
});
test('trimReplyFromFormattedBody strips up to and including mx-reply', () => {
const fb = '<mx-reply><blockquote>x</blockquote></mx-reply>real';
assert.equal(trimReplyFromFormattedBody(fb), 'real');
assert.equal(trimReplyFromFormattedBody('no reply here'), 'no reply here');
});
test('parseReplyBody builds a quoted fallback with prefixed newlines', () => {
assert.equal(parseReplyBody('@a:x', 'line1\nline2'), '> <@a:x> line1\n> line2\n\n');
});
test('parseReplyFormattedBody builds an mx-reply block with encoded links', () => {
const result = parseReplyFormattedBody('!r:x', '@a:x', '$e:x', 'hi');
assert.ok(result.startsWith('<mx-reply><blockquote>'));
assert.ok(result.endsWith('hi</blockquote></mx-reply>'));
assert.ok(result.includes(encodeURIComponent('!r:x')));
assert.ok(result.includes(encodeURIComponent('$e:x')));
assert.ok(result.includes(encodeURIComponent('@a:x')));
});
// --- member helpers -------------------------------------------------------
test('getMemberDisplayName returns name unless it equals the userId', () => {
const room = {
getMember: (id: string) =>
id === '@named:x' ? { rawDisplayName: 'Alice' } : { rawDisplayName: '@plain:x' },
} as unknown as Room;
assert.equal(getMemberDisplayName(room, '@named:x'), 'Alice');
assert.equal(getMemberDisplayName(room, '@plain:x'), undefined);
const noMember = { getMember: () => null } as unknown as Room;
assert.equal(getMemberDisplayName(noMember, '@x:x'), undefined);
});
test('getMemberSearchStr produces display and id variants', () => {
const member = {
rawDisplayName: 'Alice',
userId: '@alice:x',
} as unknown as RoomMember;
const mxIdToName = (id: string) => `name(${id})`;
// query without @ or : -> uses mapped name for second entry
assert.deepEqual(getMemberSearchStr(member, 'al', mxIdToName), ['Alice', 'name(@alice:x)']);
// query with @ -> uses raw userId for second entry
assert.deepEqual(getMemberSearchStr(member, '@al', mxIdToName), ['Alice', '@alice:x']);
// when display name equals userId, first entry falls back to mapped name
const unnamed = { rawDisplayName: '@bob:x', userId: '@bob:x' } as unknown as RoomMember;
assert.deepEqual(getMemberSearchStr(unnamed, 'bob', mxIdToName), [
'name(@bob:x)',
'name(@bob:x)',
]);
});
test('getMemberAvatarMxc returns member mxc or undefined', () => {
const room = {
getMember: (id: string) => (id === '@a:x' ? { getMxcAvatarUrl: () => 'mxc://a' } : null),
} as unknown as Room;
assert.equal(getMemberAvatarMxc(room, '@a:x'), 'mxc://a');
assert.equal(getMemberAvatarMxc(room, '@b:x'), undefined);
});
// --- isMembershipChanged --------------------------------------------------
test('isMembershipChanged compares membership and reason against prev content', () => {
const changedMembership = mockEvent({
getContent: () => ({ membership: 'join' }),
getPrevContent: () => ({ membership: 'invite' }),
});
assert.equal(isMembershipChanged(changedMembership), true);
const changedReason = mockEvent({
getContent: () => ({ membership: 'leave', reason: 'spam' }),
getPrevContent: () => ({ membership: 'leave' }),
});
assert.equal(isMembershipChanged(changedReason), true);
const unchanged = mockEvent({
getContent: () => ({ membership: 'join', reason: 'x' }),
getPrevContent: () => ({ membership: 'join', reason: 'x' }),
});
assert.equal(isMembershipChanged(unchanged), false);
});
// --- getReactionContent / getMentionContent ------------------------------
test('getReactionContent builds an annotation relation', () => {
assert.deepEqual(getReactionContent('$e:x', '👍', 'thumbsup'), {
'm.relates_to': {
event_id: '$e:x',
key: '👍',
rel_type: 'm.annotation',
},
shortcode: 'thumbsup',
});
});
test('getMentionContent includes user_ids and room only when relevant', () => {
assert.deepEqual(getMentionContent(['@a:x'], true), {
user_ids: ['@a:x'],
room: true,
});
assert.deepEqual(getMentionContent([], false), {});
assert.deepEqual(getMentionContent(['@a:x'], false), { user_ids: ['@a:x'] });
assert.deepEqual(getMentionContent([], true), { room: true });
});
// --- getLatestEdit / reactionOrEditEvent ---------------------------------
test('getLatestEdit returns newest edit from the same sender', () => {
const target = mockEvent({ getSender: () => '@a:x' });
const oldEdit = mockEvent({ getSender: () => '@a:x', getTs: () => 100 });
const newEdit = mockEvent({ getSender: () => '@a:x', getTs: () => 200 });
const otherSender = mockEvent({ getSender: () => '@b:x', getTs: () => 300 });
assert.equal(getLatestEdit(target, [oldEdit, newEdit, otherSender]), newEdit);
assert.equal(getLatestEdit(target, [otherSender]), undefined);
});
test('reactionOrEditEvent detects annotation and replace relations', () => {
assert.equal(
reactionOrEditEvent(mockEvent({ getRelation: () => ({ rel_type: 'm.annotation' }) })),
true,
);
assert.equal(
reactionOrEditEvent(mockEvent({ getRelation: () => ({ rel_type: 'm.replace' }) })),
true,
);
assert.equal(
reactionOrEditEvent(mockEvent({ getRelation: () => ({ rel_type: 'm.thread' }) })),
false,
);
assert.equal(reactionOrEditEvent(mockEvent({ getRelation: () => null })), false);
});
// --- getAllVersionsRoomCreator -------------------------------------------
test('getAllVersionsRoomCreator collects sender and additional_creators', () => {
const createEv = mockEvent({
getSender: () => '@creator:x',
getContent: () => ({ additional_creators: ['@co1:x', '@co2:x', 5] }),
});
const room = mockRoomWithState({ [StateEvent.RoomCreate]: [createEv] });
const creators = getAllVersionsRoomCreator(room);
assert.deepEqual([...creators].sort(), ['@co1:x', '@co2:x', '@creator:x']);
});
test('getAllVersionsRoomCreator returns empty set when no create event', () => {
const room = mockRoomWithState({});
assert.equal(getAllVersionsRoomCreator(room).size, 0);
});
// Reference EventTimeline so the import is used even though state mocks bypass it.
test('EventTimeline.FORWARDS constant is available', () => {
assert.equal(typeof EventTimeline.FORWARDS, 'string');
});