Files
Agent-System-Frontend/test/tasklist.test.ts
T
weichen dc70399d45 Rebuild the left nav as a task-centric sidebar (option A, R1)
Why: creation entries were split (project + at sidebar top-left, new
session at bottom-left) and the project tree forced per-project
navigation; the approved option A design makes one primary New Task
entry, organization a view mode, and identity always present.

Evidence:
- New TaskSidebar: single New Task split button (current-project context,
  Cmd-N, secondary menu for new project / new task in project), project
  <-> timeline organization with persisted collapse, pinned section,
  archive view toggle, hover row actions, account footer fed by
  /api/me (embedded mode shows local tenant; logout clears the key),
  workspace icon strip retained (D25 shared list with Cmd-1..9).
- store: tasks = cross-project list via GET /api/conversations; openTask
  switches project context; pin/archive optimistic + refresh; last
  project context persisted; stale conversations field removed.
- tasklist.ts pure helpers + 6 vitest cases; npm run build passes;
  headless walk on the real binary: cross-project open, pinned-first,
  archive hide/show/restore, new-task flow, zero console errors.

Impact: ProjectNav removed; contracts.ts regenerated from core
(pinned/archived); no composer/right-pane changes (D25/D26/D31 intact).

Refs: Agent-System-Documents docs/proposals/2026-09-30-ui-redesign-and-auth.md
2026-09-30 10:41:12 +00:00

72 lines
3.3 KiB
TypeScript

// 任务列表组织纯函数测试(方案 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> = {}): 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]])
})
})