import { describe, it, expect } from 'vitest'; import { PATTERNS } from '../../src/parser/patterns.js'; describe('Task Checkbox Pattern', () => { it('should match unchecked task', () => { const line = '- [ ] Task text'; const match = line.match(PATTERNS.TASK_CHECKBOX); expect(match).toBeTruthy(); expect(match?.[2]).toBe(' '); }); it('should match checked task with lowercase x', () => { const line = '- [x] Completed task'; const match = line.match(PATTERNS.TASK_CHECKBOX); expect(match).toBeTruthy(); expect(match?.[2]).toBe('x'); }); it('should match checked task with uppercase X', () => { const line = '- [X] Completed task'; const match = line.match(PATTERNS.TASK_CHECKBOX); expect(match).toBeTruthy(); expect(match?.[2]).toBe('X'); }); it('should match task with leading whitespace', () => { const line = ' - [ ] Indented task'; const match = line.match(PATTERNS.TASK_CHECKBOX); expect(match).toBeTruthy(); expect(match?.[1]).toBe(' '); }); it('should not match regular list items', () => { const line = '- Regular list item'; const match = line.match(PATTERNS.TASK_CHECKBOX); expect(match).toBeNull(); }); it('should not match headings', () => { const line = '## Heading'; const match = line.match(PATTERNS.TASK_CHECKBOX); expect(match).toBeNull(); }); }); describe('Assignee Pattern', () => { it('should extract username with @', () => { const text = '@nick Review PR'; const match = text.match(PATTERNS.ASSIGNEE); expect(match?.[1]).toBe('nick'); }); it('should extract username with hyphens', () => { const text = '@jane-doe Complete task'; const match = text.match(PATTERNS.ASSIGNEE); expect(match?.[1]).toBe('jane-doe'); }); it('should extract first assignee if multiple', () => { const text = '@alice and @bob Review'; const match = text.match(PATTERNS.ASSIGNEE); expect(match?.[1]).toBe('alice'); }); it('should return null if no assignee', () => { const text = 'Task without assignee'; const match = text.match(PATTERNS.ASSIGNEE); expect(match).toBeNull(); }); }); describe('Priority Patterns', () => { it('should match urgent priority (!!!)', () => { const text = 'Critical bug fix !!!'; expect(PATTERNS.PRIORITY_URGENT.test(text)).toBe(true); expect(PATTERNS.PRIORITY_HIGH.test(text)).toBe(true); // Also matches !! // PRIORITY_NORMAL uses negative lookbehind/ahead, so won't match within !!! expect(PATTERNS.PRIORITY_NORMAL.test(text)).toBe(false); }); it('should match high priority (!!)', () => { const text = 'Important task !!'; expect(PATTERNS.PRIORITY_URGENT.test(text)).toBe(false); expect(PATTERNS.PRIORITY_HIGH.test(text)).toBe(true); // PRIORITY_NORMAL uses negative lookbehind/ahead, so won't match within !! expect(PATTERNS.PRIORITY_NORMAL.test(text)).toBe(false); }); it('should match normal priority (!)', () => { const text = 'Regular task !'; expect(PATTERNS.PRIORITY_URGENT.test(text)).toBe(false); expect(PATTERNS.PRIORITY_HIGH.test(text)).toBe(false); expect(PATTERNS.PRIORITY_NORMAL.test(text)).toBe(true); }); it('should not match text without priority', () => { const text = 'Low priority task'; expect(PATTERNS.PRIORITY_URGENT.test(text)).toBe(false); expect(PATTERNS.PRIORITY_HIGH.test(text)).toBe(false); expect(PATTERNS.PRIORITY_NORMAL.test(text)).toBe(false); }); it('should match standalone exclamation', () => { const text = 'Hello! World'; // This should match because ! is followed by space, not another ! expect(PATTERNS.PRIORITY_NORMAL.test(text)).toBe(true); }); }); describe('Tag Pattern', () => { it('should extract single tag', () => { const text = 'Task with #backend tag'; const matches = Array.from(text.matchAll(PATTERNS.TAG)); expect(matches).toHaveLength(1); expect(matches[0]?.[1]).toBe('backend'); }); it('should extract multiple tags', () => { const text = 'Task #backend #urgent #review'; const matches = Array.from(text.matchAll(PATTERNS.TAG)); expect(matches).toHaveLength(3); expect(matches[0]?.[1]).toBe('backend'); expect(matches[1]?.[1]).toBe('urgent'); expect(matches[2]?.[1]).toBe('review'); }); it('should extract tags with hyphens', () => { const text = 'Task #code-review #high-priority'; const matches = Array.from(text.matchAll(PATTERNS.TAG)); expect(matches).toHaveLength(2); expect(matches[0]?.[1]).toBe('code-review'); expect(matches[1]?.[1]).toBe('high-priority'); }); it('should return empty array if no tags', () => { const text = 'Task without tags'; const matches = Array.from(text.matchAll(PATTERNS.TAG)); expect(matches).toHaveLength(0); }); it('should not match #due/ as a tag', () => { const text = 'Task #due/2026-01-25 #backend'; const matches = Array.from(text.matchAll(PATTERNS.TAG)); expect(matches).toHaveLength(1); expect(matches[0]?.[1]).toBe('backend'); }); }); describe('Due Date Patterns', () => { describe('New #due/ syntax', () => { it('should match #due/ tag syntax', () => { const text = 'Task #due/2026-01-25'; const match = text.match(PATTERNS.DUE_DATE_ABSOLUTE); expect(match?.[1]).toBe('2026-01-25'); }); it('should match #due/ in middle of text', () => { const text = 'Task #due/2026-01-25 #backend'; const match = text.match(PATTERNS.DUE_DATE_ABSOLUTE); expect(match?.[1]).toBe('2026-01-25'); }); }); describe('Legacy absolute dates', () => { it('should match ISO format date', () => { const text = 'Task [due: 2026-01-25]'; const match = text.match(PATTERNS.DUE_DATE_ABSOLUTE); expect(match?.[2]).toBe('2026-01-25'); }); it('should match case-insensitive', () => { const text = 'Task [DUE: 2026-01-25]'; const match = text.match(PATTERNS.DUE_DATE_ABSOLUTE); expect(match?.[2]).toBe('2026-01-25'); }); it('should match with extra whitespace', () => { const text = 'Task [due: 2026-01-25 ]'; const match = text.match(PATTERNS.DUE_DATE_ABSOLUTE); expect(match?.[2]).toBe('2026-01-25'); }); }); describe('Short format dates', () => { it('should match M/D format', () => { const text = 'Task [due: 1/25]'; const match = text.match(PATTERNS.DUE_DATE_SHORT); expect(match?.[1]).toBe('1/25'); }); it('should match M/D/YY format', () => { const text = 'Task [due: 1/25/26]'; const match = text.match(PATTERNS.DUE_DATE_SHORT); expect(match?.[1]).toBe('1/25/26'); }); it('should match M/D/YYYY format', () => { const text = 'Task [due: 1/25/2026]'; const match = text.match(PATTERNS.DUE_DATE_SHORT); expect(match?.[1]).toBe('1/25/2026'); }); }); describe('Relative dates', () => { it('should match "today"', () => { const text = 'Task [due: today]'; const match = text.match(PATTERNS.DUE_DATE_RELATIVE); expect(match?.[1]).toBe('today'); }); it('should match "tomorrow"', () => { const text = 'Task [due: tomorrow]'; const match = text.match(PATTERNS.DUE_DATE_RELATIVE); expect(match?.[1]).toBe('tomorrow'); }); it('should match "next week"', () => { const text = 'Task [due: next week]'; const match = text.match(PATTERNS.DUE_DATE_RELATIVE); expect(match?.[1]).toBe('next week'); }); it('should match "next month"', () => { const text = 'Task [due: next month]'; const match = text.match(PATTERNS.DUE_DATE_RELATIVE); expect(match?.[1]).toBe('next month'); }); }); }); describe('Todoist ID Pattern', () => { it('should extract ID from new brace syntax', () => { const text = 'Task {todoist:123456}'; const match = text.match(PATTERNS.TODOIST_ID); expect(match?.[1]).toBe('123456'); }); it('should extract long ID from new syntax', () => { const text = 'Task {todoist:7891234567}'; const match = text.match(PATTERNS.TODOIST_ID); expect(match?.[1]).toBe('7891234567'); }); it('should extract ID from legacy bracket syntax', () => { const text = 'Task [todoist:123456]'; const match = text.match(PATTERNS.TODOIST_ID); expect(match?.[2]).toBe('123456'); }); it('should return null if no ID', () => { const text = 'Task without Todoist ID'; const match = text.match(PATTERNS.TODOIST_ID); expect(match).toBeNull(); }); }); describe('Completed Date Pattern', () => { it('should extract completion date from new brace syntax', () => { const text = '{completed:2026-01-18}'; const match = text.match(PATTERNS.COMPLETED_DATE); expect(match?.[1]).toBe('2026-01-18'); }); it('should extract completion date from legacy bracket syntax', () => { const text = '[completed: 2026-01-18]'; const match = text.match(PATTERNS.COMPLETED_DATE); expect(match?.[2]).toBe('2026-01-18'); }); it('should match legacy case-insensitive', () => { const text = '[COMPLETED: 2026-01-18]'; const match = text.match(PATTERNS.COMPLETED_DATE); expect(match?.[2]).toBe('2026-01-18'); }); it('should return null if no completion date', () => { const text = 'Task without completion date'; const match = text.match(PATTERNS.COMPLETED_DATE); expect(match).toBeNull(); }); });