diff --git a/apps/ui/src/components/dialogs/project-path-validation-dialog.tsx b/apps/ui/src/components/dialogs/project-path-validation-dialog.tsx new file mode 100644 index 000000000..896807c39 --- /dev/null +++ b/apps/ui/src/components/dialogs/project-path-validation-dialog.tsx @@ -0,0 +1,129 @@ +import { FolderX, RefreshCw, Trash2, AlertTriangle } from 'lucide-react'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { Button } from '@/components/ui/button'; +import { HotkeyButton } from '@/components/ui/hotkey-button'; +import type { Project } from '@/lib/electron'; +import { useFileBrowser } from '@/contexts/file-browser-context'; + +interface ProjectPathValidationDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + project: Project | null; + onRefreshPath: (project: Project, newPath: string) => Promise; + onRemoveProject: (project: Project) => void; + onDismiss?: () => void; +} + +export function ProjectPathValidationDialog({ + open, + onOpenChange, + project, + onRefreshPath, + onRemoveProject, + onDismiss, +}: ProjectPathValidationDialogProps) { + const { openFileBrowser } = useFileBrowser(); + + const handleRefreshPath = async () => { + if (!project) return; + + const newPath = await openFileBrowser({ + title: 'Select New Project Location', + description: 'Choose the new directory for this project', + initialPath: project.path, + }); + + if (!newPath) { + // User cancelled - stay on dialog + return; + } + + await onRefreshPath(project, newPath); + }; + + const handleRemoveProject = () => { + if (!project) return; + onRemoveProject(project); + onOpenChange(false); + }; + + const handleDismiss = () => { + onDismiss?.(); + onOpenChange(false); + }; + + return ( + + e.preventDefault()} + showCloseButton={false} + > + +
+
+ +
+ Project Path Not Found +
+ + The project directory cannot be found at its saved location. + +
+ + {project && ( +
+
+
+ +
+

{project.name}

+

+ {project.path} +

+
+
+
+ +

+ Select the new location if it was moved, or remove it from your list. +

+
+ )} + + + + + + + Locate Project + + +
+
+ ); +} diff --git a/apps/ui/src/components/layout/sidebar.tsx b/apps/ui/src/components/layout/sidebar.tsx index a1d03e87a..340140abd 100644 --- a/apps/ui/src/components/layout/sidebar.tsx +++ b/apps/ui/src/components/layout/sidebar.tsx @@ -6,6 +6,7 @@ const logger = createLogger('Sidebar'); import { cn } from '@/lib/utils'; import { useAppStore, type ThemeMode } from '@/store/app-store'; import { useKeyboardShortcuts, useKeyboardShortcutsConfig } from '@/hooks/use-keyboard-shortcuts'; +import { useValidatedProjectCycling } from '@/hooks/use-validated-project-cycling'; import { getElectronAPI } from '@/lib/electron'; import { initializeProject, hasAppSpec, hasAutomakerDir } from '@/lib/project-init'; import { toast } from 'sonner'; @@ -51,8 +52,6 @@ export function Sidebar() { restoreTrashedProject, deleteTrashedProject, emptyTrash, - cyclePrevProject, - cycleNextProject, moveProjectToTrash, specCreatingForProject, setSpecCreatingForProject, @@ -74,6 +73,9 @@ export function Sidebar() { // State for trash dialog const [showTrashDialog, setShowTrashDialog] = useState(false); + // Validated project cycling (automatically skips invalid paths) + const { cyclePrevProject, cycleNextProject } = useValidatedProjectCycling(); + // Project theme management (must come before useProjectCreation which uses globalTheme) const { globalTheme } = useProjectTheme(); @@ -148,6 +150,7 @@ export function Sidebar() { restoreTrashedProject, deleteTrashedProject, emptyTrash, + trashedProjects, }); // Spec regeneration events diff --git a/apps/ui/src/components/layout/sidebar/components/project-selector-with-options.tsx b/apps/ui/src/components/layout/sidebar/components/project-selector-with-options.tsx index b33a7b6d9..613fcf7f4 100644 --- a/apps/ui/src/components/layout/sidebar/components/project-selector-with-options.tsx +++ b/apps/ui/src/components/layout/sidebar/components/project-selector-with-options.tsx @@ -33,6 +33,10 @@ import { SortableProjectItem, ThemeMenuItem } from './'; import { PROJECT_DARK_THEMES, PROJECT_LIGHT_THEMES } from '../constants'; import { useProjectPicker, useDragAndDrop, useProjectTheme } from '../hooks'; import { useKeyboardShortcutsConfig } from '@/hooks/use-keyboard-shortcuts'; +import { useValidatedProjectCycling } from '@/hooks/use-validated-project-cycling'; +import { useProjectPathValidation } from '@/hooks/use-project-path-validation'; +import { validateProjectPath } from '@/lib/validate-project-path'; +import { ProjectPathValidationDialog } from '@/components/dialogs/project-path-validation-dialog'; interface ProjectSelectorWithOptionsProps { sidebarOpen: boolean; @@ -53,12 +57,22 @@ export function ProjectSelectorWithOptions({ projectHistory, setCurrentProject, reorderProjects, - cyclePrevProject, - cycleNextProject, clearProjectHistory, } = useAppStore(); const shortcuts = useKeyboardShortcutsConfig(); + + const { + validationDialogOpen, + setValidationDialogOpen, + invalidProject, + showValidationDialog, + handleRefreshPath, + handleRemoveProject, + } = useProjectPathValidation({ navigateOnRefresh: true }); + + // Validated project cycling (automatically skips invalid paths) + const { cyclePrevProject, cycleNextProject } = useValidatedProjectCycling(); const { projectSearchQuery, setProjectSearchQuery, @@ -182,9 +196,17 @@ export function ProjectSelectorWithOptions({ project={project} currentProjectId={currentProject?.id} isHighlighted={index === selectedProjectIndex} - onSelect={(p) => { - setCurrentProject(p); + onSelect={async (p) => { setIsProjectPickerOpen(false); + + // Validate path before switching + const isValid = await validateProjectPath(p); + + if (!isValid) { + showValidationDialog(p); + } else { + setCurrentProject(p); + } }} /> ))} @@ -367,6 +389,14 @@ export function ProjectSelectorWithOptions({ )} + + ); } diff --git a/apps/ui/src/components/layout/sidebar/hooks/use-trash-operations.ts b/apps/ui/src/components/layout/sidebar/hooks/use-trash-operations.ts index d539ddbea..bcd31941b 100644 --- a/apps/ui/src/components/layout/sidebar/hooks/use-trash-operations.ts +++ b/apps/ui/src/components/layout/sidebar/hooks/use-trash-operations.ts @@ -4,24 +4,43 @@ import { toast } from 'sonner'; const logger = createLogger('TrashOperations'); import { getElectronAPI, type TrashedProject } from '@/lib/electron'; +import { validateProjectPath } from '@/lib/validate-project-path'; interface UseTrashOperationsProps { restoreTrashedProject: (projectId: string) => void; deleteTrashedProject: (projectId: string) => void; emptyTrash: () => void; + trashedProjects: TrashedProject[]; } export function useTrashOperations({ restoreTrashedProject, deleteTrashedProject, emptyTrash, + trashedProjects, }: UseTrashOperationsProps) { const [activeTrashId, setActiveTrashId] = useState(null); const [isEmptyingTrash, setIsEmptyingTrash] = useState(false); const handleRestoreProject = useCallback( - (projectId: string) => { + async (projectId: string) => { try { + const project = trashedProjects.find((p) => p.id === projectId); + if (!project) { + toast.error('Project not found'); + return; + } + + // Validate project path before restoring + const isValid = await validateProjectPath(project); + if (!isValid) { + toast.error('Project path not found', { + description: + 'The project directory no longer exists. Remove it from the recycle bin and re-add the project from its new location.', + }); + return; + } + restoreTrashedProject(projectId); toast.success('Project restored', { description: 'Added back to your project list.', @@ -33,7 +52,7 @@ export function useTrashOperations({ }); } }, - [restoreTrashedProject] + [restoreTrashedProject, trashedProjects] ); const handleDeleteProjectFromDisk = useCallback( diff --git a/apps/ui/src/hooks/index.ts b/apps/ui/src/hooks/index.ts index 8a354b3d9..09050f508 100644 --- a/apps/ui/src/hooks/index.ts +++ b/apps/ui/src/hooks/index.ts @@ -9,3 +9,7 @@ export { useResponsiveKanban } from './use-responsive-kanban'; export { useScrollTracking } from './use-scroll-tracking'; export { useSettingsMigration } from './use-settings-migration'; export { useWindowState } from './use-window-state'; +export { useStoreHydration } from './use-store-hydration'; +export { useValidatedProjectCycling } from './use-validated-project-cycling'; +export { useProjectRestoration } from './use-project-restoration'; +export { useProjectPathValidation } from './use-project-path-validation'; diff --git a/apps/ui/src/hooks/use-project-path-validation.ts b/apps/ui/src/hooks/use-project-path-validation.ts new file mode 100644 index 000000000..71c17e815 --- /dev/null +++ b/apps/ui/src/hooks/use-project-path-validation.ts @@ -0,0 +1,94 @@ +import { useState, useCallback } from 'react'; +import { useNavigate } from '@tanstack/react-router'; +import { toast } from 'sonner'; +import type { Project } from '@/lib/electron'; +import { useAppStore } from '@/store/app-store'; +import { validateProjectPath } from '@/lib/validate-project-path'; + +interface UseProjectPathValidationOptions { + /** + * Whether to navigate to /board after successful path refresh. + * Defaults to true. + */ + navigateOnRefresh?: boolean; +} + +export function useProjectPathValidation(options: UseProjectPathValidationOptions = {}) { + const { navigateOnRefresh = true } = options; + const navigate = useNavigate(); + const { projects, setProjects, setCurrentProject, removeProject } = useAppStore(); + + const [validationDialogOpen, setValidationDialogOpen] = useState(false); + const [invalidProject, setInvalidProject] = useState(null); + + const showValidationDialog = useCallback((project: Project) => { + setInvalidProject(project); + setValidationDialogOpen(true); + }, []); + + const handleRefreshPath = useCallback( + async (project: Project, newPath: string) => { + try { + // Validate new path + const isValid = await validateProjectPath({ ...project, path: newPath }); + + if (!isValid) { + toast.error('Invalid path', { + description: 'Selected path does not exist or is not accessible', + }); + return; // Stay on dialog + } + + // Update project in store + const updatedProject = { ...project, path: newPath, lastOpened: new Date().toISOString() }; + const updatedProjects = projects.map((p) => (p.id === project.id ? updatedProject : p)); + setProjects(updatedProjects); + + // Update current project reference + setCurrentProject(updatedProject); + + // Close dialog + setValidationDialogOpen(false); + + // Navigate to board if requested + if (navigateOnRefresh) { + navigate({ to: '/board' }); + } + + toast.success('Project path updated'); + } catch (error) { + console.error('Failed to update project path:', error); + toast.error('Failed to update path', { + description: 'An unexpected error occurred. Please try again.', + }); + } + }, + [projects, setProjects, setCurrentProject, navigate, navigateOnRefresh] + ); + + const handleRemoveProject = useCallback( + (project: Project) => { + removeProject(project.id); + setCurrentProject(null); + setValidationDialogOpen(false); + navigate({ to: '/' }); + toast.info('Project removed', { description: project.name }); + }, + [removeProject, setCurrentProject, navigate] + ); + + const handleDismiss = useCallback(() => { + setCurrentProject(null); + setValidationDialogOpen(false); + }, [setCurrentProject]); + + return { + validationDialogOpen, + setValidationDialogOpen, + invalidProject, + showValidationDialog, + handleRefreshPath, + handleRemoveProject, + handleDismiss, + }; +} diff --git a/apps/ui/src/hooks/use-project-restoration.ts b/apps/ui/src/hooks/use-project-restoration.ts new file mode 100644 index 000000000..58b89aa2a --- /dev/null +++ b/apps/ui/src/hooks/use-project-restoration.ts @@ -0,0 +1,86 @@ +import { useEffect, useCallback, useRef } from 'react'; +import { useNavigate } from '@tanstack/react-router'; +import { isElectronMode } from '@/lib/http-api-client'; +import { getElectronAPI, Project } from '@/lib/electron'; +import { validateProjectPath } from '@/lib/validate-project-path'; + +interface UseProjectRestorationOptions { + isMounted: boolean; + currentProject: Project | null; + currentPathname: string; + isAuthenticated: boolean; + authChecked: boolean; + appHydrated: boolean; + onShowValidationDialog: (project: Project) => void; +} + +/** + * Hook for restoring project on app initialization. + * Validates the project path and navigates to board if valid, + * or shows validation dialog if path is invalid. + */ +export function useProjectRestoration(options: UseProjectRestorationOptions) { + const { + isMounted, + currentProject, + currentPathname, + isAuthenticated, + authChecked, + appHydrated, + onShowValidationDialog, + } = options; + + const navigate = useNavigate(); + const isValidatingRef = useRef(false); + + const validateAndRestore = useCallback( + async (shouldCheckIpc: boolean) => { + isValidatingRef.current = true; + try { + // Electron-specific: check IPC connection first + if (shouldCheckIpc) { + const api = getElectronAPI(); + const result = await api.ping(); + if (result !== 'pong') { + console.log('IPC not connected, skipping project restoration'); + return; + } + } + + // Validate project path before restoring + const isValid = await validateProjectPath(currentProject!); + if (!isValid) { + console.log('Project path is invalid, showing validation dialog'); + onShowValidationDialog(currentProject!); + return; + } + + navigate({ to: '/board' }); + } catch (error) { + console.error('Failed to validate project or restore:', error); + } finally { + isValidatingRef.current = false; + } + }, + [currentProject, navigate, onShowValidationDialog] + ); + + useEffect(() => { + if (!isMounted || !currentProject) return; + // Only restore when on the root/welcome page + if (currentPathname !== '/') return; + if (!authChecked || !appHydrated) return; + if (!isElectronMode() && !isAuthenticated) return; + if (isValidatingRef.current) return; + + validateAndRestore(isElectronMode()); + }, [ + isMounted, + currentProject, + currentPathname, + authChecked, + appHydrated, + isAuthenticated, + validateAndRestore, + ]); +} diff --git a/apps/ui/src/hooks/use-store-hydration.ts b/apps/ui/src/hooks/use-store-hydration.ts new file mode 100644 index 000000000..d38e63e0f --- /dev/null +++ b/apps/ui/src/hooks/use-store-hydration.ts @@ -0,0 +1,36 @@ +import { useEffect, useState } from 'react'; + +interface StoreWithPersist { + persist?: { + hasHydrated?: () => boolean; + onFinishHydration?: (cb: () => void) => () => void; + }; +} + +/** + * Hook for tracking store hydration status from persistence. + * + * Monitors whether a persisted store has finished loading from storage. + * Returns true once the store has completed hydration, false otherwise. + * + * @param store - The store instance with persist configuration + * @returns Boolean indicating if the store has finished hydrating + */ +export function useStoreHydration(store: StoreWithPersist) { + const [hydrated, setHydrated] = useState(() => store.persist?.hasHydrated?.() ?? false); + + useEffect(() => { + if (store.persist?.hasHydrated?.()) { + setHydrated(true); + return; + } + + const unsubscribe = store.persist?.onFinishHydration?.(() => { + setHydrated(true); + }); + + return () => unsubscribe?.(); + }, [store]); + + return hydrated; +} diff --git a/apps/ui/src/hooks/use-validated-project-cycling.ts b/apps/ui/src/hooks/use-validated-project-cycling.ts new file mode 100644 index 000000000..8292bdce2 --- /dev/null +++ b/apps/ui/src/hooks/use-validated-project-cycling.ts @@ -0,0 +1,109 @@ +import { useCallback, useState } from 'react'; +import { useNavigate } from '@tanstack/react-router'; +import { useAppStore } from '@/store/app-store'; +import { validateProjectPath } from '@/lib/validate-project-path'; + +/** + * Hook for cycling through project history with path validation. + * + * Provides functions to navigate to the previous or next project in the history, + * automatically validating each project's path before switching. Invalid projects + * are silently skipped (no dialog shown). Only projects that exist in the store + * are considered. When a valid project is found, it switches to that project + * and navigates to the '/board' route. + * + * @returns An object containing: + * - `cyclePrevProject`: Function to cycle to the previous project in history + * - `cycleNextProject`: Function to cycle to the next project in history + * - `isValidating`: Boolean indicating if a validation/cycling operation is in progress + */ +export function useValidatedProjectCycling() { + const navigate = useNavigate(); + const { projects, projectHistory, currentProject } = useAppStore(); + const [isValidating, setIsValidating] = useState(false); + + // Helper to switch project without reordering history (mirrors store's cycling behavior) + const switchProjectForCycling = useCallback( + (project: typeof currentProject, validHistory: string[], newIndex: number) => { + useAppStore.setState({ + currentProject: project, + projectHistory: validHistory, + projectHistoryIndex: newIndex, + currentView: 'board' as const, + }); + }, + [] + ); + + // Shared helper to cycle through projects in a given direction + const cycleProject = useCallback( + async (direction: 'prev' | 'next') => { + if (isValidating || projectHistory.length <= 1) return; + + setIsValidating(true); + try { + // Filter history to only include projects that exist in the store + const validHistory = projectHistory.filter((id) => projects.some((p) => p.id === id)); + + if (validHistory.length <= 1) return; + + // Find current position in valid history + const currentProjectId = currentProject?.id; + let currentIndex = currentProjectId ? validHistory.indexOf(currentProjectId) : 0; + + if (currentIndex === -1) currentIndex = 0; + + // Try cycling through projects until we find a valid one + let attempts = 0; + const maxAttempts = validHistory.length; + + while (attempts < maxAttempts) { + // Calculate new index based on direction + const newIndex = + direction === 'prev' + ? (currentIndex + 1 + attempts) % validHistory.length + : (((currentIndex - 1 - attempts) % validHistory.length) + validHistory.length) % + validHistory.length; + + const targetProjectId = validHistory[newIndex]; + const targetProject = projects.find((p) => p.id === targetProjectId); + + if (targetProject && targetProject.id !== currentProject?.id) { + // Validate the project path + const isValid = await validateProjectPath(targetProject); + + if (isValid) { + // Found a valid project - switch to it without reordering history + switchProjectForCycling(targetProject, validHistory, newIndex); + navigate({ to: '/board' }); + return; + } + // If invalid, just skip to the next one (no dialog) + } + + attempts++; + } + + // No valid projects found in history + console.warn('No valid projects found in history'); + } finally { + setIsValidating(false); + } + }, + [isValidating, projectHistory, projects, currentProject, switchProjectForCycling, navigate] + ); + + const cyclePrevProject = useCallback(async () => { + await cycleProject('prev'); + }, [cycleProject]); + + const cycleNextProject = useCallback(async () => { + await cycleProject('next'); + }, [cycleProject]); + + return { + cyclePrevProject, + cycleNextProject, + isValidating, + }; +} diff --git a/apps/ui/src/lib/validate-project-path.ts b/apps/ui/src/lib/validate-project-path.ts new file mode 100644 index 000000000..383a2bb00 --- /dev/null +++ b/apps/ui/src/lib/validate-project-path.ts @@ -0,0 +1,34 @@ +import type { Project } from '@/lib/electron'; +import { getElectronAPI } from './electron'; + +export const validateProjectPath = async (project: Project): Promise => { + try { + if (!project?.path) { + console.error('[Validation] No project path provided for project:', project?.name); + return false; + } + + const api = getElectronAPI(); + // Check if path exists + const exists = await api.exists(project.path); + + if (exists !== true) { + console.error('[Validation] Path does not exist:', project.path); + return false; + } + + // Verify it's a directory + const statResult = await api.stat(project.path); + + if (!statResult.success || !statResult.stats?.isDirectory) { + console.error('[Validation] Path is not a directory or stat failed:', project.path); + return false; + } + + return true; + } catch (error) { + // Treat errors as invalid (permissions, network issues, etc.) + console.error('[Validation] Exception during validation for path:', project.path, error); + return false; + } +}; diff --git a/apps/ui/src/routes/__root.tsx b/apps/ui/src/routes/__root.tsx index 34dbd00e5..d0d647c55 100644 --- a/apps/ui/src/routes/__root.tsx +++ b/apps/ui/src/routes/__root.tsx @@ -23,6 +23,8 @@ import { Toaster } from 'sonner'; import { ThemeOption, themeOptions } from '@/config/theme-options'; import { SandboxRiskDialog } from '@/components/dialogs/sandbox-risk-dialog'; import { SandboxRejectionScreen } from '@/components/dialogs/sandbox-rejection-screen'; +import { useStoreHydration, useProjectRestoration, useProjectPathValidation } from '@/hooks'; +import { ProjectPathValidationDialog } from '@/components/dialogs/project-path-validation-dialog'; import { LoadingState } from '@/components/ui/loading-state'; const logger = createLogger('RootLayout'); @@ -40,13 +42,22 @@ function RootLayoutContent() { const navigate = useNavigate(); const [isMounted, setIsMounted] = useState(false); const [streamerPanelOpen, setStreamerPanelOpen] = useState(false); - const [setupHydrated, setSetupHydrated] = useState( - () => useSetupStore.persist?.hasHydrated?.() ?? false - ); + const appHydrated = useStoreHydration(useAppStore); + const setupHydrated = useStoreHydration(useSetupStore); const authChecked = useAuthStore((s) => s.authChecked); const isAuthenticated = useAuthStore((s) => s.isAuthenticated); const { openFileBrowser } = useFileBrowser(); + const { + validationDialogOpen, + setValidationDialogOpen, + invalidProject, + showValidationDialog, + handleRefreshPath, + handleRemoveProject, + handleDismiss, + } = useProjectPathValidation(); + const isSetupRoute = location.pathname === '/setup'; const isLoginRoute = location.pathname === '/login'; @@ -217,24 +228,6 @@ function RootLayoutContent() { initAuth(); }, []); // Runs once per load; auth state drives routing rules - // Wait for setup store hydration before enforcing routing rules - useEffect(() => { - if (useSetupStore.persist?.hasHydrated?.()) { - setSetupHydrated(true); - return; - } - - const unsubscribe = useSetupStore.persist?.onFinishHydration?.(() => { - setSetupHydrated(true); - }); - - return () => { - if (typeof unsubscribe === 'function') { - unsubscribe(); - } - }; - }, []); - // Routing rules (web mode): // - If not authenticated: force /login (even /setup is protected) // - If authenticated but setup incomplete: force /setup @@ -294,12 +287,16 @@ function RootLayoutContent() { testConnection(); }, [setIpcConnected]); - // Restore to board view if a project was previously open - useEffect(() => { - if (isMounted && currentProject && location.pathname === '/') { - navigate({ to: '/board' }); - } - }, [isMounted, currentProject, location.pathname, navigate]); + // Restore to board view if a project was previously open (with path validation) + useProjectRestoration({ + isMounted, + currentProject, + currentPathname: location.pathname, + isAuthenticated, + authChecked, + appHydrated, + onShowValidationDialog: showValidationDialog, + }); // Apply theme class to document - use deferred value to avoid blocking UI useEffect(() => { @@ -416,6 +413,15 @@ function RootLayoutContent() { onConfirm={handleSandboxConfirm} onDeny={handleSandboxDeny} /> + + ); } diff --git a/apps/ui/test/open-project-test-11912-qnquemi/existing-project-1767375313170 b/apps/ui/test/open-project-test-11912-qnquemi/existing-project-1767375313170 new file mode 160000 index 000000000..edeb86075 --- /dev/null +++ b/apps/ui/test/open-project-test-11912-qnquemi/existing-project-1767375313170 @@ -0,0 +1 @@ +Subproject commit edeb8607577d1cc6d21fa111b12e0d9574b823a1 diff --git a/apps/ui/test/open-project-test-16928-of33ms5/existing-project-1767378717267 b/apps/ui/test/open-project-test-16928-of33ms5/existing-project-1767378717267 new file mode 160000 index 000000000..dcc01a2cb --- /dev/null +++ b/apps/ui/test/open-project-test-16928-of33ms5/existing-project-1767378717267 @@ -0,0 +1 @@ +Subproject commit dcc01a2cbac628f9b4d7d9ce1c5b83e2262de865 diff --git a/apps/ui/test/open-project-test-19976-jp8mdh7/existing-project-1767376763963 b/apps/ui/test/open-project-test-19976-jp8mdh7/existing-project-1767376763963 new file mode 160000 index 000000000..a28443a14 --- /dev/null +++ b/apps/ui/test/open-project-test-19976-jp8mdh7/existing-project-1767376763963 @@ -0,0 +1 @@ +Subproject commit a28443a14f1e8fc0425e6498f3df2bac322cd7e5 diff --git a/apps/ui/test/open-project-test-2196-crbaapd/existing-project-1767376984352 b/apps/ui/test/open-project-test-2196-crbaapd/existing-project-1767376984352 new file mode 160000 index 000000000..e59fdfb8d --- /dev/null +++ b/apps/ui/test/open-project-test-2196-crbaapd/existing-project-1767376984352 @@ -0,0 +1 @@ +Subproject commit e59fdfb8d94a9dd23b1e34b7334c22e017a63b2f diff --git a/apps/ui/test/open-project-test-22252-cx4okff/existing-project-1767376564217 b/apps/ui/test/open-project-test-22252-cx4okff/existing-project-1767376564217 new file mode 160000 index 000000000..135ff63da --- /dev/null +++ b/apps/ui/test/open-project-test-22252-cx4okff/existing-project-1767376564217 @@ -0,0 +1 @@ +Subproject commit 135ff63dacbd68941b31ff3f6b036837b606d502 diff --git a/apps/ui/test/open-project-test-22972-y50nfk2/existing-project-1767131748852 b/apps/ui/test/open-project-test-22972-y50nfk2/existing-project-1767131748852 new file mode 160000 index 000000000..5e4e954a3 --- /dev/null +++ b/apps/ui/test/open-project-test-22972-y50nfk2/existing-project-1767131748852 @@ -0,0 +1 @@ +Subproject commit 5e4e954a3849a0630669b79d2a2feea3366d217d diff --git a/apps/ui/test/open-project-test-26484-dpecj2a/existing-project-1767377101940 b/apps/ui/test/open-project-test-26484-dpecj2a/existing-project-1767377101940 new file mode 160000 index 000000000..aa9574ec7 --- /dev/null +++ b/apps/ui/test/open-project-test-26484-dpecj2a/existing-project-1767377101940 @@ -0,0 +1 @@ +Subproject commit aa9574ec7bfaa8a2e58c91789c7e78735caf05ad diff --git a/apps/ui/test/open-project-test-27820-s3u7u8f/existing-project-1767378607730 b/apps/ui/test/open-project-test-27820-s3u7u8f/existing-project-1767378607730 new file mode 160000 index 000000000..72a16b162 --- /dev/null +++ b/apps/ui/test/open-project-test-27820-s3u7u8f/existing-project-1767378607730 @@ -0,0 +1 @@ +Subproject commit 72a16b16245e5445bb5c320f0dd2862234748d9a diff --git a/apps/ui/test/open-project-test-30552-q3hxb4m/existing-project-1767381061681 b/apps/ui/test/open-project-test-30552-q3hxb4m/existing-project-1767381061681 new file mode 160000 index 000000000..a99b27c83 --- /dev/null +++ b/apps/ui/test/open-project-test-30552-q3hxb4m/existing-project-1767381061681 @@ -0,0 +1 @@ +Subproject commit a99b27c8373e10fe47a248b2298a8b31e443c9d4 diff --git a/apps/ui/test/open-project-test-30848-brjbw2y/existing-project-1767380513961 b/apps/ui/test/open-project-test-30848-brjbw2y/existing-project-1767380513961 new file mode 160000 index 000000000..3d33ace2a --- /dev/null +++ b/apps/ui/test/open-project-test-30848-brjbw2y/existing-project-1767380513961 @@ -0,0 +1 @@ +Subproject commit 3d33ace2aea2b5ad7c59894979e7e4008174bb95 diff --git a/apps/ui/test/open-project-test-31596-5j2k1yt/existing-project-1767380144090 b/apps/ui/test/open-project-test-31596-5j2k1yt/existing-project-1767380144090 new file mode 160000 index 000000000..8c0000e77 --- /dev/null +++ b/apps/ui/test/open-project-test-31596-5j2k1yt/existing-project-1767380144090 @@ -0,0 +1 @@ +Subproject commit 8c0000e77bb1cfd0d7cd37f5333d0c90852e8eeb diff --git a/apps/ui/test/open-project-test-41780-ap44slk/existing-project-1767379683580 b/apps/ui/test/open-project-test-41780-ap44slk/existing-project-1767379683580 new file mode 160000 index 000000000..8493fe25f --- /dev/null +++ b/apps/ui/test/open-project-test-41780-ap44slk/existing-project-1767379683580 @@ -0,0 +1 @@ +Subproject commit 8493fe25fbe788b22ccb741fc5035d957e19556c diff --git a/apps/ui/test/open-project-test-50844-qufix4x/existing-project-1767314132254 b/apps/ui/test/open-project-test-50844-qufix4x/existing-project-1767314132254 new file mode 160000 index 000000000..88fbb2f29 --- /dev/null +++ b/apps/ui/test/open-project-test-50844-qufix4x/existing-project-1767314132254 @@ -0,0 +1 @@ +Subproject commit 88fbb2f293880a4bf1dbfd987597d659700ac43d diff --git a/apps/ui/test/open-project-test-59004-kysgvsd/existing-project-1767381272587 b/apps/ui/test/open-project-test-59004-kysgvsd/existing-project-1767381272587 new file mode 160000 index 000000000..8d7f82fbf --- /dev/null +++ b/apps/ui/test/open-project-test-59004-kysgvsd/existing-project-1767381272587 @@ -0,0 +1 @@ +Subproject commit 8d7f82fbf4e7c5da099593988b1e5935dd7b5811 diff --git a/apps/ui/test/project-creation-test-38436-9y0nfgu/test-project-1767378602079 b/apps/ui/test/project-creation-test-38436-9y0nfgu/test-project-1767378602079 new file mode 160000 index 000000000..199d4e438 --- /dev/null +++ b/apps/ui/test/project-creation-test-38436-9y0nfgu/test-project-1767378602079 @@ -0,0 +1 @@ +Subproject commit 199d4e438f5592fb8409e8f22caa23833c4a4b4a diff --git a/apps/ui/test/project-creation-test-59016-0x7etyc/test-project-1767381056102 b/apps/ui/test/project-creation-test-59016-0x7etyc/test-project-1767381056102 new file mode 160000 index 000000000..a48192e86 --- /dev/null +++ b/apps/ui/test/project-creation-test-59016-0x7etyc/test-project-1767381056102 @@ -0,0 +1 @@ +Subproject commit a48192e86e90013fe6d5ab98511766591e0d7236 diff --git a/apps/ui/tests/profiles/profiles-crud.spec.ts b/apps/ui/tests/profiles/profiles-crud.spec.ts index 818d18279..41b85b569 100644 --- a/apps/ui/tests/profiles/profiles-crud.spec.ts +++ b/apps/ui/tests/profiles/profiles-crud.spec.ts @@ -5,6 +5,7 @@ */ import { test, expect } from '@playwright/test'; +import * as fs from 'fs'; import { setupMockProjectWithProfiles, waitForNetworkIdle, @@ -16,11 +17,31 @@ import { countCustomProfiles, authenticateForTests, handleLoginScreenIfPresent, + createTempDirPath, + cleanupTempDir, } from '../utils'; +// Create unique temp dir for this test run +const TEST_TEMP_DIR = createTempDirPath('profiles-test'); + test.describe('AI Profiles', () => { + test.beforeAll(async () => { + // Create test temp directory (required for project path validation) + if (!fs.existsSync(TEST_TEMP_DIR)) { + fs.mkdirSync(TEST_TEMP_DIR, { recursive: true }); + } + }); + + test.afterAll(async () => { + // Cleanup temp directory + cleanupTempDir(TEST_TEMP_DIR); + }); + test('should create a new profile', async ({ page }) => { - await setupMockProjectWithProfiles(page, { customProfilesCount: 0 }); + await setupMockProjectWithProfiles(page, { + customProfilesCount: 0, + projectPath: TEST_TEMP_DIR, + }); await authenticateForTests(page); await page.goto('/'); await page.waitForLoadState('load'); diff --git a/apps/ui/tests/utils/project/setup.ts b/apps/ui/tests/utils/project/setup.ts index dacbbc1ff..343083aef 100644 --- a/apps/ui/tests/utils/project/setup.ts +++ b/apps/ui/tests/utils/project/setup.ts @@ -779,13 +779,15 @@ export async function setupMockProjectWithProfiles( options?: { customProfilesCount?: number; includeBuiltIn?: boolean; + /** Real filesystem path for the project (required to pass path validation) */ + projectPath?: string; } ): Promise { await page.addInitScript((opts: typeof options) => { const mockProject = { id: 'test-project-1', name: 'Test Project', - path: '/mock/test-project', + path: opts?.projectPath || '/mock/test-project', lastOpened: new Date().toISOString(), };