From d890180f91cfcf39b04aad53729c7eee26f4f5f9 Mon Sep 17 00:00:00 2001 From: Rasmus Widing Date: Mon, 18 Aug 2025 13:04:53 +0300 Subject: [PATCH 1/2] Fix document deletion persistence issue (#278) - Fixed projectService methods to include project_id parameter in API calls - Updated deleteDocument() to use correct endpoint: /api/projects/{projectId}/docs/{docId} - Updated getDocument() and updateDocument() to use correct endpoints with project_id - Modified DocsTab component to call backend API when deleting documents - Documents now properly persist deletion after page refresh The issue was that document deletion was only happening in UI state and never reached the backend. The service methods were using incorrect API endpoints that didn't include the required project_id parameter. --- .../src/components/project-tasks/DocsTab.tsx | 6 +++++- archon-ui-main/src/services/projectService.ts | 12 ++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/archon-ui-main/src/components/project-tasks/DocsTab.tsx b/archon-ui-main/src/components/project-tasks/DocsTab.tsx index add2ca56..87e6fa5c 100644 --- a/archon-ui-main/src/components/project-tasks/DocsTab.tsx +++ b/archon-ui-main/src/components/project-tasks/DocsTab.tsx @@ -939,13 +939,17 @@ export const DocsTab = ({ onSelect={setSelectedDocument} onDelete={async (docId) => { try { - // Remove from local state + // Call API to delete from database first + await projectService.deleteDocument(project.id, docId); + + // Then remove from local state 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'); } }} diff --git a/archon-ui-main/src/services/projectService.ts b/archon-ui-main/src/services/projectService.ts index 4e11378b..f03a9e26 100644 --- a/archon-ui-main/src/services/projectService.ts +++ b/archon-ui-main/src/services/projectService.ts @@ -561,9 +561,9 @@ export const projectService = { /** * Get a specific document with full content */ - async getDocument(docId: string): Promise { + async getDocument(projectId: string, docId: string): Promise { try { - const response = await callAPI<{document: any}>(`/api/docs/${docId}`); + const response = await callAPI<{document: any}>(`/api/projects/${projectId}/docs/${docId}`); return response.document; } catch (error) { console.error(`Failed to get document ${docId}:`, error); @@ -590,9 +590,9 @@ export const projectService = { /** * Update an existing document */ - async updateDocument(docId: string, updates: any): Promise { + async updateDocument(projectId: string, docId: string, updates: any): Promise { try { - const response = await callAPI<{document: any}>(`/api/docs/${docId}`, { + const response = await callAPI<{document: any}>(`/api/projects/${projectId}/docs/${docId}`, { method: 'PUT', body: JSON.stringify(updates) }); @@ -606,9 +606,9 @@ export const projectService = { /** * Delete a document */ - async deleteDocument(docId: string): Promise { + async deleteDocument(projectId: string, docId: string): Promise { try { - await callAPI(`/api/docs/${docId}`, { method: 'DELETE' }); + await callAPI(`/api/projects/${projectId}/docs/${docId}`, { method: 'DELETE' }); } catch (error) { console.error(`Failed to delete document ${docId}:`, error); throw error; From 4c02dfc15d322266fc6f0643690d77990e5269e1 Mon Sep 17 00:00:00 2001 From: Rasmus Widing Date: Mon, 18 Aug 2025 13:27:20 +0300 Subject: [PATCH 2/2] Add comprehensive test coverage for document CRUD operations - Add Document interface for type safety - Fix error messages to include projectId context - Add unit tests for all projectService document methods - Add integration tests for DocsTab deletion flow - Update vitest config to include new test files --- archon-ui-main/src/services/projectService.ts | 36 +- .../DocsTab.integration.test.tsx | 190 ++++++++- .../test/services/projectService.test.ts | 393 ++++++++++++++++++ archon-ui-main/vitest.config.ts | 5 +- 4 files changed, 607 insertions(+), 17 deletions(-) create mode 100644 archon-ui-main/test/services/projectService.test.ts diff --git a/archon-ui-main/src/services/projectService.ts b/archon-ui-main/src/services/projectService.ts index f03a9e26..50e8f565 100644 --- a/archon-ui-main/src/services/projectService.ts +++ b/archon-ui-main/src/services/projectService.ts @@ -24,6 +24,20 @@ import { import { dbTaskToUITask, uiStatusToDBStatus } from '../types/project'; +// Document interface for type safety +export interface Document { + id: string; + project_id: string; + title: string; + content: any; + document_type: string; + metadata?: Record; + tags?: string[]; + author?: string; + created_at: string; + updated_at: string; +} + // API configuration - use relative URL to go through Vite proxy const API_BASE_URL = '/api'; @@ -548,9 +562,9 @@ export const projectService = { /** * List all documents for a project */ - async listProjectDocuments(projectId: string): Promise { + async listProjectDocuments(projectId: string): Promise { try { - const response = await callAPI<{documents: any[]}>(`/api/projects/${projectId}/docs`); + const response = await callAPI<{documents: Document[]}>(`/api/projects/${projectId}/docs`); return response.documents || []; } catch (error) { console.error(`Failed to list documents for project ${projectId}:`, error); @@ -561,12 +575,12 @@ export const projectService = { /** * Get a specific document with full content */ - async getDocument(projectId: string, docId: string): Promise { + async getDocument(projectId: string, docId: string): Promise { try { - const response = await callAPI<{document: any}>(`/api/projects/${projectId}/docs/${docId}`); + const response = await callAPI<{document: Document}>(`/api/projects/${projectId}/docs/${docId}`); return response.document; } catch (error) { - console.error(`Failed to get document ${docId}:`, error); + console.error(`Failed to get document ${docId} from project ${projectId}:`, error); throw error; } }, @@ -574,9 +588,9 @@ export const projectService = { /** * Create a new document for a project */ - async createDocument(projectId: string, documentData: any): Promise { + async createDocument(projectId: string, documentData: Partial): Promise { try { - const response = await callAPI<{document: any}>(`/api/projects/${projectId}/docs`, { + const response = await callAPI<{document: Document}>(`/api/projects/${projectId}/docs`, { method: 'POST', body: JSON.stringify(documentData) }); @@ -590,15 +604,15 @@ export const projectService = { /** * Update an existing document */ - async updateDocument(projectId: string, docId: string, updates: any): Promise { + async updateDocument(projectId: string, docId: string, updates: Partial): Promise { try { - const response = await callAPI<{document: any}>(`/api/projects/${projectId}/docs/${docId}`, { + const response = await callAPI<{document: Document}>(`/api/projects/${projectId}/docs/${docId}`, { method: 'PUT', body: JSON.stringify(updates) }); return response.document; } catch (error) { - console.error(`Failed to update document ${docId}:`, error); + console.error(`Failed to update document ${docId} in project ${projectId}:`, error); throw error; } }, @@ -610,7 +624,7 @@ export const projectService = { try { await callAPI(`/api/projects/${projectId}/docs/${docId}`, { method: 'DELETE' }); } catch (error) { - console.error(`Failed to delete document ${docId}:`, error); + console.error(`Failed to delete document ${docId} from project ${projectId}:`, error); throw error; } }, 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 index de4081f4..64cb4f8b 100644 --- a/archon-ui-main/test/components/project-tasks/DocsTab.integration.test.tsx +++ b/archon-ui-main/test/components/project-tasks/DocsTab.integration.test.tsx @@ -3,19 +3,22 @@ import { describe, test, expect, vi, beforeEach } from 'vitest' import React from 'react' // Mock the dependencies -vi.mock('../../contexts/ToastContext', () => ({ +vi.mock('../../../src/contexts/ToastContext', () => ({ useToast: () => ({ showToast: vi.fn() }) })) -vi.mock('../../services/projectService', () => ({ +vi.mock('../../../src/services/projectService', () => ({ projectService: { - getProjectDocuments: vi.fn().mockResolvedValue([]) + 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('../../services/knowledgeBaseService', () => ({ +vi.mock('../../../src/services/knowledgeBaseService', () => ({ knowledgeBaseService: { getItems: vi.fn().mockResolvedValue([]) } @@ -185,7 +188,10 @@ describe('DocsTab Document Cards Integration', () => { fireEvent.click(screen.getByTestId('document-card-doc-2')) expect(screen.getByTestId('selected-document')).toHaveTextContent('Selected: Document 2') - // Delete doc-2 (currently selected) + // 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) @@ -224,4 +230,178 @@ describe('DocsTab Document Cards Integration', () => { 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/services/projectService.test.ts b/archon-ui-main/test/services/projectService.test.ts new file mode 100644 index 00000000..98715954 --- /dev/null +++ b/archon-ui-main/test/services/projectService.test.ts @@ -0,0 +1,393 @@ +/** + * 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/vitest.config.ts b/archon-ui-main/vitest.config.ts index f4c75f2f..7677c9c0 100644 --- a/archon-ui-main/vitest.config.ts +++ b/archon-ui-main/vitest.config.ts @@ -13,7 +13,10 @@ export default defineConfig({ 'test/components.test.tsx', 'test/pages.test.tsx', 'test/user_flows.test.tsx', - 'test/errors.test.tsx' + 'test/errors.test.tsx', + 'test/services/projectService.test.ts', + 'test/components/project-tasks/DocsTab.integration.test.tsx', + 'test/config/api.test.ts' ], exclude: ['node_modules', 'dist', '.git', '.cache', 'test.backup', '*.backup/**', 'test-backups'], reporters: ['dot', 'json'],