From 02419ec8eebf41b9b7de8737cd9b513b67f2031e Mon Sep 17 00:00:00 2001 From: 777genius Date: Thu, 23 Apr 2026 02:40:29 +0300 Subject: [PATCH] fix(team): use persisted launch member union --- .../services/team/TeamProvisioningService.ts | 19 +++++-- .../team/TeamProvisioningService.test.ts | 53 +++++++++++++++++++ 2 files changed, 67 insertions(+), 5 deletions(-) diff --git a/src/main/services/team/TeamProvisioningService.ts b/src/main/services/team/TeamProvisioningService.ts index c6f414ba..10f7a026 100644 --- a/src/main/services/team/TeamProvisioningService.ts +++ b/src/main/services/team/TeamProvisioningService.ts @@ -11858,7 +11858,9 @@ export class TeamProvisioningService { }, snapshot?: PersistedTeamLaunchSnapshot | null ): string { - const expectedTeammateCount = snapshot?.expectedMembers.length ?? run.expectedMembers.length; + const expectedTeammateCount = snapshot + ? this.getPersistedLaunchMemberNames(snapshot).length + : run.expectedMembers.length; const permissionPendingCount = snapshot ? this.countSnapshotPermissionPendingMembers(snapshot) : this.countRunPermissionPendingMembers(run); @@ -11908,7 +11910,8 @@ export class TeamProvisioningService { return this.buildPendingBootstrapStatusMessage(prefix, run, launchSummary, snapshot); } - const allPendingMembers = snapshot.expectedMembers.filter((memberName) => { + const persistedMemberNames = this.getPersistedLaunchMemberNames(snapshot); + const allPendingMembers = persistedMemberNames.filter((memberName) => { const member = snapshot.members[memberName]; if (!member) { return false; @@ -11935,7 +11938,7 @@ export class TeamProvisioningService { const primaryExpectedMembers = new Set( snapshot.bootstrapExpectedMembers ?? run.expectedMembers ); - const secondaryPendingMembers = snapshot.expectedMembers.filter((memberName) => { + const secondaryPendingMembers = persistedMemberNames.filter((memberName) => { if (primaryExpectedMembers.has(memberName)) { return false; } @@ -11976,7 +11979,7 @@ export class TeamProvisioningService { private countSnapshotPermissionPendingMembers(snapshot: PersistedTeamLaunchSnapshot): number { let count = 0; - for (const memberName of snapshot.expectedMembers) { + for (const memberName of this.getPersistedLaunchMemberNames(snapshot)) { const member = snapshot.members[memberName]; if (!member) { continue; @@ -11998,10 +12001,16 @@ export class TeamProvisioningService { }, snapshot?: PersistedTeamLaunchSnapshot | null ): boolean { - const expectedCount = snapshot?.expectedMembers.length ?? run.expectedMembers?.length ?? 0; + const expectedCount = snapshot + ? this.getPersistedLaunchMemberNames(snapshot).length + : (run.expectedMembers?.length ?? 0); return launchSummary.pendingCount > 0 && expectedCount > 0; } + private getPersistedLaunchMemberNames(snapshot: PersistedTeamLaunchSnapshot): string[] { + return Array.from(new Set([...snapshot.expectedMembers, ...Object.keys(snapshot.members)])); + } + private buildLiveLaunchSnapshotForRun( run: ProvisioningRun, launchPhase: PersistedTeamLaunchPhase = run.provisioningComplete ? 'finished' : 'active' diff --git a/test/main/services/team/TeamProvisioningService.test.ts b/test/main/services/team/TeamProvisioningService.test.ts index be9b54c2..9ea854a6 100644 --- a/test/main/services/team/TeamProvisioningService.test.ts +++ b/test/main/services/team/TeamProvisioningService.test.ts @@ -2227,6 +2227,59 @@ describe('TeamProvisioningService', () => { expect(message).not.toContain('/0'); }); + it('uses the union of persisted expected members and persisted member entries for pending launch copy', () => { + const svc = new TeamProvisioningService(); + const run = createMemberSpawnRun({ + teamName: 'pure-team', + expectedMembers: [], + memberSpawnStatuses: new Map(), + }); + + const message = (svc as any).buildAggregatePendingLaunchMessage( + 'Finishing launch', + run, + { + confirmedCount: 0, + pendingCount: 1, + failedCount: 0, + runtimeAlivePendingCount: 1, + }, + { + version: 2, + teamName: 'pure-team', + updatedAt: '2026-04-22T12:00:00.000Z', + launchPhase: 'active', + expectedMembers: [], + bootstrapExpectedMembers: [], + members: { + alice: { + name: 'alice', + providerId: 'opencode', + laneId: 'primary', + laneKind: 'primary', + laneOwnerProviderId: 'opencode', + launchState: 'runtime_pending_permission', + agentToolAccepted: true, + runtimeAlive: true, + bootstrapConfirmed: false, + hardFailure: false, + pendingPermissionRequestIds: ['perm-1'], + lastEvaluatedAt: '2026-04-22T12:00:00.000Z', + }, + }, + summary: { + confirmedCount: 0, + pendingCount: 1, + failedCount: 0, + runtimeAlivePendingCount: 1, + }, + teamLaunchState: 'partial_pending', + } + ); + + expect(message).toBe('Finishing launch — 1 teammate awaiting permission approval'); + }); + it('launches the OpenCode secondary lane with side-lane provider and member runtime identity', async () => { const svc = new TeamProvisioningService(); const adapterLaunch = vi.fn(async (input: Record) => ({