refactor(team): extract scoped state cleanup
This commit is contained in:
parent
9bd8585617
commit
c091bd8d96
3 changed files with 398 additions and 160 deletions
|
|
@ -86,6 +86,11 @@ import {
|
|||
hasTeamRefreshBurstDiagnostics,
|
||||
noteTeamRefreshBurst,
|
||||
} from '../team/teamRefreshBurstDiagnostics';
|
||||
import {
|
||||
buildTeamScopedProgressTombstones,
|
||||
collectTeamScopedStateRemovals,
|
||||
collectTeamScopedVisibleLoadingResets,
|
||||
} from '../team/teamScopedStateCleanup';
|
||||
import { noteTeamRefreshFanout } from '../teamRefreshFanoutDiagnostics';
|
||||
import { getWorktreeNavigationState } from '../utils/stateResetHelpers';
|
||||
|
||||
|
|
@ -293,166 +298,6 @@ function clearTeamScopedTransientState(teamName: string): void {
|
|||
clearTeamScopedSelectorCaches(teamName);
|
||||
}
|
||||
|
||||
function collectTeamScopedVisibleLoadingResets(
|
||||
state: Pick<
|
||||
TeamSlice,
|
||||
'teamMessagesByName' | 'selectedTeamName' | 'selectedTeamLoading' | 'selectedTeamError'
|
||||
>,
|
||||
teamName: string
|
||||
): Partial<TeamSlice> {
|
||||
const nextTeamMessagesEntry = state.teamMessagesByName[teamName];
|
||||
const nextTeamMessagesByName =
|
||||
nextTeamMessagesEntry &&
|
||||
(nextTeamMessagesEntry.loadingHead || nextTeamMessagesEntry.loadingOlder)
|
||||
? {
|
||||
...state.teamMessagesByName,
|
||||
[teamName]: {
|
||||
...nextTeamMessagesEntry,
|
||||
loadingHead: false,
|
||||
loadingOlder: false,
|
||||
},
|
||||
}
|
||||
: null;
|
||||
|
||||
const shouldResetSelectedSurface =
|
||||
state.selectedTeamName === teamName &&
|
||||
(state.selectedTeamLoading || state.selectedTeamError != null);
|
||||
|
||||
return {
|
||||
...(nextTeamMessagesByName ? { teamMessagesByName: nextTeamMessagesByName } : {}),
|
||||
...(shouldResetSelectedSurface
|
||||
? {
|
||||
selectedTeamLoading: false,
|
||||
selectedTeamError: null,
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
|
||||
function omitTeamKey<T>(record: Record<string, T>, teamName: string): Record<string, T> | null {
|
||||
if (!(teamName in record)) {
|
||||
return null;
|
||||
}
|
||||
const next = { ...record };
|
||||
delete next[teamName];
|
||||
return next;
|
||||
}
|
||||
|
||||
function collectTeamScopedStateRemovals(
|
||||
state: Pick<
|
||||
TeamSlice,
|
||||
| 'provisioningRuns'
|
||||
| 'teamDataCacheByName'
|
||||
| 'teamAgentRuntimeByTeam'
|
||||
| 'teamMessagesByName'
|
||||
| 'memberActivityMetaByTeam'
|
||||
| 'provisioningSnapshotByTeam'
|
||||
| 'currentProvisioningRunIdByTeam'
|
||||
| 'currentRuntimeRunIdByTeam'
|
||||
| 'provisioningStartedAtFloorByTeam'
|
||||
| 'leadActivityByTeam'
|
||||
| 'leadContextByTeam'
|
||||
| 'activeTaskLogActivityByTeam'
|
||||
| 'activeToolsByTeam'
|
||||
| 'finishedVisibleByTeam'
|
||||
| 'toolHistoryByTeam'
|
||||
| 'memberSpawnStatusesByTeam'
|
||||
| 'memberSpawnSnapshotsByTeam'
|
||||
| 'provisioningErrorByTeam'
|
||||
>,
|
||||
teamName: string
|
||||
): Partial<TeamSlice> {
|
||||
const nextProvisioningRuns = Object.fromEntries(
|
||||
Object.entries(state.provisioningRuns).filter(([, run]) => run.teamName !== teamName)
|
||||
) as Record<string, TeamProvisioningProgress>;
|
||||
const nextTeamDataCache = omitTeamKey(state.teamDataCacheByName, teamName);
|
||||
const nextTeamAgentRuntime = omitTeamKey(state.teamAgentRuntimeByTeam, teamName);
|
||||
const nextTeamMessages = omitTeamKey(state.teamMessagesByName, teamName);
|
||||
const nextMemberActivityMeta = omitTeamKey(state.memberActivityMetaByTeam, teamName);
|
||||
const nextProvisioningSnapshot = omitTeamKey(state.provisioningSnapshotByTeam, teamName);
|
||||
const nextCurrentProvisioningRunId = omitTeamKey(state.currentProvisioningRunIdByTeam, teamName);
|
||||
const nextCurrentRuntimeRunId = omitTeamKey(state.currentRuntimeRunIdByTeam, teamName);
|
||||
const nextProvisioningStartedAtFloor = omitTeamKey(
|
||||
state.provisioningStartedAtFloorByTeam,
|
||||
teamName
|
||||
);
|
||||
const nextLeadActivity = omitTeamKey(state.leadActivityByTeam, teamName);
|
||||
const nextLeadContext = omitTeamKey(state.leadContextByTeam, teamName);
|
||||
const nextActiveTaskLogActivity = omitTeamKey(state.activeTaskLogActivityByTeam, teamName);
|
||||
const nextActiveTools = omitTeamKey(state.activeToolsByTeam, teamName);
|
||||
const nextFinishedVisible = omitTeamKey(state.finishedVisibleByTeam, teamName);
|
||||
const nextToolHistory = omitTeamKey(state.toolHistoryByTeam, teamName);
|
||||
const nextMemberSpawnStatuses = omitTeamKey(state.memberSpawnStatusesByTeam, teamName);
|
||||
const nextMemberSpawnSnapshots = omitTeamKey(state.memberSpawnSnapshotsByTeam, teamName);
|
||||
const nextProvisioningErrors = omitTeamKey(state.provisioningErrorByTeam, teamName);
|
||||
|
||||
return {
|
||||
...(Object.keys(nextProvisioningRuns).length !== Object.keys(state.provisioningRuns).length
|
||||
? { provisioningRuns: nextProvisioningRuns }
|
||||
: {}),
|
||||
...(nextTeamDataCache ? { teamDataCacheByName: nextTeamDataCache } : {}),
|
||||
...(nextTeamAgentRuntime ? { teamAgentRuntimeByTeam: nextTeamAgentRuntime } : {}),
|
||||
...(nextTeamMessages ? { teamMessagesByName: nextTeamMessages } : {}),
|
||||
...(nextMemberActivityMeta ? { memberActivityMetaByTeam: nextMemberActivityMeta } : {}),
|
||||
...(nextProvisioningSnapshot ? { provisioningSnapshotByTeam: nextProvisioningSnapshot } : {}),
|
||||
...(nextCurrentProvisioningRunId
|
||||
? { currentProvisioningRunIdByTeam: nextCurrentProvisioningRunId }
|
||||
: {}),
|
||||
...(nextCurrentRuntimeRunId ? { currentRuntimeRunIdByTeam: nextCurrentRuntimeRunId } : {}),
|
||||
...(nextProvisioningStartedAtFloor
|
||||
? { provisioningStartedAtFloorByTeam: nextProvisioningStartedAtFloor }
|
||||
: {}),
|
||||
...(nextLeadActivity ? { leadActivityByTeam: nextLeadActivity } : {}),
|
||||
...(nextLeadContext ? { leadContextByTeam: nextLeadContext } : {}),
|
||||
...(nextActiveTaskLogActivity
|
||||
? { activeTaskLogActivityByTeam: nextActiveTaskLogActivity }
|
||||
: {}),
|
||||
...(nextActiveTools ? { activeToolsByTeam: nextActiveTools } : {}),
|
||||
...(nextFinishedVisible ? { finishedVisibleByTeam: nextFinishedVisible } : {}),
|
||||
...(nextToolHistory ? { toolHistoryByTeam: nextToolHistory } : {}),
|
||||
...(nextMemberSpawnStatuses ? { memberSpawnStatusesByTeam: nextMemberSpawnStatuses } : {}),
|
||||
...(nextMemberSpawnSnapshots ? { memberSpawnSnapshotsByTeam: nextMemberSpawnSnapshots } : {}),
|
||||
...(nextProvisioningErrors ? { provisioningErrorByTeam: nextProvisioningErrors } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function buildTeamScopedProgressTombstones(
|
||||
state: Pick<
|
||||
TeamSlice,
|
||||
| 'currentProvisioningRunIdByTeam'
|
||||
| 'currentRuntimeRunIdByTeam'
|
||||
| 'ignoredProvisioningRunIds'
|
||||
| 'ignoredRuntimeRunIds'
|
||||
| 'provisioningStartedAtFloorByTeam'
|
||||
>,
|
||||
teamName: string,
|
||||
floor: string
|
||||
): Pick<
|
||||
TeamSlice,
|
||||
'ignoredProvisioningRunIds' | 'ignoredRuntimeRunIds' | 'provisioningStartedAtFloorByTeam'
|
||||
> {
|
||||
const nextIgnoredProvisioningRunIds = { ...state.ignoredProvisioningRunIds };
|
||||
const nextIgnoredRuntimeRunIds = { ...state.ignoredRuntimeRunIds };
|
||||
|
||||
const currentProvisioningRunId = state.currentProvisioningRunIdByTeam[teamName];
|
||||
const currentRuntimeRunId = state.currentRuntimeRunIdByTeam[teamName];
|
||||
if (currentProvisioningRunId) {
|
||||
nextIgnoredProvisioningRunIds[currentProvisioningRunId] = teamName;
|
||||
}
|
||||
if (currentRuntimeRunId) {
|
||||
nextIgnoredRuntimeRunIds[currentRuntimeRunId] = teamName;
|
||||
}
|
||||
|
||||
return {
|
||||
ignoredProvisioningRunIds: nextIgnoredProvisioningRunIds,
|
||||
ignoredRuntimeRunIds: nextIgnoredRuntimeRunIds,
|
||||
provisioningStartedAtFloorByTeam: {
|
||||
...state.provisioningStartedAtFloorByTeam,
|
||||
[teamName]: floor,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function beginInFlightTeamDataRefresh(teamName: string): symbol {
|
||||
const token = Symbol(teamName);
|
||||
const existing = inFlightRefreshTeamDataCalls.get(teamName);
|
||||
|
|
|
|||
190
src/renderer/store/team/teamScopedStateCleanup.ts
Normal file
190
src/renderer/store/team/teamScopedStateCleanup.ts
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
interface TeamMessagesLoadingEntry {
|
||||
loadingHead: boolean;
|
||||
loadingOlder: boolean;
|
||||
}
|
||||
|
||||
interface TeamScopedVisibleLoadingResetState<
|
||||
TTeamMessagesEntry extends TeamMessagesLoadingEntry,
|
||||
> {
|
||||
teamMessagesByName: Record<string, TTeamMessagesEntry>;
|
||||
selectedTeamName: string | null;
|
||||
selectedTeamLoading: boolean;
|
||||
selectedTeamError: string | null;
|
||||
}
|
||||
|
||||
interface TeamScopedProvisioningRun {
|
||||
teamName: string;
|
||||
}
|
||||
|
||||
type TeamScopedRecord = Record<string, unknown>;
|
||||
|
||||
interface TeamScopedStateRemovalState<
|
||||
TProvisioningRun extends TeamScopedProvisioningRun = TeamScopedProvisioningRun,
|
||||
> {
|
||||
provisioningRuns: Record<string, TProvisioningRun>;
|
||||
teamDataCacheByName: TeamScopedRecord;
|
||||
teamAgentRuntimeByTeam: TeamScopedRecord;
|
||||
teamMessagesByName: TeamScopedRecord;
|
||||
memberActivityMetaByTeam: TeamScopedRecord;
|
||||
provisioningSnapshotByTeam: TeamScopedRecord;
|
||||
currentProvisioningRunIdByTeam: TeamScopedRecord;
|
||||
currentRuntimeRunIdByTeam: TeamScopedRecord;
|
||||
provisioningStartedAtFloorByTeam: TeamScopedRecord;
|
||||
leadActivityByTeam: TeamScopedRecord;
|
||||
leadContextByTeam: TeamScopedRecord;
|
||||
activeTaskLogActivityByTeam: TeamScopedRecord;
|
||||
activeToolsByTeam: TeamScopedRecord;
|
||||
finishedVisibleByTeam: TeamScopedRecord;
|
||||
toolHistoryByTeam: TeamScopedRecord;
|
||||
memberSpawnStatusesByTeam: TeamScopedRecord;
|
||||
memberSpawnSnapshotsByTeam: TeamScopedRecord;
|
||||
provisioningErrorByTeam: TeamScopedRecord;
|
||||
}
|
||||
|
||||
type TeamScopedStateRemovalKey = keyof TeamScopedStateRemovalState;
|
||||
|
||||
interface TeamScopedProgressTombstoneState {
|
||||
currentProvisioningRunIdByTeam: Record<string, string | null | undefined>;
|
||||
currentRuntimeRunIdByTeam: Record<string, string | null | undefined>;
|
||||
ignoredProvisioningRunIds: Record<string, string>;
|
||||
ignoredRuntimeRunIds: Record<string, string>;
|
||||
provisioningStartedAtFloorByTeam: Record<string, string>;
|
||||
}
|
||||
|
||||
export function collectTeamScopedVisibleLoadingResets<
|
||||
TTeamMessagesEntry extends TeamMessagesLoadingEntry,
|
||||
>(
|
||||
state: TeamScopedVisibleLoadingResetState<TTeamMessagesEntry>,
|
||||
teamName: string
|
||||
): Partial<TeamScopedVisibleLoadingResetState<TTeamMessagesEntry>> {
|
||||
const nextTeamMessagesEntry = state.teamMessagesByName[teamName];
|
||||
const nextTeamMessagesByName =
|
||||
nextTeamMessagesEntry &&
|
||||
(nextTeamMessagesEntry.loadingHead || nextTeamMessagesEntry.loadingOlder)
|
||||
? {
|
||||
...state.teamMessagesByName,
|
||||
[teamName]: {
|
||||
...nextTeamMessagesEntry,
|
||||
loadingHead: false,
|
||||
loadingOlder: false,
|
||||
} as TTeamMessagesEntry,
|
||||
}
|
||||
: null;
|
||||
|
||||
const shouldResetSelectedSurface =
|
||||
state.selectedTeamName === teamName &&
|
||||
(state.selectedTeamLoading || state.selectedTeamError != null);
|
||||
|
||||
return {
|
||||
...(nextTeamMessagesByName ? { teamMessagesByName: nextTeamMessagesByName } : {}),
|
||||
...(shouldResetSelectedSurface
|
||||
? {
|
||||
selectedTeamLoading: false,
|
||||
selectedTeamError: null,
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
|
||||
function omitTeamKey<TRecord extends Record<string, unknown>>(
|
||||
record: TRecord,
|
||||
teamName: string
|
||||
): TRecord | null {
|
||||
if (!(teamName in record)) {
|
||||
return null;
|
||||
}
|
||||
const next = { ...record };
|
||||
delete next[teamName];
|
||||
return next;
|
||||
}
|
||||
|
||||
export function collectTeamScopedStateRemovals<TState extends TeamScopedStateRemovalState>(
|
||||
state: TState,
|
||||
teamName: string
|
||||
): Partial<Pick<TState, TeamScopedStateRemovalKey>> {
|
||||
const nextProvisioningRuns = Object.fromEntries(
|
||||
Object.entries(state.provisioningRuns).filter(([, run]) => run.teamName !== teamName)
|
||||
) as TState['provisioningRuns'];
|
||||
const nextTeamDataCache = omitTeamKey(state.teamDataCacheByName, teamName);
|
||||
const nextTeamAgentRuntime = omitTeamKey(state.teamAgentRuntimeByTeam, teamName);
|
||||
const nextTeamMessages = omitTeamKey(state.teamMessagesByName, teamName);
|
||||
const nextMemberActivityMeta = omitTeamKey(state.memberActivityMetaByTeam, teamName);
|
||||
const nextProvisioningSnapshot = omitTeamKey(state.provisioningSnapshotByTeam, teamName);
|
||||
const nextCurrentProvisioningRunId = omitTeamKey(state.currentProvisioningRunIdByTeam, teamName);
|
||||
const nextCurrentRuntimeRunId = omitTeamKey(state.currentRuntimeRunIdByTeam, teamName);
|
||||
const nextProvisioningStartedAtFloor = omitTeamKey(
|
||||
state.provisioningStartedAtFloorByTeam,
|
||||
teamName
|
||||
);
|
||||
const nextLeadActivity = omitTeamKey(state.leadActivityByTeam, teamName);
|
||||
const nextLeadContext = omitTeamKey(state.leadContextByTeam, teamName);
|
||||
const nextActiveTaskLogActivity = omitTeamKey(state.activeTaskLogActivityByTeam, teamName);
|
||||
const nextActiveTools = omitTeamKey(state.activeToolsByTeam, teamName);
|
||||
const nextFinishedVisible = omitTeamKey(state.finishedVisibleByTeam, teamName);
|
||||
const nextToolHistory = omitTeamKey(state.toolHistoryByTeam, teamName);
|
||||
const nextMemberSpawnStatuses = omitTeamKey(state.memberSpawnStatusesByTeam, teamName);
|
||||
const nextMemberSpawnSnapshots = omitTeamKey(state.memberSpawnSnapshotsByTeam, teamName);
|
||||
const nextProvisioningErrors = omitTeamKey(state.provisioningErrorByTeam, teamName);
|
||||
|
||||
return {
|
||||
...(Object.keys(nextProvisioningRuns).length !== Object.keys(state.provisioningRuns).length
|
||||
? { provisioningRuns: nextProvisioningRuns }
|
||||
: {}),
|
||||
...(nextTeamDataCache ? { teamDataCacheByName: nextTeamDataCache } : {}),
|
||||
...(nextTeamAgentRuntime ? { teamAgentRuntimeByTeam: nextTeamAgentRuntime } : {}),
|
||||
...(nextTeamMessages ? { teamMessagesByName: nextTeamMessages } : {}),
|
||||
...(nextMemberActivityMeta ? { memberActivityMetaByTeam: nextMemberActivityMeta } : {}),
|
||||
...(nextProvisioningSnapshot ? { provisioningSnapshotByTeam: nextProvisioningSnapshot } : {}),
|
||||
...(nextCurrentProvisioningRunId
|
||||
? { currentProvisioningRunIdByTeam: nextCurrentProvisioningRunId }
|
||||
: {}),
|
||||
...(nextCurrentRuntimeRunId ? { currentRuntimeRunIdByTeam: nextCurrentRuntimeRunId } : {}),
|
||||
...(nextProvisioningStartedAtFloor
|
||||
? { provisioningStartedAtFloorByTeam: nextProvisioningStartedAtFloor }
|
||||
: {}),
|
||||
...(nextLeadActivity ? { leadActivityByTeam: nextLeadActivity } : {}),
|
||||
...(nextLeadContext ? { leadContextByTeam: nextLeadContext } : {}),
|
||||
...(nextActiveTaskLogActivity
|
||||
? { activeTaskLogActivityByTeam: nextActiveTaskLogActivity }
|
||||
: {}),
|
||||
...(nextActiveTools ? { activeToolsByTeam: nextActiveTools } : {}),
|
||||
...(nextFinishedVisible ? { finishedVisibleByTeam: nextFinishedVisible } : {}),
|
||||
...(nextToolHistory ? { toolHistoryByTeam: nextToolHistory } : {}),
|
||||
...(nextMemberSpawnStatuses ? { memberSpawnStatusesByTeam: nextMemberSpawnStatuses } : {}),
|
||||
...(nextMemberSpawnSnapshots ? { memberSpawnSnapshotsByTeam: nextMemberSpawnSnapshots } : {}),
|
||||
...(nextProvisioningErrors ? { provisioningErrorByTeam: nextProvisioningErrors } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
export function buildTeamScopedProgressTombstones<TState extends TeamScopedProgressTombstoneState>(
|
||||
state: TState,
|
||||
teamName: string,
|
||||
floor: string
|
||||
): Pick<
|
||||
TState,
|
||||
'ignoredProvisioningRunIds' | 'ignoredRuntimeRunIds' | 'provisioningStartedAtFloorByTeam'
|
||||
> {
|
||||
const nextIgnoredProvisioningRunIds = { ...state.ignoredProvisioningRunIds };
|
||||
const nextIgnoredRuntimeRunIds = { ...state.ignoredRuntimeRunIds };
|
||||
|
||||
const currentProvisioningRunId = state.currentProvisioningRunIdByTeam[teamName];
|
||||
const currentRuntimeRunId = state.currentRuntimeRunIdByTeam[teamName];
|
||||
if (currentProvisioningRunId) {
|
||||
nextIgnoredProvisioningRunIds[currentProvisioningRunId] = teamName;
|
||||
}
|
||||
if (currentRuntimeRunId) {
|
||||
nextIgnoredRuntimeRunIds[currentRuntimeRunId] = teamName;
|
||||
}
|
||||
|
||||
return {
|
||||
ignoredProvisioningRunIds: nextIgnoredProvisioningRunIds,
|
||||
ignoredRuntimeRunIds: nextIgnoredRuntimeRunIds,
|
||||
provisioningStartedAtFloorByTeam: {
|
||||
...state.provisioningStartedAtFloorByTeam,
|
||||
[teamName]: floor,
|
||||
},
|
||||
} as Pick<
|
||||
TState,
|
||||
'ignoredProvisioningRunIds' | 'ignoredRuntimeRunIds' | 'provisioningStartedAtFloorByTeam'
|
||||
>;
|
||||
}
|
||||
203
test/renderer/store/teamScopedStateCleanup.test.ts
Normal file
203
test/renderer/store/teamScopedStateCleanup.test.ts
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
buildTeamScopedProgressTombstones,
|
||||
collectTeamScopedStateRemovals,
|
||||
collectTeamScopedVisibleLoadingResets,
|
||||
} from '../../../src/renderer/store/team/teamScopedStateCleanup';
|
||||
|
||||
const teamScopedRecordKeys = [
|
||||
'teamDataCacheByName',
|
||||
'teamAgentRuntimeByTeam',
|
||||
'teamMessagesByName',
|
||||
'memberActivityMetaByTeam',
|
||||
'provisioningSnapshotByTeam',
|
||||
'currentProvisioningRunIdByTeam',
|
||||
'currentRuntimeRunIdByTeam',
|
||||
'provisioningStartedAtFloorByTeam',
|
||||
'leadActivityByTeam',
|
||||
'leadContextByTeam',
|
||||
'activeTaskLogActivityByTeam',
|
||||
'activeToolsByTeam',
|
||||
'finishedVisibleByTeam',
|
||||
'toolHistoryByTeam',
|
||||
'memberSpawnStatusesByTeam',
|
||||
'memberSpawnSnapshotsByTeam',
|
||||
'provisioningErrorByTeam',
|
||||
] as const;
|
||||
|
||||
function buildRecord(label: string): Record<string, unknown> {
|
||||
return {
|
||||
'my-team': `${label}:mine`,
|
||||
'other-team': `${label}:other`,
|
||||
};
|
||||
}
|
||||
|
||||
function buildRemovalState(): Parameters<typeof collectTeamScopedStateRemovals>[0] {
|
||||
return {
|
||||
provisioningRuns: {
|
||||
'run-mine-1': { teamName: 'my-team' },
|
||||
'run-other': { teamName: 'other-team' },
|
||||
'run-mine-2': { teamName: 'my-team' },
|
||||
},
|
||||
teamDataCacheByName: buildRecord('teamDataCacheByName'),
|
||||
teamAgentRuntimeByTeam: buildRecord('teamAgentRuntimeByTeam'),
|
||||
teamMessagesByName: buildRecord('teamMessagesByName'),
|
||||
memberActivityMetaByTeam: buildRecord('memberActivityMetaByTeam'),
|
||||
provisioningSnapshotByTeam: buildRecord('provisioningSnapshotByTeam'),
|
||||
currentProvisioningRunIdByTeam: buildRecord('currentProvisioningRunIdByTeam'),
|
||||
currentRuntimeRunIdByTeam: buildRecord('currentRuntimeRunIdByTeam'),
|
||||
provisioningStartedAtFloorByTeam: buildRecord('provisioningStartedAtFloorByTeam'),
|
||||
leadActivityByTeam: buildRecord('leadActivityByTeam'),
|
||||
leadContextByTeam: buildRecord('leadContextByTeam'),
|
||||
activeTaskLogActivityByTeam: buildRecord('activeTaskLogActivityByTeam'),
|
||||
activeToolsByTeam: buildRecord('activeToolsByTeam'),
|
||||
finishedVisibleByTeam: buildRecord('finishedVisibleByTeam'),
|
||||
toolHistoryByTeam: buildRecord('toolHistoryByTeam'),
|
||||
memberSpawnStatusesByTeam: buildRecord('memberSpawnStatusesByTeam'),
|
||||
memberSpawnSnapshotsByTeam: buildRecord('memberSpawnSnapshotsByTeam'),
|
||||
provisioningErrorByTeam: buildRecord('provisioningErrorByTeam'),
|
||||
};
|
||||
}
|
||||
|
||||
describe('teamScopedStateCleanup', () => {
|
||||
it('resets visible team loading and message loading flags for the scoped team', () => {
|
||||
const otherEntry = {
|
||||
loadingHead: true,
|
||||
loadingOlder: false,
|
||||
marker: 'other',
|
||||
};
|
||||
const patch = collectTeamScopedVisibleLoadingResets(
|
||||
{
|
||||
teamMessagesByName: {
|
||||
'my-team': {
|
||||
loadingHead: true,
|
||||
loadingOlder: true,
|
||||
marker: 'mine',
|
||||
},
|
||||
'other-team': otherEntry,
|
||||
},
|
||||
selectedTeamName: 'my-team',
|
||||
selectedTeamLoading: true,
|
||||
selectedTeamError: 'Boom',
|
||||
},
|
||||
'my-team'
|
||||
);
|
||||
|
||||
expect(patch).toEqual({
|
||||
teamMessagesByName: {
|
||||
'my-team': {
|
||||
loadingHead: false,
|
||||
loadingOlder: false,
|
||||
marker: 'mine',
|
||||
},
|
||||
'other-team': otherEntry,
|
||||
},
|
||||
selectedTeamLoading: false,
|
||||
selectedTeamError: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('does not emit visible loading changes when the scoped team is already idle', () => {
|
||||
const patch = collectTeamScopedVisibleLoadingResets(
|
||||
{
|
||||
teamMessagesByName: {
|
||||
'my-team': {
|
||||
loadingHead: false,
|
||||
loadingOlder: false,
|
||||
},
|
||||
},
|
||||
selectedTeamName: 'other-team',
|
||||
selectedTeamLoading: false,
|
||||
selectedTeamError: null,
|
||||
},
|
||||
'my-team'
|
||||
);
|
||||
|
||||
expect(patch).toEqual({});
|
||||
});
|
||||
|
||||
it('removes scoped team records and provisioning runs while preserving other teams', () => {
|
||||
const patch = collectTeamScopedStateRemovals(buildRemovalState(), 'my-team');
|
||||
|
||||
expect(patch.provisioningRuns).toEqual({
|
||||
'run-other': { teamName: 'other-team' },
|
||||
});
|
||||
for (const key of teamScopedRecordKeys) {
|
||||
expect(patch[key]).toEqual({
|
||||
'other-team': `${key}:other`,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it('does not emit removal changes when the team is absent', () => {
|
||||
const state = buildRemovalState();
|
||||
const patch = collectTeamScopedStateRemovals(state, 'missing-team');
|
||||
|
||||
expect(patch).toEqual({});
|
||||
});
|
||||
|
||||
it('tombstones current provisioning and runtime run ids for the scoped team', () => {
|
||||
const tombstones = buildTeamScopedProgressTombstones(
|
||||
{
|
||||
currentProvisioningRunIdByTeam: {
|
||||
'my-team': 'provisioning-run-1',
|
||||
'other-team': 'provisioning-run-2',
|
||||
},
|
||||
currentRuntimeRunIdByTeam: {
|
||||
'my-team': 'runtime-run-1',
|
||||
},
|
||||
ignoredProvisioningRunIds: {
|
||||
old: 'old-team',
|
||||
},
|
||||
ignoredRuntimeRunIds: {
|
||||
'old-runtime': 'old-team',
|
||||
},
|
||||
provisioningStartedAtFloorByTeam: {
|
||||
'other-team': '2026-01-01T00:00:00.000Z',
|
||||
},
|
||||
},
|
||||
'my-team',
|
||||
'2026-05-22T10:00:00.000Z'
|
||||
);
|
||||
|
||||
expect(tombstones).toEqual({
|
||||
ignoredProvisioningRunIds: {
|
||||
old: 'old-team',
|
||||
'provisioning-run-1': 'my-team',
|
||||
},
|
||||
ignoredRuntimeRunIds: {
|
||||
'old-runtime': 'old-team',
|
||||
'runtime-run-1': 'my-team',
|
||||
},
|
||||
provisioningStartedAtFloorByTeam: {
|
||||
'other-team': '2026-01-01T00:00:00.000Z',
|
||||
'my-team': '2026-05-22T10:00:00.000Z',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('still records a floor when there are no current run ids to tombstone', () => {
|
||||
const tombstones = buildTeamScopedProgressTombstones(
|
||||
{
|
||||
currentProvisioningRunIdByTeam: {},
|
||||
currentRuntimeRunIdByTeam: {
|
||||
'my-team': null,
|
||||
},
|
||||
ignoredProvisioningRunIds: {},
|
||||
ignoredRuntimeRunIds: {},
|
||||
provisioningStartedAtFloorByTeam: {},
|
||||
},
|
||||
'my-team',
|
||||
'2026-05-22T10:00:00.000Z'
|
||||
);
|
||||
|
||||
expect(tombstones).toEqual({
|
||||
ignoredProvisioningRunIds: {},
|
||||
ignoredRuntimeRunIds: {},
|
||||
provisioningStartedAtFloorByTeam: {
|
||||
'my-team': '2026-05-22T10:00:00.000Z',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue