import { buildMemberLaunchPresentation, getLaunchAwarePresenceLabel, getSpawnAwareDotClass, getSpawnAwarePresenceLabel, getSpawnCardClass, getMemberRuntimeAdvisoryLabel, getMemberRuntimeAdvisoryTitle, isOpenCodeRelaunchActionable, } from '@renderer/utils/memberHelpers'; import type { ResolvedTeamMember } from '@shared/types'; 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, }; describe('memberHelpers spawn-aware presence', () => { it('shows process-online teammates as online with a green dot', () => { expect( getSpawnAwarePresenceLabel( member, 'online', 'runtime_pending_bootstrap', 'process', true, false, true, false, undefined ) ).toBe('online'); expect( getSpawnAwareDotClass( member, 'online', 'runtime_pending_bootstrap', true, false, true, false, undefined ) ).toContain('bg-emerald-400'); expect( getSpawnAwareDotClass( member, 'online', 'runtime_pending_bootstrap', true, false, true, false, undefined ) ).toContain('animate-pulse'); }); it('keeps accepted-but-not-yet-online teammates in starting state', () => { expect( getSpawnAwarePresenceLabel( member, 'waiting', 'starting', undefined, false, false, true, false, undefined ) ).toBe('starting'); }); it('labels queued OpenCode lanes separately from active startup', () => { const openCodeMember: ResolvedTeamMember = { ...member, providerId: 'opencode' }; expect( buildMemberLaunchPresentation({ member: openCodeMember, spawnStatus: 'waiting', spawnLaunchState: 'starting', spawnLivenessSource: undefined, spawnRuntimeAlive: false, runtimeAdvisory: undefined, isLaunchSettling: true, isTeamAlive: true, isTeamProvisioning: false, }) ).toMatchObject({ presenceLabel: 'queued', launchVisualState: 'queued', launchStatusLabel: 'queued', dotClass: expect.stringContaining('bg-zinc-400'), }); }); it('does not label non-OpenCode waiting lanes as queued', () => { expect( buildMemberLaunchPresentation({ member, spawnStatus: 'waiting', spawnLaunchState: 'starting', spawnLivenessSource: undefined, spawnRuntimeAlive: false, runtimeAdvisory: undefined, isLaunchSettling: true, isTeamAlive: true, isTeamProvisioning: false, }) ).toMatchObject({ presenceLabel: 'starting', launchVisualState: 'waiting', launchStatusLabel: 'waiting to start', }); }); it('keeps OpenCode runtime evidence states more specific than queued', () => { const openCodeMember: ResolvedTeamMember = { ...member, providerId: 'opencode' }; expect( buildMemberLaunchPresentation({ member: openCodeMember, spawnStatus: 'waiting', spawnLaunchState: 'starting', spawnLivenessSource: undefined, spawnRuntimeAlive: false, runtimeEntry: { memberName: 'alice', alive: false, restartable: true, providerId: 'opencode', livenessKind: 'registered_only', runtimeDiagnostic: 'registered runtime metadata without live process', updatedAt: '2026-04-24T12:00:00.000Z', }, runtimeAdvisory: undefined, isLaunchSettling: true, isTeamAlive: true, isTeamProvisioning: false, }) ).toMatchObject({ presenceLabel: 'registered', launchVisualState: 'registered_only', launchStatusLabel: 'registered', }); }); it('keeps starting visuals after provisioning already transitioned out of active state', () => { expect( getSpawnAwarePresenceLabel( member, 'spawning', 'starting', undefined, false, false, true, false, undefined ) ).toBe('starting'); expect( getSpawnAwareDotClass(member, 'spawning', 'starting', false, false, true, false, undefined) ).toContain('bg-amber-400'); expect(getSpawnCardClass('spawning', 'starting', false, false)).toContain( 'member-waiting-shimmer' ); }); it('shows offline instead of stale starting visuals when the team is offline', () => { expect( getSpawnAwarePresenceLabel( member, 'spawning', 'starting', undefined, false, false, false, false, undefined ) ).toBe('offline'); expect( getSpawnAwareDotClass(member, 'spawning', 'starting', false, false, false, false, undefined) ).toContain('bg-red-400'); expect(getSpawnCardClass('spawning', 'starting', false, false, false, false)).toBe(''); }); it('keeps runtime-pending teammates in starting state while launch is still settling', () => { expect( getSpawnAwarePresenceLabel( member, 'online', 'runtime_pending_bootstrap', 'process', true, true, true, false, undefined ) ).toBe('starting'); expect( getSpawnAwareDotClass( member, 'online', 'runtime_pending_bootstrap', true, true, true, false, undefined ) ).toContain('bg-zinc-400'); expect( getSpawnCardClass('online', 'runtime_pending_bootstrap', true, true, true, false) ).toContain('member-waiting-shimmer'); }); it('shows confirmed teammates as ready instead of idle while launch is still settling', () => { expect( getSpawnAwarePresenceLabel( member, 'online', 'confirmed_alive', 'heartbeat', true, true, true, false, undefined ) ).toBe('ready'); }); it('derives runtime-pending and settling visual states from the same launch inputs', () => { const runtimePending = buildMemberLaunchPresentation({ member, spawnStatus: 'online', spawnLaunchState: 'runtime_pending_bootstrap', spawnLivenessSource: 'process', spawnRuntimeAlive: true, runtimeAdvisory: undefined, isLaunchSettling: false, isTeamAlive: true, isTeamProvisioning: false, }); const settling = buildMemberLaunchPresentation({ member, spawnStatus: 'online', spawnLaunchState: 'confirmed_alive', spawnLivenessSource: 'heartbeat', spawnRuntimeAlive: true, runtimeAdvisory: undefined, isLaunchSettling: true, isTeamAlive: true, isTeamProvisioning: false, }); expect(runtimePending.launchVisualState).toBe('runtime_pending'); expect(runtimePending.launchStatusLabel).toBe('waiting for bootstrap'); expect(settling.launchVisualState).toBe('settling'); expect(settling.launchStatusLabel).toBe('joining team'); }); it('surfaces permission-blocked teammates as awaiting permission instead of generic starting', () => { const permissionPending = buildMemberLaunchPresentation({ member, spawnStatus: 'online', spawnLaunchState: 'runtime_pending_permission', spawnLivenessSource: 'process', spawnRuntimeAlive: true, runtimeAdvisory: undefined, isLaunchSettling: false, isTeamAlive: true, isTeamProvisioning: false, }); expect(permissionPending.presenceLabel).toBe('awaiting permission'); expect(permissionPending.launchVisualState).toBe('permission_pending'); expect(permissionPending.launchStatusLabel).toBe('awaiting permission'); expect(permissionPending.dotClass).toContain('bg-amber-400'); expect(permissionPending.cardClass).toContain('member-waiting-shimmer'); }); it('surfaces bootstrap-stalled OpenCode teammates as actionable pending state', () => { const bootstrapStalled = buildMemberLaunchPresentation({ member: { ...member, providerId: 'opencode' }, spawnStatus: 'waiting', spawnLaunchState: 'runtime_pending_bootstrap', spawnLivenessSource: undefined, spawnRuntimeAlive: true, spawnBootstrapStalled: true, runtimeAdvisory: undefined, isLaunchSettling: false, isTeamAlive: true, isTeamProvisioning: false, }); expect(bootstrapStalled.presenceLabel).toBe('bootstrap stalled'); expect(bootstrapStalled.launchVisualState).toBe('bootstrap_stalled'); expect(bootstrapStalled.launchStatusLabel).toBe('bootstrap stalled'); expect(bootstrapStalled.dotClass).toContain('bg-amber-400'); }); it('surfaces strict runtime liveness diagnostics as launch labels', () => { expect( buildMemberLaunchPresentation({ member, spawnStatus: 'waiting', spawnLaunchState: 'runtime_pending_bootstrap', spawnLivenessSource: undefined, spawnRuntimeAlive: false, runtimeEntry: { memberName: 'alice', alive: false, restartable: true, livenessKind: 'shell_only', pidSource: 'tmux_pane', runtimeDiagnostic: 'tmux pane foreground command is zsh', updatedAt: '2026-04-24T12:00:00.000Z', }, runtimeAdvisory: undefined, isLaunchSettling: false, isTeamAlive: true, isTeamProvisioning: false, }) ).toMatchObject({ presenceLabel: 'shell only', launchVisualState: 'shell_only', launchStatusLabel: 'shell only', }); expect( buildMemberLaunchPresentation({ member, spawnStatus: 'online', spawnLaunchState: 'runtime_pending_bootstrap', spawnLivenessSource: 'process', spawnRuntimeAlive: true, runtimeEntry: { memberName: 'alice', alive: false, restartable: true, livenessKind: 'runtime_process_candidate', runtimeDiagnostic: 'OpenCode runtime process detected, but bootstrap is not confirmed', updatedAt: '2026-04-24T12:00:00.000Z', }, runtimeAdvisory: undefined, isLaunchSettling: false, isTeamAlive: true, isTeamProvisioning: false, }) ).toMatchObject({ presenceLabel: 'bootstrap unconfirmed', launchVisualState: 'runtime_candidate', launchStatusLabel: 'bootstrap unconfirmed', dotClass: expect.stringContaining('bg-amber-400'), }); expect( buildMemberLaunchPresentation({ member, spawnStatus: 'online', spawnLaunchState: 'confirmed_alive', spawnLivenessSource: 'process', spawnRuntimeAlive: true, runtimeEntry: { memberName: 'alice', alive: false, restartable: true, livenessKind: 'registered_only', runtimeDiagnostic: 'registered runtime metadata without live process', updatedAt: '2026-04-24T12:00:00.000Z', }, runtimeAdvisory: undefined, isLaunchSettling: false, isTeamAlive: true, isTeamProvisioning: false, }) ).toMatchObject({ presenceLabel: 'registered', launchVisualState: 'registered_only', launchStatusLabel: 'registered', dotClass: expect.stringContaining('bg-zinc-400'), }); }); it('marks stuck OpenCode launch states as manually relaunchable', () => { const openCodeMember: ResolvedTeamMember = { ...member, providerId: 'opencode' }; expect( isOpenCodeRelaunchActionable({ member: openCodeMember, spawnEntry: { status: 'online', launchState: 'confirmed_alive', runtimeAlive: false, bootstrapConfirmed: false, livenessKind: 'registered_only', updatedAt: '2026-04-24T12:00:00.000Z', }, runtimeEntry: { memberName: 'alice', alive: false, restartable: true, providerId: 'opencode', livenessKind: 'registered_only', updatedAt: '2026-04-24T12:00:00.000Z', }, }) ).toBe(true); expect( isOpenCodeRelaunchActionable({ member: openCodeMember, spawnEntry: { status: 'online', launchState: 'runtime_pending_bootstrap', runtimeAlive: true, bootstrapConfirmed: false, livenessKind: 'runtime_process_candidate', firstSpawnAcceptedAt: '2026-04-24T12:00:00.000Z', updatedAt: '2026-04-24T12:00:00.000Z', }, runtimeEntry: { memberName: 'alice', alive: true, restartable: true, providerId: 'opencode', livenessKind: 'runtime_process_candidate', updatedAt: '2026-04-24T12:00:00.000Z', }, nowMs: Date.parse('2026-04-24T12:06:00.000Z'), }) ).toBe(true); }); it('does not mark fresh OpenCode runtime candidates as relaunchable', () => { expect( isOpenCodeRelaunchActionable({ member: { ...member, providerId: 'opencode' }, spawnEntry: { status: 'online', launchState: 'runtime_pending_bootstrap', runtimeAlive: true, bootstrapConfirmed: false, livenessKind: 'runtime_process_candidate', firstSpawnAcceptedAt: '2026-04-24T12:00:00.000Z', updatedAt: '2026-04-24T12:00:00.000Z', }, runtimeEntry: { memberName: 'alice', alive: true, restartable: true, providerId: 'opencode', livenessKind: 'runtime_process_candidate', updatedAt: '2026-04-24T12:00:00.000Z', }, nowMs: Date.parse('2026-04-24T12:01:00.000Z'), }) ).toBe(false); }); it('does not mark fresh OpenCode not-found checks as relaunchable', () => { expect( isOpenCodeRelaunchActionable({ member: { ...member, providerId: 'opencode' }, spawnEntry: { status: 'waiting', launchState: 'runtime_pending_bootstrap', runtimeAlive: false, bootstrapConfirmed: false, livenessKind: 'not_found', firstSpawnAcceptedAt: '2026-04-24T12:00:00.000Z', updatedAt: '2026-04-24T12:00:00.000Z', }, runtimeEntry: { memberName: 'alice', alive: false, restartable: true, providerId: 'opencode', livenessKind: 'not_found', updatedAt: '2026-04-24T12:00:00.000Z', }, nowMs: Date.parse('2026-04-24T12:01:00.000Z'), }) ).toBe(false); expect( isOpenCodeRelaunchActionable({ member: { ...member, providerId: 'opencode' }, runtimeEntry: { memberName: 'alice', alive: false, restartable: true, providerId: 'opencode', livenessKind: 'not_found', updatedAt: '2026-04-24T12:00:00.000Z', }, nowMs: Date.parse('2026-04-24T12:01:00.000Z'), }) ).toBe(false); }); it('returns shared launch status labels without changing generic presence labels', () => { expect( buildMemberLaunchPresentation({ member, spawnStatus: 'waiting', spawnLaunchState: 'starting', spawnLivenessSource: undefined, spawnRuntimeAlive: false, runtimeAdvisory: undefined, isLaunchSettling: false, isTeamAlive: true, isTeamProvisioning: false, }) ).toMatchObject({ presenceLabel: 'starting', launchVisualState: 'waiting', launchStatusLabel: 'waiting to start', }); expect( buildMemberLaunchPresentation({ member, spawnStatus: 'spawning', spawnLaunchState: 'starting', spawnLivenessSource: undefined, spawnRuntimeAlive: false, runtimeAdvisory: undefined, isLaunchSettling: false, isTeamAlive: true, isTeamProvisioning: false, }) ).toMatchObject({ presenceLabel: 'starting', launchVisualState: 'spawning', launchStatusLabel: 'starting', }); expect( buildMemberLaunchPresentation({ member, spawnStatus: 'error', spawnLaunchState: 'failed_to_start', spawnLivenessSource: undefined, spawnRuntimeAlive: false, runtimeAdvisory: undefined, isLaunchSettling: false, isTeamAlive: true, isTeamProvisioning: false, }) ).toMatchObject({ presenceLabel: 'spawn failed', launchVisualState: 'error', launchStatusLabel: 'failed', }); }); it('renders unified retry advisory labels for provider retries', () => { expect( getMemberRuntimeAdvisoryLabel( { kind: 'sdk_retrying', observedAt: '2026-04-07T09:00:00.000Z', retryUntil: '2026-04-07T09:00:45.000Z', retryDelayMs: 45_000, reasonCode: 'quota_exhausted', message: 'Gemini cli backend error: capacity exceeded.', }, 'gemini', Date.parse('2026-04-07T09:00:00.000Z') ) ).toBe('Gemini quota retry · 45s'); expect( getMemberRuntimeAdvisoryTitle( { kind: 'sdk_retrying', observedAt: '2026-04-07T09:00:00.000Z', retryUntil: '2026-04-07T09:00:45.000Z', retryDelayMs: 45_000, reasonCode: 'rate_limited', message: 'Gemini cli backend error: rate limit 429.', }, 'gemini' ) ).toContain('Gemini rate limited the request'); }); it('keeps network advisories provider-neutral and appends raw details to the title', () => { expect( getMemberRuntimeAdvisoryLabel( { kind: 'sdk_retrying', observedAt: '2026-04-07T09:00:00.000Z', retryUntil: '2026-04-07T09:00:45.000Z', retryDelayMs: 45_000, reasonCode: 'network_error', message: 'Connection timed out while contacting provider.', }, 'gemini', Date.parse('2026-04-07T09:00:00.000Z') ) ).toBe('Network retry · 45s'); expect( getMemberRuntimeAdvisoryTitle( { kind: 'sdk_retrying', observedAt: '2026-04-07T09:00:00.000Z', retryUntil: '2026-04-07T09:00:45.000Z', retryDelayMs: 45_000, reasonCode: 'network_error', message: 'Connection timed out while contacting provider.', }, 'gemini' ) ).toContain('Connection timed out while contacting provider.'); }); it('renders terminal API errors as errors instead of retrying status', () => { expect( getMemberRuntimeAdvisoryLabel( { kind: 'api_error', observedAt: '2026-04-07T09:00:00.000Z', reasonCode: 'auth_error', statusCode: 500, message: 'API Error: 500 {"error":{"message":"auth_unavailable: no auth available"}}', }, 'anthropic', Date.parse('2026-04-07T09:00:00.000Z') ) ).toBe('Anthropic auth error'); expect( getMemberRuntimeAdvisoryTitle( { kind: 'api_error', observedAt: '2026-04-07T09:00:00.000Z', reasonCode: 'auth_error', statusCode: 500, message: 'auth_unavailable: no auth available', }, 'anthropic' ) ).toContain('Anthropic authentication error'); }); it('renders Codex native timeout separately from network errors', () => { const advisory = { kind: 'api_error' as const, observedAt: '2026-04-07T09:00:00.000Z', reasonCode: 'codex_native_timeout' as const, message: 'Codex native exec timed out after 120000ms.', }; expect(getMemberRuntimeAdvisoryLabel(advisory, 'codex')).toBe('Codex native timeout'); expect(getMemberRuntimeAdvisoryTitle(advisory, 'codex')).toContain( 'Codex native mailbox turn timed out' ); expect(getMemberRuntimeAdvisoryTitle(advisory, 'codex')).toContain( 'Codex native exec timed out after 120000ms.' ); }); it('marks launch presentation as an error when the runtime has a terminal API error', () => { const presentation = buildMemberLaunchPresentation({ member: { ...member, providerId: 'anthropic' }, spawnStatus: 'online', spawnLaunchState: 'runtime_pending_bootstrap', spawnLivenessSource: 'process', spawnRuntimeAlive: true, runtimeAdvisory: { kind: 'api_error', observedAt: '2026-04-07T09:00:00.000Z', reasonCode: 'auth_error', statusCode: 500, message: 'auth_unavailable: no auth available', }, isLaunchSettling: false, isTeamAlive: true, isTeamProvisioning: false, }); expect(presentation.presenceLabel).toBe('Anthropic auth error'); expect(presentation.runtimeAdvisoryTone).toBe('error'); expect(presentation.dotClass).toContain('bg-red-400'); }); it('falls back to the existing generic retry wording when no structured reason is present', () => { expect( getMemberRuntimeAdvisoryLabel( { kind: 'sdk_retrying', observedAt: '2026-04-07T09:00:00.000Z', retryUntil: '2026-04-07T09:00:45.000Z', retryDelayMs: 45_000, message: 'Gemini cli backend error: capacity exceeded.', }, 'gemini', Date.parse('2026-04-07T09:00:00.000Z') ) ).toBe('retrying now · 45s'); }); it('surfaces retry advisory text instead of plain online while bootstrap contact is still pending', () => { expect( getLaunchAwarePresenceLabel( member, 'online', 'runtime_pending_bootstrap', 'process', 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'); expect( getLaunchAwarePresenceLabel( member, 'online', 'runtime_pending_bootstrap', 'process', false, { 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 ) ).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'); }); });