import { test } from 'node:test'; import assert from 'node:assert/strict'; import { ReceiptType } from 'matrix-js-sdk'; import { markThreadAsRead } from './threadReceipt'; // The regression this guards: sending a receipt for the thread ROOT (when // replies aren't loaded, lastReply() is null / equals the root) becomes a MAIN // receipt at an old event and drags the room's read marker backwards. It must // only ever receipt a genuine loaded reply. const evt = (id: string, sending = false) => ({ getId: () => id, isSending: () => sending }) as any; const setup = (lastReply: any) => { const calls: Array<{ eventId: string; type: ReceiptType }> = []; const thread = { id: '$root', lastReply: () => lastReply } as any; const mx = { sendReadReceipt: async (e: any, type: ReceiptType) => { calls.push({ eventId: e.getId(), type }); return {}; }, } as any; return { mx, thread, calls }; }; test('REGRESSION: no loaded reply (lastReply null) → NO receipt (never the root)', async () => { const { mx, thread, calls } = setup(null); await markThreadAsRead(mx, thread, false); assert.equal(calls.length, 0); }); test('REGRESSION: lastReply IS the root → NO receipt', async () => { const { mx, thread, calls } = setup(evt('$root')); await markThreadAsRead(mx, thread, false); assert.equal(calls.length, 0); }); test('genuine loaded reply → threaded receipt at that reply', async () => { const { mx, thread, calls } = setup(evt('$reply')); await markThreadAsRead(mx, thread, false); assert.equal(calls.length, 1); assert.equal(calls[0].eventId, '$reply'); assert.equal(calls[0].type, ReceiptType.Read); }); test('sending reply is skipped', async () => { const { mx, thread, calls } = setup(evt('$reply', true)); await markThreadAsRead(mx, thread, false); assert.equal(calls.length, 0); }); test('private flag uses ReadPrivate', async () => { const { mx, thread, calls } = setup(evt('$reply')); await markThreadAsRead(mx, thread, true); assert.equal(calls[0].type, ReceiptType.ReadPrivate); });