refactor(team): extract provisioning state rules

This commit is contained in:
777genius 2026-05-22 11:08:11 +03:00
parent 3f7b793816
commit 993982311d
3 changed files with 114 additions and 31 deletions

View file

@ -82,6 +82,11 @@ import {
clearPendingReplyRefreshWaits,
setPendingReplyRefreshEnabled,
} from '../team/teamPendingReplyWaits';
import {
isActiveProvisioningState,
isTerminalProvisioningState,
shouldIgnoreProvisioningProgressRegression,
} from '../team/teamProvisioningStateRules';
import {
clearAllTeamRefreshBurstDiagnostics,
clearTeamRefreshBurstDiagnostics,
@ -532,33 +537,6 @@ function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
const ACTIVE_PROVISIONING_STATES = new Set([
'validating',
'spawning',
'configuring',
'assembling',
'finalizing',
'verifying',
]);
const TERMINAL_PROVISIONING_STATES = new Set(['ready', 'failed', 'disconnected', 'cancelled']);
function shouldIgnoreProvisioningProgressRegression(
currentState: TeamProvisioningProgress['state'],
nextState: TeamProvisioningProgress['state']
): boolean {
if (currentState === 'ready') {
return nextState !== 'ready' && nextState !== 'disconnected';
}
if (
currentState === 'failed' ||
currentState === 'cancelled' ||
currentState === 'disconnected'
) {
return nextState !== currentState;
}
return false;
}
function isPendingProvisioningRunId(runId: string): boolean {
return runId.startsWith('pending:');
}
@ -698,12 +676,12 @@ async function pollProvisioningStatus(
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
const state = getState();
const current = state.provisioningRuns[runId];
if (current && TERMINAL_PROVISIONING_STATES.has(current.state)) {
if (current && isTerminalProvisioningState(current.state)) {
return;
}
try {
const progress = await state.getProvisioningStatus(runId);
if (TERMINAL_PROVISIONING_STATES.has(progress.state)) {
if (isTerminalProvisioningState(progress.state)) {
return;
}
} catch (error) {
@ -2345,7 +2323,7 @@ export function isTeamProvisioningActive(
teamName: string
): boolean {
const current = getCurrentProvisioningProgressForTeam(state, teamName);
return current != null && ACTIVE_PROVISIONING_STATES.has(current.state);
return current != null && isActiveProvisioningState(current.state);
}
function loadAllLaunchParams(): Record<string, TeamLaunchParams> {
@ -5342,7 +5320,7 @@ export const createTeamSlice: StateCreator<AppState, [], [], TeamSlice> = (set,
}
}
if (isCanonicalRun && TERMINAL_PROVISIONING_STATES.has(progress.state)) {
if (isCanonicalRun && isTerminalProvisioningState(progress.state)) {
set((prev) => {
const next = { ...prev.memberSpawnStatusesByTeam };
const nextSnapshots = { ...prev.memberSpawnSnapshotsByTeam };

View file

@ -0,0 +1,44 @@
import type { TeamProvisioningProgress } from '@shared/types';
type TeamProvisioningProgressState = TeamProvisioningProgress['state'];
const ACTIVE_PROVISIONING_STATES: ReadonlySet<TeamProvisioningProgressState> = new Set([
'validating',
'spawning',
'configuring',
'assembling',
'finalizing',
'verifying',
]);
const TERMINAL_PROVISIONING_STATES: ReadonlySet<TeamProvisioningProgressState> = new Set([
'ready',
'failed',
'disconnected',
'cancelled',
]);
export function isActiveProvisioningState(state: TeamProvisioningProgressState): boolean {
return ACTIVE_PROVISIONING_STATES.has(state);
}
export function isTerminalProvisioningState(state: TeamProvisioningProgressState): boolean {
return TERMINAL_PROVISIONING_STATES.has(state);
}
export function shouldIgnoreProvisioningProgressRegression(
currentState: TeamProvisioningProgressState,
nextState: TeamProvisioningProgressState
): boolean {
if (currentState === 'ready') {
return nextState !== 'ready' && nextState !== 'disconnected';
}
if (
currentState === 'failed' ||
currentState === 'cancelled' ||
currentState === 'disconnected'
) {
return nextState !== currentState;
}
return false;
}

View file

@ -0,0 +1,61 @@
import { describe, expect, it } from 'vitest';
import {
isActiveProvisioningState,
isTerminalProvisioningState,
shouldIgnoreProvisioningProgressRegression,
} from '../../../src/renderer/store/team/teamProvisioningStateRules';
import type { TeamProvisioningProgress } from '../../../src/shared/types';
type ProgressState = TeamProvisioningProgress['state'];
const activeStates: ProgressState[] = [
'validating',
'spawning',
'configuring',
'assembling',
'finalizing',
'verifying',
];
const terminalStates: ProgressState[] = ['ready', 'failed', 'disconnected', 'cancelled'];
describe('teamProvisioningStateRules', () => {
it('classifies active provisioning states', () => {
for (const state of activeStates) {
expect(isActiveProvisioningState(state), state).toBe(true);
expect(isTerminalProvisioningState(state), state).toBe(false);
}
});
it('classifies terminal provisioning states', () => {
for (const state of terminalStates) {
expect(isTerminalProvisioningState(state), state).toBe(true);
expect(isActiveProvisioningState(state), state).toBe(false);
}
});
it('allows active state progressions and regressions to be processed', () => {
expect(shouldIgnoreProvisioningProgressRegression('spawning', 'validating')).toBe(false);
expect(shouldIgnoreProvisioningProgressRegression('validating', 'spawning')).toBe(false);
expect(shouldIgnoreProvisioningProgressRegression('verifying', 'ready')).toBe(false);
});
it('prevents ready from regressing except to disconnected', () => {
expect(shouldIgnoreProvisioningProgressRegression('ready', 'validating')).toBe(true);
expect(shouldIgnoreProvisioningProgressRegression('ready', 'failed')).toBe(true);
expect(shouldIgnoreProvisioningProgressRegression('ready', 'cancelled')).toBe(true);
expect(shouldIgnoreProvisioningProgressRegression('ready', 'ready')).toBe(false);
expect(shouldIgnoreProvisioningProgressRegression('ready', 'disconnected')).toBe(false);
});
it('locks failed, cancelled, and disconnected to their current terminal state', () => {
expect(shouldIgnoreProvisioningProgressRegression('failed', 'failed')).toBe(false);
expect(shouldIgnoreProvisioningProgressRegression('failed', 'ready')).toBe(true);
expect(shouldIgnoreProvisioningProgressRegression('cancelled', 'cancelled')).toBe(false);
expect(shouldIgnoreProvisioningProgressRegression('cancelled', 'spawning')).toBe(true);
expect(shouldIgnoreProvisioningProgressRegression('disconnected', 'disconnected')).toBe(false);
expect(shouldIgnoreProvisioningProgressRegression('disconnected', 'ready')).toBe(true);
});
});