@boundless-oss/atlas
Version:
Atlas - MCP Server for comprehensive startup project management
386 lines (311 loc) • 13.7 kB
text/typescript
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');
});
});
});