218 lines
6.5 KiB
TypeScript
218 lines
6.5 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
import {
|
|
__resetTeamRefreshFanoutDiagnosticsForTests,
|
|
buildTeamRefreshFanoutCountKey,
|
|
getTeamRefreshFanoutSnapshotForTests,
|
|
MAX_TEAM_REFRESH_DIAGNOSTIC_RECENT_NOTES,
|
|
MAX_TEAM_REFRESH_DIAGNOSTIC_TEAMS,
|
|
noteTeamRefreshFanout,
|
|
summarizeTeamRefreshFanout,
|
|
type TeamRefreshFanoutSnapshot,
|
|
} from '../../../src/renderer/store/teamRefreshFanoutDiagnostics';
|
|
|
|
function snapshotFor(teamName: string): TeamRefreshFanoutSnapshot {
|
|
const snapshot = getTeamRefreshFanoutSnapshotForTests(teamName);
|
|
expect(snapshot).not.toBeNull();
|
|
return snapshot as TeamRefreshFanoutSnapshot;
|
|
}
|
|
|
|
describe('teamRefreshFanoutDiagnostics', () => {
|
|
beforeEach(() => {
|
|
vi.useFakeTimers();
|
|
__resetTeamRefreshFanoutDiagnosticsForTests();
|
|
});
|
|
|
|
afterEach(() => {
|
|
__resetTeamRefreshFanoutDiagnosticsForTests();
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it('records scheduled and executed fanout counts separately', () => {
|
|
const scheduled = {
|
|
teamName: 'team-a',
|
|
surface: 'team-change-listener',
|
|
phase: 'scheduled',
|
|
reason: 'event:process',
|
|
operation: 'refreshTeamData',
|
|
} as const;
|
|
const executed = {
|
|
...scheduled,
|
|
phase: 'executed',
|
|
} as const;
|
|
|
|
noteTeamRefreshFanout(scheduled);
|
|
noteTeamRefreshFanout(executed);
|
|
|
|
const snapshot = snapshotFor('team-a');
|
|
expect(snapshot.counts[buildTeamRefreshFanoutCountKey(scheduled)]).toBe(1);
|
|
expect(snapshot.counts[buildTeamRefreshFanoutCountKey(executed)]).toBe(1);
|
|
});
|
|
|
|
it('records coalesced notes separately from scheduled notes', () => {
|
|
const scheduled = {
|
|
teamName: 'team-a',
|
|
surface: 'team-change-listener',
|
|
phase: 'scheduled',
|
|
reason: 'event:member-spawn',
|
|
operation: 'fetchMemberSpawnStatuses',
|
|
} as const;
|
|
const coalesced = {
|
|
...scheduled,
|
|
phase: 'coalesced',
|
|
} as const;
|
|
|
|
noteTeamRefreshFanout(scheduled);
|
|
noteTeamRefreshFanout(coalesced);
|
|
noteTeamRefreshFanout(coalesced);
|
|
|
|
const snapshot = snapshotFor('team-a');
|
|
expect(snapshot.counts[buildTeamRefreshFanoutCountKey(scheduled)]).toBe(1);
|
|
expect(snapshot.counts[buildTeamRefreshFanoutCountKey(coalesced)]).toBe(2);
|
|
});
|
|
|
|
it('caps recent notes per team', () => {
|
|
for (let index = 0; index < MAX_TEAM_REFRESH_DIAGNOSTIC_RECENT_NOTES + 5; index += 1) {
|
|
noteTeamRefreshFanout({
|
|
teamName: 'team-a',
|
|
surface: 'team-change-listener',
|
|
phase: 'scheduled',
|
|
reason: `event:${index}`,
|
|
operation: 'refreshTeamData',
|
|
});
|
|
}
|
|
|
|
const snapshot = snapshotFor('team-a');
|
|
expect(snapshot.recent).toHaveLength(MAX_TEAM_REFRESH_DIAGNOSTIC_RECENT_NOTES);
|
|
expect(snapshot.recent[0]?.reason).toBe('event:5');
|
|
});
|
|
|
|
it('caps team buckets by evicting the oldest bucket', () => {
|
|
for (let index = 0; index < MAX_TEAM_REFRESH_DIAGNOSTIC_TEAMS + 1; index += 1) {
|
|
noteTeamRefreshFanout({
|
|
teamName: `team-${index}`,
|
|
surface: 'team-change-listener',
|
|
phase: 'scheduled',
|
|
reason: 'event:process',
|
|
operation: 'refreshTeamData',
|
|
});
|
|
}
|
|
|
|
expect(getTeamRefreshFanoutSnapshotForTests('team-0')).toBeNull();
|
|
expect(
|
|
getTeamRefreshFanoutSnapshotForTests(`team-${MAX_TEAM_REFRESH_DIAGNOSTIC_TEAMS}`)
|
|
).not.toBeNull();
|
|
});
|
|
|
|
it('reset clears all diagnostic state', () => {
|
|
noteTeamRefreshFanout({
|
|
teamName: 'team-a',
|
|
surface: 'team-change-listener',
|
|
phase: 'scheduled',
|
|
reason: 'event:process',
|
|
operation: 'refreshTeamData',
|
|
});
|
|
|
|
__resetTeamRefreshFanoutDiagnosticsForTests();
|
|
|
|
expect(getTeamRefreshFanoutSnapshotForTests('team-a')).toBeNull();
|
|
expect(getTeamRefreshFanoutSnapshotForTests()).toEqual({});
|
|
});
|
|
|
|
it('summarizes structured counts without splitting colon-containing reasons', () => {
|
|
noteTeamRefreshFanout({
|
|
teamName: 'team-a',
|
|
surface: 'team-change-listener',
|
|
phase: 'scheduled',
|
|
reason: 'event:process',
|
|
operation: 'refreshTeamData',
|
|
});
|
|
|
|
const summary = summarizeTeamRefreshFanout('team-a');
|
|
|
|
expect(summary).toMatchObject({ teamName: 'team-a', total: 1 });
|
|
expect(summary.rows[0]).toMatchObject({
|
|
count: 1,
|
|
surface: 'team-change-listener',
|
|
reason: 'event:process',
|
|
operation: 'refreshTeamData',
|
|
phase: 'scheduled',
|
|
});
|
|
});
|
|
|
|
it('sorts summary rows by count descending and aggregates across teams', () => {
|
|
const highVolumeNote = {
|
|
teamName: 'team-a',
|
|
surface: 'team-change-listener',
|
|
phase: 'scheduled',
|
|
reason: 'event:process',
|
|
operation: 'refreshTeamData',
|
|
} as const;
|
|
const lowVolumeNote = {
|
|
teamName: 'team-b',
|
|
surface: 'team-change-listener',
|
|
phase: 'scheduled',
|
|
reason: 'event:config',
|
|
operation: 'fetchTeams',
|
|
} as const;
|
|
|
|
noteTeamRefreshFanout(highVolumeNote);
|
|
noteTeamRefreshFanout(highVolumeNote);
|
|
noteTeamRefreshFanout(lowVolumeNote);
|
|
|
|
const summary = summarizeTeamRefreshFanout();
|
|
|
|
expect(summary.total).toBe(3);
|
|
expect(summary.rows[0]).toMatchObject({
|
|
count: 2,
|
|
reason: 'event:process',
|
|
operation: 'refreshTeamData',
|
|
});
|
|
expect(summary.rows[1]).toMatchObject({
|
|
count: 1,
|
|
reason: 'event:config',
|
|
operation: 'fetchTeams',
|
|
});
|
|
});
|
|
|
|
it('returns cloned snapshots that cannot mutate internal buckets', () => {
|
|
const note = {
|
|
teamName: 'team-a',
|
|
surface: 'team-change-listener',
|
|
phase: 'scheduled',
|
|
reason: 'event:process',
|
|
operation: 'refreshTeamData',
|
|
} as const;
|
|
noteTeamRefreshFanout(note);
|
|
|
|
const key = buildTeamRefreshFanoutCountKey(note);
|
|
const snapshot = snapshotFor('team-a');
|
|
snapshot.counts[key] = 99;
|
|
snapshot.structuredCounts[key]!.count = 99;
|
|
snapshot.recent[0]!.reason = 'mutated';
|
|
|
|
const nextSnapshot = snapshotFor('team-a');
|
|
expect(nextSnapshot.counts[key]).toBe(1);
|
|
expect(nextSnapshot.structuredCounts[key]?.count).toBe(1);
|
|
expect(nextSnapshot.recent[0]?.reason).toBe('event:process');
|
|
});
|
|
|
|
it('ignores invalid empty team or reason values', () => {
|
|
noteTeamRefreshFanout({
|
|
teamName: '',
|
|
surface: 'team-change-listener',
|
|
phase: 'scheduled',
|
|
reason: 'event:process',
|
|
operation: 'refreshTeamData',
|
|
});
|
|
noteTeamRefreshFanout({
|
|
teamName: 'team-a',
|
|
surface: 'team-change-listener',
|
|
phase: 'scheduled',
|
|
reason: '',
|
|
operation: 'refreshTeamData',
|
|
});
|
|
|
|
expect(getTeamRefreshFanoutSnapshotForTests()).toEqual({});
|
|
});
|
|
});
|