Files
iofficeai--aionui/tests/unit/renderer/team/useTeamSessionCronCleanup.dom.test.ts
2026-07-13 13:20:22 +08:00

162 lines
5.3 KiB
TypeScript

import { act, renderHook } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { TChatConversation } from '@/common/config/storage';
import type { TTeam } from '@/common/types/team/teamTypes';
import { useTeamSession } from '@/renderer/pages/team/hooks/useTeamSession';
const {
getConversationOrNullMock,
revalidateAcpConfigOptionsMock,
teamEventHandlers,
makeTeamEventChannel,
eventChannel,
} = vi.hoisted(() => {
const handlers: Record<string, unknown> = {};
const makeChannel = (name: string) => ({
on: vi.fn((handler: unknown) => {
handlers[name] = handler;
return vi.fn();
}),
});
return {
getConversationOrNullMock: vi.fn(),
revalidateAcpConfigOptionsMock: vi.fn(),
teamEventHandlers: handlers,
makeTeamEventChannel: makeChannel,
eventChannel: makeChannel('shared'),
};
});
vi.mock('@/renderer/hooks/agent/useAcpConfigOptions', () => ({
revalidateAcpConfigOptions: (...args: unknown[]) => revalidateAcpConfigOptionsMock(...args),
}));
vi.mock('@/common', () => ({
ipcBridge: {
cron: {
removeJob: { invoke: vi.fn() },
},
team: {
get: { invoke: vi.fn() },
addAgent: { invoke: vi.fn() },
renameAgent: { invoke: vi.fn() },
removeAgent: { invoke: vi.fn() },
agentStatusChanged: eventChannel,
agentSpawned: eventChannel,
agentRemoved: eventChannel,
agentRenamed: eventChannel,
agentRuntimeStatusChanged: makeTeamEventChannel('agentRuntimeStatusChanged'),
sessionStatusChanged: eventChannel,
taskChanged: eventChannel,
sessionChanged: eventChannel,
},
},
}));
vi.mock('@/renderer/pages/conversation/utils/conversationCache', () => ({
getConversationOrNull: (...args: unknown[]) => getConversationOrNullMock(...args),
}));
import { ipcBridge } from '@/common';
describe('useTeamSession cron cleanup', () => {
beforeEach(() => {
vi.clearAllMocks();
for (const key of Object.keys(teamEventHandlers)) {
delete teamEventHandlers[key];
}
vi.mocked(ipcBridge.team.get.invoke).mockResolvedValue(team());
vi.mocked(ipcBridge.team.removeAgent.invoke).mockResolvedValue(undefined);
vi.mocked(ipcBridge.cron.removeJob.invoke).mockResolvedValue(undefined);
});
it('removes a member cron job before removing the member through the session hook', async () => {
getConversationOrNullMock.mockResolvedValue(conversation({ extra: { cron_job_id: 'cron-member' } }));
const { result } = renderHook(() => useTeamSession(team()));
await act(async () => {
await result.current.removeAssistant('member-slot');
});
expect(getConversationOrNullMock).toHaveBeenCalledWith('member-conv');
expect(ipcBridge.cron.removeJob.invoke).toHaveBeenCalledWith({ job_id: 'cron-member' });
expect(ipcBridge.team.removeAgent.invoke).toHaveBeenCalledWith({ team_id: 'team-1', slot_id: 'member-slot' });
expect(vi.mocked(ipcBridge.cron.removeJob.invoke).mock.invocationCallOrder[0]).toBeLessThan(
vi.mocked(ipcBridge.team.removeAgent.invoke).mock.invocationCallOrder[0]
);
});
it('refreshes runtime config options when a current team agent runtime becomes ready', () => {
renderHook(() => useTeamSession(team()));
const handler = teamEventHandlers.agentRuntimeStatusChanged as
| ((event: { team_id: string; conversation_id: string; status: string }) => void)
| undefined;
expect(handler).toBeTypeOf('function');
act(() => {
handler?.({ team_id: 'other-team', conversation_id: 'other-conv', status: 'ready' });
handler?.({ team_id: 'team-1', conversation_id: 'member-conv', status: 'pending' });
handler?.({ team_id: 'team-1', conversation_id: 'member-conv', status: 'ready' });
});
expect(revalidateAcpConfigOptionsMock).toHaveBeenCalledTimes(1);
expect(revalidateAcpConfigOptionsMock).toHaveBeenCalledWith('member-conv');
});
it('clears membership busy state when warmup reaches a terminal phase', () => {
const { result, rerender } = renderHook(({ warmupPhase }) => useTeamSession(team(), warmupPhase), {
initialProps: { warmupPhase: 'warming' as const },
});
const handler = teamEventHandlers.agentRuntimeStatusChanged as
| ((event: { team_id: string; slot_id: string; conversation_id: string; status: string }) => void)
| undefined;
act(() => {
handler?.({ team_id: 'team-1', slot_id: 'member-slot', conversation_id: 'member-conv', status: 'pending' });
});
expect(result.current.membershipMutationBusy).toBe(true);
rerender({ warmupPhase: 'ready' });
expect(result.current.membershipMutationBusy).toBe(false);
});
});
function conversation(overrides?: Partial<TChatConversation>): TChatConversation {
return {
id: 'member-conv',
type: 'acp',
name: 'Member',
created_at: 1,
updated_at: 1,
extra: {},
...overrides,
} as TChatConversation;
}
function team(): TTeam {
return {
id: 'team-1',
user_id: 'user-1',
name: 'Cron Team',
workspace: '/tmp/team',
workspace_mode: 'shared',
leader_assistant_id: 'leader-assistant',
created_at: 1,
updated_at: 1,
assistants: [
{
slot_id: 'member-slot',
conversation_id: 'member-conv',
role: 'teammate',
assistant_backend: 'codex',
assistant_name: 'Member',
status: 'idle',
},
],
};
}