From 5d05434bbb7b0b488cb2dedc15679caec16377df Mon Sep 17 00:00:00 2001 From: 777genius Date: Mon, 1 Jun 2026 21:37:23 +0300 Subject: [PATCH] fix(team): count changes badge by files --- .../useTeamChangesSummaries.test.tsx | 143 ++++++++++++++++++ .../team/useTeamChangesSummaries.ts | 77 ++++++---- 2 files changed, 192 insertions(+), 28 deletions(-) diff --git a/src/renderer/components/team/__tests__/useTeamChangesSummaries.test.tsx b/src/renderer/components/team/__tests__/useTeamChangesSummaries.test.tsx index 44780ee4..36a23f12 100644 --- a/src/renderer/components/team/__tests__/useTeamChangesSummaries.test.tsx +++ b/src/renderer/components/team/__tests__/useTeamChangesSummaries.test.tsx @@ -1053,6 +1053,149 @@ describe('useTeamChangesSummaries', () => { expect(container.textContent).not.toContain('src/app.ts'); }); + it('counts files instead of changed tasks in the closed-section counter', async () => { + hoisted.getTeamTaskChangeSummaries.mockImplementation( + async (_teamName: string, requests: TeamTaskChangeSummaryRequest[]) => ({ + teamName: 'team-a', + computedAt: '2026-05-10T10:00:01.000Z', + items: requests.map((request, index) => { + const totalFiles = index === 0 ? 3 : 4; + return { + taskId: request.taskId, + changeSet: { + ...changeSet(request.taskId), + files: [ + fileChange({ + filePath: `/repo/src/${request.taskId}.ts`, + relativePath: `src/${request.taskId}.ts`, + }), + ], + totalFiles, + totalLinesAdded: totalFiles, + }, + }; + }), + }) + ); + + const snapshots: HookSnapshot[] = []; + const onSnapshot = (snapshot: HookSnapshot): void => { + snapshots.push(snapshot); + }; + container = document.createElement('div'); + document.body.appendChild(container); + root = createRoot(container); + + await act(async () => { + root?.render( + React.createElement(HookHarness, { + tasks: changedTasks(2), + sectionOpen: false, + onSnapshot, + }) + ); + await Promise.resolve(); + await Promise.resolve(); + }); + + expect(hoisted.getTeamTaskChangeSummaries).toHaveBeenCalledTimes(1); + expect(snapshots.at(-1)?.badgeCount).toBe(7); + }); + + it('keeps the previous file counter for tasks whose summary refresh errors', async () => { + const getTotalFilesForTaskId = (taskId: string): number => (taskId === 'changed-0' ? 3 : 4); + + hoisted.getTeamTaskChangeSummaries + .mockImplementationOnce( + async (_teamName: string, requests: TeamTaskChangeSummaryRequest[]) => ({ + teamName: 'team-a', + computedAt: '2026-05-10T10:00:01.000Z', + items: requests.map((request) => { + const totalFiles = getTotalFilesForTaskId(request.taskId); + return { + taskId: request.taskId, + changeSet: { + ...changeSet(request.taskId), + files: [ + fileChange({ + filePath: `/repo/src/${request.taskId}.ts`, + relativePath: `src/${request.taskId}.ts`, + }), + ], + totalFiles, + totalLinesAdded: totalFiles, + }, + }; + }), + }) + ) + .mockImplementationOnce( + async (_teamName: string, requests: TeamTaskChangeSummaryRequest[]) => ({ + teamName: 'team-a', + computedAt: '2026-05-10T10:00:02.000Z', + items: requests.map((request) => + request.taskId === 'changed-0' + ? { taskId: request.taskId, changeSet: null, error: 'summary timed out' } + : { + taskId: request.taskId, + changeSet: { + ...changeSet(request.taskId), + files: [ + fileChange({ + filePath: `/repo/src/${request.taskId}.ts`, + relativePath: `src/${request.taskId}.ts`, + }), + ], + totalFiles: 4, + totalLinesAdded: 4, + }, + } + ), + }) + ); + + const snapshots: HookSnapshot[] = []; + const onSnapshot = (snapshot: HookSnapshot): void => { + snapshots.push(snapshot); + }; + container = document.createElement('div'); + document.body.appendChild(container); + root = createRoot(container); + + const initialTasks = changedTasks(2); + await act(async () => { + root?.render( + React.createElement(HookHarness, { + tasks: initialTasks, + sectionOpen: false, + onSnapshot, + }) + ); + await Promise.resolve(); + await Promise.resolve(); + }); + + expect(snapshots.at(-1)?.badgeCount).toBe(7); + + await act(async () => { + root?.render( + React.createElement(HookHarness, { + tasks: initialTasks.map((item) => ({ + ...item, + updatedAt: '2026-05-10T10:05:00.000Z', + })), + sectionOpen: false, + onSnapshot, + }) + ); + await Promise.resolve(); + await Promise.resolve(); + }); + + expect(hoisted.getTeamTaskChangeSummaries).toHaveBeenCalledTimes(2); + expect(snapshots.at(-1)?.badgeCount).toBe(7); + }); + it('loads staged open batches without repeating successful tasks', async () => { const first = createDeferred(); const second = createDeferred(); diff --git a/src/renderer/components/team/useTeamChangesSummaries.ts b/src/renderer/components/team/useTeamChangesSummaries.ts index 01a4a500..66c934c6 100644 --- a/src/renderer/components/team/useTeamChangesSummaries.ts +++ b/src/renderer/components/team/useTeamChangesSummaries.ts @@ -122,17 +122,17 @@ function hasSafeFileSummaries(changeSet: TaskChangeSetV2): boolean { ); } -function hasDisplayableFileSummaries(changeSet: TaskChangeSetV2): boolean { - return ( - Array.isArray(changeSet.files) && - changeSet.files.some( - (file) => - file && - typeof file === 'object' && - typeof file.filePath === 'string' && - file.filePath.trim().length > 0 - ) - ); +function countDisplayableFileSummaries(changeSet: TaskChangeSetV2): number { + if (!Array.isArray(changeSet.files)) { + return 0; + } + return changeSet.files.filter( + (file) => + file && + typeof file === 'object' && + typeof file.filePath === 'string' && + file.filePath.trim().length > 0 + ).length; } function isMinimalPresenceChangeSet(changeSet: TaskChangeSetV2): boolean { @@ -198,25 +198,37 @@ function shouldClearSelectedTaskChangePresence( ); } -function isCountableTeamChangeSummary(item: TeamTaskChangeSummaryItem): boolean { +function getTeamChangeBadgeContribution(item: TeamTaskChangeSummaryItem): number { if (item.error) { - return true; + return 1; } const changeSet = item.changeSet; if (!changeSet) { - return false; + return 0; } - if (hasDisplayableFileSummaries(changeSet)) { - return true; + + const totalFiles = Number(changeSet.totalFiles); + if (Number.isFinite(totalFiles) && totalFiles > 0) { + return Math.trunc(totalFiles); + } + + const displayableFileCount = countDisplayableFileSummaries(changeSet); + if (displayableFileCount > 0) { + return displayableFileCount; } const reviewability = classifyTaskChangeReviewability(changeSet).reviewability; - return reviewability === 'attention_required' || reviewability === 'diagnostic_only'; + return reviewability === 'attention_required' || reviewability === 'diagnostic_only' ? 1 : 0; } -function countChangedTasks(changeCountByTaskId: Record): number { - return Object.values(changeCountByTaskId).filter(Boolean).length; +function sumTeamChangeBadgeContributions(changeBadgeCountByTaskId: Record): number { + return Object.values(changeBadgeCountByTaskId).reduce((sum, value) => { + if (!Number.isFinite(value) || value <= 0) { + return sum; + } + return sum + Math.trunc(value); + }, 0); } function isDocumentHidden(): boolean { @@ -266,7 +278,9 @@ export function useTeamChangesSummaries({ const [summariesByTaskId, setSummariesByTaskId] = useState< Record >({}); - const [changeCountByTaskId, setChangeCountByTaskId] = useState>({}); + const [changeBadgeCountByTaskId, setChangeBadgeCountByTaskId] = useState>( + {} + ); const [counterLoaded, setCounterLoaded] = useState(false); const [stats, setStats] = useState({ eligibleCount: 0, @@ -405,7 +419,7 @@ export function useTeamChangesSummaries({ if (storeSummaries) { setSummariesByTaskId({}); } - setChangeCountByTaskId({}); + setChangeBadgeCountByTaskId({}); setCounterLoaded(true); autoRefreshBlockedUntilRef.current = 0; setLoading(false); @@ -454,16 +468,23 @@ export function useTeamChangesSummaries({ } } - setChangeCountByTaskId((previous) => { - const next: Record = {}; - for (const [taskId, countable] of Object.entries(previous)) { + setChangeBadgeCountByTaskId((previous) => { + const next: Record = {}; + for (const [taskId, badgeContribution] of Object.entries(previous)) { if (currentTaskIds.has(taskId) && plan.eligibleTaskIds.has(taskId)) { - next[taskId] = countable; + next[taskId] = badgeContribution; } } for (const item of responseItems) { if (!plan.requestOptionsByTaskId.has(item.taskId)) continue; - next[item.taskId] = isCountableTeamChangeSummary(item); + const nextContribution = getTeamChangeBadgeContribution(item); + const previousContribution = previous[item.taskId]; + next[item.taskId] = + item.error && + Number.isFinite(previousContribution) && + previousContribution > nextContribution + ? previousContribution + : nextContribution; } return next; }); @@ -587,7 +608,7 @@ export function useTeamChangesSummaries({ lastRequestedTasksFingerprintRef.current = null; lastCounterTasksFingerprintRef.current = null; setSummariesByTaskId({}); - setChangeCountByTaskId({}); + setChangeBadgeCountByTaskId({}); setCounterLoaded(false); setError(null); setStats({ eligibleCount: 0, requestedCount: 0, deferredCount: 0 }); @@ -723,7 +744,7 @@ export function useTeamChangesSummaries({ return { summariesByTaskId, - badgeCount: counterLoaded ? countChangedTasks(changeCountByTaskId) : null, + badgeCount: counterLoaded ? sumTeamChangeBadgeContributions(changeBadgeCountByTaskId) : null, stats, loading, refreshing,