186 lines
7.1 KiB
TypeScript
186 lines
7.1 KiB
TypeScript
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({});
|
|
});
|
|
});
|