fix(startup): prevent recovery regressions
This commit is contained in:
parent
2be35c74ec
commit
9518ce920a
7 changed files with 78 additions and 11 deletions
12
.github/workflows/release.yml
vendored
12
.github/workflows/release.yml
vendored
|
|
@ -45,6 +45,9 @@ jobs:
|
||||||
node-version: 22
|
node-version: 22
|
||||||
cache: pnpm
|
cache: pnpm
|
||||||
|
|
||||||
|
- name: Restore pnpm node-gyp executable bit
|
||||||
|
run: find "$(dirname "$(command -v pnpm)")/store" -path '*/node-gyp/gyp/gyp_main.py' -exec chmod +x {} \; 2>/dev/null || true
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
|
@ -336,6 +339,9 @@ jobs:
|
||||||
with:
|
with:
|
||||||
python-version: '3.11'
|
python-version: '3.11'
|
||||||
|
|
||||||
|
- name: Restore pnpm node-gyp executable bit
|
||||||
|
run: find "$(dirname "$(command -v pnpm)")/store" -path '*/node-gyp/gyp/gyp_main.py' -exec chmod +x {} \; 2>/dev/null || true
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
|
@ -452,6 +458,9 @@ jobs:
|
||||||
with:
|
with:
|
||||||
python-version: '3.11'
|
python-version: '3.11'
|
||||||
|
|
||||||
|
- name: Restore pnpm node-gyp executable bit
|
||||||
|
run: find "$(dirname "$(command -v pnpm)")/store" -path '*/node-gyp/gyp/gyp_main.py' -exec chmod +x {} \; 2>/dev/null || true
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
|
@ -573,6 +582,9 @@ jobs:
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y libarchive-tools rpm xvfb
|
sudo apt-get install -y libarchive-tools rpm xvfb
|
||||||
|
|
||||||
|
- name: Restore pnpm node-gyp executable bit
|
||||||
|
run: find "$(dirname "$(command -v pnpm)")/store" -path '*/node-gyp/gyp/gyp_main.py' -exec chmod +x {} \; 2>/dev/null || true
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -927,7 +927,7 @@ let shutdownComplete = false;
|
||||||
const startupTimers = new Set<ReturnType<typeof setTimeout>>();
|
const startupTimers = new Set<ReturnType<typeof setTimeout>>();
|
||||||
|
|
||||||
const SHUTDOWN_STEP_TIMEOUT_MS = 5_000;
|
const SHUTDOWN_STEP_TIMEOUT_MS = 5_000;
|
||||||
const STARTUP_RECOVERY_DELAY_MS = 60_000;
|
const STARTUP_RECOVERY_DELAY_MS = 10_000;
|
||||||
const STARTUP_CLI_WARMUP_DELAY_MS = 90_000;
|
const STARTUP_CLI_WARMUP_DELAY_MS = 90_000;
|
||||||
const STARTUP_BACKGROUND_SERVICE_DELAY_MS = 5_000;
|
const STARTUP_BACKGROUND_SERVICE_DELAY_MS = 5_000;
|
||||||
const STARTUP_RECOVERY_CONCURRENCY = 1;
|
const STARTUP_RECOVERY_CONCURRENCY = 1;
|
||||||
|
|
|
||||||
|
|
@ -274,24 +274,26 @@ export const DateGroupedSessions = memo((): React.JSX.Element => {
|
||||||
// Loading guards in the store actions prevent duplicate IPC calls
|
// Loading guards in the store actions prevent duplicate IPC calls
|
||||||
// when the centralized init chain has already started a fetch.
|
// when the centralized init chain has already started a fetch.
|
||||||
const repositoryGroupsLoading = useStore((s) => s.repositoryGroupsLoading);
|
const repositoryGroupsLoading = useStore((s) => s.repositoryGroupsLoading);
|
||||||
|
const repositoryGroupsInitialized = useStore((s) => s.repositoryGroupsInitialized);
|
||||||
const repositoryGroupsError = useStore((s) => s.repositoryGroupsError);
|
const repositoryGroupsError = useStore((s) => s.repositoryGroupsError);
|
||||||
const projectsLoading = useStore((s) => s.projectsLoading);
|
const projectsLoading = useStore((s) => s.projectsLoading);
|
||||||
|
const projectsInitialized = useStore((s) => s.projectsInitialized);
|
||||||
const projectsError = useStore((s) => s.projectsError);
|
const projectsError = useStore((s) => s.projectsError);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
viewMode === 'grouped' &&
|
viewMode === 'grouped' &&
|
||||||
repositoryGroups.length === 0 &&
|
!repositoryGroupsInitialized &&
|
||||||
!repositoryGroupsLoading &&
|
!repositoryGroupsLoading &&
|
||||||
!repositoryGroupsError
|
!repositoryGroupsError
|
||||||
) {
|
) {
|
||||||
void fetchRepositoryGroups();
|
void fetchRepositoryGroups();
|
||||||
} else if (viewMode === 'flat' && projects.length === 0 && !projectsLoading && !projectsError) {
|
} else if (viewMode === 'flat' && !projectsInitialized && !projectsLoading && !projectsError) {
|
||||||
void fetchProjects();
|
void fetchProjects();
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
viewMode,
|
viewMode,
|
||||||
repositoryGroups.length,
|
repositoryGroupsInitialized,
|
||||||
projects.length,
|
projectsInitialized,
|
||||||
repositoryGroupsLoading,
|
repositoryGroupsLoading,
|
||||||
repositoryGroupsError,
|
repositoryGroupsError,
|
||||||
projectsLoading,
|
projectsLoading,
|
||||||
|
|
|
||||||
|
|
@ -206,10 +206,12 @@ export const GlobalTaskList = memo(function GlobalTaskList({
|
||||||
softDeleteTask,
|
softDeleteTask,
|
||||||
projects,
|
projects,
|
||||||
projectsLoading,
|
projectsLoading,
|
||||||
|
projectsInitialized,
|
||||||
projectsError,
|
projectsError,
|
||||||
viewMode,
|
viewMode,
|
||||||
repositoryGroups,
|
repositoryGroups,
|
||||||
repositoryGroupsLoading,
|
repositoryGroupsLoading,
|
||||||
|
repositoryGroupsInitialized,
|
||||||
repositoryGroupsError,
|
repositoryGroupsError,
|
||||||
teams,
|
teams,
|
||||||
provisioningRuns,
|
provisioningRuns,
|
||||||
|
|
@ -226,10 +228,12 @@ export const GlobalTaskList = memo(function GlobalTaskList({
|
||||||
softDeleteTask: s.softDeleteTask,
|
softDeleteTask: s.softDeleteTask,
|
||||||
projects: s.projects,
|
projects: s.projects,
|
||||||
projectsLoading: s.projectsLoading,
|
projectsLoading: s.projectsLoading,
|
||||||
|
projectsInitialized: s.projectsInitialized,
|
||||||
projectsError: s.projectsError,
|
projectsError: s.projectsError,
|
||||||
viewMode: s.viewMode,
|
viewMode: s.viewMode,
|
||||||
repositoryGroups: s.repositoryGroups,
|
repositoryGroups: s.repositoryGroups,
|
||||||
repositoryGroupsLoading: s.repositoryGroupsLoading,
|
repositoryGroupsLoading: s.repositoryGroupsLoading,
|
||||||
|
repositoryGroupsInitialized: s.repositoryGroupsInitialized,
|
||||||
repositoryGroupsError: s.repositoryGroupsError,
|
repositoryGroupsError: s.repositoryGroupsError,
|
||||||
teams: s.teams,
|
teams: s.teams,
|
||||||
provisioningRuns: s.provisioningRuns,
|
provisioningRuns: s.provisioningRuns,
|
||||||
|
|
@ -457,22 +461,22 @@ export const GlobalTaskList = memo(function GlobalTaskList({
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
viewMode === 'grouped' &&
|
viewMode === 'grouped' &&
|
||||||
repositoryGroups.length === 0 &&
|
!repositoryGroupsInitialized &&
|
||||||
!repositoryGroupsLoading &&
|
!repositoryGroupsLoading &&
|
||||||
!repositoryGroupsError
|
!repositoryGroupsError
|
||||||
) {
|
) {
|
||||||
void fetchRepositoryGroups();
|
void fetchRepositoryGroups();
|
||||||
} else if (viewMode === 'flat' && projects.length === 0 && !projectsLoading && !projectsError) {
|
} else if (viewMode === 'flat' && !projectsInitialized && !projectsLoading && !projectsError) {
|
||||||
void fetchProjects();
|
void fetchProjects();
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
fetchProjects,
|
fetchProjects,
|
||||||
fetchRepositoryGroups,
|
fetchRepositoryGroups,
|
||||||
projects.length,
|
|
||||||
projectsError,
|
projectsError,
|
||||||
|
projectsInitialized,
|
||||||
projectsLoading,
|
projectsLoading,
|
||||||
repositoryGroups.length,
|
|
||||||
repositoryGroupsError,
|
repositoryGroupsError,
|
||||||
|
repositoryGroupsInitialized,
|
||||||
repositoryGroupsLoading,
|
repositoryGroupsLoading,
|
||||||
viewMode,
|
viewMode,
|
||||||
]);
|
]);
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ export interface ProjectSlice {
|
||||||
projects: Project[];
|
projects: Project[];
|
||||||
selectedProjectId: string | null;
|
selectedProjectId: string | null;
|
||||||
projectsLoading: boolean;
|
projectsLoading: boolean;
|
||||||
|
projectsInitialized: boolean;
|
||||||
projectsError: string | null;
|
projectsError: string | null;
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
|
|
@ -35,6 +36,7 @@ export const createProjectSlice: StateCreator<AppState, [], [], ProjectSlice> =
|
||||||
projects: [],
|
projects: [],
|
||||||
selectedProjectId: null,
|
selectedProjectId: null,
|
||||||
projectsLoading: false,
|
projectsLoading: false,
|
||||||
|
projectsInitialized: false,
|
||||||
projectsError: null,
|
projectsError: null,
|
||||||
|
|
||||||
// Fetch all projects from main process
|
// Fetch all projects from main process
|
||||||
|
|
@ -48,7 +50,7 @@ export const createProjectSlice: StateCreator<AppState, [], [], ProjectSlice> =
|
||||||
const sorted = [...projects].sort(
|
const sorted = [...projects].sort(
|
||||||
(a, b) => (b.mostRecentSession ?? 0) - (a.mostRecentSession ?? 0)
|
(a, b) => (b.mostRecentSession ?? 0) - (a.mostRecentSession ?? 0)
|
||||||
);
|
);
|
||||||
set({ projects: sorted, projectsLoading: false });
|
set({ projects: sorted, projectsLoading: false, projectsInitialized: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
set({
|
set({
|
||||||
projectsError: error instanceof Error ? error.message : 'Failed to fetch projects',
|
projectsError: error instanceof Error ? error.message : 'Failed to fetch projects',
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@ export interface RepositorySlice {
|
||||||
selectedRepositoryId: string | null;
|
selectedRepositoryId: string | null;
|
||||||
selectedWorktreeId: string | null;
|
selectedWorktreeId: string | null;
|
||||||
repositoryGroupsLoading: boolean;
|
repositoryGroupsLoading: boolean;
|
||||||
|
repositoryGroupsInitialized: boolean;
|
||||||
repositoryGroupsError: string | null;
|
repositoryGroupsError: string | null;
|
||||||
viewMode: 'flat' | 'grouped';
|
viewMode: 'flat' | 'grouped';
|
||||||
|
|
||||||
|
|
@ -62,6 +63,7 @@ export const createRepositorySlice: StateCreator<AppState, [], [], RepositorySli
|
||||||
selectedRepositoryId: null,
|
selectedRepositoryId: null,
|
||||||
selectedWorktreeId: null,
|
selectedWorktreeId: null,
|
||||||
repositoryGroupsLoading: false,
|
repositoryGroupsLoading: false,
|
||||||
|
repositoryGroupsInitialized: false,
|
||||||
repositoryGroupsError: null,
|
repositoryGroupsError: null,
|
||||||
viewMode: 'grouped', // Default to grouped view
|
viewMode: 'grouped', // Default to grouped view
|
||||||
|
|
||||||
|
|
@ -78,7 +80,11 @@ export const createRepositorySlice: StateCreator<AppState, [], [], RepositorySli
|
||||||
'get-repository-groups'
|
'get-repository-groups'
|
||||||
);
|
);
|
||||||
// Already sorted by most recent session in the scanner
|
// Already sorted by most recent session in the scanner
|
||||||
set({ repositoryGroups: groups, repositoryGroupsLoading: false });
|
set({
|
||||||
|
repositoryGroups: groups,
|
||||||
|
repositoryGroupsLoading: false,
|
||||||
|
repositoryGroupsInitialized: true,
|
||||||
|
});
|
||||||
const ms = Date.now() - startedAt;
|
const ms = Date.now() - startedAt;
|
||||||
if (ms >= 2000) {
|
if (ms >= 2000) {
|
||||||
logger.warn(`fetchRepositoryGroups slow ms=${ms} count=${groups.length}`);
|
logger.warn(`fetchRepositoryGroups slow ms=${ms} count=${groups.length}`);
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ interface StoreState {
|
||||||
softDeleteTask: ReturnType<typeof vi.fn>;
|
softDeleteTask: ReturnType<typeof vi.fn>;
|
||||||
projects: { path: string; name: string; sessions: unknown[]; totalSessions?: number }[];
|
projects: { path: string; name: string; sessions: unknown[]; totalSessions?: number }[];
|
||||||
projectsLoading: boolean;
|
projectsLoading: boolean;
|
||||||
|
projectsInitialized: boolean;
|
||||||
projectsError: string | null;
|
projectsError: string | null;
|
||||||
viewMode: 'flat' | 'grouped';
|
viewMode: 'flat' | 'grouped';
|
||||||
repositoryGroups: {
|
repositoryGroups: {
|
||||||
|
|
@ -24,6 +25,7 @@ interface StoreState {
|
||||||
worktrees: { path: string }[];
|
worktrees: { path: string }[];
|
||||||
}[];
|
}[];
|
||||||
repositoryGroupsLoading: boolean;
|
repositoryGroupsLoading: boolean;
|
||||||
|
repositoryGroupsInitialized: boolean;
|
||||||
repositoryGroupsError: string | null;
|
repositoryGroupsError: string | null;
|
||||||
teams: (Pick<TeamSummary, 'teamName' | 'displayName'> & Partial<TeamSummary>)[];
|
teams: (Pick<TeamSummary, 'teamName' | 'displayName'> & Partial<TeamSummary>)[];
|
||||||
provisioningRuns: Record<string, { state: string; runId: string; updatedAt: string }>;
|
provisioningRuns: Record<string, { state: string; runId: string; updatedAt: string }>;
|
||||||
|
|
@ -216,10 +218,12 @@ describe('GlobalTaskList project grouping', () => {
|
||||||
storeState.softDeleteTask = vi.fn(() => Promise.resolve(undefined));
|
storeState.softDeleteTask = vi.fn(() => Promise.resolve(undefined));
|
||||||
storeState.projects = [];
|
storeState.projects = [];
|
||||||
storeState.projectsLoading = false;
|
storeState.projectsLoading = false;
|
||||||
|
storeState.projectsInitialized = false;
|
||||||
storeState.projectsError = null;
|
storeState.projectsError = null;
|
||||||
storeState.viewMode = 'flat';
|
storeState.viewMode = 'flat';
|
||||||
storeState.repositoryGroups = [];
|
storeState.repositoryGroups = [];
|
||||||
storeState.repositoryGroupsLoading = false;
|
storeState.repositoryGroupsLoading = false;
|
||||||
|
storeState.repositoryGroupsInitialized = false;
|
||||||
storeState.repositoryGroupsError = null;
|
storeState.repositoryGroupsError = null;
|
||||||
storeState.teams = [{ teamName: 'alpha-team', displayName: 'Alpha Team' }];
|
storeState.teams = [{ teamName: 'alpha-team', displayName: 'Alpha Team' }];
|
||||||
storeState.provisioningRuns = {};
|
storeState.provisioningRuns = {};
|
||||||
|
|
@ -310,6 +314,43 @@ describe('GlobalTaskList project grouping', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not refetch repository groups after an empty grouped result is initialized', async () => {
|
||||||
|
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
||||||
|
storeState.viewMode = 'grouped';
|
||||||
|
|
||||||
|
const host = document.createElement('div');
|
||||||
|
document.body.appendChild(host);
|
||||||
|
const root = createRoot(host);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
root.render(React.createElement(GlobalTaskList));
|
||||||
|
await flushMicrotasks();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(storeState.fetchRepositoryGroups).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
storeState.repositoryGroupsLoading = true;
|
||||||
|
await act(async () => {
|
||||||
|
notifyStoreUpdate();
|
||||||
|
await flushMicrotasks();
|
||||||
|
});
|
||||||
|
|
||||||
|
storeState.repositoryGroupsLoading = false;
|
||||||
|
storeState.repositoryGroupsInitialized = true;
|
||||||
|
storeState.repositoryGroups = [];
|
||||||
|
await act(async () => {
|
||||||
|
notifyStoreUpdate();
|
||||||
|
await flushMicrotasks();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(storeState.fetchRepositoryGroups).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
root.unmount();
|
||||||
|
await flushMicrotasks();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('shows five tasks first, then expands and collapses with Show more and Show less', async () => {
|
it('shows five tasks first, then expands and collapses with Show more and Show less', async () => {
|
||||||
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
||||||
storeState.globalTasks = Array.from({ length: 6 }, (_, index) => makeTask(index + 1));
|
storeState.globalTasks = Array.from({ length: 6 }, (_, index) => makeTask(index + 1));
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue