fix(team): prefer persisted join state over missing live entries
This commit is contained in:
parent
02419ec8ee
commit
f036cf0386
2 changed files with 75 additions and 10 deletions
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue