fix(team): keep permission launch updates in store

This commit is contained in:
777genius 2026-04-23 04:26:33 +03:00
parent 6ea8b469f0
commit e309eb8a0d
2 changed files with 73 additions and 1 deletions

View file

@ -727,6 +727,8 @@ function areMemberSpawnStatusEntriesEqual(
): boolean {
if (left === right) return true;
if (!left || !right) return left === right;
const leftPendingPermissionIds = [...(left.pendingPermissionRequestIds ?? [])].sort();
const rightPendingPermissionIds = [...(right.pendingPermissionRequestIds ?? [])].sort();
// Renderer equality intentionally ignores raw timing fields that do not change
// visible member status. This suppresses heartbeat-only churn in TeamDetailView.
return (
@ -738,7 +740,9 @@ function areMemberSpawnStatusEntriesEqual(
left.runtimeAlive === right.runtimeAlive &&
left.runtimeModel === right.runtimeModel &&
left.bootstrapConfirmed === right.bootstrapConfirmed &&
left.hardFailure === right.hardFailure
left.hardFailure === right.hardFailure &&
leftPendingPermissionIds.length === rightPendingPermissionIds.length &&
leftPendingPermissionIds.every((value, index) => value === rightPendingPermissionIds[index])
);
}

View file

@ -3612,6 +3612,74 @@ describe('teamSlice actions', () => {
expect(store.getState().memberSpawnSnapshotsByTeam['my-team']).toBe(previousSnapshot);
});
it('does not suppress spawn snapshots when pending permission request ids change', async () => {
const store = createSliceStore();
const previousSnapshot = createMemberSpawnSnapshot({
teamLaunchState: 'partial_pending',
launchPhase: 'active',
summary: {
confirmedCount: 0,
pendingCount: 1,
failedCount: 0,
runtimeAlivePendingCount: 0,
},
statuses: {
alice: createMemberSpawnStatus({
status: 'waiting',
launchState: 'runtime_pending_bootstrap',
runtimeAlive: false,
livenessSource: undefined,
bootstrapConfirmed: false,
firstSpawnAcceptedAt: '2026-03-12T09:59:30.000Z',
lastHeartbeatAt: undefined,
}),
},
});
store.setState({
memberSpawnStatusesByTeam: {
'my-team': previousSnapshot.statuses,
},
memberSpawnSnapshotsByTeam: {
'my-team': previousSnapshot,
},
});
const nextSnapshot = createMemberSpawnSnapshot({
teamLaunchState: 'partial_pending',
launchPhase: 'active',
summary: {
confirmedCount: 0,
pendingCount: 1,
failedCount: 0,
runtimeAlivePendingCount: 0,
},
statuses: {
alice: createMemberSpawnStatus({
status: 'waiting',
launchState: 'runtime_pending_bootstrap',
runtimeAlive: false,
livenessSource: undefined,
bootstrapConfirmed: false,
firstSpawnAcceptedAt: '2026-03-12T09:59:30.000Z',
lastHeartbeatAt: undefined,
pendingPermissionRequestIds: ['perm-1'],
}),
},
});
hoisted.getMemberSpawnStatuses.mockResolvedValue(nextSnapshot);
await store.getState().fetchMemberSpawnStatuses('my-team');
expect(store.getState().memberSpawnSnapshotsByTeam['my-team']).not.toBe(previousSnapshot);
expect(store.getState().memberSpawnStatusesByTeam['my-team']).not.toBe(
previousSnapshot.statuses
);
expect(
store.getState().memberSpawnStatusesByTeam['my-team']?.alice?.pendingPermissionRequestIds
).toEqual(['perm-1']);
});
it('ignores stale spawn-status fetches after runtime already went offline', async () => {
const store = createSliceStore();
store.setState({