import { test } from 'node:test'; import assert from 'node:assert/strict'; import { MatrixEvent } from 'matrix-js-sdk'; import { myMainReceiptPresent, receiptIsMine, setMarkedUnread } from './markedUnread'; // MSC2867 mark-as-unread: reading a room (our own receipt) clears the flag, so // `receiptIsMine` must detect only OUR receipt and ignore others'. And a write // must land on BOTH the stable `m.marked_unread` and the unstable // `com.famedly.marked_unread` key so it round-trips across servers/clients. const ME = '@me:server'; const OTHER = '@friend:server'; const receiptEvent = (content: object): MatrixEvent => ({ getContent: () => content }) as MatrixEvent; test('receiptIsMine: true when the receipt content carries our user id', () => { const event = receiptEvent({ $abc: { 'm.read': { [ME]: { ts: 1 } } }, }); assert.equal(receiptIsMine(event, ME), true); }); test('receiptIsMine: false when only another user has a receipt', () => { const event = receiptEvent({ $abc: { 'm.read': { [OTHER]: { ts: 1 } } }, }); assert.equal(receiptIsMine(event, ME), false); }); test('receiptIsMine: tolerates empty / malformed content', () => { assert.equal(receiptIsMine(receiptEvent({}), ME), false); assert.equal(receiptIsMine(receiptEvent({ $x: {} }), ME), false); }); // myMainReceiptPresent gates the auto-clear to main-timeline reads, so reading a // single thread does not wipe the whole-room marked-unread flag. test('myMainReceiptPresent: true for an unthreaded receipt (no thread_id)', () => { const event = receiptEvent({ $abc: { 'm.read': { [ME]: { ts: 1 } } } }); assert.equal(myMainReceiptPresent(event, ME), true); }); test('myMainReceiptPresent: true for a thread_id "main" receipt', () => { const event = receiptEvent({ $abc: { 'm.read': { [ME]: { ts: 1, thread_id: 'main' } } } }); assert.equal(myMainReceiptPresent(event, ME), true); }); test('myMainReceiptPresent: false for a thread-scoped receipt', () => { const event = receiptEvent({ $abc: { 'm.read': { [ME]: { ts: 1, thread_id: '$root:server' } } }, }); assert.equal(myMainReceiptPresent(event, ME), false); }); test('myMainReceiptPresent: false when only another user has a main receipt', () => { const event = receiptEvent({ $abc: { 'm.read': { [OTHER]: { ts: 1 } } } }); assert.equal(myMainReceiptPresent(event, ME), false); }); test('setMarkedUnread writes both the stable and unstable keys with the flag', async () => { const calls: Array<{ type: string; content: unknown }> = []; const mx = { setRoomAccountData: (_roomId: string, type: string, content: unknown) => { calls.push({ type, content }); return Promise.resolve(); }, } as any; await setMarkedUnread(mx, '!room:server', true); const types = calls.map((c) => c.type).sort(); assert.deepEqual(types, ['com.famedly.marked_unread', 'm.marked_unread']); assert.ok(calls.every((c) => (c.content as { unread: boolean }).unread === true)); }); test('setMarkedUnread(false) clears both keys and does not reject if the unstable write fails', async () => { const seen: string[] = []; const mx = { setRoomAccountData: (_roomId: string, type: string) => { seen.push(type); // Simulate an older server rejecting the unstable key — must not reject. if (type === 'com.famedly.marked_unread') return Promise.reject(new Error('unknown type')); return Promise.resolve(); }, } as any; await assert.doesNotReject(() => setMarkedUnread(mx, '!room:server', false)); assert.ok(seen.includes('m.marked_unread')); });