import { IssueReadModel, IssueTimelineEntry, ProjectReadModel, ProjectRole, ReleaseReadModel, ReleaseStatus, SprintReadModel, SprintStatus, StatusCategory as C, } from '@task-stream/api/tracker/domain'; import { computeHome, HomeProjectInput, lastDays } from './home'; const NOW = new Date('2026-10-15T12:00:00Z'); const ana = { id: 'ana', name: 'Ana' }; const bob = { id: 'bob', name: 'Bob' }; const project = ( key: string, role: ProjectRole, extra: Partial = {}, ): ProjectReadModel => ({ id: key.toLowerCase(), key, name: `Proyecto ${key}`, isPublic: false, archivedAt: null, members: [ { user: ana, role }, { user: bob, role: ProjectRole.MEMBER }, ], ...extra, }) as ProjectReadModel; const issue = ( id: string, category: C, extra: Partial = {}, ): IssueReadModel => ({ id, key: id, projectId: 'web', title: `Ticket ${id}`, status: category, statusCategory: category, reporter: bob, assignee: null, watchers: [], labels: [], dueDate: null, trashedAt: null, storyPoints: null, sprint: null, createdAt: '2026-09-01T09:00:00Z', updatedAt: '2026-10-01T09:00:00Z', ...extra, }) as IssueReadModel; let seq = 0; const done = ( issueId: string, at: string, assigneeId: string, ): IssueTimelineEntry[] => [ { id: `${issueId}:${++seq}`, issueId, projectId: 'web', at: '2026-09-01T09:00:00Z', kind: 'status', status: 'IN_PROGRESS', category: C.IN_PROGRESS, assigneeId, }, { id: `${issueId}:${++seq}`, issueId, projectId: 'web', at, kind: 'status', status: 'DONE', category: C.DONE, assigneeId, }, ]; const input = (projects: HomeProjectInput[]) => ({ viewerId: 'ana', projects, activity: [], hasCalendar: false, online: [], now: NOW, }); const web = (extra: Partial = {}): HomeProjectInput => ({ project: project('WEB', ProjectRole.OWNER), issues: [], timeline: [], sprints: [], releases: [], activity: new Array(14).fill(0), ...extra, }); describe('computeHome', () => { it('puts my coming week on the agenda: tickets, sprint ends, releases', () => { const home = computeHome( input([ web({ issues: [ issue('MINE', C.TODO, { assignee: ana, dueDate: '2026-10-17' }), issue('FOLLOWED', C.TODO, { watchers: [ana], dueDate: '2026-10-15', }), issue('NOT_MINE', C.TODO, { assignee: bob, dueDate: '2026-10-16' }), issue('LATER', C.TODO, { assignee: ana, dueDate: '2026-10-30' }), issue('DONE', C.DONE, { assignee: ana, dueDate: '2026-10-16' }), ], sprints: [ { id: 's', name: 'Sprint 7', status: SprintStatus.ACTIVE, startDate: '2026-10-08', endDate: '2026-10-21', } as SprintReadModel, ], releases: [ { id: 'r', name: '2.4', status: ReleaseStatus.PLANNED, targetDate: '2026-10-20', } as ReleaseReadModel, ], }), ]), ); expect(home.agenda.map((a) => [a.day, a.kind, a.title])).toEqual([ ['2026-10-15', 'ISSUE', 'Ticket FOLLOWED'], ['2026-10-17', 'ISSUE', 'Ticket MINE'], ['2026-10-20', 'RELEASE', '2.4'], ['2026-10-21', 'SPRINT_END', 'Sprint 7'], ]); }); it("counts the team's week and mine, and my streak", () => { const closed = [ ['A', '2026-10-15T10:00:00Z', 'ana'], ['B', '2026-10-14T10:00:00Z', 'ana'], ['C', '2026-10-13T10:00:00Z', 'ana'], ['D', '2026-10-11T10:00:00Z', 'ana'], ['E', '2026-10-05T10:00:00Z', 'bob'], ] as const; const home = computeHome( input([ web({ issues: closed.map(([id]) => issue(id, C.DONE)), timeline: closed.flatMap(([id, at, who]) => done(id, at, who)), }), ]), ); expect(home.team.perDay).toHaveLength(14); expect(home.team.perDay[13]).toBe(1); expect(home.team).toMatchObject({ thisWeek: 4, lastWeek: 1 }); // Today, yesterday and the day before: three in a row (the 12th breaks it). expect(home.me).toEqual({ thisWeek: 4, lastWeek: 0, streak: 3 }); }); it('suggests what to do, and keeps a guest out of project-wide views', () => { const home = computeHome( input([ web({ issues: [ issue('U1', C.TODO), issue('U2', C.TODO, { labels: ['propuesta'] }), issue('Q1', C.QA, { assignee: bob }), ], }), { ...web(), project: project('CLI', ProjectRole.GUEST), issues: [issue('X', C.TODO)], }, ]), ); expect( home.suggestions.map((s) => [s.kind, s.projectKey, s.count]), ).toEqual([ ['UNASSIGNED', 'WEB', 2], ['QA_WAITING', 'WEB', 1], ['PROPOSALS', 'WEB', 1], ]); // Only the project seen whole has a card. expect(home.projects.map((p) => p.projectId)).toEqual(['web']); expect(home.projects[0]).toMatchObject({ open: 3, overdue: 0 }); expect(home.firstSteps).toEqual({ project: true, invited: true, ticket: false, calendar: false, }); }); it('lists the last 14 days, oldest first', () => { const days = lastDays(NOW); expect(days).toHaveLength(14); expect(days[0]).toBe('2026-10-02'); expect(days[13]).toBe('2026-10-15'); }); });