import { parseTaskStore, renderTaskStore, addTask, updateTask, completeTask, summarizeActiveTasks, TaskStore, } from '../src/features/tasks/taskStore'; import { _parseTaskAttrs } from '../src/agent'; describe('parseTaskStore', () => { test('returns empty store on empty markdown', () => { const s = parseTaskStore(''); expect(s).toEqual({ active: [], done: [] }); }); test('parses round-tripped output', () => { const original: TaskStore = { active: [ { id: 't_001', title: '광고주 자료', owner: '@me', due: '2026-05-24T18:00', status: 'in_progress', notes: '자료 대기' }, { id: 't_002', title: '디자인 리뷰', owner: '@planner', due: '', status: 'open', notes: '' }, ], done: [ { id: 't_000', title: '셋업', owner: '@me', due: '', status: 'done', notes: '', completedAt: '2026-05-20T10:00' }, ], }; const md = renderTaskStore(original); const parsed = parseTaskStore(md); expect(parsed.active).toHaveLength(2); expect(parsed.active[0].id).toBe('t_001'); expect(parsed.active[0].status).toBe('in_progress'); expect(parsed.done).toHaveLength(1); expect(parsed.done[0].id).toBe('t_000'); expect(parsed.done[0].completedAt).toBe('2026-05-20T10:00'); }); test('normalizes status variants', () => { const md = `# Tasks ## Active | ID | Title | Owner | Due | Status | Notes | |---|---|---|---|---|---| | t_001 | a | @me | | In Progress | | | t_002 | b | @me | | INPROGRESS | | | t_003 | c | @me | | blocked | | | t_004 | d | @me | | unknown_status | |`; const s = parseTaskStore(md); expect(s.active.map((t) => t.status)).toEqual(['in_progress', 'in_progress', 'blocked', 'open']); }); test('ignores malformed rows', () => { const md = `# Tasks ## Active | ID | Title | Owner | Due | Status | Notes | |---|---|---|---|---|---| | t_001 | ok | @me | | open | | | no_t_prefix | bad | | just text not a row | t_003 | ok2 | @me | | open | |`; const s = parseTaskStore(md); expect(s.active.map((t) => t.id)).toEqual(['t_001', 't_003']); }); }); describe('addTask', () => { test('assigns incremental t_NNN ids based on max across active + done', () => { const store: TaskStore = { active: [{ id: 't_003', title: 'a', owner: '', due: '', status: 'open', notes: '' }], done: [{ id: 't_010', title: 'b', owner: '', due: '', status: 'done', notes: '', completedAt: '' }], }; const t = addTask(store, { title: '신규' }); expect(t.id).toBe('t_011'); }); test('starts at t_001 for empty store', () => { const store: TaskStore = { active: [], done: [] }; const t = addTask(store, { title: '첫 task' }); expect(t.id).toBe('t_001'); }); test('trims input fields and defaults status to open', () => { const store: TaskStore = { active: [], done: [] }; const t = addTask(store, { title: ' 광고주 ', owner: ' @me ', notes: '' }); expect(t.title).toBe('광고주'); expect(t.owner).toBe('@me'); expect(t.status).toBe('open'); expect(t.notes).toBe(''); }); }); describe('updateTask', () => { test('patches only provided fields, preserves id', () => { const store: TaskStore = { active: [{ id: 't_001', title: 'a', owner: '@me', due: '', status: 'open', notes: '' }], done: [], }; const r = updateTask(store, 't_001', { status: 'in_progress', notes: '시작' }); expect(r?.title).toBe('a'); expect(r?.status).toBe('in_progress'); expect(r?.notes).toBe('시작'); }); test('returns null for unknown id', () => { const store: TaskStore = { active: [], done: [] }; expect(updateTask(store, 't_999', { notes: 'x' })).toBeNull(); }); }); describe('completeTask', () => { test('moves active task to done with completedAt', () => { const store: TaskStore = { active: [{ id: 't_001', title: 'a', owner: '', due: '', status: 'open', notes: '' }], done: [], }; const r = completeTask(store, 't_001', '2026-05-21T15:00'); expect(r?.status).toBe('done'); expect(store.active).toHaveLength(0); expect(store.done).toHaveLength(1); expect(store.done[0].completedAt).toBe('2026-05-21T15:00'); }); test('returns null when already done or unknown', () => { const store: TaskStore = { active: [], done: [] }; expect(completeTask(store, 't_001')).toBeNull(); }); }); describe('summarizeActiveTasks', () => { test('returns empty string when no active', () => { expect(summarizeActiveTasks({ active: [], done: [] })).toBe(''); }); test('sorts due-set tasks before unset, asc by due', () => { const store: TaskStore = { active: [ { id: 't_001', title: '늦은', owner: '', due: '2026-06-01T10:00', status: 'open', notes: '' }, { id: 't_002', title: '미정', owner: '', due: '', status: 'open', notes: '' }, { id: 't_003', title: '빠른', owner: '', due: '2026-05-21T10:00', status: 'open', notes: '' }, ], done: [], }; const summary = summarizeActiveTasks(store); const lines = summary.split('\n'); // 빠른 → 늦은 → 미정 순 expect(lines[0]).toContain('빠른'); expect(lines[1]).toContain('늦은'); expect(lines[2]).toContain('미정'); }); test('truncates beyond max', () => { const active = Array.from({ length: 20 }, (_, i) => ({ id: `t_${String(i).padStart(3, '0')}`, title: `task ${i}`, owner: '', due: '', status: 'open' as const, notes: '', })); const summary = summarizeActiveTasks({ active, done: [] }, 5); expect(summary).toContain('task 0'); expect(summary).toContain('task 4'); expect(summary).not.toContain('task 5'); expect(summary).toContain('15 more'); }); }); describe('_parseTaskAttrs', () => { test('parses standard task attrs', () => { const a = _parseTaskAttrs('id="t_001" title="광고주 자료" owner="@me" due="2026-05-24T18:00"'); expect(a.id).toBe('t_001'); expect(a.title).toBe('광고주 자료'); expect(a.owner).toBe('@me'); expect(a.due).toBe('2026-05-24T18:00'); }); test('normalizes status variants', () => { expect(_parseTaskAttrs('status="In Progress"').status).toBe('in_progress'); expect(_parseTaskAttrs('status="INPROGRESS"').status).toBe('in_progress'); expect(_parseTaskAttrs('status="blocked"').status).toBe('blocked'); expect(_parseTaskAttrs('status="done"').status).toBe('done'); expect(_parseTaskAttrs('status="garbage"').status).toBe('open'); }); test('returns empty object when nothing parseable', () => { expect(_parseTaskAttrs('')).toEqual({}); expect(_parseTaskAttrs('garbage')).toEqual({}); }); });