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
|
||||
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
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
|
|
@ -336,6 +339,9 @@ jobs:
|
|||
with:
|
||||
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
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
|
|
@ -452,6 +458,9 @@ jobs:
|
|||
with:
|
||||
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
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
|
|
@ -573,6 +582,9 @@ jobs:
|
|||
sudo apt-get update
|
||||
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
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
|
|
|
|||
|
|
@ -927,7 +927,7 @@ let shutdownComplete = false;
|
|||
const startupTimers = new Set<ReturnType<typeof setTimeout>>();
|
||||
|
||||
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_BACKGROUND_SERVICE_DELAY_MS = 5_000;
|
||||
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
|
||||
// when the centralized init chain has already started a fetch.
|
||||
const repositoryGroupsLoading = useStore((s) => s.repositoryGroupsLoading);
|
||||
const repositoryGroupsInitialized = useStore((s) => s.repositoryGroupsInitialized);
|
||||
const repositoryGroupsError = useStore((s) => s.repositoryGroupsError);
|
||||
const projectsLoading = useStore((s) => s.projectsLoading);
|
||||
const projectsInitialized = useStore((s) => s.projectsInitialized);
|
||||
const projectsError = useStore((s) => s.projectsError);
|
||||
useEffect(() => {
|
||||
if (
|
||||
viewMode === 'grouped' &&
|
||||
repositoryGroups.length === 0 &&
|
||||
!repositoryGroupsInitialized &&
|
||||
!repositoryGroupsLoading &&
|
||||
!repositoryGroupsError
|
||||
) {
|
||||
void fetchRepositoryGroups();
|
||||
} else if (viewMode === 'flat' && projects.length === 0 && !projectsLoading && !projectsError) {
|
||||
} else if (viewMode === 'flat' && !projectsInitialized && !projectsLoading && !projectsError) {
|
||||
void fetchProjects();
|
||||
}
|
||||
}, [
|
||||
viewMode,
|
||||
repositoryGroups.length,
|
||||
projects.length,
|
||||
repositoryGroupsInitialized,
|
||||
projectsInitialized,
|
||||
repositoryGroupsLoading,
|
||||
repositoryGroupsError,
|
||||
projectsLoading,
|
||||
|
|
|
|||
|
|
@ -206,10 +206,12 @@ export const GlobalTaskList = memo(function GlobalTaskList({
|
|||
softDeleteTask,
|
||||
projects,
|
||||
projectsLoading,
|
||||
projectsInitialized,
|
||||
projectsError,
|
||||
viewMode,
|
||||
repositoryGroups,
|
||||
repositoryGroupsLoading,
|
||||
repositoryGroupsInitialized,
|
||||
repositoryGroupsError,
|
||||
teams,
|
||||
provisioningRuns,
|
||||
|
|
@ -226,10 +228,12 @@ export const GlobalTaskList = memo(function GlobalTaskList({
|
|||
softDeleteTask: s.softDeleteTask,
|
||||
projects: s.projects,
|
||||
projectsLoading: s.projectsLoading,
|
||||
projectsInitialized: s.projectsInitialized,
|
||||
projectsError: s.projectsError,
|
||||
viewMode: s.viewMode,
|
||||
repositoryGroups: s.repositoryGroups,
|
||||
repositoryGroupsLoading: s.repositoryGroupsLoading,
|
||||
repositoryGroupsInitialized: s.repositoryGroupsInitialized,
|
||||
repositoryGroupsError: s.repositoryGroupsError,
|
||||
teams: s.teams,
|
||||
provisioningRuns: s.provisioningRuns,
|
||||
|
|
@ -457,22 +461,22 @@ export const GlobalTaskList = memo(function GlobalTaskList({
|
|||
useEffect(() => {
|
||||
if (
|
||||
viewMode === 'grouped' &&
|
||||
repositoryGroups.length === 0 &&
|
||||
!repositoryGroupsInitialized &&
|
||||
!repositoryGroupsLoading &&
|
||||
!repositoryGroupsError
|
||||
) {
|
||||
void fetchRepositoryGroups();
|
||||
} else if (viewMode === 'flat' && projects.length === 0 && !projectsLoading && !projectsError) {
|
||||
} else if (viewMode === 'flat' && !projectsInitialized && !projectsLoading && !projectsError) {
|
||||
void fetchProjects();
|
||||
}
|
||||
}, [
|
||||
fetchProjects,
|
||||
fetchRepositoryGroups,
|
||||
projects.length,
|
||||
projectsError,
|
||||
projectsInitialized,
|
||||
projectsLoading,
|
||||
repositoryGroups.length,
|
||||
repositoryGroupsError,
|
||||
repositoryGroupsInitialized,
|
||||
repositoryGroupsLoading,
|
||||
viewMode,
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ export interface ProjectSlice {
|
|||
projects: Project[];
|
||||
selectedProjectId: string | null;
|
||||
projectsLoading: boolean;
|
||||
projectsInitialized: boolean;
|
||||
projectsError: string | null;
|
||||
|
||||
// Actions
|
||||
|
|
@ -35,6 +36,7 @@ export const createProjectSlice: StateCreator<AppState, [], [], ProjectSlice> =
|
|||
projects: [],
|
||||
selectedProjectId: null,
|
||||
projectsLoading: false,
|
||||
projectsInitialized: false,
|
||||
projectsError: null,
|
||||
|
||||
// Fetch all projects from main process
|
||||
|
|
@ -48,7 +50,7 @@ export const createProjectSlice: StateCreator<AppState, [], [], ProjectSlice> =
|
|||
const sorted = [...projects].sort(
|
||||
(a, b) => (b.mostRecentSession ?? 0) - (a.mostRecentSession ?? 0)
|
||||
);
|
||||
set({ projects: sorted, projectsLoading: false });
|
||||
set({ projects: sorted, projectsLoading: false, projectsInitialized: true });
|
||||
} catch (error) {
|
||||
set({
|
||||
projectsError: error instanceof Error ? error.message : 'Failed to fetch projects',
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ export interface RepositorySlice {
|
|||
selectedRepositoryId: string | null;
|
||||
selectedWorktreeId: string | null;
|
||||
repositoryGroupsLoading: boolean;
|
||||
repositoryGroupsInitialized: boolean;
|
||||
repositoryGroupsError: string | null;
|
||||
viewMode: 'flat' | 'grouped';
|
||||
|
||||
|
|
@ -62,6 +63,7 @@ export const createRepositorySlice: StateCreator<AppState, [], [], RepositorySli
|
|||
selectedRepositoryId: null,
|
||||
selectedWorktreeId: null,
|
||||
repositoryGroupsLoading: false,
|
||||
repositoryGroupsInitialized: false,
|
||||
repositoryGroupsError: null,
|
||||
viewMode: 'grouped', // Default to grouped view
|
||||
|
||||
|
|
@ -78,7 +80,11 @@ export const createRepositorySlice: StateCreator<AppState, [], [], RepositorySli
|
|||
'get-repository-groups'
|
||||
);
|
||||
// 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;
|
||||
if (ms >= 2000) {
|
||||
logger.warn(`fetchRepositoryGroups slow ms=${ms} count=${groups.length}`);
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ interface StoreState {
|
|||
softDeleteTask: ReturnType<typeof vi.fn>;
|
||||
projects: { path: string; name: string; sessions: unknown[]; totalSessions?: number }[];
|
||||
projectsLoading: boolean;
|
||||
projectsInitialized: boolean;
|
||||
projectsError: string | null;
|
||||
viewMode: 'flat' | 'grouped';
|
||||
repositoryGroups: {
|
||||
|
|
@ -24,6 +25,7 @@ interface StoreState {
|
|||
worktrees: { path: string }[];
|
||||
}[];
|
||||
repositoryGroupsLoading: boolean;
|
||||
repositoryGroupsInitialized: boolean;
|
||||
repositoryGroupsError: string | null;
|
||||
teams: (Pick<TeamSummary, 'teamName' | 'displayName'> & Partial<TeamSummary>)[];
|
||||
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.projects = [];
|
||||
storeState.projectsLoading = false;
|
||||
storeState.projectsInitialized = false;
|
||||
storeState.projectsError = null;
|
||||
storeState.viewMode = 'flat';
|
||||
storeState.repositoryGroups = [];
|
||||
storeState.repositoryGroupsLoading = false;
|
||||
storeState.repositoryGroupsInitialized = false;
|
||||
storeState.repositoryGroupsError = null;
|
||||
storeState.teams = [{ teamName: 'alpha-team', displayName: 'Alpha Team' }];
|
||||
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 () => {
|
||||
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
||||
storeState.globalTasks = Array.from({ length: 6 }, (_, index) => makeTask(index + 1));
|
||||
|
|
|
|||
Loading…
Reference in a new issue