diff --git a/archon-ui-main/.dockerignore b/archon-ui-main/.dockerignore
index bbae0365..9e1e2818 100644
--- a/archon-ui-main/.dockerignore
+++ b/archon-ui-main/.dockerignore
@@ -43,6 +43,16 @@ docker-compose.yml
# Tests
coverage
test-results
+tests/
+**/*.test.ts
+**/*.test.tsx
+**/*.spec.ts
+**/*.spec.tsx
+**/__tests__
+**/*.e2e.test.ts
+**/*.integration.test.ts
+vitest.config.ts
+tsconfig.prod.json
# Documentation
README.md
diff --git a/archon-ui-main/__mocks__/lucide-react.tsx b/archon-ui-main/__mocks__/lucide-react.tsx
deleted file mode 100644
index a3553fe1..00000000
--- a/archon-ui-main/__mocks__/lucide-react.tsx
+++ /dev/null
@@ -1,296 +0,0 @@
-import React from 'react'
-import { vi } from 'vitest'
-
-const createMockIcon = (name: string) => {
- const MockIcon = React.forwardRef(({ className, ...props }: any, ref: any) => (
-
- {name}
-
- ))
- MockIcon.displayName = name
- return MockIcon
-}
-
-// Export all icons used in the app
-export const Settings = createMockIcon('Settings')
-export const Check = createMockIcon('Check')
-export const CheckCircle = createMockIcon('CheckCircle')
-export const X = createMockIcon('X')
-export const XCircle = createMockIcon('XCircle')
-export const Eye = createMockIcon('Eye')
-export const EyeOff = createMockIcon('EyeOff')
-export const Save = createMockIcon('Save')
-export const Loader = createMockIcon('Loader')
-export const Loader2 = createMockIcon('Loader2')
-export const RefreshCw = createMockIcon('RefreshCw')
-export const Play = createMockIcon('Play')
-export const Pause = createMockIcon('Pause')
-export const Square = createMockIcon('Square')
-export const FileText = createMockIcon('FileText')
-export const Download = createMockIcon('Download')
-export const Upload = createMockIcon('Upload')
-export const ChevronDown = createMockIcon('ChevronDown')
-export const ChevronUp = createMockIcon('ChevronUp')
-export const ChevronLeft = createMockIcon('ChevronLeft')
-export const ChevronRight = createMockIcon('ChevronRight')
-export const Plus = createMockIcon('Plus')
-export const Minus = createMockIcon('Minus')
-export const Edit = createMockIcon('Edit')
-export const Edit2 = createMockIcon('Edit2')
-export const Edit3 = createMockIcon('Edit3')
-export const Trash = createMockIcon('Trash')
-export const Trash2 = createMockIcon('Trash2')
-export const User = createMockIcon('User')
-export const Users = createMockIcon('Users')
-export const Bot = createMockIcon('Bot')
-export const Database = createMockIcon('Database')
-export const Server = createMockIcon('Server')
-export const Globe = createMockIcon('Globe')
-export const Search = createMockIcon('Search')
-export const Filter = createMockIcon('Filter')
-export const Copy = createMockIcon('Copy')
-export const ExternalLink = createMockIcon('ExternalLink')
-export const Info = createMockIcon('Info')
-export const AlertCircle = createMockIcon('AlertCircle')
-export const AlertTriangle = createMockIcon('AlertTriangle')
-export const Zap = createMockIcon('Zap')
-export const Code = createMockIcon('Code')
-export const Terminal = createMockIcon('Terminal')
-export const Book = createMockIcon('Book')
-export const BookOpen = createMockIcon('BookOpen')
-export const Folder = createMockIcon('Folder')
-export const FolderOpen = createMockIcon('FolderOpen')
-export const File = createMockIcon('File')
-export const Hash = createMockIcon('Hash')
-export const Tag = createMockIcon('Tag')
-export const Clock = createMockIcon('Clock')
-export const Calendar = createMockIcon('Calendar')
-export const MapPin = createMockIcon('MapPin')
-export const Link = createMockIcon('Link')
-export const Mail = createMockIcon('Mail')
-export const Phone = createMockIcon('Phone')
-export const Home = createMockIcon('Home')
-export const Menu = createMockIcon('Menu')
-export const MoreHorizontal = createMockIcon('MoreHorizontal')
-export const MoreVertical = createMockIcon('MoreVertical')
-export const Refresh = createMockIcon('Refresh')
-export const RotateCcw = createMockIcon('RotateCcw')
-export const RotateCw = createMockIcon('RotateCw')
-export const Sun = createMockIcon('Sun')
-export const Moon = createMockIcon('Moon')
-export const Monitor = createMockIcon('Monitor')
-export const Wifi = createMockIcon('Wifi')
-export const WifiOff = createMockIcon('WifiOff')
-export const Volume2 = createMockIcon('Volume2')
-export const VolumeX = createMockIcon('VolumeX')
-export const BarChart = createMockIcon('BarChart')
-export const PieChart = createMockIcon('PieChart')
-export const TrendingUp = createMockIcon('TrendingUp')
-export const TrendingDown = createMockIcon('TrendingDown')
-export const ArrowUp = createMockIcon('ArrowUp')
-export const ArrowDown = createMockIcon('ArrowDown')
-export const ArrowLeft = createMockIcon('ArrowLeft')
-export const ArrowRight = createMockIcon('ArrowRight')
-export const Send = createMockIcon('Send')
-export const MessageSquare = createMockIcon('MessageSquare')
-export const MessageCircle = createMockIcon('MessageCircle')
-export const Heart = createMockIcon('Heart')
-export const Star = createMockIcon('Star')
-export const Bookmark = createMockIcon('Bookmark')
-export const Share = createMockIcon('Share')
-export const Share2 = createMockIcon('Share2')
-export const Maximize = createMockIcon('Maximize')
-export const Minimize = createMockIcon('Minimize')
-export const Expand = createMockIcon('Expand')
-export const Shrink = createMockIcon('Shrink')
-export const Move = createMockIcon('Move')
-export const Shuffle = createMockIcon('Shuffle')
-export const Repeat = createMockIcon('Repeat')
-export const StopCircle = createMockIcon('StopCircle')
-export const SkipBack = createMockIcon('SkipBack')
-export const SkipForward = createMockIcon('SkipForward')
-export const FastForward = createMockIcon('FastForward')
-export const Rewind = createMockIcon('Rewind')
-export const Camera = createMockIcon('Camera')
-export const Image = createMockIcon('Image')
-export const Video = createMockIcon('Video')
-export const Mic = createMockIcon('Mic')
-export const MicOff = createMockIcon('MicOff')
-export const Headphones = createMockIcon('Headphones')
-export const Speaker = createMockIcon('Speaker')
-export const Bell = createMockIcon('Bell')
-export const BellOff = createMockIcon('BellOff')
-export const Shield = createMockIcon('Shield')
-export const ShieldCheck = createMockIcon('ShieldCheck')
-export const ShieldAlert = createMockIcon('ShieldAlert')
-export const Key = createMockIcon('Key')
-export const Lock = createMockIcon('Lock')
-export const Unlock = createMockIcon('Unlock')
-export const LogIn = createMockIcon('LogIn')
-export const LogOut = createMockIcon('LogOut')
-export const UserPlus = createMockIcon('UserPlus')
-export const UserMinus = createMockIcon('UserMinus')
-export const UserCheck = createMockIcon('UserCheck')
-export const UserX = createMockIcon('UserX')
-export const Package = createMockIcon('Package')
-export const Package2 = createMockIcon('Package2')
-export const ShoppingCart = createMockIcon('ShoppingCart')
-export const ShoppingBag = createMockIcon('ShoppingBag')
-export const CreditCard = createMockIcon('CreditCard')
-export const DollarSign = createMockIcon('DollarSign')
-export const Percent = createMockIcon('Percent')
-export const Activity = createMockIcon('Activity')
-export const Cpu = createMockIcon('Cpu')
-export const HardDrive = createMockIcon('HardDrive')
-export const MemoryStick = createMockIcon('MemoryStick')
-export const Smartphone = createMockIcon('Smartphone')
-export const Tablet = createMockIcon('Tablet')
-export const Laptop = createMockIcon('Laptop')
-export const Monitor2 = createMockIcon('Monitor2')
-export const Tv = createMockIcon('Tv')
-export const Watch = createMockIcon('Watch')
-export const Gamepad2 = createMockIcon('Gamepad2')
-export const Mouse = createMockIcon('Mouse')
-export const Keyboard = createMockIcon('Keyboard')
-export const Printer = createMockIcon('Printer')
-export const Scanner = createMockIcon('Scanner')
-export const Webcam = createMockIcon('Webcam')
-export const Bluetooth = createMockIcon('Bluetooth')
-export const Usb = createMockIcon('Usb')
-export const Zap2 = createMockIcon('Zap2')
-export const Battery = createMockIcon('Battery')
-export const BatteryCharging = createMockIcon('BatteryCharging')
-export const Plug = createMockIcon('Plug')
-export const Power = createMockIcon('Power')
-export const PowerOff = createMockIcon('PowerOff')
-export const BarChart2 = createMockIcon('BarChart2')
-export const BarChart3 = createMockIcon('BarChart3')
-export const BarChart4 = createMockIcon('BarChart4')
-export const LineChart = createMockIcon('LineChart')
-export const PieChart2 = createMockIcon('PieChart2')
-export const Layers = createMockIcon('Layers')
-export const Layers2 = createMockIcon('Layers2')
-export const Layers3 = createMockIcon('Layers3')
-export const Grid = createMockIcon('Grid')
-export const Grid2x2 = createMockIcon('Grid2x2')
-export const Grid3x3 = createMockIcon('Grid3x3')
-export const List = createMockIcon('List')
-export const ListChecks = createMockIcon('ListChecks')
-export const ListTodo = createMockIcon('ListTodo')
-export const CheckSquare = createMockIcon('CheckSquare')
-export const Square2 = createMockIcon('Square2')
-export const Circle = createMockIcon('Circle')
-export const CircleCheck = createMockIcon('CircleCheck')
-export const CircleX = createMockIcon('CircleX')
-export const CircleDot = createMockIcon('CircleDot')
-export const Target = createMockIcon('Target')
-export const Focus = createMockIcon('Focus')
-export const Crosshair = createMockIcon('Crosshair')
-export const Locate = createMockIcon('Locate')
-export const LocateFixed = createMockIcon('LocateFixed')
-export const Navigation = createMockIcon('Navigation')
-export const Navigation2 = createMockIcon('Navigation2')
-export const Compass = createMockIcon('Compass')
-export const Map = createMockIcon('Map')
-export const TestTube = createMockIcon('TestTube')
-export const FlaskConical = createMockIcon('FlaskConical')
-export const Bug = createMockIcon('Bug')
-export const GitBranch = createMockIcon('GitBranch')
-export const GitCommit = createMockIcon('GitCommit')
-export const GitMerge = createMockIcon('GitMerge')
-export const GitPullRequest = createMockIcon('GitPullRequest')
-export const Github = createMockIcon('Github')
-export const Gitlab = createMockIcon('Gitlab')
-export const Bitbucket = createMockIcon('Bitbucket')
-export const Network = createMockIcon('Network')
-export const GitGraph = createMockIcon('GitGraph')
-export const ListFilter = createMockIcon('ListFilter')
-export const CheckSquare2 = createMockIcon('CheckSquare2')
-export const CircleSlash2 = createMockIcon('CircleSlash2')
-export const Clock3 = createMockIcon('Clock3')
-export const GitCommitHorizontal = createMockIcon('GitCommitHorizontal')
-export const CalendarDays = createMockIcon('CalendarDays')
-export const Sparkles = createMockIcon('Sparkles')
-export const Layout = createMockIcon('Layout')
-export const Table = createMockIcon('Table')
-export const Columns = createMockIcon('Columns')
-export const GitPullRequestDraft = createMockIcon('GitPullRequestDraft')
-export const BrainCircuit = createMockIcon('BrainCircuit')
-export const Wrench = createMockIcon('Wrench')
-export const PlugZap = createMockIcon('PlugZap')
-export const BoxIcon = createMockIcon('BoxIcon')
-export const Box = createMockIcon('Box')
-export const Brain = createMockIcon('Brain')
-export const LinkIcon = createMockIcon('LinkIcon')
-export const Sparkle = createMockIcon('Sparkle')
-export const FolderTree = createMockIcon('FolderTree')
-export const Lightbulb = createMockIcon('Lightbulb')
-export const Rocket = createMockIcon('Rocket')
-export const Building = createMockIcon('Building')
-export const FileCode = createMockIcon('FileCode')
-export const FileJson = createMockIcon('FileJson')
-export const Braces = createMockIcon('Braces')
-export const Binary = createMockIcon('Binary')
-export const Palette = createMockIcon('Palette')
-export const Paintbrush = createMockIcon('Paintbrush')
-export const Type = createMockIcon('Type')
-export const Heading = createMockIcon('Heading')
-export const AlignLeft = createMockIcon('AlignLeft')
-export const AlignCenter = createMockIcon('AlignCenter')
-export const AlignRight = createMockIcon('AlignRight')
-export const Bold = createMockIcon('Bold')
-export const Italic = createMockIcon('Italic')
-export const Underline = createMockIcon('Underline')
-export const Strikethrough = createMockIcon('Strikethrough')
-export const FileCheck = createMockIcon('FileCheck')
-export const FileX = createMockIcon('FileX')
-export const FilePlus = createMockIcon('FilePlus')
-export const FileMinus = createMockIcon('FileMinus')
-export const FolderPlus = createMockIcon('FolderPlus')
-export const FolderMinus = createMockIcon('FolderMinus')
-export const FolderCheck = createMockIcon('FolderCheck')
-export const FolderX = createMockIcon('FolderX')
-export const startMCPServer = createMockIcon('startMCPServer')
-export const Pin = createMockIcon('Pin')
-export const CheckCircle2 = createMockIcon('CheckCircle2')
-export const Clipboard = createMockIcon('Clipboard')
-export const LayoutGrid = createMockIcon('LayoutGrid')
-export const Pencil = createMockIcon('Pencil')
-export const MousePointer = createMockIcon('MousePointer')
-export const GripVertical = createMockIcon('GripVertical')
-export const History = createMockIcon('History')
-export const PlusCircle = createMockIcon('PlusCircle')
-export const MinusCircle = createMockIcon('MinusCircle')
-export const ChevronDownIcon = createMockIcon('ChevronDownIcon')
-export const FileIcon = createMockIcon('FileIcon')
-export const AlertCircleIcon = createMockIcon('AlertCircleIcon')
-export const Clock4 = createMockIcon('Clock4')
-export const XIcon = createMockIcon('XIcon')
-export const CheckIcon = createMockIcon('CheckIcon')
-export const TrashIcon = createMockIcon('TrashIcon')
-export const EyeIcon = createMockIcon('EyeIcon')
-export const EditIcon = createMockIcon('EditIcon')
-export const DownloadIcon = createMockIcon('DownloadIcon')
-export const RefreshIcon = createMockIcon('RefreshIcon')
-export const SearchIcon = createMockIcon('SearchIcon')
-export const FilterIcon = createMockIcon('FilterIcon')
-export const PlusIcon = createMockIcon('PlusIcon')
-export const FolderIcon = createMockIcon('FolderIcon')
-export const FileTextIcon = createMockIcon('FileTextIcon')
-export const BookOpenIcon = createMockIcon('BookOpenIcon')
-export const DatabaseIcon = createMockIcon('DatabaseIcon')
-export const GlobeIcon = createMockIcon('GlobeIcon')
-export const TagIcon = createMockIcon('TagIcon')
-export const CalendarIcon = createMockIcon('CalendarIcon')
-export const ClockIcon = createMockIcon('ClockIcon')
-export const UserIcon = createMockIcon('UserIcon')
-export const SettingsIcon = createMockIcon('SettingsIcon')
-export const InfoIcon = createMockIcon('InfoIcon')
-export const WarningIcon = createMockIcon('WarningIcon')
-export const ErrorIcon = createMockIcon('ErrorIcon')
\ No newline at end of file
diff --git a/archon-ui-main/src/features/projects/components/tests/ProjectCard.test.tsx b/archon-ui-main/src/features/projects/components/tests/ProjectCard.test.tsx
new file mode 100644
index 00000000..e119c749
--- /dev/null
+++ b/archon-ui-main/src/features/projects/components/tests/ProjectCard.test.tsx
@@ -0,0 +1,189 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { render, screen, fireEvent } from '../../../testing/test-utils';
+import { ProjectCard } from '../ProjectCard';
+import type { Project } from '../../types';
+
+describe('ProjectCard', () => {
+ const mockProject: Project = {
+ id: 'project-1',
+ title: 'Test Project',
+ description: 'Test Description',
+ created_at: '2024-01-01T00:00:00Z',
+ updated_at: '2024-01-01T00:00:00Z',
+ pinned: false,
+ features: [],
+ docs: [],
+ };
+
+ const mockTaskCounts = {
+ todo: 5,
+ doing: 3,
+ review: 2,
+ done: 10,
+ };
+
+ const mockHandlers = {
+ onSelect: vi.fn(),
+ onPin: vi.fn(),
+ onDelete: vi.fn(),
+ };
+
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it('should render project title', () => {
+ render(
+
+ );
+
+ expect(screen.getByText('Test Project')).toBeInTheDocument();
+ });
+
+ it('should display task counts', () => {
+ render(
+
+ );
+
+ // Task count badges should be visible
+ // Note: Component only shows todo, doing, and done (not review)
+ const fives = screen.getAllByText('5');
+ expect(fives.length).toBeGreaterThan(0); // todo count
+ expect(screen.getByText('10')).toBeInTheDocument(); // done
+ // Doing count might be displayed as 3 or duplicated - implementation detail
+ });
+
+ it('should call onSelect when clicked', () => {
+ render(
+
+ );
+
+ const card = screen.getByRole('listitem');
+ fireEvent.click(card);
+
+ expect(mockHandlers.onSelect).toHaveBeenCalledWith(mockProject);
+ expect(mockHandlers.onSelect).toHaveBeenCalledTimes(1);
+ });
+
+ it('should apply selected styles when isSelected is true', () => {
+ const { container } = render(
+
+ );
+
+ const card = container.querySelector('[role="listitem"]');
+ // Check for selected-specific classes
+ expect(card?.className).toContain('scale-[1.02]');
+ expect(card?.className).toContain('border-purple');
+ });
+
+ it('should apply pinned styles when project is pinned', () => {
+ const pinnedProject = { ...mockProject, pinned: true };
+
+ const { container } = render(
+
+ );
+
+ const card = container.querySelector('[role="listitem"]');
+ // Check for pinned-specific classes
+ expect(card?.className).toContain('from-purple');
+ expect(card?.className).toContain('border-purple-500');
+ });
+
+ it('should render aurora glow effect when selected', () => {
+ const { container } = render(
+
+ );
+
+ // Aurora glow div should exist when selected
+ const glowEffect = container.querySelector('.animate-\\[pulse_8s_ease-in-out_infinite\\]');
+ expect(glowEffect).toBeInTheDocument();
+ });
+
+ it('should not render aurora glow effect when not selected', () => {
+ const { container } = render(
+
+ );
+
+ // Aurora glow div should not exist when not selected
+ const glowEffect = container.querySelector('.animate-\\[pulse_8s_ease-in-out_infinite\\]');
+ expect(glowEffect).not.toBeInTheDocument();
+ });
+
+ it('should show zero task counts correctly', () => {
+ const zeroTaskCounts = {
+ todo: 0,
+ doing: 0,
+ review: 0,
+ done: 0,
+ };
+
+ render(
+
+ );
+
+ // All counts should show 0 (ProjectCard may not show review count)
+ const zeros = screen.getAllByText('0');
+ expect(zeros.length).toBeGreaterThanOrEqual(3); // At least todo, doing, done
+ });
+
+ it('should handle very long project titles', () => {
+ const longTitleProject = {
+ ...mockProject,
+ title: 'This is an extremely long project title that should be truncated properly to avoid breaking the layout of the card component',
+ };
+
+ render(
+
+ );
+
+ const title = screen.getByText(/This is an extremely long project title/);
+ expect(title).toBeInTheDocument();
+ // Title should have line-clamp-2 class
+ expect(title.className).toContain('line-clamp-2');
+ });
+});
\ No newline at end of file
diff --git a/archon-ui-main/src/features/projects/hooks/tests/useProjectQueries.test.ts b/archon-ui-main/src/features/projects/hooks/tests/useProjectQueries.test.ts
new file mode 100644
index 00000000..0b90ba95
--- /dev/null
+++ b/archon-ui-main/src/features/projects/hooks/tests/useProjectQueries.test.ts
@@ -0,0 +1,208 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { renderHook, waitFor } from '@testing-library/react';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import { projectKeys, useProjects, useCreateProject, useUpdateProject, useDeleteProject } from '../useProjectQueries';
+import type { Project } from '../../types';
+import React from 'react';
+
+// Mock the services
+vi.mock('../../services', () => ({
+ projectService: {
+ listProjects: vi.fn(),
+ createProject: vi.fn(),
+ updateProject: vi.fn(),
+ deleteProject: vi.fn(),
+ getProjectFeatures: vi.fn(),
+ },
+ taskService: {
+ getTaskCountsForAllProjects: vi.fn(),
+ },
+}));
+
+// Mock the toast hook
+vi.mock('../../../ui/hooks/useToast', () => ({
+ useToast: () => ({
+ showToast: vi.fn(),
+ }),
+}));
+
+// Mock smart polling
+vi.mock('../../../ui/hooks', () => ({
+ useSmartPolling: () => ({
+ refetchInterval: 5000,
+ isPaused: false,
+ }),
+}));
+
+// Test wrapper with QueryClient
+const createWrapper = () => {
+ const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: { retry: false },
+ mutations: { retry: false },
+ },
+ });
+
+ return ({ children }: { children: React.ReactNode }) =>
+ React.createElement(QueryClientProvider, { client: queryClient }, children);
+};
+
+describe('useProjectQueries', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ describe('projectKeys', () => {
+ it('should generate correct query keys', () => {
+ expect(projectKeys.all).toEqual(['projects']);
+ expect(projectKeys.lists()).toEqual(['projects', 'list']);
+ expect(projectKeys.detail('123')).toEqual(['projects', 'detail', '123']);
+ expect(projectKeys.tasks('123')).toEqual(['projects', 'detail', '123', 'tasks']);
+ expect(projectKeys.features('123')).toEqual(['projects', 'detail', '123', 'features']);
+ expect(projectKeys.documents('123')).toEqual(['projects', 'detail', '123', 'documents']);
+ });
+ });
+
+ describe('useProjects', () => {
+ it('should fetch projects list', async () => {
+ const mockProjects: Project[] = [
+ {
+ id: '1',
+ title: 'Test Project',
+ description: 'Test Description',
+ created_at: '2024-01-01T00:00:00Z',
+ updated_at: '2024-01-01T00:00:00Z',
+ pinned: false,
+ features: [],
+ docs: [],
+ },
+ ];
+
+ const { projectService } = await import('../../services');
+ vi.mocked(projectService.listProjects).mockResolvedValue(mockProjects);
+
+ const { result } = renderHook(() => useProjects(), {
+ wrapper: createWrapper(),
+ });
+
+ await waitFor(() => {
+ expect(result.current.isSuccess).toBe(true);
+ expect(result.current.data).toEqual(mockProjects);
+ });
+
+ expect(projectService.listProjects).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe('useCreateProject', () => {
+ it('should optimistically add project and replace with server response', async () => {
+ const newProject: Project = {
+ id: 'real-id',
+ title: 'New Project',
+ description: 'New Description',
+ created_at: '2024-01-01T00:00:00Z',
+ updated_at: '2024-01-01T00:00:00Z',
+ pinned: false,
+ features: [],
+ docs: [],
+ };
+
+ const { projectService } = await import('../../services');
+ vi.mocked(projectService.createProject).mockResolvedValue({
+ project: newProject,
+ message: 'Created',
+ });
+
+ const wrapper = createWrapper();
+ const { result } = renderHook(() => useCreateProject(), { wrapper });
+
+ await result.current.mutateAsync({
+ title: 'New Project',
+ description: 'New Description',
+ });
+
+ await waitFor(() => {
+ expect(result.current.isSuccess).toBe(true);
+ expect(projectService.createProject).toHaveBeenCalledWith({
+ title: 'New Project',
+ description: 'New Description',
+ });
+ });
+ });
+
+ it('should rollback on error', async () => {
+ const { projectService } = await import('../../services');
+ vi.mocked(projectService.createProject).mockRejectedValue(new Error('Network error'));
+
+ const wrapper = createWrapper();
+ const { result } = renderHook(() => useCreateProject(), { wrapper });
+
+ await expect(
+ result.current.mutateAsync({
+ title: 'New Project',
+ description: 'New Description',
+ })
+ ).rejects.toThrow('Network error');
+ });
+ });
+
+ describe('useUpdateProject', () => {
+ it('should handle pinning a project', async () => {
+ const updatedProject: Project = {
+ id: '1',
+ title: 'Test Project',
+ description: 'Test Description',
+ created_at: '2024-01-01T00:00:00Z',
+ updated_at: '2024-01-01T00:00:00Z',
+ pinned: true,
+ features: [],
+ docs: [],
+ };
+
+ const { projectService } = await import('../../services');
+ vi.mocked(projectService.updateProject).mockResolvedValue(updatedProject);
+
+ const wrapper = createWrapper();
+ const { result } = renderHook(() => useUpdateProject(), { wrapper });
+
+ await result.current.mutateAsync({
+ projectId: '1',
+ updates: { pinned: true },
+ });
+
+ await waitFor(() => {
+ expect(result.current.isSuccess).toBe(true);
+ expect(projectService.updateProject).toHaveBeenCalledWith('1', { pinned: true });
+ });
+ });
+ });
+
+ describe('useDeleteProject', () => {
+ it('should optimistically remove project', async () => {
+ const { projectService } = await import('../../services');
+ vi.mocked(projectService.deleteProject).mockResolvedValue(undefined);
+
+ const wrapper = createWrapper();
+ const { result } = renderHook(() => useDeleteProject(), { wrapper });
+
+ await result.current.mutateAsync('project-to-delete');
+
+ await waitFor(() => {
+ expect(result.current.isSuccess).toBe(true);
+ expect(projectService.deleteProject).toHaveBeenCalledWith('project-to-delete');
+ });
+ });
+
+ it('should rollback on delete error', async () => {
+ const { projectService } = await import('../../services');
+ vi.mocked(projectService.deleteProject).mockRejectedValue(new Error('Permission denied'));
+
+ const wrapper = createWrapper();
+ const { result } = renderHook(() => useDeleteProject(), { wrapper });
+
+ await expect(
+ result.current.mutateAsync('project-to-delete')
+ ).rejects.toThrow('Permission denied');
+ });
+ });
+});
\ No newline at end of file
diff --git a/archon-ui-main/src/features/projects/tasks/hooks/tests/useTaskQueries.test.ts b/archon-ui-main/src/features/projects/tasks/hooks/tests/useTaskQueries.test.ts
new file mode 100644
index 00000000..a9282987
--- /dev/null
+++ b/archon-ui-main/src/features/projects/tasks/hooks/tests/useTaskQueries.test.ts
@@ -0,0 +1,195 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { renderHook, waitFor } from '@testing-library/react';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import { taskKeys, useProjectTasks, useCreateTask } from '../useTaskQueries';
+import type { Task } from '../../types';
+import React from 'react';
+
+// Mock the services
+vi.mock('../../services', () => ({
+ taskService: {
+ getTasksByProject: vi.fn(),
+ createTask: vi.fn(),
+ updateTask: vi.fn(),
+ deleteTask: vi.fn(),
+ },
+}));
+
+// Mock the toast hook
+vi.mock('../../../../ui/hooks/useToast', () => ({
+ useToast: () => ({
+ showToast: vi.fn(),
+ }),
+}));
+
+// Mock smart polling
+vi.mock('../../../../ui/hooks', () => ({
+ useSmartPolling: () => ({
+ refetchInterval: 5000,
+ isPaused: false,
+ }),
+}));
+
+// Test wrapper with QueryClient
+const createWrapper = () => {
+ const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: { retry: false },
+ mutations: { retry: false },
+ },
+ });
+
+ return ({ children }: { children: React.ReactNode }) =>
+ React.createElement(QueryClientProvider, { client: queryClient }, children);
+};
+
+describe('useTaskQueries', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ describe('taskKeys', () => {
+ it('should generate correct query keys', () => {
+ expect(taskKeys.all('project-123')).toEqual(['projects', 'project-123', 'tasks']);
+ });
+ });
+
+ describe('useProjectTasks', () => {
+ it('should fetch tasks for a project', async () => {
+ const mockTasks: Task[] = [
+ {
+ id: 'task-1',
+ project_id: 'project-123',
+ title: 'Test Task',
+ description: 'Test Description',
+ status: 'todo',
+ assignee: 'User',
+ task_order: 100,
+ created_at: '2024-01-01T00:00:00Z',
+ updated_at: '2024-01-01T00:00:00Z',
+ },
+ ];
+
+ const { taskService } = await import('../../services');
+ vi.mocked(taskService.getTasksByProject).mockResolvedValue(mockTasks);
+
+ const { result } = renderHook(() => useProjectTasks('project-123'), {
+ wrapper: createWrapper(),
+ });
+
+ await waitFor(() => {
+ expect(result.current.isSuccess).toBe(true);
+ expect(result.current.data).toEqual(mockTasks);
+ });
+
+ expect(taskService.getTasksByProject).toHaveBeenCalledWith('project-123');
+ });
+
+ it('should not fetch tasks when projectId is undefined', () => {
+ const { result } = renderHook(() => useProjectTasks(undefined), {
+ wrapper: createWrapper(),
+ });
+
+ expect(result.current.isLoading).toBe(false);
+ expect(result.current.isFetching).toBe(false);
+ expect(result.current.data).toBeUndefined();
+ });
+
+ it('should respect enabled flag', () => {
+ const { result } = renderHook(() => useProjectTasks('project-123', false), {
+ wrapper: createWrapper(),
+ });
+
+ expect(result.current.isLoading).toBe(false);
+ expect(result.current.isFetching).toBe(false);
+ expect(result.current.data).toBeUndefined();
+ });
+ });
+
+ describe('useCreateTask', () => {
+ it('should optimistically add task and replace with server response', async () => {
+ const newTask: Task = {
+ id: 'real-task-id',
+ project_id: 'project-123',
+ title: 'New Task',
+ description: 'New Description',
+ status: 'todo',
+ assignee: 'User',
+ task_order: 100,
+ created_at: '2024-01-01T00:00:00Z',
+ updated_at: '2024-01-01T00:00:00Z',
+ };
+
+ const { taskService } = await import('../../services');
+ vi.mocked(taskService.createTask).mockResolvedValue(newTask);
+
+ const wrapper = createWrapper();
+ const { result } = renderHook(() => useCreateTask(), { wrapper });
+
+ await result.current.mutateAsync({
+ project_id: 'project-123',
+ title: 'New Task',
+ description: 'New Description',
+ status: 'todo',
+ assignee: 'User',
+ });
+
+ await waitFor(() => {
+ expect(result.current.isSuccess).toBe(true);
+ expect(taskService.createTask).toHaveBeenCalledWith({
+ project_id: 'project-123',
+ title: 'New Task',
+ description: 'New Description',
+ status: 'todo',
+ assignee: 'User',
+ });
+ });
+ });
+
+ it('should provide default values for optional fields', async () => {
+ const newTask: Task = {
+ id: 'real-task-id',
+ project_id: 'project-123',
+ title: 'Minimal Task',
+ description: '',
+ status: 'todo',
+ assignee: 'User',
+ task_order: 100,
+ created_at: '2024-01-01T00:00:00Z',
+ updated_at: '2024-01-01T00:00:00Z',
+ };
+
+ const { taskService } = await import('../../services');
+ vi.mocked(taskService.createTask).mockResolvedValue(newTask);
+
+ const wrapper = createWrapper();
+ const { result } = renderHook(() => useCreateTask(), { wrapper });
+
+ await result.current.mutateAsync({
+ project_id: 'project-123',
+ title: 'Minimal Task',
+ description: '',
+ });
+
+ await waitFor(() => {
+ expect(result.current.isSuccess).toBe(true);
+ });
+ });
+
+ it('should rollback on error', async () => {
+ const { taskService } = await import('../../services');
+ vi.mocked(taskService.createTask).mockRejectedValue(new Error('Network error'));
+
+ const wrapper = createWrapper();
+ const { result } = renderHook(() => useCreateTask(), { wrapper });
+
+ await expect(
+ result.current.mutateAsync({
+ project_id: 'project-123',
+ title: 'Failed Task',
+ description: 'This will fail',
+ })
+ ).rejects.toThrow('Network error');
+ });
+ });
+});
\ No newline at end of file
diff --git a/archon-ui-main/src/features/testing/test-utils.tsx b/archon-ui-main/src/features/testing/test-utils.tsx
new file mode 100644
index 00000000..fbdb1e5b
--- /dev/null
+++ b/archon-ui-main/src/features/testing/test-utils.tsx
@@ -0,0 +1,42 @@
+import React from 'react';
+import { render as rtlRender } from '@testing-library/react';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import { ToastProvider } from '../ui/components/ToastProvider';
+import { TooltipProvider } from '../ui/primitives/tooltip';
+
+/**
+ * Custom render function that wraps components with all necessary providers
+ * This follows the best practice of having a centralized test render utility
+ */
+export function renderWithProviders(
+ ui: React.ReactElement,
+ {
+ queryClient = new QueryClient({
+ defaultOptions: {
+ queries: { retry: false },
+ mutations: { retry: false },
+ },
+ }),
+ ...renderOptions
+ } = {}
+) {
+ function Wrapper({ children }: { children: React.ReactNode }) {
+ return (
+
+
+
+ {children}
+
+
+
+ );
+ }
+
+ return rtlRender(ui, { wrapper: Wrapper, ...renderOptions });
+}
+
+// Re-export everything from React Testing Library
+export * from '@testing-library/react';
+
+// Override the default render with our custom one
+export { renderWithProviders as render };
\ No newline at end of file
diff --git a/archon-ui-main/src/features/ui/hooks/tests/useSmartPolling.test.ts b/archon-ui-main/src/features/ui/hooks/tests/useSmartPolling.test.ts
new file mode 100644
index 00000000..7c84c40e
--- /dev/null
+++ b/archon-ui-main/src/features/ui/hooks/tests/useSmartPolling.test.ts
@@ -0,0 +1,189 @@
+import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
+import { renderHook, act } from '@testing-library/react';
+import { useSmartPolling } from '../useSmartPolling';
+
+describe('useSmartPolling', () => {
+ beforeEach(() => {
+ // Reset document visibility state
+ Object.defineProperty(document, 'visibilityState', {
+ value: 'visible',
+ writable: true,
+ configurable: true,
+ });
+ Object.defineProperty(document, 'hidden', {
+ value: false,
+ writable: true,
+ configurable: true,
+ });
+ // Mock document.hasFocus
+ document.hasFocus = vi.fn(() => true);
+ });
+
+ afterEach(() => {
+ vi.clearAllTimers();
+ vi.clearAllMocks();
+ });
+
+ it('should return the base interval when document is visible and focused', () => {
+ const { result } = renderHook(() => useSmartPolling(5000));
+
+ expect(result.current.refetchInterval).toBe(5000);
+ expect(result.current.isActive).toBe(true);
+ expect(result.current.isVisible).toBe(true);
+ expect(result.current.hasFocus).toBe(true);
+ });
+
+ it('should disable polling when document is hidden', () => {
+ const { result } = renderHook(() => useSmartPolling(5000));
+
+ // Initially should be active
+ expect(result.current.isActive).toBe(true);
+ expect(result.current.refetchInterval).toBe(5000);
+
+ // Simulate tab becoming hidden
+ act(() => {
+ Object.defineProperty(document, 'hidden', {
+ value: true,
+ writable: true,
+ configurable: true,
+ });
+ document.dispatchEvent(new Event('visibilitychange'));
+ });
+
+ // Should be disabled (returns false)
+ expect(result.current.isVisible).toBe(false);
+ expect(result.current.isActive).toBe(false);
+ expect(result.current.refetchInterval).toBe(false);
+ });
+
+ it('should resume polling when document becomes visible again', () => {
+ const { result } = renderHook(() => useSmartPolling(5000));
+
+ // Make hidden
+ act(() => {
+ Object.defineProperty(document, 'hidden', {
+ value: true,
+ writable: true,
+ configurable: true,
+ });
+ document.dispatchEvent(new Event('visibilitychange'));
+ });
+
+ expect(result.current.refetchInterval).toBe(false);
+
+ // Make visible again
+ act(() => {
+ Object.defineProperty(document, 'hidden', {
+ value: false,
+ writable: true,
+ configurable: true,
+ });
+ document.dispatchEvent(new Event('visibilitychange'));
+ });
+
+ expect(result.current.isVisible).toBe(true);
+ expect(result.current.isActive).toBe(true);
+ expect(result.current.refetchInterval).toBe(5000);
+ });
+
+ it('should slow down to 60 seconds when window loses focus', () => {
+ const { result } = renderHook(() => useSmartPolling(5000));
+
+ // Initially focused
+ expect(result.current.refetchInterval).toBe(5000);
+ expect(result.current.hasFocus).toBe(true);
+
+ // Simulate window blur
+ act(() => {
+ window.dispatchEvent(new Event('blur'));
+ });
+
+ // Should be slowed down to 60 seconds
+ expect(result.current.hasFocus).toBe(false);
+ expect(result.current.isActive).toBe(false);
+ expect(result.current.refetchInterval).toBe(60000);
+ });
+
+ it('should resume normal speed when window regains focus', () => {
+ const { result } = renderHook(() => useSmartPolling(5000));
+
+ // Blur window
+ act(() => {
+ window.dispatchEvent(new Event('blur'));
+ });
+
+ expect(result.current.refetchInterval).toBe(60000);
+
+ // Focus window again
+ act(() => {
+ window.dispatchEvent(new Event('focus'));
+ });
+
+ expect(result.current.hasFocus).toBe(true);
+ expect(result.current.isActive).toBe(true);
+ expect(result.current.refetchInterval).toBe(5000);
+ });
+
+ it('should handle different base intervals', () => {
+ const { result: result1 } = renderHook(() => useSmartPolling(1000));
+ const { result: result2 } = renderHook(() => useSmartPolling(10000));
+
+ expect(result1.current.refetchInterval).toBe(1000);
+ expect(result2.current.refetchInterval).toBe(10000);
+
+ // When blurred, both should be 60 seconds
+ act(() => {
+ window.dispatchEvent(new Event('blur'));
+ });
+
+ expect(result1.current.refetchInterval).toBe(60000);
+ expect(result2.current.refetchInterval).toBe(60000);
+ });
+
+ it('should use default interval of 10000ms when not specified', () => {
+ const { result } = renderHook(() => useSmartPolling());
+
+ expect(result.current.refetchInterval).toBe(10000);
+ });
+
+ it('should cleanup event listeners on unmount', () => {
+ const removeEventListenerSpy = vi.spyOn(document, 'removeEventListener');
+ const windowRemoveEventListenerSpy = vi.spyOn(window, 'removeEventListener');
+
+ const { unmount } = renderHook(() => useSmartPolling(5000));
+
+ unmount();
+
+ expect(removeEventListenerSpy).toHaveBeenCalledWith('visibilitychange', expect.any(Function));
+ expect(windowRemoveEventListenerSpy).toHaveBeenCalledWith('focus', expect.any(Function));
+ expect(windowRemoveEventListenerSpy).toHaveBeenCalledWith('blur', expect.any(Function));
+
+ removeEventListenerSpy.mockRestore();
+ windowRemoveEventListenerSpy.mockRestore();
+ });
+
+ it('should correctly report isActive state', () => {
+ const { result } = renderHook(() => useSmartPolling(5000));
+
+ // Active when both visible and focused
+ expect(result.current.isActive).toBe(true);
+
+ // Not active when not focused
+ act(() => {
+ window.dispatchEvent(new Event('blur'));
+ });
+ expect(result.current.isActive).toBe(false);
+
+ // Not active when hidden
+ act(() => {
+ window.dispatchEvent(new Event('focus')); // Focus first
+ Object.defineProperty(document, 'hidden', {
+ value: true,
+ writable: true,
+ configurable: true,
+ });
+ document.dispatchEvent(new Event('visibilitychange'));
+ });
+ expect(result.current.isActive).toBe(false);
+ });
+});
\ No newline at end of file
diff --git a/archon-ui-main/test/components.test.tsx b/archon-ui-main/test/components.test.tsx
deleted file mode 100644
index d38d15f5..00000000
--- a/archon-ui-main/test/components.test.tsx
+++ /dev/null
@@ -1,294 +0,0 @@
-import { render, screen, fireEvent } from '@testing-library/react'
-import { describe, test, expect, vi } from 'vitest'
-import React from 'react'
-
-describe('Component Tests', () => {
- test('button component works', () => {
- const onClick = vi.fn()
- const MockButton = ({ children, ...props }: any) => (
-
- )
-
- render(Click me)
-
- const button = screen.getByRole('button')
- fireEvent.click(button)
- expect(onClick).toHaveBeenCalledTimes(1)
- })
-
- test('input component works', () => {
- const MockInput = () => {
- const [value, setValue] = React.useState('')
- return (
- setValue(e.target.value)}
- placeholder="Test input"
- />
- )
- }
-
- render()
- const input = screen.getByPlaceholderText('Test input')
-
- fireEvent.change(input, { target: { value: 'test' } })
- expect((input as HTMLInputElement).value).toBe('test')
- })
-
- test('modal component works', () => {
- const MockModal = () => {
- const [isOpen, setIsOpen] = React.useState(false)
- return (
-
-
- {isOpen && (
-
-
Modal Title
-
-
- )}
-
- )
- }
-
- render()
-
- // Modal not visible initially
- expect(screen.queryByRole('dialog')).not.toBeInTheDocument()
-
- // Open modal
- fireEvent.click(screen.getByText('Open Modal'))
- expect(screen.getByRole('dialog')).toBeInTheDocument()
-
- // Close modal
- fireEvent.click(screen.getByText('Close'))
- expect(screen.queryByRole('dialog')).not.toBeInTheDocument()
- })
-
- test('progress bar component works', () => {
- const MockProgressBar = ({ value, max }: { value: number; max: number }) => (
-
-
Progress: {Math.round((value / max) * 100)}%
-
Bar
-
- )
-
- const { rerender } = render()
- expect(screen.getByText('Progress: 0%')).toBeInTheDocument()
-
- rerender()
- expect(screen.getByText('Progress: 50%')).toBeInTheDocument()
-
- rerender()
- expect(screen.getByText('Progress: 100%')).toBeInTheDocument()
- })
-
- test('tooltip component works', () => {
- const MockTooltip = ({ children, tooltip }: any) => {
- const [show, setShow] = React.useState(false)
- return (
-
-
- {show &&
{tooltip}
}
-
- )
- }
-
- render(Hover me)
-
- const button = screen.getByText('Hover me')
- expect(screen.queryByRole('tooltip')).not.toBeInTheDocument()
-
- fireEvent.mouseEnter(button)
- expect(screen.getByRole('tooltip')).toBeInTheDocument()
-
- fireEvent.mouseLeave(button)
- expect(screen.queryByRole('tooltip')).not.toBeInTheDocument()
- })
-
- test('accordion component works', () => {
- const MockAccordion = () => {
- const [expanded, setExpanded] = React.useState(false)
- return (
-
-
- {expanded &&
Section content
}
-
- )
- }
-
- render()
-
- expect(screen.queryByText('Section content')).not.toBeInTheDocument()
-
- fireEvent.click(screen.getByText('Section 1 +'))
- expect(screen.getByText('Section content')).toBeInTheDocument()
-
- fireEvent.click(screen.getByText('Section 1 −'))
- expect(screen.queryByText('Section content')).not.toBeInTheDocument()
- })
-
- test('table sorting works', () => {
- const MockTable = () => {
- const [data, setData] = React.useState([
- { name: 'Alice', age: 30 },
- { name: 'Bob', age: 25 },
- { name: 'Charlie', age: 35 }
- ])
-
- const sortByName = () => {
- setData([...data].sort((a, b) => a.name.localeCompare(b.name)))
- }
-
- return (
-
-
-
- |
- Name
- |
- Age |
-
-
-
- {data.map((row, index) => (
-
- | {row.name} |
- {row.age} |
-
- ))}
-
-
- )
- }
-
- render()
-
- const cells = screen.getAllByRole('cell')
- expect(cells[0]).toHaveTextContent('Alice')
-
- fireEvent.click(screen.getByText('Name'))
-
- // After sorting, Alice should still be first (already sorted)
- const sortedCells = screen.getAllByRole('cell')
- expect(sortedCells[0]).toHaveTextContent('Alice')
- })
-
- test('pagination works', () => {
- const MockPagination = () => {
- const [page, setPage] = React.useState(1)
- return (
-
-
Page {page}
-
-
-
- )
- }
-
- render()
-
- expect(screen.getByText('Page 1')).toBeInTheDocument()
-
- fireEvent.click(screen.getByText('Next'))
- expect(screen.getByText('Page 2')).toBeInTheDocument()
-
- fireEvent.click(screen.getByText('Previous'))
- expect(screen.getByText('Page 1')).toBeInTheDocument()
- })
-
- test('form validation works', () => {
- const MockForm = () => {
- const [email, setEmail] = React.useState('')
- const [error, setError] = React.useState('')
-
- const validate = (value: string) => {
- if (!value) {
- setError('Email is required')
- } else if (!value.includes('@')) {
- setError('Invalid email format')
- } else {
- setError('')
- }
- }
-
- return (
-
-
{
- setEmail(e.target.value)
- validate(e.target.value)
- }}
- />
- {error &&
{error}
}
-
- )
- }
-
- render()
-
- const input = screen.getByPlaceholderText('Email')
-
- fireEvent.change(input, { target: { value: 'invalid' } })
- expect(screen.getByRole('alert')).toHaveTextContent('Invalid email format')
-
- fireEvent.change(input, { target: { value: 'valid@email.com' } })
- expect(screen.queryByRole('alert')).not.toBeInTheDocument()
- })
-
- test('search filtering works', () => {
- const MockSearch = () => {
- const [query, setQuery] = React.useState('')
- const items = ['Apple', 'Banana', 'Cherry', 'Date']
- const filtered = items.filter(item =>
- item.toLowerCase().includes(query.toLowerCase())
- )
-
- return (
-
-
setQuery(e.target.value)}
- />
-
- {filtered.map((item, index) => (
- - {item}
- ))}
-
-
- )
- }
-
- render()
-
- // All items visible initially
- expect(screen.getByText('Apple')).toBeInTheDocument()
- expect(screen.getByText('Banana')).toBeInTheDocument()
-
- // Filter items
- const input = screen.getByPlaceholderText('Search items')
- fireEvent.change(input, { target: { value: 'a' } })
-
- expect(screen.getByText('Apple')).toBeInTheDocument()
- expect(screen.getByText('Banana')).toBeInTheDocument()
- expect(screen.queryByText('Cherry')).not.toBeInTheDocument()
- })
-})
\ No newline at end of file
diff --git a/archon-ui-main/test/components/common/DeleteConfirmModal.test.tsx b/archon-ui-main/test/components/common/DeleteConfirmModal.test.tsx
deleted file mode 100644
index cef0203e..00000000
--- a/archon-ui-main/test/components/common/DeleteConfirmModal.test.tsx
+++ /dev/null
@@ -1,107 +0,0 @@
-import React from 'react';
-import { render, screen, fireEvent } from '@testing-library/react';
-import { describe, it, expect, vi } from 'vitest';
-import { DeleteConfirmModal } from '../../../src/components/common/DeleteConfirmModal';
-
-describe('DeleteConfirmModal', () => {
- const defaultProps = {
- itemName: 'Test Item',
- onConfirm: vi.fn(),
- onCancel: vi.fn(),
- type: 'task' as const,
- };
-
- beforeEach(() => {
- vi.clearAllMocks();
- });
-
- it('renders with correct title and message for task type', () => {
- render();
-
- expect(screen.getByText('Delete Task')).toBeInTheDocument();
- expect(screen.getByText(/Are you sure you want to delete the "Test Item" task/)).toBeInTheDocument();
- });
-
- it('renders with correct title and message for project type', () => {
- render();
-
- expect(screen.getByText('Delete Project')).toBeInTheDocument();
- expect(screen.getByText(/Are you sure you want to delete the "Test Item" project/)).toBeInTheDocument();
- });
-
- it('renders with correct title and message for client type', () => {
- render();
-
- expect(screen.getByText('Delete MCP Client')).toBeInTheDocument();
- expect(screen.getByText(/Are you sure you want to delete the "Test Item" client/)).toBeInTheDocument();
- });
-
- it('calls onConfirm when Delete button is clicked', () => {
- render();
-
- fireEvent.click(screen.getByText('Delete'));
-
- expect(defaultProps.onConfirm).toHaveBeenCalledTimes(1);
- });
-
- it('calls onCancel when Cancel button is clicked', () => {
- render();
-
- fireEvent.click(screen.getByText('Cancel'));
-
- expect(defaultProps.onCancel).toHaveBeenCalledTimes(1);
- });
-
- it('calls onCancel when Escape key is pressed', () => {
- render();
-
- fireEvent.keyDown(screen.getByRole('dialog'), { key: 'Escape' });
-
- expect(defaultProps.onCancel).toHaveBeenCalledTimes(1);
- });
-
- it('calls onCancel when backdrop is clicked', () => {
- render();
-
- // Click the backdrop
- const backdrop = screen.getByTestId('modal-backdrop');
- fireEvent.click(backdrop);
-
- expect(defaultProps.onCancel).toHaveBeenCalledTimes(1);
- });
-
- it('does not call onCancel when modal content is clicked', () => {
- render();
-
- // Click the modal dialog itself
- fireEvent.click(screen.getByRole('dialog'));
-
- expect(defaultProps.onCancel).not.toHaveBeenCalled();
- });
-
- it('has proper accessibility attributes', () => {
- render();
-
- const dialog = screen.getByRole('dialog');
- expect(dialog).toHaveAttribute('aria-modal', 'true');
- expect(dialog).toHaveAttribute('aria-labelledby');
- expect(dialog).toHaveAttribute('aria-describedby');
- });
-
- it('focuses Cancel button by default', () => {
- render();
-
- const cancelButton = screen.getByText('Cancel');
- expect(cancelButton).toHaveFocus();
- });
-
- it('has proper button types', () => {
- render();
-
- const cancelButton = screen.getByText('Cancel');
- const deleteButton = screen.getByText('Delete');
-
- expect(cancelButton).toHaveAttribute('type', 'button');
- expect(deleteButton).toHaveAttribute('type', 'button');
- });
-});
\ No newline at end of file
diff --git a/archon-ui-main/test/components/project-tasks/DocsTab.integration.test.tsx b/archon-ui-main/test/components/project-tasks/DocsTab.integration.test.tsx
deleted file mode 100644
index 64cb4f8b..00000000
--- a/archon-ui-main/test/components/project-tasks/DocsTab.integration.test.tsx
+++ /dev/null
@@ -1,407 +0,0 @@
-import { render, screen, fireEvent, waitFor } from '@testing-library/react'
-import { describe, test, expect, vi, beforeEach } from 'vitest'
-import React from 'react'
-
-// Mock the dependencies
-vi.mock('../../../src/contexts/ToastContext', () => ({
- useToast: () => ({
- showToast: vi.fn()
- })
-}))
-
-vi.mock('../../../src/services/projectService', () => ({
- projectService: {
- getProjectDocuments: vi.fn().mockResolvedValue([]),
- deleteDocument: vi.fn().mockResolvedValue(undefined),
- updateDocument: vi.fn().mockResolvedValue({ id: 'doc-1', title: 'Updated' }),
- getDocument: vi.fn().mockResolvedValue({ id: 'doc-1', title: 'Document 1' })
- }
-}))
-
-vi.mock('../../../src/services/knowledgeBaseService', () => ({
- knowledgeBaseService: {
- getItems: vi.fn().mockResolvedValue([])
- }
-}))
-
-// Create a minimal DocsTab component for testing
-const DocsTabTest = () => {
- const [documents, setDocuments] = React.useState([
- {
- id: 'doc-1',
- title: 'Document 1',
- content: { type: 'prp' },
- document_type: 'prp',
- updated_at: '2025-07-30T12:00:00Z'
- },
- {
- id: 'doc-2',
- title: 'Document 2',
- content: { type: 'technical' },
- document_type: 'technical',
- updated_at: '2025-07-30T13:00:00Z'
- },
- {
- id: 'doc-3',
- title: 'Document 3',
- content: { type: 'business' },
- document_type: 'business',
- updated_at: '2025-07-30T14:00:00Z'
- }
- ])
-
- const [selectedDocument, setSelectedDocument] = React.useState(documents[0])
- const { showToast } = { showToast: vi.fn() }
-
- return (
-
-
- {documents.map(doc => (
-
setSelectedDocument(doc)}
- >
-
{doc.document_type}
-
{doc.title}
- {selectedDocument?.id !== doc.id && (
-
- )}
-
- ))}
-
console.log('New document')}
- >
- New Document
-
-
- {selectedDocument && (
-
- Selected: {selectedDocument.title}
-
- )}
-
- )
-}
-
-describe('DocsTab Document Cards Integration', () => {
- beforeEach(() => {
- vi.clearAllMocks()
- })
-
- test('renders all document cards', () => {
- render()
-
- expect(screen.getByTestId('document-card-doc-1')).toBeInTheDocument()
- expect(screen.getByTestId('document-card-doc-2')).toBeInTheDocument()
- expect(screen.getByTestId('document-card-doc-3')).toBeInTheDocument()
- expect(screen.getByTestId('new-document-card')).toBeInTheDocument()
- })
-
- test('shows active state on selected document', () => {
- render()
-
- const doc1 = screen.getByTestId('document-card-doc-1')
- expect(doc1.className).toContain('border-blue-500')
-
- const doc2 = screen.getByTestId('document-card-doc-2')
- expect(doc2.className).not.toContain('border-blue-500')
- })
-
- test('switches between documents', () => {
- render()
-
- // Initially doc-1 is selected
- expect(screen.getByTestId('selected-document')).toHaveTextContent('Selected: Document 1')
-
- // Click on doc-2
- fireEvent.click(screen.getByTestId('document-card-doc-2'))
-
- // Now doc-2 should be selected
- expect(screen.getByTestId('selected-document')).toHaveTextContent('Selected: Document 2')
-
- // Check active states
- expect(screen.getByTestId('document-card-doc-1').className).not.toContain('border-blue-500')
- expect(screen.getByTestId('document-card-doc-2').className).toContain('border-blue-500')
- })
-
- test('deletes document with confirmation', () => {
- const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(true)
-
- render()
-
- // Click delete on doc-2
- const deleteButton = screen.getByTestId('delete-doc-2')
- fireEvent.click(deleteButton)
-
- expect(confirmSpy).toHaveBeenCalledWith('Delete "Document 2"?')
-
- // Document should be removed
- expect(screen.queryByTestId('document-card-doc-2')).not.toBeInTheDocument()
-
- confirmSpy.mockRestore()
- })
-
- test('cancels delete when user declines', () => {
- const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(false)
-
- render()
-
- // Click delete on doc-2
- const deleteButton = screen.getByTestId('delete-doc-2')
- fireEvent.click(deleteButton)
-
- // Document should still be there
- expect(screen.getByTestId('document-card-doc-2')).toBeInTheDocument()
-
- confirmSpy.mockRestore()
- })
-
- test('selects next document when deleting active document', () => {
- const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(true)
-
- render()
-
- // doc-1 is initially selected
- expect(screen.getByTestId('selected-document')).toHaveTextContent('Selected: Document 1')
-
- // Switch to doc-2
- fireEvent.click(screen.getByTestId('document-card-doc-2'))
- expect(screen.getByTestId('selected-document')).toHaveTextContent('Selected: Document 2')
-
- // Switch to doc-1 to delete a non-selected document
- fireEvent.click(screen.getByTestId('document-card-doc-1'))
-
- // Delete doc-2 (not currently selected - it should have delete button)
- const deleteButton = screen.getByTestId('delete-doc-2')
- fireEvent.click(deleteButton)
-
- // Should automatically select another document
- expect(screen.getByTestId('selected-document')).toHaveTextContent('Selected: Document')
- expect(screen.queryByTestId('document-card-doc-2')).not.toBeInTheDocument()
-
- confirmSpy.mockRestore()
- })
-
- test('does not show delete button on active card', () => {
- render()
-
- // doc-1 is active, should not have delete button
- expect(screen.queryByTestId('delete-doc-1')).not.toBeInTheDocument()
-
- // doc-2 is not active, should have delete button
- expect(screen.getByTestId('delete-doc-2')).toBeInTheDocument()
- })
-
- test('horizontal scroll container has correct classes', () => {
- const { container } = render()
-
- const scrollContainer = container.querySelector('.overflow-x-auto')
- expect(scrollContainer).toBeInTheDocument()
- expect(scrollContainer?.className).toContain('scrollbar-thin')
- expect(scrollContainer?.className).toContain('scrollbar-thumb-gray-300')
- })
-
- test('document cards maintain fixed width', () => {
- render()
-
- const cards = screen.getAllByTestId(/document-card-doc-/)
- cards.forEach(card => {
- expect(card.className).toContain('flex-shrink-0')
- expect(card.className).toContain('w-48')
- })
- })
-})
-
-describe('DocsTab Document API Integration', () => {
- test('calls deleteDocument API when deleting a document', async () => {
- const { projectService } = await import('../../../src/services/projectService')
- const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(true)
-
- // Create a test component that uses the actual deletion logic
- const DocsTabWithAPI = () => {
- const [documents, setDocuments] = React.useState([
- { id: 'doc-1', title: 'Document 1', content: {}, document_type: 'prp', updated_at: '2025-07-30' },
- { id: 'doc-2', title: 'Document 2', content: {}, document_type: 'spec', updated_at: '2025-07-30' }
- ])
- const [selectedDocument, setSelectedDocument] = React.useState(documents[0])
- const project = { id: 'proj-123', title: 'Test Project' }
- const { showToast } = { showToast: vi.fn() }
-
- const handleDelete = async (docId: string) => {
- try {
- // This mirrors the actual DocsTab deletion logic
- await projectService.deleteDocument(project.id, docId)
- setDocuments(prev => prev.filter(d => d.id !== docId))
- if (selectedDocument?.id === docId) {
- setSelectedDocument(documents.find(d => d.id !== docId) || null)
- }
- showToast('Document deleted', 'success')
- } catch (error) {
- console.error('Failed to delete document:', error)
- showToast('Failed to delete document', 'error')
- }
- }
-
- return (
-
- {documents.map(doc => (
-
- {doc.title}
-
-
- ))}
-
- )
- }
-
- render()
-
- // Click delete button
- fireEvent.click(screen.getByTestId('delete-doc-2'))
-
- // Wait for async operations
- await waitFor(() => {
- expect(projectService.deleteDocument).toHaveBeenCalledWith('proj-123', 'doc-2')
- })
-
- // Verify document is removed from UI
- expect(screen.queryByTestId('doc-doc-2')).not.toBeInTheDocument()
-
- confirmSpy.mockRestore()
- })
-
- test('handles deletion API errors gracefully', async () => {
- const { projectService } = await import('../../../src/services/projectService')
- const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(true)
- const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
-
- // Make deleteDocument reject
- projectService.deleteDocument = vi.fn().mockRejectedValue(new Error('API Error'))
-
- const DocsTabWithError = () => {
- const [documents, setDocuments] = React.useState([
- { id: 'doc-1', title: 'Document 1', content: {}, document_type: 'prp', updated_at: '2025-07-30' }
- ])
- const project = { id: 'proj-123', title: 'Test Project' }
- const showToast = vi.fn()
-
- const handleDelete = async (docId: string) => {
- try {
- await projectService.deleteDocument(project.id, docId)
- setDocuments(prev => prev.filter(d => d.id !== docId))
- showToast('Document deleted', 'success')
- } catch (error) {
- console.error('Failed to delete document:', error)
- showToast('Failed to delete document', 'error')
- }
- }
-
- return (
-
- {documents.map(doc => (
-
-
-
- ))}
-
-
- )
- }
-
- render()
-
- // Click delete button
- fireEvent.click(screen.getByTestId('delete-doc-1'))
-
- // Wait for async operations
- await waitFor(() => {
- expect(projectService.deleteDocument).toHaveBeenCalledWith('proj-123', 'doc-1')
- })
-
- // Document should still be in UI due to error
- expect(screen.getByTestId('doc-doc-1')).toBeInTheDocument()
-
- // Error should be logged
- expect(consoleSpy).toHaveBeenCalledWith('Failed to delete document:', expect.any(Error))
-
- confirmSpy.mockRestore()
- consoleSpy.mockRestore()
- })
-
- test('deletion persists after page refresh', async () => {
- const { projectService } = await import('../../../src/services/projectService')
-
- // Simulate documents before deletion
- let mockDocuments = [
- { id: 'doc-1', title: 'Document 1', content: {}, document_type: 'prp', updated_at: '2025-07-30' },
- { id: 'doc-2', title: 'Document 2', content: {}, document_type: 'spec', updated_at: '2025-07-30' }
- ]
-
- // First render - before deletion
- const { rerender } = render({mockDocuments.length}
)
- expect(screen.getByTestId('docs-count')).toHaveTextContent('2')
-
- // Mock deleteDocument to also update the mock data
- projectService.deleteDocument = vi.fn().mockImplementation(async (projectId, docId) => {
- mockDocuments = mockDocuments.filter(d => d.id !== docId)
- return Promise.resolve()
- })
-
- // Mock the list function to return current state
- projectService.listProjectDocuments = vi.fn().mockImplementation(async () => {
- return mockDocuments
- })
-
- // Perform deletion
- await projectService.deleteDocument('proj-123', 'doc-2')
-
- // Simulate page refresh by re-fetching documents
- const refreshedDocs = await projectService.listProjectDocuments('proj-123')
-
- // Re-render with refreshed data
- rerender({refreshedDocs.length}
)
-
- // Should only have 1 document after refresh
- expect(screen.getByTestId('docs-count')).toHaveTextContent('1')
- expect(refreshedDocs).toHaveLength(1)
- expect(refreshedDocs[0].id).toBe('doc-1')
- })
-})
\ No newline at end of file
diff --git a/archon-ui-main/test/components/project-tasks/DocumentCard.test.tsx b/archon-ui-main/test/components/project-tasks/DocumentCard.test.tsx
deleted file mode 100644
index 08a4906b..00000000
--- a/archon-ui-main/test/components/project-tasks/DocumentCard.test.tsx
+++ /dev/null
@@ -1,227 +0,0 @@
-import { render, screen, fireEvent } from '@testing-library/react'
-import { describe, test, expect, vi } from 'vitest'
-import React from 'react'
-import { DocumentCard, NewDocumentCard } from '../../../src/components/project-tasks/DocumentCard'
-import type { ProjectDoc } from '../../../src/components/project-tasks/DocumentCard'
-
-describe('DocumentCard', () => {
- const mockDocument: ProjectDoc = {
- id: 'doc-1',
- title: 'Test Document',
- content: { test: 'content' },
- document_type: 'prp',
- updated_at: '2025-07-30T12:00:00Z',
- }
-
- const mockHandlers = {
- onSelect: vi.fn(),
- onDelete: vi.fn(),
- }
-
- beforeEach(() => {
- vi.clearAllMocks()
- })
-
- test('renders document card with correct content', () => {
- render(
-
- )
-
- expect(screen.getByText('Test Document')).toBeInTheDocument()
- expect(screen.getByText('prp')).toBeInTheDocument()
- expect(screen.getByText('7/30/2025')).toBeInTheDocument()
- })
-
- test('shows correct icon and color for different document types', () => {
- const documentTypes = [
- { type: 'prp', expectedClass: 'text-blue-600' },
- { type: 'technical', expectedClass: 'text-green-600' },
- { type: 'business', expectedClass: 'text-purple-600' },
- { type: 'meeting_notes', expectedClass: 'text-orange-600' },
- ]
-
- documentTypes.forEach(({ type, expectedClass }) => {
- const { container, rerender } = render(
-
- )
-
- const badge = container.querySelector(`.${expectedClass}`)
- expect(badge).toBeInTheDocument()
- })
- })
-
- test('applies active styles when selected', () => {
- const { container } = render(
-
- )
-
- const card = container.firstChild as HTMLElement
- expect(card.className).toContain('border-blue-500')
- expect(card.className).toContain('scale-105')
- })
-
- test('calls onSelect when clicked', () => {
- render(
-
- )
-
- const card = screen.getByText('Test Document').closest('div')
- fireEvent.click(card!)
-
- expect(mockHandlers.onSelect).toHaveBeenCalledWith(mockDocument)
- })
-
- test('shows delete button on hover', () => {
- const { container } = render(
-
- )
-
- const card = container.firstChild as HTMLElement
-
- // Delete button should not be visible initially
- expect(screen.queryByLabelText('Delete Test Document')).not.toBeInTheDocument()
-
- // Hover over the card
- fireEvent.mouseEnter(card)
-
- // Delete button should now be visible
- expect(screen.getByLabelText('Delete Test Document')).toBeInTheDocument()
-
- // Mouse leave
- fireEvent.mouseLeave(card)
-
- // Delete button should be hidden again
- expect(screen.queryByLabelText('Delete Test Document')).not.toBeInTheDocument()
- })
-
- test('does not show delete button on active card', () => {
- const { container } = render(
-
- )
-
- const card = container.firstChild as HTMLElement
- fireEvent.mouseEnter(card)
-
- expect(screen.queryByLabelText('Delete Test Document')).not.toBeInTheDocument()
- })
-
- test('confirms before deleting', () => {
- const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(true)
-
- const { container } = render(
-
- )
-
- const card = container.firstChild as HTMLElement
- fireEvent.mouseEnter(card)
-
- const deleteButton = screen.getByLabelText('Delete Test Document')
- fireEvent.click(deleteButton)
-
- expect(confirmSpy).toHaveBeenCalledWith('Delete "Test Document"?')
- expect(mockHandlers.onDelete).toHaveBeenCalledWith('doc-1')
-
- confirmSpy.mockRestore()
- })
-
- test('cancels delete when user declines', () => {
- const confirmSpy = vi.spyOn(window, 'confirm').mockReturnValue(false)
-
- const { container } = render(
-
- )
-
- const card = container.firstChild as HTMLElement
- fireEvent.mouseEnter(card)
-
- const deleteButton = screen.getByLabelText('Delete Test Document')
- fireEvent.click(deleteButton)
-
- expect(confirmSpy).toHaveBeenCalled()
- expect(mockHandlers.onDelete).not.toHaveBeenCalled()
-
- confirmSpy.mockRestore()
- })
-
- test('applies dark mode styles correctly', () => {
- const { container } = render(
-
- )
-
- const card = container.firstChild as HTMLElement
- expect(card.className).toContain('dark:')
- })
-})
-
-describe('NewDocumentCard', () => {
- test('renders new document card', () => {
- const onClick = vi.fn()
- render()
-
- expect(screen.getByText('New Document')).toBeInTheDocument()
- })
-
- test('calls onClick when clicked', () => {
- const onClick = vi.fn()
- render()
-
- const card = screen.getByText('New Document').closest('div')
- fireEvent.click(card!)
-
- expect(onClick).toHaveBeenCalledTimes(1)
- })
-})
\ No newline at end of file
diff --git a/archon-ui-main/test/components/project-tasks/MilkdownEditor.test.tsx b/archon-ui-main/test/components/project-tasks/MilkdownEditor.test.tsx
deleted file mode 100644
index 0fe48778..00000000
--- a/archon-ui-main/test/components/project-tasks/MilkdownEditor.test.tsx
+++ /dev/null
@@ -1,272 +0,0 @@
-import { describe, test, expect } from 'vitest'
-
-// Test the PRP to Markdown conversion logic
-describe('MilkdownEditor PRP Conversion', () => {
- // Helper function to format values (extracted from component)
- const formatValue = (value: any, indent = ''): string => {
- if (Array.isArray(value)) {
- return value.map(item => `${indent}- ${formatValue(item, indent + ' ')}`).join('\n') + '\n'
- }
-
- if (typeof value === 'object' && value !== null) {
- let result = ''
- Object.entries(value).forEach(([key, val]) => {
- const formattedKey = key.replace(/_/g, ' ')
- .split(' ')
- .map(word => word.charAt(0).toUpperCase() + word.slice(1))
- .join(' ')
-
- if (typeof val === 'string' || typeof val === 'number') {
- result += `${indent}**${formattedKey}:** ${val}\n\n`
- } else {
- result += `${indent}### ${formattedKey}\n\n${formatValue(val, indent)}`
- }
- })
- return result
- }
-
- return String(value)
- }
-
- // Simplified version of convertPRPToMarkdown for testing
- const convertPRPToMarkdown = (content: any, docTitle = 'Test Doc'): string => {
- let markdown = `# ${content.title || docTitle}\n\n`
-
- // Metadata section
- if (content.version || content.author || content.date || content.status) {
- markdown += `## Metadata\n\n`
- if (content.version) markdown += `- **Version:** ${content.version}\n`
- if (content.author) markdown += `- **Author:** ${content.author}\n`
- if (content.date) markdown += `- **Date:** ${content.date}\n`
- if (content.status) markdown += `- **Status:** ${content.status}\n`
- markdown += '\n'
- }
-
- // Goal section
- if (content.goal) {
- markdown += `## Goal\n\n${content.goal}\n\n`
- }
-
- // Why section
- if (content.why) {
- markdown += `## Why\n\n`
- if (Array.isArray(content.why)) {
- content.why.forEach(item => markdown += `- ${item}\n`)
- } else {
- markdown += `${content.why}\n`
- }
- markdown += '\n'
- }
-
- // What section
- if (content.what) {
- markdown += `## What\n\n`
- if (typeof content.what === 'string') {
- markdown += `${content.what}\n\n`
- } else if (content.what.description) {
- markdown += `${content.what.description}\n\n`
-
- if (content.what.success_criteria) {
- markdown += `### Success Criteria\n\n`
- content.what.success_criteria.forEach((criterion: string) => {
- markdown += `- [ ] ${criterion}\n`
- })
- markdown += '\n'
- }
- }
- }
-
- // Handle all other sections dynamically
- const handledKeys = [
- 'title', 'version', 'author', 'date', 'status', 'goal', 'why', 'what',
- 'document_type'
- ]
-
- Object.entries(content).forEach(([key, value]) => {
- if (!handledKeys.includes(key) && value) {
- const sectionTitle = key.replace(/_/g, ' ')
- .split(' ')
- .map(word => word.charAt(0).toUpperCase() + word.slice(1))
- .join(' ')
-
- markdown += `## ${sectionTitle}\n\n`
- markdown += formatValue(value)
- markdown += '\n'
- }
- })
-
- return markdown
- }
-
- test('converts basic PRP structure to markdown', () => {
- const prp = {
- title: 'Test PRP',
- version: '1.0',
- author: 'Test Author',
- date: '2025-07-30',
- status: 'draft',
- goal: 'Test goal'
- }
-
- const markdown = convertPRPToMarkdown(prp)
-
- expect(markdown).toContain('# Test PRP')
- expect(markdown).toContain('## Metadata')
- expect(markdown).toContain('- **Version:** 1.0')
- expect(markdown).toContain('- **Author:** Test Author')
- expect(markdown).toContain('- **Date:** 2025-07-30')
- expect(markdown).toContain('- **Status:** draft')
- expect(markdown).toContain('## Goal\n\nTest goal')
- })
-
- test('handles array why section', () => {
- const prp = {
- title: 'Test PRP',
- why: ['Reason 1', 'Reason 2', 'Reason 3']
- }
-
- const markdown = convertPRPToMarkdown(prp)
-
- expect(markdown).toContain('## Why')
- expect(markdown).toContain('- Reason 1')
- expect(markdown).toContain('- Reason 2')
- expect(markdown).toContain('- Reason 3')
- })
-
- test('handles string why section', () => {
- const prp = {
- title: 'Test PRP',
- why: 'Single reason for the change'
- }
-
- const markdown = convertPRPToMarkdown(prp)
-
- expect(markdown).toContain('## Why')
- expect(markdown).toContain('Single reason for the change')
- })
-
- test('handles complex what section with success criteria', () => {
- const prp = {
- title: 'Test PRP',
- what: {
- description: 'Main description of what we are building',
- success_criteria: [
- 'Criterion 1',
- 'Criterion 2',
- 'Criterion 3'
- ]
- }
- }
-
- const markdown = convertPRPToMarkdown(prp)
-
- expect(markdown).toContain('## What')
- expect(markdown).toContain('Main description of what we are building')
- expect(markdown).toContain('### Success Criteria')
- expect(markdown).toContain('- [ ] Criterion 1')
- expect(markdown).toContain('- [ ] Criterion 2')
- expect(markdown).toContain('- [ ] Criterion 3')
- })
-
- test('handles dynamic sections', () => {
- const prp = {
- title: 'Test PRP',
- user_personas: {
- developer: {
- name: 'Developer Dan',
- goals: ['Write clean code', 'Ship features fast']
- }
- },
- technical_requirements: {
- frontend: 'React 18',
- backend: 'FastAPI',
- database: 'PostgreSQL'
- }
- }
-
- const markdown = convertPRPToMarkdown(prp)
-
- expect(markdown).toContain('## User Personas')
- expect(markdown).toContain('### Developer')
- expect(markdown).toContain('**Name:** Developer Dan')
- expect(markdown).toContain('## Technical Requirements')
- expect(markdown).toContain('**Frontend:** React 18')
- expect(markdown).toContain('**Backend:** FastAPI')
- })
-
- test('formats nested objects correctly', () => {
- const value = {
- level1: {
- level2: {
- level3: 'Deep value'
- }
- }
- }
-
- const formatted = formatValue(value)
-
- expect(formatted).toContain('### Level1')
- expect(formatted).toContain('### Level2')
- expect(formatted).toContain('**Level3:** Deep value')
- })
-
- test('formats arrays correctly', () => {
- const value = ['Item 1', 'Item 2', { nested: 'Nested item' }]
-
- const formatted = formatValue(value)
-
- expect(formatted).toContain('- Item 1')
- expect(formatted).toContain('- Item 2')
- expect(formatted).toContain('**Nested:** Nested item')
- })
-
- test('handles empty content', () => {
- const prp = {}
-
- const markdown = convertPRPToMarkdown(prp, 'Default Title')
-
- expect(markdown).toBe('# Default Title\n\n')
- })
-
- test('skips null and undefined values', () => {
- const prp = {
- title: 'Test PRP',
- null_field: null,
- undefined_field: undefined,
- empty_string: '',
- valid_field: 'Valid content'
- }
-
- const markdown = convertPRPToMarkdown(prp)
-
- expect(markdown).not.toContain('Null Field')
- expect(markdown).not.toContain('Undefined Field')
- expect(markdown).not.toContain('Empty String')
- expect(markdown).toContain('## Valid Field')
- expect(markdown).toContain('Valid content')
- })
-
- test('converts snake_case to Title Case', () => {
- const prp = {
- title: 'Test PRP',
- user_journey_mapping: 'Content',
- api_endpoint_design: 'More content'
- }
-
- const markdown = convertPRPToMarkdown(prp)
-
- expect(markdown).toContain('## User Journey Mapping')
- expect(markdown).toContain('## Api Endpoint Design')
- })
-
- test('preserves markdown formatting in content', () => {
- const prp = {
- title: 'Test PRP',
- description: '**Bold text** and *italic text* with `code`'
- }
-
- const markdown = convertPRPToMarkdown(prp)
-
- expect(markdown).toContain('**Bold text** and *italic text* with `code`')
- })
-})
\ No newline at end of file
diff --git a/archon-ui-main/test/components/project-tasks/TasksTab.dragdrop.test.tsx b/archon-ui-main/test/components/project-tasks/TasksTab.dragdrop.test.tsx
deleted file mode 100644
index 8f90811f..00000000
--- a/archon-ui-main/test/components/project-tasks/TasksTab.dragdrop.test.tsx
+++ /dev/null
@@ -1,60 +0,0 @@
-import { describe, it, expect } from 'vitest';
-import { readFileSync } from 'fs';
-import { join } from 'path';
-
-describe('TasksTab Drag and Drop Integration', () => {
- it('should properly manage movingTaskIds during drag operations', () => {
- const tasksTabPath = join(process.cwd(), 'src/components/project-tasks/TasksTab.tsx');
- const fileContent = readFileSync(tasksTabPath, 'utf-8');
-
- // Check that moveTask adds task to movingTaskIds
- expect(fileContent).toContain('setMovingTaskIds(prev => new Set([...prev, taskId]))');
-
- // Check that moveTask removes task from movingTaskIds in finally block
- expect(fileContent).toContain('finally {');
- expect(fileContent).toMatch(/finally\s*{\s*\/\/\s*Remove from loading set\s*setMovingTaskIds/);
-
- // Check that the cleanup happens even on error
- const moveTaskMatch = fileContent.match(/const moveTask[\s\S]*?\n{2}\};/);
- expect(moveTaskMatch).toBeTruthy();
- if (moveTaskMatch) {
- const moveTaskFunction = moveTaskMatch[0];
- expect(moveTaskFunction).toContain('try {');
- expect(moveTaskFunction).toContain('catch (error)');
- expect(moveTaskFunction).toContain('finally {');
- }
- });
-
- it('should pass movingTaskIds to TaskBoardView', () => {
- const tasksTabPath = join(process.cwd(), 'src/components/project-tasks/TasksTab.tsx');
- const fileContent = readFileSync(tasksTabPath, 'utf-8');
-
- // Check that movingTaskIds is passed to TaskBoardView
- expect(fileContent).toContain('movingTaskIds={movingTaskIds}');
- });
-
- it('should handle task completion through moveTask', () => {
- const tasksTabPath = join(process.cwd(), 'src/components/project-tasks/TasksTab.tsx');
- const fileContent = readFileSync(tasksTabPath, 'utf-8');
-
- // Check that completeTask calls moveTask
- expect(fileContent).toMatch(/completeTask.*moveTask\(taskId, 'done'\)/s);
- });
-
- it('should have optimistic updates in moveTask', () => {
- const tasksTabPath = join(process.cwd(), 'src/components/project-tasks/TasksTab.tsx');
- const fileContent = readFileSync(tasksTabPath, 'utf-8');
-
- // Check for optimistic update comment and implementation
- expect(fileContent).toContain('// Optimistically update UI for immediate feedback');
- expect(fileContent).toContain('setTasks(prev => prev.map(task =>');
- });
-
- it('should revert on error as indicated by comment', () => {
- const tasksTabPath = join(process.cwd(), 'src/components/project-tasks/TasksTab.tsx');
- const fileContent = readFileSync(tasksTabPath, 'utf-8');
-
- // Check for revert comment
- expect(fileContent).toContain('// Revert optimistic update - polling will sync correct state');
- });
-});
\ No newline at end of file
diff --git a/archon-ui-main/test/components/prp/PRPViewer.test.tsx b/archon-ui-main/test/components/prp/PRPViewer.test.tsx
deleted file mode 100644
index 1112fe1a..00000000
--- a/archon-ui-main/test/components/prp/PRPViewer.test.tsx
+++ /dev/null
@@ -1,186 +0,0 @@
-import { render, screen, fireEvent } from '@testing-library/react'
-import { describe, test, expect, vi } from 'vitest'
-import React from 'react'
-import { PRPViewer } from '../../../src/components/prp/PRPViewer'
-import type { PRPContent } from '../../../src/components/prp/types/prp.types'
-
-describe('PRPViewer', () => {
- const mockContent: PRPContent = {
- title: 'Test PRP',
- version: '1.0',
- author: 'Test Author',
- date: '2025-07-30',
- status: 'draft',
- goal: 'Test goal with [Image #1] placeholder',
- why: 'Test reason with [Image #2] reference',
- what: {
- description: 'Test description with [Image #3] and [Image #4]',
- success_criteria: ['Criterion 1', 'Criterion 2 with [Image #5]']
- },
- context: {
- background: 'Background with [Image #6]',
- objectives: ['Objective 1', 'Objective 2']
- }
- }
-
- test('renders without [Image #N] placeholders', () => {
- render()
-
- // Check that [Image #N] placeholders are replaced
- expect(screen.queryByText(/\[Image #\d+\]/)).not.toBeInTheDocument()
-
- // Check that content is present
- expect(screen.getByText(/Test goal/)).toBeInTheDocument()
- expect(screen.getByText(/Test reason/)).toBeInTheDocument()
- expect(screen.getByText(/Test description/)).toBeInTheDocument()
- })
-
- test('processes nested content with image placeholders', () => {
- const { container } = render()
-
- // Check that the content has been processed
- const htmlContent = container.innerHTML
-
- // Should not contain raw [Image #N] text
- expect(htmlContent).not.toMatch(/\[Image #\d+\]/)
-
- // Should contain processed markdown image syntax
- expect(htmlContent).toContain('Image 1')
- expect(htmlContent).toContain('Image 2')
- })
-
- test('renders metadata section correctly', () => {
- render()
-
- expect(screen.getByText('Test PRP')).toBeInTheDocument()
- expect(screen.getByText('1.0')).toBeInTheDocument()
- expect(screen.getByText('Test Author')).toBeInTheDocument()
- expect(screen.getByText('draft')).toBeInTheDocument()
- })
-
- test('handles empty content gracefully', () => {
- render()
-
- // Should render without errors
- expect(screen.getByText(/Metadata/)).toBeInTheDocument()
- })
-
- test('handles null content', () => {
- render()
-
- expect(screen.getByText('No PRP content available')).toBeInTheDocument()
- })
-
- test('handles string content in objects', () => {
- const stringContent = {
- title: 'String Test',
- description: 'This has [Image #1] in it'
- }
-
- render()
-
- // Should process the image placeholder
- expect(screen.queryByText(/\[Image #1\]/)).not.toBeInTheDocument()
- expect(screen.getByText(/This has/)).toBeInTheDocument()
- })
-
- test('handles array content with image placeholders', () => {
- const arrayContent = {
- title: 'Array Test',
- items: [
- 'Item 1 with [Image #1]',
- 'Item 2 with [Image #2]',
- { nested: 'Nested with [Image #3]' }
- ]
- }
-
- render()
-
- // Should process all image placeholders
- expect(screen.queryByText(/\[Image #\d+\]/)).not.toBeInTheDocument()
- })
-
- test('renders collapsible sections', () => {
- render()
-
- // Find collapsible sections
- const contextSection = screen.getByText('Context').closest('div')
- expect(contextSection).toBeInTheDocument()
-
- // Should have chevron icon for collapsible sections
- const chevrons = screen.getAllByTestId('chevron-icon')
- expect(chevrons.length).toBeGreaterThan(0)
- })
-
- test('toggles section visibility', () => {
- render()
-
- // Find a collapsible section header
- const contextHeader = screen.getByText('Context').closest('button')
-
- // The section should be visible initially (defaultOpen for first 5 sections)
- expect(screen.getByText(/Background with/)).toBeInTheDocument()
-
- // Click to collapse
- fireEvent.click(contextHeader!)
-
- // Content should be hidden
- expect(screen.queryByText(/Background with/)).not.toBeInTheDocument()
-
- // Click to expand
- fireEvent.click(contextHeader!)
-
- // Content should be visible again
- expect(screen.getByText(/Background with/)).toBeInTheDocument()
- })
-
- test('applies dark mode styles', () => {
- const { container } = render()
-
- const viewer = container.querySelector('.prp-viewer')
- expect(viewer?.className).toContain('dark')
- })
-
- test('uses section overrides when provided', () => {
- const CustomSection = ({ data, title }: any) => (
-
-
{title}
-
Custom rendering of: {JSON.stringify(data)}
-
- )
-
- const overrides = {
- context: CustomSection
- }
-
- render()
-
- expect(screen.getByTestId('custom-section')).toBeInTheDocument()
- expect(screen.getByText(/Custom rendering of/)).toBeInTheDocument()
- })
-
- test('sorts sections by group', () => {
- const complexContent = {
- title: 'Complex PRP',
- // These should be sorted in a specific order
- validation_gates: { test: 'validation' },
- user_personas: { test: 'personas' },
- context: { test: 'context' },
- user_flows: { test: 'flows' },
- success_metrics: { test: 'metrics' }
- }
-
- const { container } = render()
-
- // Get all section titles in order
- const sectionTitles = Array.from(
- container.querySelectorAll('h3')
- ).map(el => el.textContent)
-
- // Context should come before personas
- const contextIndex = sectionTitles.findIndex(t => t?.includes('Context'))
- const personasIndex = sectionTitles.findIndex(t => t?.includes('Personas'))
-
- expect(contextIndex).toBeLessThan(personasIndex)
- })
-})
\ No newline at end of file
diff --git a/archon-ui-main/test/config/api.test.ts b/archon-ui-main/test/config/api.test.ts
deleted file mode 100644
index f5243961..00000000
--- a/archon-ui-main/test/config/api.test.ts
+++ /dev/null
@@ -1,206 +0,0 @@
-/**
- * Tests for API configuration port requirements
- */
-
-import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
-
-describe('API Configuration', () => {
- let originalEnv: any;
-
- beforeEach(() => {
- // Save original environment
- originalEnv = { ...import.meta.env };
-
- // Clear the module cache to ensure fresh imports
- vi.resetModules();
- });
-
- afterEach(() => {
- // Restore original environment
- Object.keys(import.meta.env).forEach(key => {
- delete (import.meta.env as any)[key];
- });
- Object.assign(import.meta.env, originalEnv);
- });
-
- describe('getApiUrl', () => {
- it('should use VITE_API_URL when provided', async () => {
- // Set VITE_API_URL
- (import.meta.env as any).VITE_API_URL = 'http://custom-api:9999';
-
- const { getApiUrl } = await import('../../src/config/api');
- expect(getApiUrl()).toBe('http://custom-api:9999');
- });
-
- it('should return empty string in production mode', async () => {
- // Set production mode
- (import.meta.env as any).PROD = true;
-
- // It should not use VITE_API_URL
- (import.meta.env as any).VITE_API_URL = 'http://custom-api:9999';
-
- const { getApiUrl } = await import('../../src/config/api');
- expect(getApiUrl()).toBe('');
- });
-
- it('should use default port 8181 when no port environment variables are set in development', async () => {
- // Development mode without any port variables
- delete (import.meta.env as any).PROD;
- delete (import.meta.env as any).VITE_API_URL;
- delete (import.meta.env as any).VITE_ARCHON_SERVER_PORT;
- delete (import.meta.env as any).VITE_PORT;
- delete (import.meta.env as any).ARCHON_SERVER_PORT;
-
- // Mock window.location
- Object.defineProperty(window, 'location', {
- value: {
- protocol: 'http:',
- hostname: 'localhost'
- },
- writable: true
- });
-
- const { getApiUrl } = await import('../../src/config/api');
-
- expect(getApiUrl()).toBe('http://localhost:8181');
- });
-
- it('should use VITE_ARCHON_SERVER_PORT when set in development', async () => {
- // Development mode with custom port via VITE_ prefix
- delete (import.meta.env as any).PROD;
- delete (import.meta.env as any).VITE_API_URL;
- (import.meta.env as any).VITE_ARCHON_SERVER_PORT = '9191';
-
- // Mock window.location
- Object.defineProperty(window, 'location', {
- value: {
- protocol: 'http:',
- hostname: 'localhost'
- },
- writable: true
- });
-
- const { getApiUrl } = await import('../../src/config/api');
- expect(getApiUrl()).toBe('http://localhost:9191');
- });
-
- it('should use custom port with https protocol', async () => {
- // Development mode with custom port and https via VITE_ prefix
- delete (import.meta.env as any).PROD;
- delete (import.meta.env as any).VITE_API_URL;
- (import.meta.env as any).VITE_ARCHON_SERVER_PORT = '8443';
-
- // Mock window.location with https
- Object.defineProperty(window, 'location', {
- value: {
- protocol: 'https:',
- hostname: 'example.com'
- },
- writable: true
- });
-
- const { getApiUrl } = await import('../../src/config/api');
- expect(getApiUrl()).toBe('https://example.com:8443');
- });
- });
-
-
- describe('Port validation', () => {
- it('should handle various port formats', async () => {
- const testCases = [
- { port: '80', expected: 'http://localhost:80' },
- { port: '443', expected: 'http://localhost:443' },
- { port: '3000', expected: 'http://localhost:3000' },
- { port: '8080', expected: 'http://localhost:8080' },
- { port: '65535', expected: 'http://localhost:65535' },
- ];
-
- for (const { port, expected } of testCases) {
- vi.resetModules();
- delete (import.meta.env as any).PROD;
- delete (import.meta.env as any).VITE_API_URL;
- (import.meta.env as any).VITE_ARCHON_SERVER_PORT = port;
-
- Object.defineProperty(window, 'location', {
- value: {
- protocol: 'http:',
- hostname: 'localhost'
- },
- writable: true
- });
-
- const { getApiUrl } = await import('../../src/config/api');
- expect(getApiUrl()).toBe(expected);
- }
- });
- });
-});
-
-describe('MCP Client Service Configuration', () => {
- let originalEnv: any;
-
- beforeEach(() => {
- originalEnv = { ...import.meta.env };
- vi.resetModules();
- });
-
- afterEach(() => {
- Object.keys(import.meta.env).forEach(key => {
- delete (import.meta.env as any)[key];
- });
- Object.assign(import.meta.env, originalEnv);
- });
-
- it('should throw error when ARCHON_MCP_PORT is not set', async () => {
- delete (import.meta.env as any).ARCHON_MCP_PORT;
-
- const { mcpClientService } = await import('../../src/services/mcpClientService');
-
- await expect(mcpClientService.createArchonClient()).rejects.toThrow('ARCHON_MCP_PORT environment variable is required');
- await expect(mcpClientService.createArchonClient()).rejects.toThrow('Default value: 8051');
- });
-
- it('should use ARCHON_MCP_PORT when set', async () => {
- (import.meta.env as any).ARCHON_MCP_PORT = '9051';
- (import.meta.env as any).ARCHON_SERVER_PORT = '8181';
-
- // Mock window.location
- Object.defineProperty(window, 'location', {
- value: {
- protocol: 'http:',
- hostname: 'localhost'
- },
- writable: true
- });
-
- // Mock the API call
- global.fetch = vi.fn().mockResolvedValue({
- ok: true,
- json: async () => ({
- id: 'test-id',
- name: 'Archon',
- transport_type: 'http',
- connection_status: 'connected'
- })
- });
-
- const { mcpClientService } = await import('../../src/services/mcpClientService');
-
- try {
- await mcpClientService.createArchonClient();
-
- // Verify the fetch was called with the correct URL
- expect(global.fetch).toHaveBeenCalledWith(
- expect.stringContaining('/api/mcp/clients'),
- expect.objectContaining({
- method: 'POST',
- body: expect.stringContaining('9051')
- })
- );
- } catch (error) {
- // If it fails due to actual API call, that's okay for this test
- // We're mainly testing that it constructs the URL correctly
- expect(error).toBeDefined();
- }
- });
-});
diff --git a/archon-ui-main/test/errors.test.tsx b/archon-ui-main/test/errors.test.tsx
deleted file mode 100644
index 3971f4af..00000000
--- a/archon-ui-main/test/errors.test.tsx
+++ /dev/null
@@ -1,236 +0,0 @@
-import { render, screen, fireEvent } from '@testing-library/react'
-import { describe, test, expect, vi, beforeEach, afterEach } from 'vitest'
-import React from 'react'
-import { credentialsService } from '../src/services/credentialsService'
-
-describe('Error Handling Tests', () => {
- test('api error simulation', () => {
- const MockApiComponent = () => {
- const [error, setError] = React.useState('')
- const [loading, setLoading] = React.useState(false)
-
- const fetchData = async () => {
- setLoading(true)
- try {
- // Simulate API error
- throw new Error('Network error')
- } catch (err) {
- setError('Failed to load data')
- } finally {
- setLoading(false)
- }
- }
-
- return (
-
-
- {loading &&
Loading...
}
- {error &&
{error}
}
-
- )
- }
-
- render()
-
- fireEvent.click(screen.getByText('Load Data'))
- expect(screen.getByRole('alert')).toHaveTextContent('Failed to load data')
- })
-
- test('timeout error simulation', () => {
- const MockTimeoutComponent = () => {
- const [status, setStatus] = React.useState('idle')
-
- const handleTimeout = () => {
- setStatus('loading')
- setTimeout(() => {
- setStatus('timeout')
- }, 100)
- }
-
- return (
-
-
- {status === 'loading' &&
Loading...
}
- {status === 'timeout' &&
Request timed out
}
-
- )
- }
-
- render()
-
- fireEvent.click(screen.getByText('Start Request'))
- expect(screen.getByText('Loading...')).toBeInTheDocument()
-
- // Wait for timeout
- setTimeout(() => {
- expect(screen.getByRole('alert')).toHaveTextContent('Request timed out')
- }, 150)
- })
-
- test('form validation errors', () => {
- const MockFormErrors = () => {
- const [values, setValues] = React.useState({ name: '', email: '' })
- const [errors, setErrors] = React.useState([])
-
- const validate = () => {
- const newErrors: string[] = []
- if (!values.name) newErrors.push('Name is required')
- if (!values.email) newErrors.push('Email is required')
- if (values.email && !values.email.includes('@')) {
- newErrors.push('Invalid email format')
- }
- setErrors(newErrors)
- }
-
- return (
-
-
setValues({ ...values, name: e.target.value })}
- />
-
setValues({ ...values, email: e.target.value })}
- />
-
- {errors.length > 0 && (
-
- {errors.map((error, index) => (
-
{error}
- ))}
-
- )}
-
- )
- }
-
- render()
-
- // Submit empty form
- fireEvent.click(screen.getByText('Submit'))
-
- const alert = screen.getByRole('alert')
- expect(alert).toHaveTextContent('Name is required')
- expect(alert).toHaveTextContent('Email is required')
- })
-
- test('connection error recovery', () => {
- const MockConnection = () => {
- const [connected, setConnected] = React.useState(true)
- const [error, setError] = React.useState('')
-
- const handleDisconnect = () => {
- setConnected(false)
- setError('Connection lost')
- }
-
- const handleReconnect = () => {
- setConnected(true)
- setError('')
- }
-
- return (
-
-
Status: {connected ? 'Connected' : 'Disconnected'}
- {error &&
{error}
}
-
-
-
- )
- }
-
- render()
-
- expect(screen.getByText('Status: Connected')).toBeInTheDocument()
-
- fireEvent.click(screen.getByText('Simulate Disconnect'))
- expect(screen.getByText('Status: Disconnected')).toBeInTheDocument()
- expect(screen.getByRole('alert')).toHaveTextContent('Connection lost')
-
- fireEvent.click(screen.getByText('Reconnect'))
- expect(screen.getByText('Status: Connected')).toBeInTheDocument()
- expect(screen.queryByRole('alert')).not.toBeInTheDocument()
- })
-
- test('user friendly error messages', () => {
- const MockErrorMessages = () => {
- const [errorType, setErrorType] = React.useState('')
-
- const getErrorMessage = (type: string) => {
- switch (type) {
- case '401':
- return 'Please log in to continue'
- case '403':
- return "You don't have permission to access this"
- case '404':
- return "We couldn't find what you're looking for"
- case '500':
- return 'Something went wrong on our end'
- default:
- return ''
- }
- }
-
- return (
-
-
-
-
-
- {errorType && (
-
{getErrorMessage(errorType)}
- )}
-
- )
- }
-
- render()
-
- fireEvent.click(screen.getByText('401 Error'))
- expect(screen.getByRole('alert')).toHaveTextContent('Please log in to continue')
-
- fireEvent.click(screen.getByText('404 Error'))
- expect(screen.getByRole('alert')).toHaveTextContent("We couldn't find what you're looking for")
-
- fireEvent.click(screen.getByText('500 Error'))
- expect(screen.getByRole('alert')).toHaveTextContent('Something went wrong on our end')
- })
-})
-
-describe('CredentialsService Error Handling', () => {
- const originalFetch = global.fetch
-
- beforeEach(() => {
- global.fetch = vi.fn() as any
- })
-
- afterEach(() => {
- global.fetch = originalFetch
- })
-
- test('should handle network errors with context', async () => {
- const mockError = new Error('Network request failed')
- ;(global.fetch as any).mockRejectedValueOnce(mockError)
-
- await expect(credentialsService.createCredential({
- key: 'TEST_KEY',
- value: 'test',
- is_encrypted: false,
- category: 'test'
- })).rejects.toThrow(/Network error while creating credential 'test_key'/)
- })
-
- test('should preserve context in error messages', async () => {
- const mockError = new Error('database error')
- ;(global.fetch as any).mockRejectedValueOnce(mockError)
-
- await expect(credentialsService.updateCredential({
- key: 'OPENAI_API_KEY',
- value: 'sk-test',
- is_encrypted: true,
- category: 'api_keys'
- })).rejects.toThrow(/Updating credential 'OPENAI_API_KEY' failed/)
- })
-})
\ No newline at end of file
diff --git a/archon-ui-main/test/hooks/usePolling.test.ts b/archon-ui-main/test/hooks/usePolling.test.ts
deleted file mode 100644
index f374da70..00000000
--- a/archon-ui-main/test/hooks/usePolling.test.ts
+++ /dev/null
@@ -1,246 +0,0 @@
-import { renderHook, act, waitFor } from '@testing-library/react';
-import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
-import { usePolling } from '../../src/hooks/usePolling';
-
-describe('usePolling Hook - REAL Tests', () => {
- beforeEach(() => {
- vi.useFakeTimers({ shouldAdvanceTime: true });
- // Mock fetch globally
- global.fetch = vi.fn();
- // Reset document visibility state
- Object.defineProperty(document, 'hidden', {
- value: false,
- writable: true,
- configurable: true
- });
- Object.defineProperty(document, 'visibilityState', {
- value: 'visible',
- writable: true,
- configurable: true
- });
- });
-
- afterEach(() => {
- vi.clearAllTimers();
- vi.useRealTimers();
- vi.clearAllMocks();
- vi.restoreAllMocks();
- });
-
- it('should poll the endpoint at specified intervals', async () => {
- const mockResponse = { data: 'test' };
- (global.fetch as any).mockResolvedValue({
- ok: true,
- status: 200,
- json: async () => mockResponse,
- headers: new Headers({ 'etag': '"v1"' })
- });
-
- const { result } = renderHook(() =>
- usePolling('/api/test', { interval: 1000 })
- );
-
- // Initially loading
- expect(result.current.isLoading).toBe(true);
- expect(result.current.data).toBeUndefined();
-
- // Wait for first fetch to complete
- await waitFor(() => {
- expect(result.current.data).toEqual(mockResponse);
- expect(result.current.isLoading).toBe(false);
- }, { timeout: 5000 });
-
- expect(global.fetch).toHaveBeenCalledTimes(1);
-
- // Advance timer to trigger second poll
- await act(async () => {
- vi.advanceTimersByTime(1000);
- });
-
- await waitFor(() => {
- expect(global.fetch).toHaveBeenCalledTimes(2);
- }, { timeout: 5000 });
-
- // Check ETag header was sent on second request
- const secondCall = (global.fetch as any).mock.calls[1];
- expect(secondCall[1].headers['If-None-Match']).toBe('"v1"');
- }, 15000);
-
- it('should handle 304 Not Modified responses correctly', async () => {
- const initialData = { value: 'initial' };
-
- // First call returns data
- (global.fetch as any).mockResolvedValueOnce({
- ok: true,
- status: 200,
- json: async () => initialData,
- headers: new Headers({ 'etag': '"v1"' })
- });
-
- const { result } = renderHook(() =>
- usePolling('/api/test', { interval: 1000 })
- );
-
- await waitFor(() => {
- expect(result.current.data).toEqual(initialData);
- expect(result.current.isLoading).toBe(false);
- }, { timeout: 5000 });
-
- // Second call returns 304 Not Modified
- (global.fetch as any).mockResolvedValueOnce({
- ok: true,
- status: 304,
- json: async () => null,
- headers: new Headers({ 'etag': '"v1"' })
- });
-
- await act(async () => {
- vi.advanceTimersByTime(1000);
- });
-
- await waitFor(() => {
- expect(global.fetch).toHaveBeenCalledTimes(2);
- }, { timeout: 5000 });
-
- // Data should remain unchanged after 304
- expect(result.current.data).toEqual(initialData);
- }, 15000);
-
- it('should pause polling when tab becomes inactive', async () => {
- // This test verifies that polling stops when the tab is hidden
- // The hook behavior is complex due to multiple useEffect hooks
- // so we'll just verify the key behavior: no excessive polling when hidden
-
- (global.fetch as any).mockResolvedValue({
- ok: true,
- status: 200,
- json: async () => ({ data: 'test' }),
- headers: new Headers()
- });
-
- const { result } = renderHook(() => usePolling('/api/test', { interval: 1000 }));
-
- // Wait for initial fetch
- await waitFor(() => {
- expect(result.current.data).toEqual({ data: 'test' });
- expect(result.current.isLoading).toBe(false);
- }, { timeout: 5000 });
-
- // Clear the mock to start fresh
- vi.clearAllMocks();
-
- // Simulate tab becoming hidden
- await act(async () => {
- Object.defineProperty(document, 'visibilityState', {
- value: 'hidden',
- writable: true,
- configurable: true
- });
- Object.defineProperty(document, 'hidden', {
- value: true,
- writable: true,
- configurable: true
- });
- document.dispatchEvent(new Event('visibilitychange'));
- });
-
- // Advance timers significantly while hidden
- await act(async () => {
- vi.advanceTimersByTime(5000);
- });
-
- // Should have minimal or no calls while hidden (allowing for edge cases)
- const hiddenCallCount = (global.fetch as any).mock.calls.length;
- expect(hiddenCallCount).toBeLessThanOrEqual(1);
-
- // Simulate tab becoming visible again
- await act(async () => {
- Object.defineProperty(document, 'visibilityState', {
- value: 'visible',
- writable: true,
- configurable: true
- });
- Object.defineProperty(document, 'hidden', {
- value: false,
- writable: true,
- configurable: true
- });
- document.dispatchEvent(new Event('visibilitychange'));
- });
-
- // Should trigger immediate refetch when becoming visible
- await waitFor(() => {
- expect((global.fetch as any).mock.calls.length).toBeGreaterThan(hiddenCallCount);
- }, { timeout: 5000 });
- }, 15000);
-
- it('should handle errors and retry with backoff', async () => {
- // First call fails
- (global.fetch as any).mockRejectedValueOnce(new Error('Network error'));
-
- const { result } = renderHook(() =>
- usePolling('/api/test', { interval: 1000 })
- );
-
- await waitFor(() => {
- expect(result.current.error).toBeInstanceOf(Error);
- expect(result.current.error?.message).toBe('Network error');
- expect(result.current.isLoading).toBe(false);
- }, { timeout: 5000 });
-
- expect(global.fetch).toHaveBeenCalledTimes(1);
-
- // Second call succeeds
- (global.fetch as any).mockResolvedValueOnce({
- ok: true,
- status: 200,
- json: async () => ({ data: 'recovered' }),
- headers: new Headers()
- });
-
- // Advance timer for retry
- await act(async () => {
- vi.advanceTimersByTime(1000);
- });
-
- await waitFor(() => {
- expect(result.current.data).toEqual({ data: 'recovered' });
- expect(result.current.error).toBeNull();
- }, { timeout: 5000 });
- }, 15000);
-
- it('should cleanup on unmount', async () => {
- (global.fetch as any).mockResolvedValue({
- ok: true,
- status: 200,
- json: async () => ({ data: 'test' }),
- headers: new Headers()
- });
-
- const { unmount, result } = renderHook(() =>
- usePolling('/api/test', { interval: 1000 })
- );
-
- // Wait for initial fetch to complete
- await waitFor(() => {
- expect(global.fetch).toHaveBeenCalledTimes(1);
- expect(result.current.isLoading).toBe(false);
- }, { timeout: 5000 });
-
- // Clear any pending timers before unmount
- vi.clearAllTimers();
-
- unmount();
-
- // Reset mocks to clear call count
- const callCountBeforeAdvance = (global.fetch as any).mock.calls.length;
-
- // Advance timers after unmount
- await act(async () => {
- vi.advanceTimersByTime(5000);
- });
-
- // No additional calls should be made after unmount
- expect((global.fetch as any).mock.calls.length).toBe(callCountBeforeAdvance);
- }, 15000);
-});
\ No newline at end of file
diff --git a/archon-ui-main/test/pages.test.tsx b/archon-ui-main/test/pages.test.tsx
deleted file mode 100644
index bd7111be..00000000
--- a/archon-ui-main/test/pages.test.tsx
+++ /dev/null
@@ -1,116 +0,0 @@
-import { render, screen } from '@testing-library/react'
-import { describe, test, expect, vi } from 'vitest'
-import React from 'react'
-import { isLmConfigured } from '../src/utils/onboarding'
-import type { NormalizedCredential } from '../src/utils/onboarding'
-
-// Mock useNavigate for onboarding page test
-vi.mock('react-router-dom', () => ({
- useNavigate: () => vi.fn()
-}))
-
-describe('Page Load Tests', () => {
- test('simple page component renders', () => {
- const MockPage = () => Projects
- render()
- expect(screen.getByText('Projects')).toBeInTheDocument()
- })
-
- test('knowledge base mock renders', () => {
- const MockKnowledgePage = () => Knowledge Base
- render()
- expect(screen.getByText('Knowledge Base')).toBeInTheDocument()
- })
-
- test('settings mock renders', () => {
- const MockSettingsPage = () => Settings
- render()
- expect(screen.getByText('Settings')).toBeInTheDocument()
- })
-
- test('mcp mock renders', () => {
- const MockMCPPage = () => MCP Servers
- render()
- expect(screen.getByText('MCP Servers')).toBeInTheDocument()
- })
-
- test('tasks mock renders', () => {
- const MockTasksPage = () => (
-
-
Tasks
-
TODO
-
In Progress
-
Done
-
- )
- render()
- expect(screen.getByText('Tasks')).toBeInTheDocument()
- expect(screen.getByText('TODO')).toBeInTheDocument()
- expect(screen.getByText('In Progress')).toBeInTheDocument()
- expect(screen.getByText('Done')).toBeInTheDocument()
- })
-
- test('onboarding page renders', () => {
- const MockOnboardingPage = () => Welcome to Archon
- render()
- expect(screen.getByText('Welcome to Archon')).toBeInTheDocument()
- })
-})
-
-describe('Onboarding Detection Tests', () => {
- test('isLmConfigured returns true when provider is openai and OPENAI_API_KEY exists', () => {
- const ragCreds: NormalizedCredential[] = [
- { key: 'LLM_PROVIDER', value: 'openai', category: 'rag_strategy' }
- ]
- const apiKeyCreds: NormalizedCredential[] = [
- { key: 'OPENAI_API_KEY', value: 'sk-test123', category: 'api_keys' }
- ]
-
- expect(isLmConfigured(ragCreds, apiKeyCreds)).toBe(true)
- })
-
- test('isLmConfigured returns true when provider is openai and OPENAI_API_KEY is encrypted', () => {
- const ragCreds: NormalizedCredential[] = [
- { key: 'LLM_PROVIDER', value: 'openai', category: 'rag_strategy' }
- ]
- const apiKeyCreds: NormalizedCredential[] = [
- { key: 'OPENAI_API_KEY', is_encrypted: true, encrypted_value: 'encrypted_sk-test123', category: 'api_keys' }
- ]
-
- expect(isLmConfigured(ragCreds, apiKeyCreds)).toBe(true)
- })
-
- test('isLmConfigured returns false when provider is openai and no OPENAI_API_KEY', () => {
- const ragCreds: NormalizedCredential[] = [
- { key: 'LLM_PROVIDER', value: 'openai', category: 'rag_strategy' }
- ]
- const apiKeyCreds: NormalizedCredential[] = []
-
- expect(isLmConfigured(ragCreds, apiKeyCreds)).toBe(false)
- })
-
- test('isLmConfigured returns true when provider is ollama regardless of API keys', () => {
- const ragCreds: NormalizedCredential[] = [
- { key: 'LLM_PROVIDER', value: 'ollama', category: 'rag_strategy' }
- ]
- const apiKeyCreds: NormalizedCredential[] = []
-
- expect(isLmConfigured(ragCreds, apiKeyCreds)).toBe(true)
- })
-
- test('isLmConfigured returns true when no provider but OPENAI_API_KEY exists', () => {
- const ragCreds: NormalizedCredential[] = []
- const apiKeyCreds: NormalizedCredential[] = [
- { key: 'OPENAI_API_KEY', value: 'sk-test123', category: 'api_keys' }
- ]
-
- expect(isLmConfigured(ragCreds, apiKeyCreds)).toBe(true)
- })
-
- test('isLmConfigured returns false when no provider and no OPENAI_API_KEY', () => {
- const ragCreds: NormalizedCredential[] = []
- const apiKeyCreds: NormalizedCredential[] = []
-
- expect(isLmConfigured(ragCreds, apiKeyCreds)).toBe(false)
- })
-})
\ No newline at end of file
diff --git a/archon-ui-main/test/pages/ProjectPage.performance.test.tsx b/archon-ui-main/test/pages/ProjectPage.performance.test.tsx
deleted file mode 100644
index 4019ee95..00000000
--- a/archon-ui-main/test/pages/ProjectPage.performance.test.tsx
+++ /dev/null
@@ -1,124 +0,0 @@
-import { describe, it, expect } from 'vitest';
-import { readFileSync } from 'fs';
-import { join } from 'path';
-
-describe('ProjectPage Performance Optimizations', () => {
- const projectPagePath = join(process.cwd(), 'src/pages/ProjectPage.tsx');
- const projectServicePath = join(process.cwd(), 'src/services/projectService.ts');
-
- it('should use batch API call for task counts instead of N+1 queries', () => {
- const fileContent = readFileSync(projectPagePath, 'utf-8');
-
- // Verify batch endpoint is being used
- expect(fileContent).toContain('getTaskCountsForAllProjects');
-
- // Verify we're NOT using Promise.allSettled for parallel fetching
- expect(fileContent).not.toContain('Promise.allSettled');
-
- // Verify single batch API call pattern
- expect(fileContent).toContain('await projectService.getTaskCountsForAllProjects()');
- });
-
- it('should have memoized handleProjectSelect to prevent duplicate calls', () => {
- const fileContent = readFileSync(projectPagePath, 'utf-8');
-
- // Check that handleProjectSelect is wrapped with useCallback
- expect(fileContent).toMatch(/const handleProjectSelect = useCallback\(/);
-
- // Check for early return if same project
- expect(fileContent).toContain('if (selectedProject?.id === project.id) return');
-
- // Check dependency array includes selectedProject?.id
- expect(fileContent).toMatch(/\}, \[.*selectedProject\?\.id.*\]\)/);
- });
-
- it('should implement task counts cache with TTL', () => {
- const fileContent = readFileSync(projectPagePath, 'utf-8');
-
- // Check cache ref is defined
- expect(fileContent).toContain('const taskCountsCache = useRef');
-
- // Check cache structure includes timestamp
- expect(fileContent).toContain('timestamp: number');
-
- // Check cache is checked before API call (5-minute TTL = 300000ms)
- expect(fileContent).toContain('(now - taskCountsCache.current.timestamp) < 300000');
-
- // Check cache is updated after successful API call
- expect(fileContent).toContain('taskCountsCache.current = {');
- });
-
- it('should disable polling during project switching and drag operations', () => {
- const fileContent = readFileSync(projectPagePath, 'utf-8');
-
- // Check useTaskPolling enabled parameter includes conditions
- expect(fileContent).toMatch(/enabled:.*!isSwitchingProject.*movingTaskIds\.size === 0/);
-
- // Verify isSwitchingProject state exists
- expect(fileContent).toContain('const [isSwitchingProject, setIsSwitchingProject]');
- });
-
- it('should have debounce utility implemented', () => {
- const debouncePath = join(process.cwd(), 'src/utils/debounce.ts');
- const fileContent = readFileSync(debouncePath, 'utf-8');
-
- // Check debounce function exists
- expect(fileContent).toContain('export function debounce');
-
- // Check it has proper TypeScript types
- expect(fileContent).toContain('T extends (...args: any[]) => any');
-
- // Check timeout clearing logic
- expect(fileContent).toContain('clearTimeout(timeoutId)');
- });
-
- it('should apply debouncing to loadTaskCountsForAllProjects', () => {
- const fileContent = readFileSync(projectPagePath, 'utf-8');
-
- // Check debounce is imported
- expect(fileContent).toContain('import { debounce } from "../utils/debounce"');
-
- // Check debounced version is created
- expect(fileContent).toContain('const debouncedLoadTaskCounts = useMemo');
- expect(fileContent).toContain('debounce((projectIds: string[])');
-
- // Check debounced version is used instead of direct calls
- expect(fileContent).toContain('debouncedLoadTaskCounts(projectIds)');
-
- // Verify 1000ms delay
- expect(fileContent).toContain('}, 1000)');
- });
-
- it('should have batch task counts endpoint in backend service', () => {
- const serviceContent = readFileSync(projectServicePath, 'utf-8');
-
- // Check the service method exists
- expect(serviceContent).toContain('async getTaskCountsForAllProjects()');
-
- // Check it calls the correct endpoint
- expect(serviceContent).toContain('/api/projects/task-counts');
-
- // Check return type
- expect(serviceContent).toContain('Promise>');
- });
-
- it('should not make duplicate API calls on project switch', () => {
- const fileContent = readFileSync(projectPagePath, 'utf-8');
-
- // Check that tasks are cleared immediately on switch
- expect(fileContent).toContain('setTasks([]); // Clear stale tasks immediately');
-
- // Check loading state is managed properly
- expect(fileContent).toContain('setIsSwitchingProject(true)');
- expect(fileContent).toContain('setIsSwitchingProject(false)');
- });
-
- it('should have correct import statements for performance utilities', () => {
- const fileContent = readFileSync(projectPagePath, 'utf-8');
-
- // Check all necessary React hooks are imported
- expect(fileContent).toContain('useCallback');
- expect(fileContent).toContain('useMemo');
- expect(fileContent).toContain('useRef');
- });
-});
\ No newline at end of file
diff --git a/archon-ui-main/test/pages/ProjectPage.polling.test.tsx b/archon-ui-main/test/pages/ProjectPage.polling.test.tsx
deleted file mode 100644
index 43022150..00000000
--- a/archon-ui-main/test/pages/ProjectPage.polling.test.tsx
+++ /dev/null
@@ -1,42 +0,0 @@
-import { describe, it, expect } from 'vitest';
-import { readFileSync } from 'fs';
-import { join } from 'path';
-
-describe('ProjectPage Polling Conflict Prevention', () => {
- it('should have movingTaskIds check in polling useEffect', () => {
- // Read the actual source file to verify the implementation
- const projectPagePath = join(process.cwd(), 'src/pages/ProjectPage.tsx');
- const fileContent = readFileSync(projectPagePath, 'utf-8');
-
- // Check that movingTaskIds state is declared
- expect(fileContent).toContain('const [movingTaskIds, setMovingTaskIds] = useState>(new Set())');
-
- // Check that movingTaskIds is checked before updating tasks
- expect(fileContent).toContain('if (movingTaskIds.size === 0)');
-
- // Check that merge logic is present for non-moving tasks
- expect(fileContent).toContain('if (movingTaskIds.has(task.id))');
- expect(fileContent).toContain('return task; // Preserve local state for moving tasks');
-
- // Check that movingTaskIds is in the dependency array
- expect(fileContent).toMatch(/\}, \[.*movingTaskIds.*\]\)/);
- });
-
- it('should pass movingTaskIds props to TasksTab', () => {
- const projectPagePath = join(process.cwd(), 'src/pages/ProjectPage.tsx');
- const fileContent = readFileSync(projectPagePath, 'utf-8');
-
- // Check that movingTaskIds is passed as prop
- expect(fileContent).toContain('movingTaskIds={movingTaskIds}');
- expect(fileContent).toContain('setMovingTaskIds={setMovingTaskIds}');
- });
-
- it('should have TasksTab accept movingTaskIds props', () => {
- const tasksTabPath = join(process.cwd(), 'src/components/project-tasks/TasksTab.tsx');
- const fileContent = readFileSync(tasksTabPath, 'utf-8');
-
- // Check that TasksTab accepts the props
- expect(fileContent).toContain('movingTaskIds: Set');
- expect(fileContent).toContain('setMovingTaskIds: (ids: Set) => void');
- });
-});
\ No newline at end of file
diff --git a/archon-ui-main/test/services/projectService.test.ts b/archon-ui-main/test/services/projectService.test.ts
deleted file mode 100644
index 98715954..00000000
--- a/archon-ui-main/test/services/projectService.test.ts
+++ /dev/null
@@ -1,393 +0,0 @@
-/**
- * Unit tests for projectService document CRUD operations
- */
-
-import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
-import type { Document } from '../../src/services/projectService';
-
-// Mock fetch globally
-global.fetch = vi.fn();
-
-describe('projectService Document Operations', () => {
- let projectService: any;
-
- beforeEach(async () => {
- // Reset all mocks
- vi.resetAllMocks();
- vi.resetModules();
-
- // Import fresh instance of projectService
- const module = await import('../../src/services/projectService');
- projectService = module.projectService;
- });
-
- afterEach(() => {
- vi.clearAllMocks();
- });
-
- describe('getDocument', () => {
- const mockDocument: Document = {
- id: 'doc-123',
- project_id: 'proj-456',
- title: 'Test Document',
- content: { type: 'markdown', text: 'Test content' },
- document_type: 'prp',
- metadata: { version: '1.0' },
- tags: ['test', 'sample'],
- author: 'test-user',
- created_at: '2025-08-18T10:00:00Z',
- updated_at: '2025-08-18T10:00:00Z'
- };
-
- it('should successfully fetch a document', async () => {
- // Mock successful response
- (global.fetch as any).mockResolvedValueOnce({
- ok: true,
- json: async () => ({ document: mockDocument })
- });
-
- const result = await projectService.getDocument('proj-456', 'doc-123');
-
- expect(result).toEqual(mockDocument);
- expect(global.fetch).toHaveBeenCalledWith(
- '/api/projects/proj-456/docs/doc-123',
- expect.objectContaining({
- headers: expect.objectContaining({
- 'Content-Type': 'application/json'
- })
- })
- );
- });
-
- it('should include projectId in error message when fetch fails', async () => {
- // Mock failed response
- (global.fetch as any).mockResolvedValueOnce({
- ok: false,
- status: 404,
- text: async () => '{"error": "Document not found"}'
- });
-
- const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
-
- await expect(projectService.getDocument('proj-456', 'doc-123')).rejects.toThrow();
-
- expect(consoleSpy).toHaveBeenCalledWith(
- 'Failed to get document doc-123 from project proj-456:',
- expect.any(Error)
- );
-
- consoleSpy.mockRestore();
- });
-
- it('should handle network errors', async () => {
- // Mock network error
- (global.fetch as any).mockRejectedValueOnce(new Error('Network error'));
-
- const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
-
- await expect(projectService.getDocument('proj-456', 'doc-123')).rejects.toThrow('Network error');
-
- expect(consoleSpy).toHaveBeenCalledWith(
- 'Failed to get document doc-123 from project proj-456:',
- expect.any(Error)
- );
-
- consoleSpy.mockRestore();
- });
- });
-
- describe('updateDocument', () => {
- const mockUpdatedDocument: Document = {
- id: 'doc-123',
- project_id: 'proj-456',
- title: 'Updated Document',
- content: { type: 'markdown', text: 'Updated content' },
- document_type: 'prp',
- metadata: { version: '2.0' },
- tags: ['updated', 'test'],
- author: 'test-user',
- created_at: '2025-08-18T10:00:00Z',
- updated_at: '2025-08-18T11:00:00Z'
- };
-
- const updates = {
- title: 'Updated Document',
- content: { type: 'markdown', text: 'Updated content' },
- tags: ['updated', 'test']
- };
-
- it('should successfully update a document', async () => {
- // Mock successful response
- (global.fetch as any).mockResolvedValueOnce({
- ok: true,
- json: async () => ({ document: mockUpdatedDocument })
- });
-
- const result = await projectService.updateDocument('proj-456', 'doc-123', updates);
-
- expect(result).toEqual(mockUpdatedDocument);
- expect(global.fetch).toHaveBeenCalledWith(
- '/api/projects/proj-456/docs/doc-123',
- expect.objectContaining({
- method: 'PUT',
- headers: expect.objectContaining({
- 'Content-Type': 'application/json'
- }),
- body: JSON.stringify(updates)
- })
- );
- });
-
- it('should include projectId in error message when update fails', async () => {
- // Mock failed response
- (global.fetch as any).mockResolvedValueOnce({
- ok: false,
- status: 400,
- text: async () => '{"error": "Invalid update data"}'
- });
-
- const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
-
- await expect(projectService.updateDocument('proj-456', 'doc-123', updates)).rejects.toThrow();
-
- expect(consoleSpy).toHaveBeenCalledWith(
- 'Failed to update document doc-123 in project proj-456:',
- expect.any(Error)
- );
-
- consoleSpy.mockRestore();
- });
-
- it('should handle partial updates', async () => {
- const partialUpdate = { title: 'Only Title Updated' };
-
- (global.fetch as any).mockResolvedValueOnce({
- ok: true,
- json: async () => ({ document: { ...mockUpdatedDocument, title: 'Only Title Updated' } })
- });
-
- const result = await projectService.updateDocument('proj-456', 'doc-123', partialUpdate);
-
- expect(result.title).toBe('Only Title Updated');
- expect(global.fetch).toHaveBeenCalledWith(
- '/api/projects/proj-456/docs/doc-123',
- expect.objectContaining({
- body: JSON.stringify(partialUpdate)
- })
- );
- });
- });
-
- describe('deleteDocument', () => {
- it('should successfully delete a document', async () => {
- // Mock successful response
- (global.fetch as any).mockResolvedValueOnce({
- ok: true,
- json: async () => ({})
- });
-
- await expect(projectService.deleteDocument('proj-456', 'doc-123')).resolves.toBeUndefined();
-
- expect(global.fetch).toHaveBeenCalledWith(
- '/api/projects/proj-456/docs/doc-123',
- expect.objectContaining({
- method: 'DELETE',
- headers: expect.objectContaining({
- 'Content-Type': 'application/json'
- })
- })
- );
- });
-
- it('should include projectId in error message when deletion fails', async () => {
- // Mock failed response
- (global.fetch as any).mockResolvedValueOnce({
- ok: false,
- status: 403,
- text: async () => '{"error": "Permission denied"}'
- });
-
- const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
-
- await expect(projectService.deleteDocument('proj-456', 'doc-123')).rejects.toThrow();
-
- expect(consoleSpy).toHaveBeenCalledWith(
- 'Failed to delete document doc-123 from project proj-456:',
- expect.any(Error)
- );
-
- consoleSpy.mockRestore();
- });
-
- it('should handle 404 errors appropriately', async () => {
- // Mock 404 response
- (global.fetch as any).mockResolvedValueOnce({
- ok: false,
- status: 404,
- text: async () => '{"error": "Document not found"}'
- });
-
- const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
-
- await expect(projectService.deleteDocument('proj-456', 'doc-123')).rejects.toThrow();
-
- // Verify the error is logged with project context
- expect(consoleSpy).toHaveBeenCalled();
- const errorLog = consoleSpy.mock.calls[0];
- expect(errorLog[0]).toContain('proj-456');
- expect(errorLog[0]).toContain('doc-123');
-
- consoleSpy.mockRestore();
- });
-
- it('should handle network timeouts', async () => {
- // Mock timeout error
- const timeoutError = new Error('Request timeout');
- (global.fetch as any).mockRejectedValueOnce(timeoutError);
-
- const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
-
- await expect(projectService.deleteDocument('proj-456', 'doc-123')).rejects.toThrow('Failed to call API');
-
- expect(consoleSpy).toHaveBeenCalledWith(
- 'Failed to delete document doc-123 from project proj-456:',
- expect.objectContaining({
- message: expect.stringContaining('Request timeout')
- })
- );
-
- consoleSpy.mockRestore();
- });
- });
-
- describe('listProjectDocuments', () => {
- const mockDocuments: Document[] = [
- {
- id: 'doc-1',
- project_id: 'proj-456',
- title: 'Document 1',
- content: { type: 'markdown', text: 'Content 1' },
- document_type: 'prp',
- created_at: '2025-08-18T10:00:00Z',
- updated_at: '2025-08-18T10:00:00Z'
- },
- {
- id: 'doc-2',
- project_id: 'proj-456',
- title: 'Document 2',
- content: { type: 'markdown', text: 'Content 2' },
- document_type: 'spec',
- created_at: '2025-08-18T11:00:00Z',
- updated_at: '2025-08-18T11:00:00Z'
- }
- ];
-
- it('should successfully list all project documents', async () => {
- // Mock successful response
- (global.fetch as any).mockResolvedValueOnce({
- ok: true,
- json: async () => ({ documents: mockDocuments })
- });
-
- const result = await projectService.listProjectDocuments('proj-456');
-
- expect(result).toEqual(mockDocuments);
- expect(result).toHaveLength(2);
- expect(global.fetch).toHaveBeenCalledWith(
- '/api/projects/proj-456/docs',
- expect.objectContaining({
- headers: expect.objectContaining({
- 'Content-Type': 'application/json'
- })
- })
- );
- });
-
- it('should return empty array when no documents exist', async () => {
- // Mock response with no documents
- (global.fetch as any).mockResolvedValueOnce({
- ok: true,
- json: async () => ({ documents: [] })
- });
-
- const result = await projectService.listProjectDocuments('proj-456');
-
- expect(result).toEqual([]);
- expect(result).toHaveLength(0);
- });
-
- it('should handle null documents field gracefully', async () => {
- // Mock response with null documents
- (global.fetch as any).mockResolvedValueOnce({
- ok: true,
- json: async () => ({ documents: null })
- });
-
- const result = await projectService.listProjectDocuments('proj-456');
-
- expect(result).toEqual([]);
- });
- });
-
- describe('createDocument', () => {
- const newDocumentData = {
- title: 'New Document',
- content: { type: 'markdown', text: 'New content' },
- document_type: 'prp',
- tags: ['new', 'test']
- };
-
- const mockCreatedDocument: Document = {
- id: 'doc-new',
- project_id: 'proj-456',
- ...newDocumentData,
- author: 'test-user',
- created_at: '2025-08-18T12:00:00Z',
- updated_at: '2025-08-18T12:00:00Z'
- };
-
- it('should successfully create a new document', async () => {
- // Mock successful response
- (global.fetch as any).mockResolvedValueOnce({
- ok: true,
- json: async () => ({ document: mockCreatedDocument })
- });
-
- const result = await projectService.createDocument('proj-456', newDocumentData);
-
- expect(result).toEqual(mockCreatedDocument);
- expect(result.id).toBeDefined();
- expect(global.fetch).toHaveBeenCalledWith(
- '/api/projects/proj-456/docs',
- expect.objectContaining({
- method: 'POST',
- headers: expect.objectContaining({
- 'Content-Type': 'application/json'
- }),
- body: JSON.stringify(newDocumentData)
- })
- );
- });
-
- it('should handle validation errors', async () => {
- // Mock validation error response
- (global.fetch as any).mockResolvedValueOnce({
- ok: false,
- status: 422,
- text: async () => '{"error": "Title is required"}'
- });
-
- const invalidData = { content: 'Missing title' };
- const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
-
- await expect(projectService.createDocument('proj-456', invalidData)).rejects.toThrow();
-
- expect(consoleSpy).toHaveBeenCalledWith(
- 'Failed to create document for project proj-456:',
- expect.any(Error)
- );
-
- consoleSpy.mockRestore();
- });
- });
-});
\ No newline at end of file
diff --git a/archon-ui-main/test/user_flows.test.tsx b/archon-ui-main/test/user_flows.test.tsx
deleted file mode 100644
index 71e97dfd..00000000
--- a/archon-ui-main/test/user_flows.test.tsx
+++ /dev/null
@@ -1,243 +0,0 @@
-import { render, screen, fireEvent } from '@testing-library/react'
-import { describe, test, expect, vi } from 'vitest'
-import React from 'react'
-
-describe('User Flow Tests', () => {
- test('create project flow mock', () => {
- const MockCreateProject = () => {
- const [project, setProject] = React.useState('')
- return (
-
-
Create Project
- setProject(e.target.value)}
- />
-
-
- )
- }
-
- render()
- expect(screen.getByText('Create Project')).toBeInTheDocument()
- expect(screen.getByPlaceholderText('Project title')).toBeInTheDocument()
- expect(screen.getByRole('button', { name: 'Create' })).toBeInTheDocument()
- })
-
- test('search functionality mock', () => {
- const MockSearch = () => {
- const [query, setQuery] = React.useState('')
- return (
-
-
Search
-
setQuery(e.target.value)}
- />
- {query &&
Results for: {query}
}
-
- )
- }
-
- render()
- const input = screen.getByPlaceholderText('Search knowledge base')
- fireEvent.change(input, { target: { value: 'test query' } })
- expect(screen.getByText('Results for: test query')).toBeInTheDocument()
- })
-
- test('settings toggle mock', () => {
- const MockSettings = () => {
- const [theme, setTheme] = React.useState('light')
- return (
-
-
Settings
-
-
- )
- }
-
- render()
- const button = screen.getByText('Theme: light')
- fireEvent.click(button)
- expect(screen.getByText('Theme: dark')).toBeInTheDocument()
- })
-
- test('file upload mock', () => {
- const MockUpload = () => {
- const [uploaded, setUploaded] = React.useState(false)
- return (
-
-
Upload Documents
-
setUploaded(true)} data-testid="file-input" />
- {uploaded &&
File uploaded successfully
}
-
- )
- }
-
- render()
- const input = screen.getByTestId('file-input')
- fireEvent.change(input)
- expect(screen.getByText('File uploaded successfully')).toBeInTheDocument()
- })
-
- test('connection status mock', () => {
- const MockConnection = () => {
- const [connected, setConnected] = React.useState(true)
- return (
-
-
Connection Status
-
{connected ? 'Connected' : 'Disconnected'}
-
-
- )
- }
-
- render()
- expect(screen.getByText('Connected')).toBeInTheDocument()
-
- fireEvent.click(screen.getByText('Toggle Connection'))
- expect(screen.getByText('Disconnected')).toBeInTheDocument()
- })
-
- test('task management mock', () => {
- const MockTasks = () => {
- const [tasks, setTasks] = React.useState(['Task 1', 'Task 2'])
- const addTask = () => setTasks([...tasks, `Task ${tasks.length + 1}`])
-
- return (
-
-
Task Management
-
-
- {tasks.map((task, index) => (
- - {task}
- ))}
-
-
- )
- }
-
- render()
- expect(screen.getByText('Task 1')).toBeInTheDocument()
-
- fireEvent.click(screen.getByText('Add Task'))
- expect(screen.getByText('Task 3')).toBeInTheDocument()
- })
-
- test('navigation mock', () => {
- const MockNav = () => {
- const [currentPage, setCurrentPage] = React.useState('home')
- return (
-
-
-
- Current page: {currentPage}
-
-
- )
- }
-
- render()
- expect(screen.getByText('Current page: home')).toBeInTheDocument()
-
- fireEvent.click(screen.getByText('Projects'))
- expect(screen.getByText('Current page: projects')).toBeInTheDocument()
- })
-
- test('form validation mock', () => {
- const MockForm = () => {
- const [email, setEmail] = React.useState('')
- const [error, setError] = React.useState('')
-
- const handleSubmit = () => {
- if (!email.includes('@')) {
- setError('Invalid email')
- } else {
- setError('')
- }
- }
-
- return (
-
-
Form Validation
-
setEmail(e.target.value)}
- />
-
- {error &&
{error}
}
-
- )
- }
-
- render()
- const input = screen.getByPlaceholderText('Email')
-
- fireEvent.change(input, { target: { value: 'invalid' } })
- fireEvent.click(screen.getByText('Submit'))
- expect(screen.getByRole('alert')).toHaveTextContent('Invalid email')
- })
-
- test('theme switching mock', () => {
- const MockTheme = () => {
- const [isDark, setIsDark] = React.useState(false)
- return (
-
-
Theme Test
-
-
- )
- }
-
- render()
- const button = screen.getByText('Switch to Dark')
- fireEvent.click(button)
- expect(screen.getByText('Switch to Light')).toBeInTheDocument()
- })
-
- test('data filtering mock', () => {
- const MockFilter = () => {
- const [filter, setFilter] = React.useState('')
- const items = ['Apple', 'Banana', 'Cherry']
- const filtered = items.filter(item =>
- item.toLowerCase().includes(filter.toLowerCase())
- )
-
- return (
-
-
Filter Test
-
setFilter(e.target.value)}
- />
-
- {filtered.map((item, index) => (
- - {item}
- ))}
-
-
- )
- }
-
- render()
- const input = screen.getByPlaceholderText('Filter items')
-
- fireEvent.change(input, { target: { value: 'a' } })
- expect(screen.getByText('Apple')).toBeInTheDocument()
- expect(screen.getByText('Banana')).toBeInTheDocument()
- expect(screen.queryByText('Cherry')).not.toBeInTheDocument()
- })
-})
\ No newline at end of file
diff --git a/archon-ui-main/test/utils/taskOrdering.test.ts b/archon-ui-main/test/utils/taskOrdering.test.ts
deleted file mode 100644
index cb6816fb..00000000
--- a/archon-ui-main/test/utils/taskOrdering.test.ts
+++ /dev/null
@@ -1,138 +0,0 @@
-import { describe, it, expect } from 'vitest';
-import { calculateTaskOrder, calculateReorderPosition, getDefaultTaskOrder } from '../../src/utils/taskOrdering';
-import { Task } from '../../src/types/project';
-
-// Mock task factory
-const createMockTask = (id: string, task_order: number): Task => ({
- id,
- title: `Task ${id}`,
- description: '',
- status: 'todo',
- assignee: { name: 'Test User', avatar: '' },
- feature: '',
- featureColor: '#3b82f6',
- task_order,
- created_at: new Date().toISOString(),
- updated_at: new Date().toISOString(),
- project_id: 'test-project'
-});
-
-describe('taskOrdering utilities', () => {
- describe('calculateTaskOrder', () => {
- it('does not mutate existingTasks', () => {
- const existingTasks = [createMockTask('1', 200), createMockTask('2', 100)];
- const snapshot = existingTasks.map(t => t.task_order);
- calculateTaskOrder({ position: 'first', existingTasks });
- expect(existingTasks.map(t => t.task_order)).toEqual(snapshot);
- });
-
- it('should return seed value for first task when no existing tasks', () => {
- const result = calculateTaskOrder({
- position: 'first',
- existingTasks: []
- });
- expect(result).toBe(65536);
- });
-
- it('should calculate first position correctly', () => {
- const existingTasks = [createMockTask('1', 100), createMockTask('2', 200)];
- const result = calculateTaskOrder({
- position: 'first',
- existingTasks
- });
- expect(result).toBe(50); // Math.floor(100 / 2)
- });
-
- it('should calculate last position correctly', () => {
- const existingTasks = [createMockTask('1', 100), createMockTask('2', 200)];
- const result = calculateTaskOrder({
- position: 'last',
- existingTasks
- });
- expect(result).toBe(1224); // 200 + 1024
- });
-
- it('should calculate between position correctly', () => {
- const result = calculateTaskOrder({
- position: 'between',
- existingTasks: [],
- beforeTaskOrder: 100,
- afterTaskOrder: 200
- });
- expect(result).toBe(150); // Math.floor((100 + 200) / 2)
- });
- });
-
- describe('getDefaultTaskOrder', () => {
- it('should return seed value when no existing tasks', () => {
- const result = getDefaultTaskOrder([]);
- expect(result).toBe(65536);
- });
-
- it('should return first position when existing tasks present', () => {
- const existingTasks = [createMockTask('1', 100), createMockTask('2', 200)];
- const result = getDefaultTaskOrder(existingTasks);
- expect(result).toBe(50); // Math.floor(100 / 2)
- });
- });
-
- describe('calculateReorderPosition', () => {
- const statusTasks = [
- createMockTask('1', 100),
- createMockTask('2', 200),
- createMockTask('3', 300)
- ];
-
- it('should calculate position for moving to first', () => {
- const result = calculateReorderPosition(statusTasks, 1, 0);
- expect(result).toBeLessThan(statusTasks[0].task_order);
- });
-
- it('should calculate position for moving to last', () => {
- const result = calculateReorderPosition(statusTasks, 0, 2);
- expect(result).toBeGreaterThan(statusTasks[2].task_order);
- });
-
- it('should calculate position for moving down within middle (1 -> 2)', () => {
- const result = calculateReorderPosition(statusTasks, 1, 2);
- // After excluding moving index 1, insert between 300 and end => should be >300 (or handled by "last" path)
- expect(result).toBeGreaterThan(statusTasks[2].task_order);
- });
-
- it('should calculate position for moving up within middle (2 -> 1)', () => {
- const result = calculateReorderPosition(statusTasks, 2, 1);
- // With fixed neighbor calculation, this should work correctly
- expect(result).toBeGreaterThan(statusTasks[0].task_order); // > 100
- expect(result).toBeLessThan(statusTasks[1].task_order); // < 200
- });
-
- it('should calculate position for moving between items', () => {
- const result = calculateReorderPosition(statusTasks, 0, 1);
- // Moving task 0 (order 100) to position 1 should place it before task 1 (order 200)
- // Since we removed the moving task, it should be between start and 200
- expect(result).toBeLessThan(statusTasks[1].task_order); // < 200
- expect(result).toBeGreaterThan(0); // > 0
- });
-
- it('should return integer values only', () => {
- const result1 = calculateReorderPosition(statusTasks, 1, 0);
- const result2 = calculateReorderPosition(statusTasks, 0, 2);
- const result3 = calculateReorderPosition(statusTasks, 2, 1);
-
- expect(Number.isInteger(result1)).toBe(true);
- expect(Number.isInteger(result2)).toBe(true);
- expect(Number.isInteger(result3)).toBe(true);
- });
-
- it('should handle bounds checking correctly', () => {
- // Test with tasks that have equal order values (edge case)
- const equalTasks = [
- createMockTask('1', 100),
- createMockTask('2', 100)
- ];
- const result = calculateReorderPosition(equalTasks, 0, 1);
- expect(Number.isInteger(result)).toBe(true);
- expect(result).toBeGreaterThan(100);
- });
- });
-});
\ No newline at end of file
diff --git a/archon-ui-main/tests/README.md b/archon-ui-main/tests/README.md
new file mode 100644
index 00000000..16fee148
--- /dev/null
+++ b/archon-ui-main/tests/README.md
@@ -0,0 +1,58 @@
+# Test Structure
+
+## Test Organization
+
+We follow a hybrid testing strategy:
+
+### Unit Tests (Colocated)
+Unit tests live next to the code they test in the `src/features` directory:
+```
+src/features/projects/
+├── components/
+│ ├── ProjectCard.tsx
+│ └── ProjectCard.test.tsx
+```
+
+### Integration Tests
+Tests that cross multiple features/systems:
+```
+tests/integration/
+└── api.integration.test.ts
+```
+
+### E2E Tests
+Full user flow tests:
+```
+tests/e2e/
+└── user-flows.e2e.test.ts
+```
+
+## Running Tests
+
+```bash
+# Run all tests
+npm run test
+
+# Run tests in watch mode
+npm run test:watch
+
+# Run with coverage
+npm run test:coverage
+
+# Run specific test file
+npx vitest run src/features/ui/hooks/useSmartPolling.test.ts
+```
+
+## Test Naming Conventions
+
+- **Unit tests**: `ComponentName.test.tsx` or `hookName.test.ts`
+- **Integration tests**: `feature.integration.test.ts`
+- **E2E tests**: `flow-name.e2e.test.ts`
+
+## Test Setup
+
+Global test setup is in `tests/setup.ts` which:
+- Sets environment variables
+- Mocks fetch and localStorage
+- Mocks DOM APIs
+- Mocks external libraries (lucide-react)
\ No newline at end of file
diff --git a/archon-ui-main/test/setup.ts b/archon-ui-main/tests/setup.ts
similarity index 61%
rename from archon-ui-main/test/setup.ts
rename to archon-ui-main/tests/setup.ts
index 54a4ccb5..0fddd2b4 100644
--- a/archon-ui-main/test/setup.ts
+++ b/archon-ui-main/tests/setup.ts
@@ -35,17 +35,28 @@ Object.defineProperty(window, 'localStorage', {
Element.prototype.scrollIntoView = vi.fn()
window.HTMLElement.prototype.scrollIntoView = vi.fn()
-// Mock lucide-react icons - create a proxy that returns icon name for any icon
-vi.mock('lucide-react', () => {
- return new Proxy({}, {
- get: (target, prop) => {
- if (typeof prop === 'string') {
- return () => prop
- }
- return undefined
- }
- })
-})
+// Mock lucide-react icons - simple implementation
+vi.mock('lucide-react', () => ({
+ Trash2: () => 'Trash2',
+ X: () => 'X',
+ AlertCircle: () => 'AlertCircle',
+ Loader2: () => 'Loader2',
+ BookOpen: () => 'BookOpen',
+ Settings: () => 'Settings',
+ WifiOff: () => 'WifiOff',
+ ChevronDown: () => 'ChevronDown',
+ ChevronRight: () => 'ChevronRight',
+ Plus: () => 'Plus',
+ Search: () => 'Search',
+ Activity: () => 'Activity',
+ CheckCircle2: () => 'CheckCircle2',
+ ListTodo: () => 'ListTodo',
+ MoreHorizontal: () => 'MoreHorizontal',
+ Pin: () => 'Pin',
+ PinOff: () => 'PinOff',
+ Clipboard: () => 'Clipboard',
+ // Add more icons as needed
+}))
// Mock ResizeObserver
global.ResizeObserver = vi.fn().mockImplementation(() => ({
diff --git a/archon-ui-main/tsconfig.json b/archon-ui-main/tsconfig.json
index a7d46b8c..6db9f331 100644
--- a/archon-ui-main/tsconfig.json
+++ b/archon-ui-main/tsconfig.json
@@ -23,6 +23,6 @@
/* Path mapping */
"paths": { "@/*": ["./src/*"] }
},
- "include": ["src", "test"],
+ "include": ["src", "tests"],
"references": [{ "path": "./tsconfig.node.json" }]
}
diff --git a/archon-ui-main/tsconfig.prod.json b/archon-ui-main/tsconfig.prod.json
new file mode 100644
index 00000000..9dfb2834
--- /dev/null
+++ b/archon-ui-main/tsconfig.prod.json
@@ -0,0 +1,16 @@
+{
+ "extends": "./tsconfig.json",
+ "exclude": [
+ "**/*.test.ts",
+ "**/*.test.tsx",
+ "**/*.spec.ts",
+ "**/*.spec.tsx",
+ "**/__tests__/**",
+ "**/tests/**",
+ "src/features/testing/**",
+ "test/**",
+ "tests/**",
+ "coverage/**",
+ "**/*.d.ts"
+ ]
+}
\ No newline at end of file
diff --git a/archon-ui-main/vite.config.ts b/archon-ui-main/vite.config.ts
index 9a986523..329b98b7 100644
--- a/archon-ui-main/vite.config.ts
+++ b/archon-ui-main/vite.config.ts
@@ -321,15 +321,18 @@ export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
test: {
globals: true,
environment: 'jsdom',
- setupFiles: './test/setup.ts',
+ setupFiles: './tests/setup.ts',
css: true,
+ include: [
+ 'src/**/*.{test,spec}.{ts,tsx}', // Tests colocated in features
+ 'tests/**/*.{test,spec}.{ts,tsx}' // Tests in tests directory
+ ],
exclude: [
'**/node_modules/**',
'**/dist/**',
'**/cypress/**',
'**/.{idea,git,cache,output,temp}/**',
- '**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*',
- '**/*.test.{ts,tsx}',
+ '**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*'
],
env: {
VITE_HOST: host,
diff --git a/archon-ui-main/vitest.config.ts b/archon-ui-main/vitest.config.ts
index c27ee106..51e20e1c 100644
--- a/archon-ui-main/vitest.config.ts
+++ b/archon-ui-main/vitest.config.ts
@@ -8,10 +8,12 @@ export default defineConfig({
test: {
globals: true,
environment: 'jsdom',
- setupFiles: './test/setup.ts',
+ setupFiles: './tests/setup.ts',
include: [
- 'test/**/*.test.{ts,tsx}',
- 'test/**/*.spec.{ts,tsx}'
+ 'src/**/*.test.{ts,tsx}', // Colocated tests in features
+ 'src/**/*.spec.{ts,tsx}',
+ 'tests/**/*.test.{ts,tsx}', // Tests in tests directory
+ 'tests/**/*.spec.{ts,tsx}'
],
exclude: ['node_modules', 'dist', '.git', '.cache', 'test.backup', '*.backup/**', 'test-backups'],
reporters: ['dot', 'json'],
@@ -35,7 +37,7 @@ export default defineConfig({
reportOnFailure: true, // Generate coverage reports even when tests fail
exclude: [
'node_modules/',
- 'test/',
+ 'tests/',
'**/*.d.ts',
'**/*.config.*',
'**/mockData.ts',