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,
|
hasTeamRefreshBurstDiagnostics,
|
||||||
noteTeamRefreshBurst,
|
noteTeamRefreshBurst,
|
||||||
} from '../team/teamRefreshBurstDiagnostics';
|
} from '../team/teamRefreshBurstDiagnostics';
|
||||||
|
import {
|
||||||
|
buildTeamScopedProgressTombstones,
|
||||||
|
collectTeamScopedStateRemovals,
|
||||||
|
collectTeamScopedVisibleLoadingResets,
|
||||||
|
} from '../team/teamScopedStateCleanup';
|
||||||
import { noteTeamRefreshFanout } from '../teamRefreshFanoutDiagnostics';
|
import { noteTeamRefreshFanout } from '../teamRefreshFanoutDiagnostics';
|
||||||
import { getWorktreeNavigationState } from '../utils/stateResetHelpers';
|
import { getWorktreeNavigationState } from '../utils/stateResetHelpers';
|
||||||
|
|
||||||
|
|
@ -293,166 +298,6 @@ function clearTeamScopedTransientState(teamName: string): void {
|
||||||
clearTeamScopedSelectorCaches(teamName);
|
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 {
|
function beginInFlightTeamDataRefresh(teamName: string): symbol {
|
||||||
const token = Symbol(teamName);
|
const token = Symbol(teamName);
|
||||||
const existing = inFlightRefreshTeamDataCalls.get(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