import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; import { parseDateOperand, parseSearchQuery, resolveRoomOperand } from './searchOperators'; const NOW = new Date(2026, 8, 25, 12, 0, 0).getTime(); const DAY = 86_400_000; describe('parseSearchQuery', () => { it('plain text stays the term', () => { const p = parseSearchQuery('loot rules tonight', NOW); assert.equal(p.term, 'loot rules tonight'); assert.deepEqual(p.senders, []); }); it('pulls every operator out of the term', () => { const p = parseSearchQuery( 'raid from:bob in:general after:7d before:2026-09-24 has:link has:image is:pinned plan', NOW, ); assert.equal(p.term, 'raid plan'); assert.deepEqual(p.senders, ['@bob']); assert.deepEqual(p.rooms, ['general']); assert.equal(p.fromTs, NOW - 7 * DAY); assert.equal(p.toTs, new Date(2026, 8, 24).getTime()); assert.equal(p.containsUrl, true); assert.deepEqual(p.msgTypes, ['m.image']); assert.equal(p.pinnedOnly, true); assert.deepEqual(p.unknown, []); }); it('keeps @ on from:, accepts quoted in:, and is case-insensitive on keys', () => { const p = parseSearchQuery('FROM:@alice:x.org In:"voice lounge" hello', NOW); assert.deepEqual(p.senders, ['@alice:x.org']); assert.deepEqual(p.rooms, ['voice lounge']); assert.equal(p.term, 'hello'); }); it('leaves operators it cannot understand in the term', () => { const p = parseSearchQuery('has:gif after:someday is:starred hi', NOW); assert.equal(p.term, 'has:gif after:someday is:starred hi'); assert.deepEqual(p.unknown, ['has:gif', 'after:someday', 'is:starred']); }); it('does not treat a URL or a mid-word colon as an operator', () => { const p = parseSearchQuery('see https://in:example.org and 10:30 before-lunch', NOW); assert.equal(p.term, 'see https://in:example.org and 10:30 before-lunch'); assert.deepEqual(p.rooms, []); }); it('has: file/video map to the media filters without duplicates', () => { const p = parseSearchQuery('has:file has:files has:video', NOW); assert.deepEqual(p.msgTypes, ['m.file', 'm.video']); assert.equal(p.term, ''); }); }); describe('parseDateOperand', () => { it('relative units', () => { assert.equal(parseDateOperand('3h', NOW), NOW - 3 * 3_600_000); assert.equal(parseDateOperand('2w', NOW), NOW - 14 * DAY); assert.equal(parseDateOperand('1m', NOW), NOW - 30 * DAY); }); it('rejects impossible dates and junk', () => { assert.equal(parseDateOperand('2026-02-30', NOW), undefined); assert.equal(parseDateOperand('yesterday', NOW), undefined); }); }); describe('resolveRoomOperand', () => { const rooms = [ { roomId: '!a:x', name: 'General', aliases: ['#general:x.org'] }, { roomId: '!b:x', name: 'Voice Lounge', aliases: [] }, { roomId: '!c:x', name: 'Off Topic', aliases: ['#offtopic:x.org'] }, ]; it('by alias, alias localpart, name, room id', () => { assert.equal(resolveRoomOperand('#general:x.org', rooms), '!a:x'); assert.equal(resolveRoomOperand('offtopic', rooms), '!c:x'); assert.equal(resolveRoomOperand('voice lounge', rooms), '!b:x'); assert.equal(resolveRoomOperand('!b:x', rooms), '!b:x'); }); it('a unique partial name match, otherwise nothing', () => { assert.equal(resolveRoomOperand('lounge', rooms), '!b:x'); assert.equal(resolveRoomOperand('o', rooms), undefined); assert.equal(resolveRoomOperand('nope', rooms), undefined); }); });