UNPKG

@boundless-oss/atlas

Version:

Atlas - MCP Server for comprehensive startup project management

386 lines (311 loc) 13.7 kB
import { describe, it, expect, beforeEach, vi } from 'vitest'; import { WorkspaceDataStore } from '../store.js'; import { ConfigManager } from '../../../config/config-manager.js'; import { Workspace, Repository } from '../types.js'; import { InMemoryFileSystemAdapter } from '../file-system-adapter.js'; vi.mock('../../../config/config-manager.js'); const MockConfigManager = ConfigManager as vi.MockedClass<typeof ConfigManager>; describe('WorkspaceDataStore', () => { let workspaceStore: WorkspaceDataStore; let mockConfigManager: vi.Mocked<ConfigManager>; let mockFs: InMemoryFileSystemAdapter; beforeEach(async () => { vi.clearAllMocks(); mockConfigManager = new MockConfigManager() as vi.Mocked<ConfigManager>; mockConfigManager.getDataPath = vi.fn().mockReturnValue('/test/data/workspace'); mockConfigManager.getStorageManager = vi.fn().mockReturnValue({ ensureStorageDirectories: vi.fn().mockResolvedValue(undefined), loadData: vi.fn().mockResolvedValue(null), saveData: vi.fn().mockResolvedValue(undefined), getModuleDataPath: vi.fn().mockResolvedValue('/test/data/workspace/workspaces.json') }); mockFs = new InMemoryFileSystemAdapter(); workspaceStore = new WorkspaceDataStore(mockConfigManager, mockFs); await workspaceStore.init(); }); describe('initialization', () => { it('should initialize with empty data when file does not exist', async () => { const newStore = new WorkspaceDataStore(mockConfigManager, mockFs); await newStore.init(); expect(newStore.getWorkspaces()).toEqual([]); expect(mockConfigManager.getStorageManager().saveData).toHaveBeenCalledWith( 'workspace', 'workspace.json', expect.objectContaining({ workspaces: {} }) ); }); it('should load existing workspace data', async () => { const existingData = { workspaces: { 'ws-1': { id: 'ws-1', name: 'Test Workspace', rootPath: '/test/workspace', repositories: [], createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }, }, activeWorkspaceId: 'ws-1', }; const newConfigManager = new MockConfigManager() as vi.Mocked<ConfigManager>; newConfigManager.getStorageManager = vi.fn().mockReturnValue({ ensureStorageDirectories: vi.fn().mockResolvedValue(undefined), loadData: vi.fn().mockResolvedValue(existingData), saveData: vi.fn().mockResolvedValue(undefined), }); const newStore = new WorkspaceDataStore(newConfigManager, mockFs); await newStore.init(); expect(newStore.getWorkspaces()).toHaveLength(1); expect(newStore.getActiveWorkspace()?.name).toBe('Test Workspace'); }); }); describe('createWorkspace', () => { it('should create a new workspace', () => { const workspace = workspaceStore.createWorkspace( 'My Workspace', '/path/to/workspace', 'Test workspace description' ); expect(workspace.name).toBe('My Workspace'); expect(workspace.rootPath).toBe('/path/to/workspace'); expect(workspace.description).toBe('Test workspace description'); expect(workspace.repositories).toEqual([]); expect(workspace.id).toMatch(/^ws-[a-f0-9-]+$/); expect(workspace.createdAt).toBeDefined(); expect(workspace.updatedAt).toBeDefined(); }); it('should set first workspace as active automatically', () => { const workspace = workspaceStore.createWorkspace('First Workspace', '/path'); expect(workspaceStore.getActiveWorkspace()).toBe(workspace); }); it('should not change active workspace when creating additional workspaces', () => { const firstWorkspace = workspaceStore.createWorkspace('First', '/path1'); const secondWorkspace = workspaceStore.createWorkspace('Second', '/path2'); expect(workspaceStore.getActiveWorkspace()).toBe(firstWorkspace); expect(workspaceStore.getActiveWorkspace()).not.toBe(secondWorkspace); }); }); describe('addRepository', () => { let workspace: Workspace; beforeEach(() => { workspace = workspaceStore.createWorkspace('Test Workspace', '/workspace'); }); it('should add repository to workspace by workspace ID', () => { const repoData = { name: 'frontend', path: '/workspace/frontend', type: 'git' as const, primary: true, }; const repository = workspaceStore.addRepository(workspace.id, repoData); expect(repository).toBeDefined(); expect(repository!.name).toBe('frontend'); expect(repository!.primary).toBe(true); expect(repository!.id).toMatch(/^repo-[a-f0-9-]+$/); expect(workspace.repositories).toHaveLength(1); }); it('should add repository to workspace by workspace name', () => { const repoData = { name: 'backend', path: '/workspace/backend', type: 'git' as const, }; const repository = workspaceStore.addRepository(workspace.name, repoData); expect(repository).toBeDefined(); expect(repository!.name).toBe('backend'); expect(repository!.primary).toBeFalsy(); }); it('should return null if workspace not found', () => { const repoData = { name: 'repo', path: '/path', type: 'git' as const, }; const repository = workspaceStore.addRepository('nonexistent', repoData); expect(repository).toBeNull(); }); it('should update workspace updatedAt timestamp', async () => { const originalUpdatedAt = workspace.updatedAt; // Wait a bit to ensure timestamp difference await new Promise(resolve => setTimeout(resolve, 10)); workspaceStore.addRepository(workspace.id, { name: 'repo', path: '/path', type: 'git' as const, }); // Get the workspace again to check updated timestamp const updatedWorkspace = workspaceStore.getWorkspace(workspace.id); expect(updatedWorkspace?.updatedAt).not.toBe(originalUpdatedAt); }); }); describe('detectRepositories', () => { it('should detect Git repositories in directory', async () => { // Create a fresh instance with fresh mockFs const freshMockFs = new InMemoryFileSystemAdapter(); const freshStore = new WorkspaceDataStore(mockConfigManager, freshMockFs); await freshStore.init(); const workspacePath = '/workspace'; // Set up directory structure freshMockFs.addDirectory(workspacePath, [ { name: 'frontend', isDirectory: true }, { name: 'backend', isDirectory: true }, { name: 'shared', isDirectory: true }, { name: 'node_modules', isDirectory: true }, { name: '.git', isDirectory: true }, ]); // Add .git directories to frontend and backend freshMockFs.addGitDirectory('/workspace/frontend'); freshMockFs.addGitDirectory('/workspace/backend'); const repositories = await freshStore.detectRepositories(workspacePath); expect(repositories).toHaveLength(2); expect(repositories[0].name).toBe('frontend'); expect(repositories[0].path).toBe('/workspace/frontend'); expect(repositories[0].type).toBe('git'); expect(repositories[1].name).toBe('backend'); }); it('should handle directories without Git repositories', async () => { // Create a fresh instance with fresh mockFs const freshMockFs = new InMemoryFileSystemAdapter(); const freshStore = new WorkspaceDataStore(mockConfigManager, freshMockFs); await freshStore.init(); // Set up directory structure without .git directories freshMockFs.addDirectory('/workspace', [ { name: 'folder1', isDirectory: true }, { name: 'folder2', isDirectory: true } ]); // Don't add any .git directories const repositories = await freshStore.detectRepositories('/workspace'); expect(repositories).toHaveLength(0); }); it('should handle file system errors gracefully', async () => { // Create a fresh mockFs that will throw an error const errorMockFs = new InMemoryFileSystemAdapter(); const errorStore = new WorkspaceDataStore(mockConfigManager, errorMockFs); await errorStore.init(); // Don't add the directory, so readdir will throw const repositories = await errorStore.detectRepositories('/workspace'); expect(repositories).toHaveLength(0); }); }); describe('workspace management', () => { let workspace1: Workspace; let workspace2: Workspace; beforeEach(() => { workspace1 = workspaceStore.createWorkspace('Workspace 1', '/ws1'); workspace2 = workspaceStore.createWorkspace('Workspace 2', '/ws2'); }); it('should get workspace by name', () => { const found = workspaceStore.getWorkspace('Workspace 1'); expect(found).toBe(workspace1); }); it('should get workspace by ID', () => { const found = workspaceStore.getWorkspace(workspace2.id); expect(found).toBe(workspace2); }); it('should return undefined for nonexistent workspace', () => { const found = workspaceStore.getWorkspace('Nonexistent'); expect(found).toBeUndefined(); }); it('should get all workspaces', () => { const workspaces = workspaceStore.getWorkspaces(); expect(workspaces).toHaveLength(2); expect(workspaces).toContain(workspace1); expect(workspaces).toContain(workspace2); }); it('should set active workspace', () => { const success = workspaceStore.setActiveWorkspace('Workspace 2'); expect(success).toBe(true); expect(workspaceStore.getActiveWorkspace()).toBe(workspace2); }); it('should return false when setting nonexistent workspace as active', () => { const success = workspaceStore.setActiveWorkspace('Nonexistent'); expect(success).toBe(false); expect(workspaceStore.getActiveWorkspace()).toBe(workspace1); // Should remain unchanged }); }); describe('repository queries', () => { let workspace: Workspace; beforeEach(() => { workspace = workspaceStore.createWorkspace('Test Workspace', '/workspace'); workspaceStore.addRepository(workspace.id, { name: 'primary-repo', path: '/workspace/primary', type: 'git', primary: true, }); workspaceStore.addRepository(workspace.id, { name: 'secondary-repo', path: '/workspace/secondary', type: 'git', }); }); it('should find repository by name', () => { const repo = workspaceStore.findRepository('primary-repo'); expect(repo).toBeDefined(); expect(repo!.name).toBe('primary-repo'); expect(repo!.primary).toBe(true); }); it('should find repository by path', () => { const repo = workspaceStore.findRepositoryByPath('/workspace/secondary'); expect(repo).toBeDefined(); expect(repo!.name).toBe('secondary-repo'); }); it('should get primary repository', () => { const primaryRepo = workspaceStore.getPrimaryRepository(); expect(primaryRepo).toBeDefined(); expect(primaryRepo!.name).toBe('primary-repo'); expect(primaryRepo!.primary).toBe(true); }); it('should return undefined for nonexistent repository', () => { const repo = workspaceStore.findRepository('nonexistent'); expect(repo).toBeUndefined(); }); }); describe('data persistence', () => { it('should save workspace data to file', async () => { workspaceStore.createWorkspace('Test Save', '/test'); await workspaceStore.save(); expect(mockConfigManager.getStorageManager().saveData).toHaveBeenCalledWith( 'workspace', 'workspace.json', expect.objectContaining({ workspaces: expect.objectContaining({}) }) ); }); it('should handle save errors gracefully', async () => { mockConfigManager.getStorageManager().saveData = vi.fn() .mockRejectedValueOnce(new Error('Write failed')); await expect(workspaceStore.save()).rejects.toThrow('Write failed'); }); it('should preserve data through save/load cycle', async () => { const workspace = workspaceStore.createWorkspace('Persistence Test', '/test'); workspaceStore.addRepository(workspace.id, { name: 'test-repo', path: '/test/repo', type: 'git', primary: true, }); // Save data await workspaceStore.save(); // Get the saved data from mock const savedData = (mockConfigManager.getStorageManager().saveData as vi.Mock) .mock.calls[0][2]; // Third argument is the data // Create new store with the saved data const newConfigManager = new MockConfigManager() as vi.Mocked<ConfigManager>; newConfigManager.getStorageManager = vi.fn().mockReturnValue({ ensureStorageDirectories: vi.fn().mockResolvedValue(undefined), loadData: vi.fn().mockResolvedValue(savedData), saveData: vi.fn().mockResolvedValue(undefined), }); const newStore = new WorkspaceDataStore(newConfigManager, mockFs); await newStore.init(); // Verify data was preserved const loadedWorkspace = newStore.getWorkspace('Persistence Test'); expect(loadedWorkspace).toBeDefined(); expect(loadedWorkspace!.repositories).toHaveLength(1); expect(loadedWorkspace!.repositories[0].name).toBe('test-repo'); }); }); });