Files
connectai/tests/taskStore.test.ts
T

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({});
});
});