fix(team): prefer persisted join state over missing live entries

This commit is contained in:
777genius 2026-04-23 03:01:34 +03:00
parent 02419ec8ee
commit f036cf0386
2 changed files with 75 additions and 10 deletions

View file

@ -54,12 +54,15 @@ function getSpawnEntry(
function summarizeLiveLaunchJoinMilestones(params: {
teammateNames: readonly string[];
memberSpawnStatuses?: MemberSpawnStatusCollection;
}): Omit<LaunchJoinMilestones, 'expectedTeammateCount'> {
}): Omit<LaunchJoinMilestones, 'expectedTeammateCount'> & {
observedTeammateCount: number;
} {
const { teammateNames, memberSpawnStatuses } = params;
let heartbeatConfirmedCount = 0;
let processOnlyAliveCount = 0;
let pendingSpawnCount = 0;
let failedSpawnCount = 0;
let observedTeammateCount = 0;
for (const memberName of teammateNames) {
const entry = getSpawnEntry(memberSpawnStatuses, memberName);
@ -67,6 +70,7 @@ function summarizeLiveLaunchJoinMilestones(params: {
pendingSpawnCount += 1;
continue;
}
observedTeammateCount += 1;
if (entry.launchState === 'failed_to_start') {
failedSpawnCount += 1;
continue;
@ -96,6 +100,7 @@ function summarizeLiveLaunchJoinMilestones(params: {
processOnlyAliveCount,
pendingSpawnCount,
failedSpawnCount,
observedTeammateCount,
};
}
@ -106,20 +111,28 @@ export function getLaunchJoinMilestonesFromMembers({
}: {
members: readonly LaunchJoinMemberLike[];
memberSpawnStatuses?: MemberSpawnStatusCollection;
memberSpawnSnapshot?: Pick<MemberSpawnStatusesSnapshot, 'expectedMembers' | 'summary'>;
memberSpawnSnapshot?: Pick<MemberSpawnStatusesSnapshot, 'expectedMembers' | 'summary'> & {
statuses?: MemberSpawnStatusesSnapshot['statuses'];
};
}): LaunchJoinMilestones {
const removedTeammateNameSet = new Set(
members
.filter((member) => member.removedAt && !isLeadMember(member))
.map((member) => member.name)
);
const teammates = members.filter((member) => !member.removedAt && !isLeadMember(member));
const activeTeammateNames = teammates.map((member) => member.name);
const activeTeammateNameSet = new Set(activeTeammateNames);
const snapshotExpectedNames = memberSpawnSnapshot?.expectedMembers ?? [];
const snapshotStatusNames = Object.keys(memberSpawnSnapshot?.statuses ?? {});
const teammateNames =
memberSpawnSnapshot?.expectedMembers?.length && memberSpawnSnapshot.expectedMembers.length > 0
snapshotExpectedNames.length > 0 || snapshotStatusNames.length > 0
? Array.from(
new Set([
...memberSpawnSnapshot.expectedMembers.filter((memberName) =>
activeTeammateNameSet.has(memberName)
),
...activeTeammateNames,
])
new Set([...snapshotExpectedNames, ...snapshotStatusNames, ...activeTeammateNames])
).filter(
(memberName) =>
memberName.trim().length > 0 &&
!isLeadMember({ name: memberName }) &&
!removedTeammateNameSet.has(memberName)
)
: activeTeammateNames;
const expectedTeammateCount = teammateNames.length;
@ -155,6 +168,7 @@ export function getLaunchJoinMilestonesFromMembers({
liveSummary.heartbeatConfirmedCount > snapshotMilestones.heartbeatConfirmedCount ||
liveSummary.processOnlyAliveCount > snapshotMilestones.processOnlyAliveCount ||
(snapshotMilestones.failedSpawnCount === 0 &&
liveSummary.observedTeammateCount > 0 &&
liveSummary.pendingSpawnCount > snapshotMilestones.pendingSpawnCount) ||
liveAccountedFor > snapshotAccountedFor;

View file

@ -460,4 +460,55 @@ describe('TeamProvisioningBanner launch-step alignment', () => {
await Promise.resolve();
});
});
it('trusts persisted snapshot member statuses even when expectedMembers and team cache are stale', async () => {
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
storeState.selectedTeamData.members = [{ name: 'team-lead', agentType: 'team-lead' }];
storeState.teamDataCacheByName['northstar-core'] = {
members: [...storeState.selectedTeamData.members],
};
storeState.memberSpawnStatusesByTeam['northstar-core'] = {};
storeState.memberSpawnSnapshotsByTeam['northstar-core'] = {
runId: 'run-1',
expectedMembers: [],
statuses: {
alice: {
status: 'online',
launchState: 'runtime_pending_bootstrap',
updatedAt: '2026-04-09T10:00:00.000Z',
runtimeAlive: true,
livenessSource: 'process',
bootstrapConfirmed: false,
hardFailure: false,
agentToolAccepted: true,
},
},
summary: {
confirmedCount: 0,
pendingCount: 1,
failedCount: 0,
runtimeAlivePendingCount: 1,
},
source: 'persisted',
};
const host = document.createElement('div');
document.body.appendChild(host);
const root = createRoot(host);
await act(async () => {
root.render(React.createElement(TeamProvisioningBanner, { teamName: 'northstar-core' }));
await Promise.resolve();
});
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('1 teammate still joining');
await act(async () => {
root.unmount();
await Promise.resolve();
});
});
});