diff --git a/src/main/services/team/TeamDataService.ts b/src/main/services/team/TeamDataService.ts index a2cc19fe..c74c85a5 100644 --- a/src/main/services/team/TeamDataService.ts +++ b/src/main/services/team/TeamDataService.ts @@ -1041,9 +1041,21 @@ export class TeamDataService { const deletedTeams = new Set(teams.filter((t) => t.deletedAt).map((t) => t.teamName)); - const teamNames = [ - ...new Set(rawTasks.map((t) => t.teamName).filter((n) => teamInfoMap.has(n))), - ]; + const MAX_GLOBAL_TASKS_EXPORTED = 500; + let tasksToExport = rawTasks.filter((task) => teamInfoMap.has(task.teamName)); + if (tasksToExport.length > MAX_GLOBAL_TASKS_EXPORTED) { + // Prefer newest first before reading kanban and building the lightweight IPC projection. + tasksToExport = tasksToExport + .slice() + .sort((a, b) => { + const at = Date.parse(a.updatedAt ?? a.createdAt ?? '') || 0; + const bt = Date.parse(b.updatedAt ?? b.createdAt ?? '') || 0; + return bt - at; + }) + .slice(0, MAX_GLOBAL_TASKS_EXPORTED); + } + + const teamNames = [...new Set(tasksToExport.map((task) => task.teamName))]; const kanbanByTeam = new Map(); await Promise.all( teamNames.map(async (teamName) => { @@ -1058,10 +1070,7 @@ export class TeamDataService { const out: GlobalTask[] = []; let processed = 0; - for (const task of rawTasks) { - if (!teamInfoMap.has(task.teamName)) { - continue; - } + for (const task of tasksToExport) { const info = teamInfoMap.get(task.teamName)!; const kanbanTaskState = kanbanByTeam.get(task.teamName)?.tasks[task.id]; const reviewState = this.resolveTaskReviewState(task, kanbanTaskState); @@ -1117,18 +1126,6 @@ export class TeamDataService { } } - // Hard cap: keep renderer responsive even with huge task sets. - const MAX_GLOBAL_TASKS_EXPORTED = 500; - if (out.length > MAX_GLOBAL_TASKS_EXPORTED) { - // Prefer newest first if timestamps exist. - out.sort((a, b) => { - const at = Date.parse(a.updatedAt ?? a.createdAt ?? '') || 0; - const bt = Date.parse(b.updatedAt ?? b.createdAt ?? '') || 0; - return bt - at; - }); - return out.slice(0, MAX_GLOBAL_TASKS_EXPORTED); - } - return out; } diff --git a/test/main/services/team/TeamDataService.test.ts b/test/main/services/team/TeamDataService.test.ts index 22c0c57a..264ed816 100644 --- a/test/main/services/team/TeamDataService.test.ts +++ b/test/main/services/team/TeamDataService.test.ts @@ -1524,7 +1524,7 @@ describe('TeamDataService', () => { {} as never, {} as never, {} as never, - (teamName: string) => + (_teamName: string) => ({ tasks: { createTask: createTaskMock, @@ -1612,7 +1612,7 @@ describe('TeamDataService', () => { {} as never, {} as never, {} as never, - (teamName: string) => + (_teamName: string) => ({ tasks: { createTask: createTaskMock, @@ -1724,7 +1724,7 @@ describe('TeamDataService', () => { {} as never, {} as never, {} as never, - (teamName: string) => + (_teamName: string) => ({ tasks: { createTask: createTaskMock, @@ -2235,6 +2235,71 @@ describe('TeamDataService', () => { }); }); + it('caps global task projections before building lightweight comment payloads', async () => { + const rawTasks = Array.from({ length: 501 }, (_, index) => ({ + id: `task-${index}`, + teamName: index === 0 ? 'old-team' : 'my-team', + subject: `Task ${index}`, + status: 'pending' as const, + owner: 'bob', + createdAt: `2026-03-01T00:${String(index % 60).padStart(2, '0')}:00.000Z`, + updatedAt: `2026-03-01T${String(Math.floor(index / 60)).padStart(2, '0')}:${String( + index % 60 + ).padStart(2, '0')}:00.000Z`, + comments: [ + { + id: `comment-${index}`, + author: 'bob', + text: `Comment ${index}`, + createdAt: '2026-03-01T09:00:00.000Z', + type: 'comment' as const, + }, + ], + })); + const getState = vi.fn(async (teamName: string) => ({ + teamName, + reviewers: [], + tasks: {}, + })); + const service = new TeamDataService( + { + listTeams: vi.fn(async () => [ + { + teamName: 'my-team', + displayName: 'My team', + projectPath: '/repo', + }, + { + teamName: 'old-team', + displayName: 'Old team', + projectPath: '/old-repo', + }, + ]), + } as never, + { + getAllTasks: vi.fn(async () => rawTasks), + } as never, + {} as never, + {} as never, + {} as never, + {} as never, + { + getState, + } as never + ); + + const tasks = await service.getAllTasks(); + + expect(tasks).toHaveLength(500); + expect(tasks[0]?.id).toBe('task-500'); + expect(tasks.some((task) => task.id === 'task-0')).toBe(false); + expect(tasks[0]?.comments?.[0]).toMatchObject({ + id: 'comment-500', + text: 'Comment 500', + }); + expect(getState).not.toHaveBeenCalledWith('old-team'); + }); + it('lets kanban approved overlay win over stale review history in global task projections', async () => { const service = new TeamDataService( {