fix(team-ui): clarify launch progress and retry states
This commit is contained in:
parent
a03c22aace
commit
0dd4746700
13 changed files with 280 additions and 25 deletions
|
|
@ -2,7 +2,15 @@ import { useEffect, useRef, useState } from 'react';
|
|||
|
||||
import { Button } from '@renderer/components/ui/button';
|
||||
import { cn } from '@renderer/lib/utils';
|
||||
import { AlertTriangle, CheckCircle2, ChevronDown, ChevronRight, Loader2, X } from 'lucide-react';
|
||||
import {
|
||||
AlertTriangle,
|
||||
CheckCircle2,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
Info,
|
||||
Loader2,
|
||||
X,
|
||||
} from 'lucide-react';
|
||||
|
||||
import { MarkdownViewer } from '../chat/viewers/MarkdownViewer';
|
||||
|
||||
|
|
@ -24,7 +32,7 @@ export interface ProvisioningProgressBlockProps {
|
|||
/** Optional status message */
|
||||
message?: string | null;
|
||||
/** Visual severity for the message subtitle */
|
||||
messageSeverity?: 'error' | 'warning';
|
||||
messageSeverity?: 'error' | 'warning' | 'info';
|
||||
/** Visual tone (e.g. highlight errors) */
|
||||
tone?: 'default' | 'error';
|
||||
/** Whether Live output is expanded by default */
|
||||
|
|
@ -40,7 +48,7 @@ export interface ProvisioningProgressBlockProps {
|
|||
/** Success message shown inside the block header (e.g. "Team launched — all N teammates online") */
|
||||
successMessage?: string | null;
|
||||
/** Visual tone for the status banner above the block. */
|
||||
successMessageSeverity?: 'success' | 'warning';
|
||||
successMessageSeverity?: 'success' | 'warning' | 'info';
|
||||
/** Dismiss handler — renders an X button in the block header top-right */
|
||||
onDismiss?: (() => void) | null;
|
||||
/** ISO timestamp when provisioning started */
|
||||
|
|
@ -204,6 +212,8 @@ export const ProvisioningProgressBlock = ({
|
|||
<div className="mb-1.5 flex items-center gap-2">
|
||||
{successMessageSeverity === 'warning' ? (
|
||||
<AlertTriangle size={14} className="shrink-0 text-amber-400" />
|
||||
) : successMessageSeverity === 'info' ? (
|
||||
<Info size={14} className="shrink-0 text-sky-400" />
|
||||
) : (
|
||||
<CheckCircle2 size={14} className="shrink-0 text-[var(--step-done-text)]" />
|
||||
)}
|
||||
|
|
@ -212,7 +222,9 @@ export const ProvisioningProgressBlock = ({
|
|||
'flex-1 text-xs',
|
||||
successMessageSeverity === 'warning'
|
||||
? 'text-amber-400'
|
||||
: 'text-[var(--step-success-text)]'
|
||||
: successMessageSeverity === 'info'
|
||||
? 'text-sky-400'
|
||||
: 'text-[var(--step-success-text)]'
|
||||
)}
|
||||
>
|
||||
{successMessage}
|
||||
|
|
@ -274,7 +286,9 @@ export const ProvisioningProgressBlock = ({
|
|||
? 'text-red-400'
|
||||
: messageSeverity === 'warning'
|
||||
? 'text-amber-400'
|
||||
: 'text-[var(--color-text-muted)]'
|
||||
: messageSeverity === 'info'
|
||||
? 'text-sky-400'
|
||||
: 'text-[var(--color-text-muted)]'
|
||||
)}
|
||||
>
|
||||
{message}
|
||||
|
|
|
|||
|
|
@ -151,10 +151,10 @@ export const TeamProvisioningBanner = memo(function TeamProvisioningBanner({
|
|||
: allTeammatesConfirmedAlive
|
||||
? `Team provisioned - all ${fallbackTeammateCount} teammates joined`
|
||||
: hasMembersStillJoining
|
||||
? `Waiting for ${joiningPhrase.replace('still joining', 'to finish joining')}`
|
||||
? joiningPhrase
|
||||
: 'Team provisioned - teammates are still joining';
|
||||
const readyDetailSeverity =
|
||||
failedSpawnCount > 0 || hasMembersStillJoining ? 'warning' : undefined;
|
||||
failedSpawnCount > 0 ? 'warning' : hasMembersStillJoining ? 'info' : undefined;
|
||||
const readyMessage =
|
||||
failedSpawnCount > 0
|
||||
? `Launch finished with errors - ${failedSpawnCount}/${Math.max(fallbackTeammateCount, failedSpawnCount)} teammates failed to start`
|
||||
|
|
@ -163,8 +163,8 @@ export const TeamProvisioningBanner = memo(function TeamProvisioningBanner({
|
|||
: allTeammatesConfirmedAlive
|
||||
? `Team launched - all ${fallbackTeammateCount} teammates joined`
|
||||
: hasMembersStillJoining
|
||||
? `Team launched - ${joiningPhrase}`
|
||||
: 'Team launched - teammates are still joining';
|
||||
? 'Finishing launch'
|
||||
: 'Finishing launch';
|
||||
const readyStepIndex = hasMembersStillJoining ? 2 : DISPLAY_COMPLETE_STEP_INDEX;
|
||||
|
||||
return (
|
||||
|
|
@ -183,7 +183,7 @@ export const TeamProvisioningBanner = memo(function TeamProvisioningBanner({
|
|||
onCancel={null}
|
||||
successMessage={readyMessage}
|
||||
successMessageSeverity={
|
||||
failedSpawnCount > 0 || hasMembersStillJoining ? 'warning' : 'success'
|
||||
failedSpawnCount > 0 ? 'warning' : hasMembersStillJoining ? 'info' : 'success'
|
||||
}
|
||||
onDismiss={() => setDismissed(true)}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@ function getStatusLabel(status: ProvisioningProviderCheckStatus): string {
|
|||
return 'ERR';
|
||||
case 'pending':
|
||||
default:
|
||||
return 'queued';
|
||||
return 'waiting';
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -138,6 +138,12 @@ export const MemberCard = ({
|
|||
spawnLaunchState !== 'failed_to_start' &&
|
||||
!activityTask;
|
||||
const showStartingBadge = !isRemoved && presenceLabel === 'starting' && !activityTask;
|
||||
const showRuntimeAdvisoryBadge =
|
||||
!isRemoved &&
|
||||
Boolean(runtimeAdvisoryLabel) &&
|
||||
!showStartingBadge &&
|
||||
spawnStatus !== 'error' &&
|
||||
(Boolean(activityTask) || !isAwaitingReply);
|
||||
const cardTint = scaleColorAlpha(getThemedBadge(colors, isLight), 0.5);
|
||||
|
||||
return (
|
||||
|
|
@ -277,6 +283,24 @@ export const MemberCard = ({
|
|||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">{spawnError ?? 'Spawn failed'}</TooltipContent>
|
||||
</Tooltip>
|
||||
) : showRuntimeAdvisoryBadge ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="flex shrink-0 items-center gap-1">
|
||||
<AlertTriangle className="size-3.5 shrink-0 text-amber-400" />
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="shrink-0 bg-amber-500/15 px-1.5 py-0.5 text-[10px] font-normal leading-none text-amber-300"
|
||||
title={runtimeAdvisoryTitle}
|
||||
>
|
||||
{runtimeAdvisoryLabel}
|
||||
</Badge>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">
|
||||
{runtimeAdvisoryTitle ?? runtimeAdvisoryLabel}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
) : !activityTask ? (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
agentAvatarUrl,
|
||||
displayMemberName,
|
||||
getLaunchAwarePresenceLabel,
|
||||
getMemberRuntimeAdvisoryTitle,
|
||||
getSpawnAwareDotClass,
|
||||
} from '@renderer/utils/memberHelpers';
|
||||
import { isLeadMember } from '@shared/utils/leadDetection';
|
||||
|
|
@ -82,6 +83,10 @@ export const MemberDetailHeader = ({
|
|||
isTeamProvisioning,
|
||||
leadActivity
|
||||
);
|
||||
const runtimeAdvisoryTitle = getMemberRuntimeAdvisoryTitle(
|
||||
member.runtimeAdvisory,
|
||||
member.providerId
|
||||
);
|
||||
|
||||
const canEditRole =
|
||||
!isLeadMember(member) && !member.removedAt && !isTeamProvisioning && !!onUpdateRole;
|
||||
|
|
@ -140,6 +145,7 @@ export const MemberDetailHeader = ({
|
|||
<Badge
|
||||
variant="secondary"
|
||||
className="px-1.5 py-0.5 text-[10px] font-normal leading-none text-[var(--color-text-muted)]"
|
||||
title={runtimeAdvisoryTitle}
|
||||
>
|
||||
{presenceLabel}
|
||||
</Badge>
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import {
|
|||
agentAvatarUrl,
|
||||
displayMemberName,
|
||||
getLaunchAwarePresenceLabel,
|
||||
getMemberRuntimeAdvisoryTitle,
|
||||
getSpawnAwareDotClass,
|
||||
} from '@renderer/utils/memberHelpers';
|
||||
import { isLeadMember } from '@shared/utils/leadDetection';
|
||||
|
|
@ -124,6 +125,10 @@ export const MemberHoverCard = ({
|
|||
false,
|
||||
isLeadMember(member) ? leadActivity : undefined
|
||||
);
|
||||
const runtimeAdvisoryTitle = getMemberRuntimeAdvisoryTitle(
|
||||
member.runtimeAdvisory,
|
||||
member.providerId
|
||||
);
|
||||
const currentTask: TeamTaskWithKanban | null =
|
||||
member.currentTaskId && tasks
|
||||
? (tasks.find((t) => t.id === member.currentTaskId) ?? null)
|
||||
|
|
@ -167,6 +172,7 @@ export const MemberHoverCard = ({
|
|||
<Badge
|
||||
variant="secondary"
|
||||
className="shrink-0 px-1.5 py-0 text-[10px] font-normal leading-tight"
|
||||
title={runtimeAdvisoryTitle}
|
||||
style={{
|
||||
backgroundColor: getThemedBadge(colors, isLight),
|
||||
color: getThemedText(colors, isLight),
|
||||
|
|
|
|||
|
|
@ -384,15 +384,7 @@ export function getLaunchAwarePresenceLabel(
|
|||
isTeamProvisioning?: boolean,
|
||||
leadActivity?: LeadActivityState
|
||||
): string {
|
||||
const keepLaunchSettlingVisuals = isTeamProvisioning === true || isLaunchSettling;
|
||||
const advisoryLabel =
|
||||
!keepLaunchSettlingVisuals && spawnLaunchState === 'runtime_pending_bootstrap' && runtimeAlive
|
||||
? getMemberRuntimeAdvisoryLabel(runtimeAdvisory, member.providerId)
|
||||
: null;
|
||||
if (advisoryLabel) {
|
||||
return advisoryLabel;
|
||||
}
|
||||
return getSpawnAwarePresenceLabel(
|
||||
const basePresenceLabel = getSpawnAwarePresenceLabel(
|
||||
member,
|
||||
spawnStatus,
|
||||
spawnLaunchState,
|
||||
|
|
@ -403,6 +395,17 @@ export function getLaunchAwarePresenceLabel(
|
|||
isTeamProvisioning,
|
||||
leadActivity
|
||||
);
|
||||
if (
|
||||
basePresenceLabel === 'starting' ||
|
||||
basePresenceLabel === 'connecting' ||
|
||||
basePresenceLabel === 'spawn failed' ||
|
||||
basePresenceLabel === 'offline' ||
|
||||
basePresenceLabel === 'terminated'
|
||||
) {
|
||||
return basePresenceLabel;
|
||||
}
|
||||
const advisoryLabel = getMemberRuntimeAdvisoryLabel(runtimeAdvisory, member.providerId);
|
||||
return advisoryLabel ?? basePresenceLabel;
|
||||
}
|
||||
|
||||
export const TASK_STATUS_STYLES: Record<TeamTaskStatus, { bg: string; text: string }> = {
|
||||
|
|
|
|||
|
|
@ -40,10 +40,12 @@ vi.mock('@renderer/components/ui/button', () => ({
|
|||
vi.mock('@renderer/components/team/ProvisioningProgressBlock', () => ({
|
||||
ProvisioningProgressBlock: ({
|
||||
currentStepIndex,
|
||||
message,
|
||||
successMessage,
|
||||
successMessageSeverity,
|
||||
}: {
|
||||
currentStepIndex: number;
|
||||
message?: string | null;
|
||||
successMessage?: string | null;
|
||||
successMessageSeverity?: string;
|
||||
}) =>
|
||||
|
|
@ -54,7 +56,7 @@ vi.mock('@renderer/components/team/ProvisioningProgressBlock', () => ({
|
|||
'data-current-step-index': String(currentStepIndex),
|
||||
'data-success-severity': successMessageSeverity ?? '',
|
||||
},
|
||||
successMessage ?? ''
|
||||
[successMessage, message].filter(Boolean).join(' ')
|
||||
),
|
||||
}));
|
||||
|
||||
|
|
@ -112,6 +114,7 @@ describe('TeamProvisioningBanner launch-step alignment', () => {
|
|||
|
||||
const block = host.querySelector('[data-testid="progress-block"]');
|
||||
expect(block?.getAttribute('data-current-step-index')).toBe('2');
|
||||
expect(block?.textContent).toContain('Finishing launch');
|
||||
expect(block?.textContent).toContain('3 teammates still joining');
|
||||
|
||||
await act(async () => {
|
||||
|
|
@ -264,7 +267,7 @@ describe('TeamProvisioningBanner launch-step alignment', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('keeps warning severity while runtimes are online but teammate contact is still pending', async () => {
|
||||
it('uses info severity while runtimes are online but teammate contact is still pending', async () => {
|
||||
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
||||
storeState.memberSpawnSnapshotsByTeam['northstar-core'] = {
|
||||
runId: 'run-1',
|
||||
|
|
@ -290,7 +293,8 @@ describe('TeamProvisioningBanner launch-step alignment', () => {
|
|||
|
||||
const block = host.querySelector('[data-testid="progress-block"]');
|
||||
expect(block?.getAttribute('data-current-step-index')).toBe('2');
|
||||
expect(block?.getAttribute('data-success-severity')).toBe('warning');
|
||||
expect(block?.getAttribute('data-success-severity')).toBe('info');
|
||||
expect(block?.textContent).toContain('Finishing launch');
|
||||
expect(block?.textContent).toContain('3 teammates still joining');
|
||||
|
||||
await act(async () => {
|
||||
|
|
@ -377,8 +381,9 @@ describe('TeamProvisioningBanner launch-step alignment', () => {
|
|||
});
|
||||
|
||||
const block = host.querySelector('[data-testid="progress-block"]');
|
||||
expect(block?.textContent).toContain('Finishing launch');
|
||||
expect(block?.textContent).toContain('2 teammates still joining');
|
||||
expect(block?.getAttribute('data-success-severity')).toBe('warning');
|
||||
expect(block?.getAttribute('data-success-severity')).toBe('info');
|
||||
|
||||
await act(async () => {
|
||||
root.unmount();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
import React, { act } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import {
|
||||
ProvisioningProviderStatusList,
|
||||
createInitialProviderChecks,
|
||||
} from '@renderer/components/team/dialogs/ProvisioningProviderStatusList';
|
||||
|
||||
describe('ProvisioningProviderStatusList', () => {
|
||||
afterEach(() => {
|
||||
document.body.innerHTML = '';
|
||||
});
|
||||
|
||||
it('shows waiting for pending provider checks', 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(ProvisioningProviderStatusList, {
|
||||
checks: createInitialProviderChecks(['anthropic', 'codex']),
|
||||
})
|
||||
);
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(host.textContent).toContain('Anthropic: waiting');
|
||||
expect(host.textContent).toContain('Codex: waiting');
|
||||
|
||||
await act(async () => {
|
||||
root.unmount();
|
||||
await Promise.resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -2,7 +2,7 @@ import React, { act } from 'react';
|
|||
import { createRoot } from 'react-dom/client';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { ResolvedTeamMember } from '@shared/types';
|
||||
import type { ResolvedTeamMember, TeamTaskWithKanban } from '@shared/types';
|
||||
|
||||
vi.mock('@renderer/components/ui/badge', () => ({
|
||||
Badge: ({
|
||||
|
|
@ -49,6 +49,13 @@ const member: ResolvedTeamMember = {
|
|||
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 = '';
|
||||
|
|
@ -125,6 +132,47 @@ describe('MemberCard starting-state visuals', () => {
|
|||
});
|
||||
});
|
||||
|
||||
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 skeleton visible while a runtime is alive but still joining', async () => {
|
||||
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
||||
const host = document.createElement('div');
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ const member: ResolvedTeamMember = {
|
|||
lastActiveAt: null,
|
||||
messageCount: 0,
|
||||
color: 'blue',
|
||||
providerId: 'gemini',
|
||||
agentType: 'reviewer',
|
||||
role: 'Reviewer',
|
||||
removedAt: undefined,
|
||||
|
|
@ -98,4 +99,43 @@ describe('MemberDetailHeader spawn-aware presence', () => {
|
|||
await Promise.resolve();
|
||||
});
|
||||
});
|
||||
|
||||
it('shows runtime retry text after the teammate has already joined', 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(MemberDetailHeader, {
|
||||
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',
|
||||
message: 'Gemini cli backend error: capacity exceeded.',
|
||||
},
|
||||
},
|
||||
isTeamAlive: true,
|
||||
isTeamProvisioning: false,
|
||||
spawnStatus: 'online',
|
||||
spawnLaunchState: 'confirmed_alive',
|
||||
spawnRuntimeAlive: true,
|
||||
})
|
||||
);
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(host.textContent).toContain('Gemini quota retry');
|
||||
expect(host.textContent).not.toContain('idle');
|
||||
|
||||
await act(async () => {
|
||||
root.unmount();
|
||||
await Promise.resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ const member: ResolvedTeamMember = {
|
|||
lastActiveAt: null,
|
||||
messageCount: 0,
|
||||
color: 'blue',
|
||||
providerId: 'gemini',
|
||||
agentType: 'reviewer',
|
||||
role: 'Reviewer',
|
||||
removedAt: undefined,
|
||||
|
|
@ -185,4 +186,50 @@ describe('MemberHoverCard spawn-aware presence', () => {
|
|||
await Promise.resolve();
|
||||
});
|
||||
});
|
||||
|
||||
it('surfaces runtime retry state in the hover card after the teammate has already joined', async () => {
|
||||
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
||||
storeState.selectedTeamData.members = [
|
||||
{
|
||||
...member,
|
||||
runtimeAdvisory: {
|
||||
kind: 'sdk_retrying',
|
||||
observedAt: '2026-04-09T10:00:00.000Z',
|
||||
retryUntil: '2099-04-09T10:00:45.000Z',
|
||||
retryDelayMs: 45_000,
|
||||
reasonCode: 'quota_exhausted',
|
||||
message: 'Gemini cli backend error: capacity exceeded.',
|
||||
},
|
||||
},
|
||||
];
|
||||
storeState.memberSpawnStatusesByTeam['northstar-core'].alice = {
|
||||
status: 'online',
|
||||
launchState: 'confirmed_alive',
|
||||
updatedAt: '2026-04-09T10:00:00.000Z',
|
||||
runtimeAlive: true,
|
||||
livenessSource: 'heartbeat',
|
||||
};
|
||||
|
||||
const host = document.createElement('div');
|
||||
document.body.appendChild(host);
|
||||
const root = createRoot(host);
|
||||
|
||||
await act(async () => {
|
||||
root.render(
|
||||
React.createElement(MemberHoverCard, {
|
||||
name: 'alice',
|
||||
children: React.createElement('button', { type: 'button' }, 'alice'),
|
||||
})
|
||||
);
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(host.textContent).toContain('Gemini quota retry');
|
||||
expect(host.textContent).not.toContain('idle');
|
||||
|
||||
await act(async () => {
|
||||
root.unmount();
|
||||
await Promise.resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -300,4 +300,28 @@ describe('memberHelpers spawn-aware presence', () => {
|
|||
)
|
||||
).toBe('starting');
|
||||
});
|
||||
|
||||
it('keeps retry advisory visible after contact when the teammate is otherwise just idle or ready', () => {
|
||||
expect(
|
||||
getLaunchAwarePresenceLabel(
|
||||
member,
|
||||
'online',
|
||||
'confirmed_alive',
|
||||
'heartbeat',
|
||||
true,
|
||||
{
|
||||
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.',
|
||||
},
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
undefined
|
||||
)
|
||||
).toContain('Gemini quota retry');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue