agent-ecosystem/test/renderer/components/team/members/MemberCard.test.ts
2026-04-16 22:52:56 +03:00

243 lines
7.3 KiB
TypeScript

import React, { act } from 'react';
import { createRoot } from 'react-dom/client';
import { afterEach, describe, expect, it, vi } from 'vitest';
import type { ResolvedTeamMember, TeamTaskWithKanban } from '@shared/types';
vi.mock('@renderer/components/ui/badge', () => ({
Badge: ({
children,
className,
title,
}: {
children: React.ReactNode;
className?: string;
title?: string;
}) => React.createElement('span', { className, title }, children),
}));
vi.mock('@renderer/components/ui/tooltip', () => ({
Tooltip: ({ children }: { children: React.ReactNode }) =>
React.createElement(React.Fragment, null, children),
TooltipTrigger: ({ children }: { children: React.ReactNode }) =>
React.createElement(React.Fragment, null, children),
TooltipContent: ({ children }: { children: React.ReactNode }) =>
React.createElement('div', null, children),
}));
vi.mock('@renderer/hooks/useTheme', () => ({
useTheme: () => ({ isLight: false }),
}));
vi.mock('@renderer/components/team/members/CurrentTaskIndicator', () => ({
CurrentTaskIndicator: () => null,
}));
import { MemberCard } from '@renderer/components/team/members/MemberCard';
const member: ResolvedTeamMember = {
name: 'alice',
status: 'unknown',
taskCount: 0,
currentTaskId: null,
lastActiveAt: null,
messageCount: 0,
color: 'blue',
agentType: 'reviewer',
role: 'Reviewer',
providerId: 'gemini',
removedAt: undefined,
};
const currentTask: TeamTaskWithKanban = {
id: 'task-1',
displayId: 'abc12345',
subject: 'Build calculator UI',
status: 'in_progress',
} as unknown as TeamTaskWithKanban;
describe('MemberCard starting-state visuals', () => {
afterEach(() => {
document.body.innerHTML = '';
});
it('shows runtime summary while keeping the starting treatment after provisioning stops', async () => {
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
const host = document.createElement('div');
document.body.appendChild(host);
const root = createRoot(host);
await act(async () => {
root.render(
React.createElement(MemberCard, {
member,
memberColor: 'blue',
runtimeSummary: 'Anthropic · haiku · Medium',
isTeamAlive: true,
isTeamProvisioning: false,
spawnStatus: 'spawning',
spawnLaunchState: 'starting',
spawnRuntimeAlive: false,
})
);
await Promise.resolve();
});
expect(host.textContent).toContain('starting');
expect(host.textContent).toContain('Anthropic · haiku · Medium');
expect(host.querySelector('.member-waiting-shimmer')).not.toBeNull();
expect(host.querySelectorAll('.skeleton-shimmer').length).toBe(0);
await act(async () => {
root.unmount();
await Promise.resolve();
});
});
it('shows provider retry advisory instead of plain online while bootstrap contact is still pending', async () => {
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
const host = document.createElement('div');
document.body.appendChild(host);
const root = createRoot(host);
await act(async () => {
root.render(
React.createElement(MemberCard, {
member: {
...member,
runtimeAdvisory: {
kind: 'sdk_retrying',
observedAt: '2026-04-07T09:00:00.000Z',
retryUntil: '2099-04-07T09:00:45.000Z',
retryDelayMs: 45_000,
reasonCode: 'quota_exhausted',
},
},
memberColor: 'blue',
isTeamAlive: true,
isTeamProvisioning: false,
spawnStatus: 'online',
spawnLaunchState: 'runtime_pending_bootstrap',
spawnRuntimeAlive: true,
})
);
await Promise.resolve();
});
expect(host.textContent).toContain('Gemini quota retry');
expect(host.textContent).not.toContain('online');
await act(async () => {
root.unmount();
await Promise.resolve();
});
});
it('keeps runtime retry visible even while the teammate already has an active task', async () => {
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
const host = document.createElement('div');
document.body.appendChild(host);
const root = createRoot(host);
await act(async () => {
root.render(
React.createElement(MemberCard, {
member: {
...member,
currentTaskId: currentTask.id,
runtimeAdvisory: {
kind: 'sdk_retrying',
observedAt: '2026-04-07T09:00:00.000Z',
retryUntil: '2099-04-07T09:00:45.000Z',
retryDelayMs: 45_000,
reasonCode: 'quota_exhausted',
message: 'Gemini cli backend error: capacity exceeded.',
},
},
memberColor: 'blue',
currentTask,
isTeamAlive: true,
isTeamProvisioning: false,
spawnStatus: 'online',
spawnLaunchState: 'confirmed_alive',
spawnRuntimeAlive: true,
})
);
await Promise.resolve();
});
expect(host.textContent).toContain('Gemini quota retry');
await act(async () => {
root.unmount();
await Promise.resolve();
});
});
it('keeps the starting treatment and runtime summary visible while a runtime is still joining', async () => {
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
const host = document.createElement('div');
document.body.appendChild(host);
const root = createRoot(host);
await act(async () => {
root.render(
React.createElement(MemberCard, {
member,
memberColor: 'blue',
runtimeSummary: 'Anthropic · sonnet · Medium',
isTeamAlive: true,
isTeamProvisioning: false,
isLaunchSettling: true,
spawnStatus: 'online',
spawnLaunchState: 'runtime_pending_bootstrap',
spawnRuntimeAlive: true,
})
);
await Promise.resolve();
});
expect(host.textContent).toContain('starting');
expect(host.textContent).toContain('Anthropic · sonnet · Medium');
expect(host.textContent).not.toContain('online');
expect(host.querySelector('.member-waiting-shimmer')).not.toBeNull();
expect(host.querySelectorAll('.skeleton-shimmer').length).toBe(0);
await act(async () => {
root.unmount();
await Promise.resolve();
});
});
it('shows ready instead of idle for confirmed teammates while launch is still settling', async () => {
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
const host = document.createElement('div');
document.body.appendChild(host);
const root = createRoot(host);
await act(async () => {
root.render(
React.createElement(MemberCard, {
member,
memberColor: 'blue',
runtimeSummary: 'Anthropic · sonnet · Medium',
isTeamAlive: true,
isTeamProvisioning: false,
isLaunchSettling: true,
spawnStatus: 'online',
spawnLaunchState: 'confirmed_alive',
spawnRuntimeAlive: true,
})
);
await Promise.resolve();
});
expect(host.textContent).toContain('ready');
expect(host.textContent).not.toContain('idle');
await act(async () => {
root.unmount();
await Promise.resolve();
});
});
});