// 任务列表组织纯函数测试(方案 A R1)。 import { describe, it, expect } from 'vitest' import { groupByProject, groupByTimeline, relativeTime, filterTasks } from '../src/tasklist' import type { Conversation, Project } from '../src/contracts' const NOW = new Date('2026-09-30T15:00:00').getTime() const H = 3_600_000 function conv(id: string, projectId: string, updatedAt: string, extra: Partial = {}): Conversation { return { id, projectId, title: id, createdAt: updatedAt, updatedAt, ...extra } } describe('groupByProject', () => { it('groups tasks under projects in project order and never drops orphans', () => { const projects: Project[] = [ { id: 'p2', name: '乙', createdAt: '', updatedAt: '' }, { id: 'p1', name: '甲', createdAt: '', updatedAt: '' }, ] const tasks = [conv('a', 'p1', '2026-09-30T10:00:00Z'), conv('b', 'p2', '2026-09-29T10:00:00Z'), conv('c', 'pX', '2026-09-28T10:00:00Z')] const groups = groupByProject(tasks, projects) expect(groups.map(g => g.label)).toEqual(['乙', '甲', '未分组']) expect(groups[0].projectId).toBe('p2') expect(groups[2].tasks.map(t => t.id)).toEqual(['c']) }) it('skips projects with zero visible tasks', () => { const projects: Project[] = [{ id: 'p1', name: '甲', createdAt: '', updatedAt: '' }] expect(groupByProject([], projects)).toEqual([]) }) }) describe('groupByTimeline', () => { it('buckets by local day and week boundaries', () => { // NOW=周三 15:00(本地);构造今天/昨天/本周更早(周一)/更早 各一条。 const tasks = [ conv('today', 'p', new Date(NOW - H).toISOString()), conv('yesterday', 'p', new Date(NOW - 26 * H).toISOString()), conv('week', 'p', new Date(NOW - 50 * H).toISOString()), conv('older', 'p', new Date(NOW - 10 * 24 * H).toISOString()), ] const groups = groupByTimeline(tasks, NOW) expect(groups.map(g => g.label)).toEqual(['今天', '昨天', '本周更早', '更早']) expect(groups[0].tasks[0].id).toBe('today') }) it('hides empty buckets and tolerates bad timestamps', () => { const groups = groupByTimeline([conv('x', 'p', 'not-a-date')], NOW) expect(groups).toHaveLength(1) expect(groups[0].label).toBe('更早') }) }) describe('relativeTime', () => { it('formats gradients and year rollover', () => { expect(relativeTime(new Date(NOW - 30_000).toISOString(), NOW)).toBe('刚刚') expect(relativeTime(new Date(NOW - 5 * 60_000).toISOString(), NOW)).toBe('5 分钟') expect(relativeTime(new Date(NOW - 3 * H).toISOString(), NOW)).toBe('3 小时') expect(relativeTime(new Date(NOW - 30 * H).toISOString(), NOW)).toBe('昨天') expect(relativeTime(new Date(NOW - 3 * 24 * H).toISOString(), NOW)).toBe('3 天') expect(relativeTime('2026-05-01T00:00:00', NOW)).toBe('5/1') expect(relativeTime('2025-12-31T00:00:00', NOW)).toBe('2025/12/31') expect(relativeTime('bad')).toBe('') }) }) describe('filterTasks', () => { it('matches title case-insensitively and passes through on empty query', () => { const tasks = [conv('a', 'p', '', { title: '周报 Draft' }), conv('b', 'p', '', { title: '接口评审' })] expect(filterTasks(tasks, '周报')).toEqual([tasks[0]]) expect(filterTasks(tasks, ' ')).toEqual(tasks) expect(filterTasks(tasks, 'DRAFT')).toEqual([tasks[0]]) }) })