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