diff --git a/src/main/services/team/TeamLaunchSummaryProjection.ts b/src/main/services/team/TeamLaunchSummaryProjection.ts index 200e75be..2ee602dc 100644 --- a/src/main/services/team/TeamLaunchSummaryProjection.ts +++ b/src/main/services/team/TeamLaunchSummaryProjection.ts @@ -30,6 +30,10 @@ export interface PersistedTeamLaunchSummaryProjection extends LaunchStateSummary mixedAware?: true; } +function getPersistedLaunchMemberNames(snapshot: PersistedTeamLaunchSnapshot): string[] { + return Array.from(new Set([...snapshot.expectedMembers, ...Object.keys(snapshot.members)])); +} + function normalizeIsoDate(value: unknown): string | null { if (typeof value !== 'string') { return null; @@ -48,7 +52,8 @@ function toMillis(value: string | undefined | null): number { export function createLaunchStateSummary( snapshot: PersistedTeamLaunchSnapshot ): LaunchStateSummary { - const missingMembers = snapshot.expectedMembers.filter((name) => { + const persistedMemberNames = getPersistedLaunchMemberNames(snapshot); + const missingMembers = persistedMemberNames.filter((name) => { const member = snapshot.members[name]; return member?.launchState === 'failed_to_start'; }); @@ -57,8 +62,8 @@ export function createLaunchStateSummary( ...(snapshot.teamLaunchState === 'partial_failure' ? { partialLaunchFailure: true as const } : {}), - ...(snapshot.expectedMembers.length > 0 - ? { expectedMemberCount: snapshot.expectedMembers.length } + ...(persistedMemberNames.length > 0 + ? { expectedMemberCount: persistedMemberNames.length } : {}), ...(snapshot.summary.confirmedCount > 0 ? { confirmedMemberCount: snapshot.summary.confirmedCount } diff --git a/test/main/services/team/TeamLaunchSummaryProjection.test.ts b/test/main/services/team/TeamLaunchSummaryProjection.test.ts index 6a69c1d4..abbdc3fe 100644 --- a/test/main/services/team/TeamLaunchSummaryProjection.test.ts +++ b/test/main/services/team/TeamLaunchSummaryProjection.test.ts @@ -174,4 +174,52 @@ describe('TeamLaunchSummaryProjection', () => { }) ).toBe(false); }); + + it('uses the union of expectedMembers and persisted members for summary projection', () => { + const summary = createPersistedLaunchSummaryProjection({ + version: 2, + teamName: 'mixed-team', + updatedAt: '2026-04-22T12:00:00.000Z', + launchPhase: 'finished', + expectedMembers: ['alice'], + members: { + alice: { + name: 'alice', + providerId: 'codex', + launchState: 'confirmed_alive', + agentToolAccepted: true, + runtimeAlive: true, + bootstrapConfirmed: true, + hardFailure: false, + lastEvaluatedAt: '2026-04-22T12:00:00.000Z', + }, + bob: { + name: 'bob', + providerId: 'opencode', + launchState: 'failed_to_start', + agentToolAccepted: true, + runtimeAlive: false, + bootstrapConfirmed: false, + hardFailure: true, + hardFailureReason: 'Side lane failed', + lastEvaluatedAt: '2026-04-22T12:00:00.000Z', + }, + }, + summary: { + confirmedCount: 1, + pendingCount: 0, + failedCount: 1, + runtimeAlivePendingCount: 0, + }, + teamLaunchState: 'partial_failure', + } as never); + + expect(summary).toMatchObject({ + expectedMemberCount: 2, + confirmedMemberCount: 1, + missingMembers: ['bob'], + failedCount: 1, + teamLaunchState: 'partial_failure', + }); + }); });