agent-ecosystem/test/renderer/store/teamChangeThrottle.test.ts
iliya 1d3080f0f9 feat: add scheduled tasks with cron execution and rich log viewer
- Cron-based task scheduling with SchedulerService (create, pause, resume, delete)
- One-shot executor using `claude -p` with stream-json output for rich log display
- CLAUDECODE env var stripped to prevent nested session detection
- JsonScheduleRepository for persistent schedule/run/log storage
- Full IPC pipeline: handlers, preload bridge, API types, HttpClient stubs
- ScheduleSection UI with create/edit dialog, run history, status badges
- ScheduleRunLogDialog with CliLogsRichView (thinking blocks, tool cards, markdown)
- Real-time run status updates via store subscription
- Retry logic with configurable max retries and auto-pause on consecutive failures
- CronScheduleInput with human-readable descriptions via cronstrue
- 3 test suites: SchedulerService, ScheduledTaskExecutor, JsonScheduleRepository
2026-03-08 00:58:07 +02:00

138 lines
4.3 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
const hoisted = vi.hoisted(() => ({
onTeamChangeCb: null as ((event: unknown, data: { teamName: string }) => void) | null,
onProvisioningProgressCb: null as
| ((event: unknown, data: { runId: string; teamName: string }) => void)
| null,
}));
vi.mock('@renderer/api', () => ({
api: {
config: {
get: vi.fn(async () => ({
general: { theme: 'dark' },
notifications: { enabled: true, triggers: [] },
})),
},
getRepositoryGroups: vi.fn(async () => []),
notifications: {
onNew: vi.fn(() => () => undefined),
onUpdated: vi.fn(() => () => undefined),
onClicked: vi.fn(() => () => undefined),
get: vi.fn(async () => ({
notifications: [],
total: 0,
totalCount: 0,
unreadCount: 0,
hasMore: false,
})),
},
teams: {
onTeamChange: vi.fn(
(cb: (event: unknown, data: { teamName: string }) => void): (() => void) => {
hoisted.onTeamChangeCb = cb;
return () => {
hoisted.onTeamChangeCb = null;
};
}
),
onProvisioningProgress: vi.fn(
(cb: (event: unknown, data: { runId: string; teamName: string }) => void): (() => void) => {
hoisted.onProvisioningProgressCb = cb;
return () => {
hoisted.onProvisioningProgressCb = null;
};
}
),
getAllTasks: vi.fn(async () => []),
list: vi.fn(async () => []),
},
schedules: {
list: vi.fn(async () => []),
onScheduleChange: vi.fn(() => () => undefined),
},
},
}));
import { initializeNotificationListeners, useStore } from '../../../src/renderer/store';
describe('team change throttling', () => {
let cleanup: (() => void) | null = null;
beforeEach(async () => {
vi.useFakeTimers();
const fetchTeams = vi.fn(async () => undefined);
const refreshTeamData = vi.fn(async () => undefined);
useStore.setState({
fetchTeams,
refreshTeamData,
paneLayout: {
focusedPaneId: 'p1',
panes: [
{
id: 'p1',
widthFraction: 1,
tabs: [{ id: 't1', type: 'team', teamName: 'my-team', label: 'my-team' }],
activeTabId: 't1',
},
],
},
} as never);
cleanup = initializeNotificationListeners();
// Flush microtask queue so the sequential init chain completes
// before test assertions start (prevents init calls from leaking into spies).
await vi.advanceTimersByTimeAsync(0);
});
afterEach(() => {
cleanup?.();
cleanup = null;
vi.useRealTimers();
});
it('throttles both team list and detail refresh', async () => {
const state = useStore.getState();
const fetchTeamsSpy = vi.spyOn(state, 'fetchTeams');
const refreshTeamDataSpy = vi.spyOn(state, 'refreshTeamData');
// Fire 3 rapid events
hoisted.onTeamChangeCb?.({}, { teamName: 'my-team' });
hoisted.onTeamChangeCb?.({}, { teamName: 'my-team' });
hoisted.onTeamChangeCb?.({}, { teamName: 'my-team' });
// Both are throttled — nothing called synchronously
expect(fetchTeamsSpy).not.toHaveBeenCalled();
expect(refreshTeamDataSpy).not.toHaveBeenCalled();
// Detail refresh fires at 800ms
await vi.advanceTimersByTimeAsync(799);
expect(refreshTeamDataSpy).not.toHaveBeenCalled();
await vi.advanceTimersByTimeAsync(1);
expect(refreshTeamDataSpy).toHaveBeenCalledTimes(1);
expect(refreshTeamDataSpy).toHaveBeenCalledWith('my-team');
// List refresh fires at 2000ms
expect(fetchTeamsSpy).not.toHaveBeenCalled();
await vi.advanceTimersByTimeAsync(1200);
expect(fetchTeamsSpy).toHaveBeenCalledTimes(1);
});
it('allows next refresh after throttle window passes', async () => {
const state = useStore.getState();
const refreshTeamDataSpy = vi.spyOn(state, 'refreshTeamData');
hoisted.onTeamChangeCb?.({}, { teamName: 'my-team' });
await vi.advanceTimersByTimeAsync(800);
expect(refreshTeamDataSpy).toHaveBeenCalledTimes(1);
// Second event after throttle window
hoisted.onTeamChangeCb?.({}, { teamName: 'my-team' });
await vi.advanceTimersByTimeAsync(800);
expect(refreshTeamDataSpy).toHaveBeenCalledTimes(2);
});
});