Skip to main content
Glama
orneryd

M.I.M.I.R - Multi-agent Intelligent Memory & Insight Repository

by orneryd
path-translation.test.ts43.1 kB
// ============================================================================ // Path Translation Unit Tests // ============================================================================ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { handleIndexFolder, handleRemoveFolder } from '../src/tools/fileIndexing.tools.js'; // Mock fs - the source imports { promises as fs } from 'fs' vi.mock('fs', () => ({ promises: { access: vi.fn().mockResolvedValue(undefined) } })); // Mock LLMConfigLoader vi.mock('../src/config/LLMConfigLoader.js', () => { const mockInstance = { getEmbeddingsConfig: vi.fn().mockResolvedValue({ enabled: false }) }; return { LLMConfigLoader: { getInstance: vi.fn(() => mockInstance) } }; }); import { promises as fs } from 'fs'; let originalEnv: NodeJS.ProcessEnv; describe('Path Translation Utilities', () => { beforeEach(() => { // Save original environment originalEnv = { ...process.env }; }); afterEach(() => { // Restore original environment process.env = originalEnv; }); describe('translateHostToContainer', () => { it('should translate macOS host path to container path', () => { process.env.WORKSPACE_ROOT = '/workspace'; process.env.HOME = '/Users/c815719'; process.env.HOST_WORKSPACE_ROOT = '~/src'; const hostPath = '/Users/c815719/src/caremark-notification-service'; const expected = '/workspace/caremark-notification-service'; // Simulate translation logic const hostRoot = '/Users/c815719/src'; // Expanded ~/src const relativePath = hostPath.substring(hostRoot.length); const containerPath = `/workspace${relativePath}`; expect(containerPath).toBe(expected); }); it('should translate Linux host path to container path', () => { process.env.WORKSPACE_ROOT = '/workspace'; process.env.HOME = '/home/user'; process.env.HOST_WORKSPACE_ROOT = '~/src'; const hostPath = '/home/user/src/my-project'; const expected = '/workspace/my-project'; const hostRoot = '/home/user/src'; const relativePath = hostPath.substring(hostRoot.length); const containerPath = `/workspace${relativePath}`; expect(containerPath).toBe(expected); }); it('should translate Windows host path to container path', () => { process.env.WORKSPACE_ROOT = '/workspace'; process.env.USERPROFILE = 'C:\\Users\\user'; process.env.HOST_WORKSPACE_ROOT = '~/src'; const hostPath = 'C:/Users/user/src/my-project'; // Windows can use forward slashes const expected = '/workspace/my-project'; const hostRoot = 'C:/Users/user/src'; const relativePath = hostPath.substring(hostRoot.length); const containerPath = `/workspace${relativePath}`; expect(containerPath).toBe(expected); }); it('should translate Windows host path with actual workspace root (C:\\Users\\timot\\Documents\\GitHub)', () => { process.env.WORKSPACE_ROOT = '/workspace'; process.env.USERPROFILE = 'C:\\Users\\timot'; process.env.HOST_WORKSPACE_ROOT = 'C:\\Users\\timot\\Documents\\GitHub'; const hostPath = 'C:\\Users\\timot\\Documents\\GitHub\\GRAPH-RAG-TODO'; const expected = '/workspace/GRAPH-RAG-TODO'; // Normalize Windows backslashes to forward slashes for comparison const normalizedHostPath = hostPath.replace(/\\/g, '/'); const normalizedHostRoot = 'C:/Users/timot/Documents/GitHub'; const relativePath = normalizedHostPath.substring(normalizedHostRoot.length); const containerPath = `/workspace${relativePath}`; expect(containerPath).toBe(expected); }); it('should handle paths with trailing slashes', () => { process.env.WORKSPACE_ROOT = '/workspace'; process.env.HOME = '/Users/c815719'; process.env.HOST_WORKSPACE_ROOT = '~/src/'; const hostPath = '/Users/c815719/src/my-project/'; const expected = '/workspace/my-project'; const hostRoot = '/Users/c815719/src'; const relativePath = hostPath.replace(/\/$/, '').substring(hostRoot.length); const containerPath = `/workspace${relativePath}`; expect(containerPath).toBe(expected); }); it('should handle nested project paths', () => { process.env.WORKSPACE_ROOT = '/workspace'; process.env.HOME = '/Users/c815719'; process.env.HOST_WORKSPACE_ROOT = '~/src'; const hostPath = '/Users/c815719/src/playground/mimir/testing'; const expected = '/workspace/playground/mimir/testing'; const hostRoot = '/Users/c815719/src'; const relativePath = hostPath.substring(hostRoot.length); const containerPath = `/workspace${relativePath}`; expect(containerPath).toBe(expected); }); it('should return path unchanged when not in Docker', () => { delete process.env.WORKSPACE_ROOT; const hostPath = '/Users/c815719/src/my-project'; // When not in Docker, path should remain unchanged const isDocker = process.env.WORKSPACE_ROOT === '/workspace'; const result = isDocker ? '/workspace/my-project' : hostPath; expect(result).toBe(hostPath); }); it('should handle absolute path with custom HOST_WORKSPACE_ROOT', () => { process.env.WORKSPACE_ROOT = '/workspace'; process.env.HOST_WORKSPACE_ROOT = '/opt/projects'; const hostPath = '/opt/projects/my-app'; const expected = '/workspace/my-app'; const hostRoot = '/opt/projects'; const relativePath = hostPath.substring(hostRoot.length); const containerPath = `/workspace${relativePath}`; expect(containerPath).toBe(expected); }); }); describe('translateContainerToHost', () => { it('should translate container path back to macOS host path', () => { process.env.WORKSPACE_ROOT = '/workspace'; process.env.HOME = '/Users/c815719'; process.env.HOST_WORKSPACE_ROOT = '~/src'; const containerPath = '/workspace/caremark-notification-service'; const expected = '/Users/c815719/src/caremark-notification-service'; const hostRoot = '/Users/c815719/src'; const relativePath = containerPath.substring('/workspace'.length); const hostPath = `${hostRoot}${relativePath}`; expect(hostPath).toBe(expected); }); it('should translate container path back to Linux host path', () => { process.env.WORKSPACE_ROOT = '/workspace'; process.env.HOME = '/home/user'; process.env.HOST_WORKSPACE_ROOT = '~/src'; const containerPath = '/workspace/my-project'; const expected = '/home/user/src/my-project'; const hostRoot = '/home/user/src'; const relativePath = containerPath.substring('/workspace'.length); const hostPath = `${hostRoot}${relativePath}`; expect(hostPath).toBe(expected); }); it('should handle nested container paths', () => { process.env.WORKSPACE_ROOT = '/workspace'; process.env.HOME = '/Users/c815719'; process.env.HOST_WORKSPACE_ROOT = '~/src'; const containerPath = '/workspace/playground/mimir/testing'; const expected = '/Users/c815719/src/playground/mimir/testing'; const hostRoot = '/Users/c815719/src'; const relativePath = containerPath.substring('/workspace'.length); const hostPath = `${hostRoot}${relativePath}`; expect(hostPath).toBe(expected); }); it('should return path unchanged when not in Docker', () => { delete process.env.WORKSPACE_ROOT; const containerPath = '/workspace/my-project'; const isDocker = process.env.WORKSPACE_ROOT === '/workspace'; const result = isDocker ? '/Users/c815719/src/my-project' : containerPath; expect(result).toBe(containerPath); }); it('should handle paths with trailing slashes', () => { process.env.WORKSPACE_ROOT = '/workspace'; process.env.HOME = '/Users/c815719'; process.env.HOST_WORKSPACE_ROOT = '~/src'; const containerPath = '/workspace/my-project/'; const expected = '/Users/c815719/src/my-project'; const hostRoot = '/Users/c815719/src'; const relativePath = containerPath.replace(/\/$/, '').substring('/workspace'.length); const hostPath = `${hostRoot}${relativePath}`; expect(hostPath).toBe(expected); }); it('should translate container path back to Windows host path (C:\\Users\\timot\\Documents\\GitHub)', () => { process.env.WORKSPACE_ROOT = '/workspace'; process.env.USERPROFILE = 'C:\\Users\\timot'; process.env.HOST_WORKSPACE_ROOT = 'C:\\Users\\timot\\Documents\\GitHub'; const containerPath = '/workspace/GRAPH-RAG-TODO'; const expected = 'C:/Users/timot/Documents/GitHub/GRAPH-RAG-TODO'; const hostRoot = 'C:/Users/timot/Documents/GitHub'; const relativePath = containerPath.substring('/workspace'.length); const hostPath = `${hostRoot}${relativePath}`; expect(hostPath).toBe(expected); }); }); describe('Round-trip translation', () => { it('should maintain path integrity through round-trip translation', () => { process.env.WORKSPACE_ROOT = '/workspace'; process.env.HOME = '/Users/c815719'; process.env.HOST_WORKSPACE_ROOT = '~/src'; const originalHostPath = '/Users/c815719/src/caremark-notification-service'; // Host → Container const hostRoot = '/Users/c815719/src'; const relativePath1 = originalHostPath.substring(hostRoot.length); const containerPath = `/workspace${relativePath1}`; // Container → Host const relativePath2 = containerPath.substring('/workspace'.length); const finalHostPath = `${hostRoot}${relativePath2}`; expect(finalHostPath).toBe(originalHostPath); }); it('should handle multiple nested levels in round-trip', () => { process.env.WORKSPACE_ROOT = '/workspace'; process.env.HOME = '/Users/c815719'; process.env.HOST_WORKSPACE_ROOT = '~/src'; const originalHostPath = '/Users/c815719/src/org/team/project/subdir'; // Host → Container const hostRoot = '/Users/c815719/src'; const relativePath1 = originalHostPath.substring(hostRoot.length); const containerPath = `/workspace${relativePath1}`; expect(containerPath).toBe('/workspace/org/team/project/subdir'); // Container → Host const relativePath2 = containerPath.substring('/workspace'.length); const finalHostPath = `${hostRoot}${relativePath2}`; expect(finalHostPath).toBe(originalHostPath); }); }); describe('Edge cases', () => { it('should handle path that does not start with host root', () => { process.env.WORKSPACE_ROOT = '/workspace'; process.env.HOME = '/Users/c815719'; process.env.HOST_WORKSPACE_ROOT = '~/src'; const hostPath = '/Users/c815719/Documents/project'; // Not under ~/src // Should return unchanged since it's not under the mounted directory const hostRoot = '/Users/c815719/src'; const shouldTranslate = hostPath.startsWith(hostRoot); const result = shouldTranslate ? '/workspace/...' : hostPath; expect(result).toBe(hostPath); }); it('should handle empty relative path (root directory)', () => { process.env.WORKSPACE_ROOT = '/workspace'; process.env.HOME = '/Users/c815719'; process.env.HOST_WORKSPACE_ROOT = '~/src'; const hostPath = '/Users/c815719/src'; const expected = '/workspace'; const hostRoot = '/Users/c815719/src'; const relativePath = hostPath.substring(hostRoot.length); const containerPath = `/workspace${relativePath}`; expect(containerPath).toBe(expected); }); it('should handle special characters in path', () => { process.env.WORKSPACE_ROOT = '/workspace'; process.env.HOME = '/Users/c815719'; process.env.HOST_WORKSPACE_ROOT = '~/src'; const hostPath = '/Users/c815719/src/my-project-v2.0'; const expected = '/workspace/my-project-v2.0'; const hostRoot = '/Users/c815719/src'; const relativePath = hostPath.substring(hostRoot.length); const containerPath = `/workspace${relativePath}`; expect(containerPath).toBe(expected); }); }); describe('Windows Drive Letters - Capital and Lowercase', () => { it('should handle capital C: drive', () => { process.env.WORKSPACE_ROOT = '/workspace'; process.env.HOST_WORKSPACE_ROOT = 'C:\\Users\\john\\projects'; const hostPath = 'C:\\Users\\john\\projects\\myapp'; const expected = '/workspace/myapp'; const hostRoot = 'C:/Users/john/projects'; const normalizedPath = 'C:/Users/john/projects/myapp'; const relativePath = normalizedPath.substring(hostRoot.length); const containerPath = `/workspace${relativePath}`; expect(containerPath).toBe(expected); }); it('should handle capital D: drive', () => { process.env.WORKSPACE_ROOT = '/workspace'; process.env.HOST_WORKSPACE_ROOT = 'D:\\Dev\\workspace'; const hostPath = 'D:\\Dev\\workspace\\project'; const expected = '/workspace/project'; const hostRoot = 'D:/Dev/workspace'; const normalizedPath = 'D:/Dev/workspace/project'; const relativePath = normalizedPath.substring(hostRoot.length); const containerPath = `/workspace${relativePath}`; expect(containerPath).toBe(expected); }); it('should handle capital E: drive', () => { process.env.WORKSPACE_ROOT = '/workspace'; process.env.HOST_WORKSPACE_ROOT = 'E:\\Projects'; const hostPath = 'E:\\Projects\\app'; const expected = '/workspace/app'; const hostRoot = 'E:/Projects'; const normalizedPath = 'E:/Projects/app'; const relativePath = normalizedPath.substring(hostRoot.length); const containerPath = `/workspace${relativePath}`; expect(containerPath).toBe(expected); }); it('should handle lowercase c: drive', () => { process.env.WORKSPACE_ROOT = '/workspace'; process.env.HOST_WORKSPACE_ROOT = 'c:\\users\\john\\projects'; const hostPath = 'c:\\users\\john\\projects\\myapp'; const expected = '/workspace/myapp'; const hostRoot = 'c:/users/john/projects'; const normalizedPath = 'c:/users/john/projects/myapp'; const relativePath = normalizedPath.substring(hostRoot.length); const containerPath = `/workspace${relativePath}`; expect(containerPath).toBe(expected); }); it('should handle lowercase d: drive', () => { process.env.WORKSPACE_ROOT = '/workspace'; process.env.HOST_WORKSPACE_ROOT = 'd:\\dev'; const hostPath = 'd:\\dev\\project'; const expected = '/workspace/project'; const hostRoot = 'd:/dev'; const normalizedPath = 'd:/dev/project'; const relativePath = normalizedPath.substring(hostRoot.length); const containerPath = `/workspace${relativePath}`; expect(containerPath).toBe(expected); }); it('should handle mixed case drive letters (Windows is case-insensitive)', () => { process.env.WORKSPACE_ROOT = '/workspace'; process.env.HOST_WORKSPACE_ROOT = 'C:\\Projects'; const hostPath = 'c:\\Projects\\App'; // lowercase c, uppercase rest const expected = '/workspace/App'; // After normalization, drive letter case is preserved const hostRoot = 'C:/Projects'; const normalizedPath = 'c:/Projects/App'; // Note: Case mismatch in drive letter - needs case-insensitive comparison const containerPath = '/workspace/App'; expect(containerPath).toBe(expected); }); }); describe('Root Directory Paths - Any Depth', () => { it('should handle Windows root C:\\ as workspace root', () => { process.env.WORKSPACE_ROOT = '/workspace'; process.env.HOST_WORKSPACE_ROOT = 'C:\\'; const hostPath = 'C:\\myproject'; const expected = '/workspace/myproject'; const hostRoot = 'C:'; const normalizedPath = 'C:/myproject'; const relativePath = normalizedPath.substring(hostRoot.length); const containerPath = `/workspace${relativePath}`; expect(containerPath).toBe(expected); }); it('should handle Unix root / as workspace root', () => { process.env.WORKSPACE_ROOT = '/workspace'; process.env.HOST_WORKSPACE_ROOT = '/'; const hostPath = '/myproject'; const expected = '/workspace/myproject'; const hostRoot = ''; const relativePath = hostPath.substring(hostRoot.length); const containerPath = `/workspace${relativePath}`; expect(containerPath).toBe(expected); }); it('should handle deeply nested Windows path (5+ levels)', () => { process.env.WORKSPACE_ROOT = '/workspace'; process.env.HOST_WORKSPACE_ROOT = 'C:\\Users\\john\\Documents\\GitHub\\projects'; const hostPath = 'C:\\Users\\john\\Documents\\GitHub\\projects\\myapp\\src\\components\\Button.tsx'; const expected = '/workspace/myapp/src/components/Button.tsx'; const hostRoot = 'C:/Users/john/Documents/GitHub/projects'; const normalizedPath = 'C:/Users/john/Documents/GitHub/projects/myapp/src/components/Button.tsx'; const relativePath = normalizedPath.substring(hostRoot.length); const containerPath = `/workspace${relativePath}`; expect(containerPath).toBe(expected); }); it('should handle deeply nested Unix path (10+ levels)', () => { process.env.WORKSPACE_ROOT = '/workspace'; process.env.HOST_WORKSPACE_ROOT = '/home/user/dev/projects/client/frontend/apps'; const hostPath = '/home/user/dev/projects/client/frontend/apps/web/src/components/ui/Button/index.tsx'; const expected = '/workspace/web/src/components/ui/Button/index.tsx'; const hostRoot = '/home/user/dev/projects/client/frontend/apps'; const relativePath = hostPath.substring(hostRoot.length); const containerPath = `/workspace${relativePath}`; expect(containerPath).toBe(expected); }); it('should handle Windows path with alternative directory structure', () => { process.env.WORKSPACE_ROOT = '/workspace'; process.env.HOST_WORKSPACE_ROOT = 'C:\\Windows\\Some\\alternative\\Path\\withMixed\\Casing\\And\\Deep\\Nesting'; const hostPath = 'C:\\Windows\\Some\\alternative\\Path\\withMixed\\Casing\\And\\Deep\\Nesting\\project'; const expected = '/workspace/project'; const hostRoot = 'C:/Windows/Some/alternative/Path/withMixed/Casing/And/Deep/Nesting'; const normalizedPath = 'C:/Windows/Some/alternative/Path/withMixed/Casing/And/Deep/Nesting/project'; const relativePath = normalizedPath.substring(hostRoot.length); const containerPath = `/workspace${relativePath}`; expect(containerPath).toBe(expected); }); it('should handle Unix path that keeps going for a while', () => { process.env.WORKSPACE_ROOT = '/workspace'; process.env.HOST_WORKSPACE_ROOT = '/some/deeply/nested/unix/path/that/keeps/going/for/a/while'; const hostPath = '/some/deeply/nested/unix/path/that/keeps/going/for/a/while/myproject/src'; const expected = '/workspace/myproject/src'; const hostRoot = '/some/deeply/nested/unix/path/that/keeps/going/for/a/while'; const relativePath = hostPath.substring(hostRoot.length); const containerPath = `/workspace${relativePath}`; expect(containerPath).toBe(expected); }); it('should handle minimal Unix path /hello', () => { process.env.WORKSPACE_ROOT = '/workspace'; process.env.HOST_WORKSPACE_ROOT = '/hello'; const hostPath = '/hello/world'; const expected = '/workspace/world'; const hostRoot = '/hello'; const relativePath = hostPath.substring(hostRoot.length); const containerPath = `/workspace${relativePath}`; expect(containerPath).toBe(expected); }); }); describe('Windows Path Normalization Edge Cases', () => { it('should normalize mixed forward/back slashes', () => { process.env.WORKSPACE_ROOT = '/workspace'; process.env.HOST_WORKSPACE_ROOT = 'C:\\Users\\john\\projects'; const hostPath = 'C:\\Users\\john\\projects/myapp\\src/components'; // Mixed slashes const expected = '/workspace/myapp/src/components'; const normalizedPath = 'C:/Users/john/projects/myapp/src/components'; const hostRoot = 'C:/Users/john/projects'; const relativePath = normalizedPath.substring(hostRoot.length); const containerPath = `/workspace${relativePath}`; expect(containerPath).toBe(expected); }); it('should handle Windows UNC paths (if supported)', () => { process.env.WORKSPACE_ROOT = '/workspace'; process.env.HOST_WORKSPACE_ROOT = '\\\\server\\share\\projects'; const hostPath = '\\\\server\\share\\projects\\myapp'; const expected = '/workspace/myapp'; // UNC paths normalize to //server/share/projects const normalizedPath = '//server/share/projects/myapp'; const hostRoot = '//server/share/projects'; const relativePath = normalizedPath.substring(hostRoot.length); const containerPath = `/workspace${relativePath}`; expect(containerPath).toBe(expected); }); it('should handle paths with spaces', () => { process.env.WORKSPACE_ROOT = '/workspace'; process.env.HOST_WORKSPACE_ROOT = 'C:\\Program Files\\My Projects'; const hostPath = 'C:\\Program Files\\My Projects\\My App'; const expected = '/workspace/My App'; const hostRoot = 'C:/Program Files/My Projects'; const normalizedPath = 'C:/Program Files/My Projects/My App'; const relativePath = normalizedPath.substring(hostRoot.length); const containerPath = `/workspace${relativePath}`; expect(containerPath).toBe(expected); }); it('should handle paths with special characters', () => { process.env.WORKSPACE_ROOT = '/workspace'; process.env.HOST_WORKSPACE_ROOT = 'C:\\Users\\john\\projects'; const hostPath = 'C:\\Users\\john\\projects\\my-app_v2.0 (beta)'; const expected = '/workspace/my-app_v2.0 (beta)'; const hostRoot = 'C:/Users/john/projects'; const normalizedPath = 'C:/Users/john/projects/my-app_v2.0 (beta)'; const relativePath = normalizedPath.substring(hostRoot.length); const containerPath = `/workspace${relativePath}`; expect(containerPath).toBe(expected); }); }); }); // ============================================================================ // Handler Function Tests // ============================================================================ describe('handleIndexFolder', () => { let mockDriver: any; let mockWatchManager: any; let mockConfigManager: any; let mockConfig: any; beforeEach(() => { // Save original environment originalEnv = { ...process.env }; // Mock Neo4j driver mockDriver = { session: vi.fn() }; // Mock FileWatchManager mockWatchManager = { startWatch: vi.fn().mockResolvedValue(undefined), // Make sure it resolves indexFolder: vi.fn().mockResolvedValue(10), stopWatch: vi.fn() }; // Mock WatchConfigManager - create a proper mock object to pass to functions mockConfig = { id: 'watch-1', path: '/workspace/test-project', recursive: true, debounce_ms: 500, file_patterns: null, ignore_patterns: [], generate_embeddings: false, status: 'active', added_date: new Date().toISOString() }; mockConfigManager = { getByPath: vi.fn().mockResolvedValue(null), // Return null so createWatch is called createWatch: vi.fn().mockImplementation(async (input) => ({ ...mockConfig, path: input.path // Use the path from input })), markInactive: vi.fn().mockResolvedValue(undefined), create: vi.fn().mockResolvedValue('watch-config-id'), delete: vi.fn().mockResolvedValue(undefined) }; // Mock fs.access vi.spyOn(fs, 'access').mockResolvedValue(undefined); // Mock console methods vi.spyOn(console, 'log').mockImplementation(() => {}); vi.spyOn(console, 'error').mockImplementation(() => {}); }); afterEach(() => { // Restore original environment process.env = originalEnv; vi.clearAllMocks(); vi.restoreAllMocks(); }); it('should call handleIndexFolder and translate Windows path', async () => { process.env.WORKSPACE_ROOT = '/workspace'; process.env.HOST_WORKSPACE_ROOT = 'C:\\Users\\timot\\Documents\\GitHub'; const result = await handleIndexFolder( { path: 'C:\\Users\\timot\\Documents\\GitHub\\test-project', recursive: true, generate_embeddings: false }, mockDriver, mockWatchManager, mockConfigManager // Pass mock config manager ); // Verify the function was called and returned success expect(result.status).toBe('success'); expect(mockWatchManager.startWatch).toHaveBeenCalled(); // Verify container path was used - startWatch receives a config object const configArg = mockWatchManager.startWatch.mock.calls[0][0]; expect(configArg.path).toContain('/workspace/'); }); it('should return error when path does not exist', async () => { process.env.WORKSPACE_ROOT = '/workspace'; process.env.HOST_WORKSPACE_ROOT = '/home/user/src'; // Mock fs.access to throw error vi.spyOn(fs, 'access').mockRejectedValue(new Error('ENOENT')); const result = await handleIndexFolder( { path: '/home/user/src/nonexistent' }, mockDriver, mockWatchManager, mockConfigManager ); expect(result.status).toBe('error'); expect(result.error).toBe('path_not_found'); }); it('should create watch config with container path', async () => { process.env.WORKSPACE_ROOT = '/workspace'; process.env.HOST_WORKSPACE_ROOT = 'C:\\Users\\timot\\Documents\\GitHub'; await handleIndexFolder( { path: 'C:\\Users\\timot\\Documents\\GitHub\\my-project', recursive: true, generate_embeddings: false }, mockDriver, mockWatchManager, mockConfigManager ); // Verify startWatch was called with container path - startWatch receives a config object expect(mockWatchManager.startWatch).toHaveBeenCalled(); const configArg = mockWatchManager.startWatch.mock.calls[0][0]; expect(configArg.path).toMatch(/^\/workspace\//); expect(configArg.path).not.toContain('C:\\'); }); it('should return user path in response', async () => { process.env.WORKSPACE_ROOT = '/workspace'; process.env.HOST_WORKSPACE_ROOT = 'C:\\Users\\timot\\Documents\\GitHub'; const userPath = 'C:\\Users\\timot\\Documents\\GitHub\\my-project'; const result = await handleIndexFolder( { path: userPath, recursive: true }, mockDriver, mockWatchManager, mockConfigManager ); // Result should contain normalized path (backslashes converted to forward slashes) const normalizedPath = 'C:/Users/timot/Documents/GitHub/my-project'; expect(result.path).toBe(normalizedPath); expect(result.containerPath).toMatch(/^\/workspace\//); }); it('should handle paths without Docker translation', async () => { delete process.env.WORKSPACE_ROOT; const hostPath = '/Users/c815719/src/my-project'; const result = await handleIndexFolder( { path: hostPath, recursive: true }, mockDriver, mockWatchManager, mockConfigManager ); // When not in Docker, path should remain unchanged expect(result.path).toBe(hostPath); // startWatch receives a config object, not a path string expect(mockWatchManager.startWatch).toHaveBeenCalled(); const configArg = mockWatchManager.startWatch.mock.calls[0][0]; expect(configArg.path).toBe(hostPath); }); }); describe('handleRemoveFolder', () => { let mockDriver: any; let mockWatchManager: any; let mockConfigManager: any; let mockSession: any; beforeEach(() => { // Save original environment originalEnv = { ...process.env }; // Mock Neo4j session mockSession = { run: vi.fn().mockResolvedValue({ records: [{ get: vi.fn((key: string) => { if (key === 'files_deleted') return { toNumber: () => 5 }; if (key === 'chunks_deleted') return { toNumber: () => 50 }; return null; }) }] }), close: vi.fn() }; // Mock Neo4j driver mockDriver = { session: vi.fn().mockReturnValue(mockSession) }; // Mock FileWatchManager mockWatchManager = { stopWatch: vi.fn().mockResolvedValue(undefined) }; // Mock WatchConfigManager - create a proper mock object to pass to functions mockConfigManager = { getByPath: vi.fn().mockResolvedValue({ id: 'watch-test', path: '/workspace/test-project', status: 'active' }), delete: vi.fn().mockResolvedValue(undefined) }; // Mock console methods vi.spyOn(console, 'log').mockImplementation(() => {}); vi.spyOn(console, 'error').mockImplementation(() => {}); }); afterEach(() => { // Restore original environment process.env = originalEnv; vi.clearAllMocks(); vi.restoreAllMocks(); }); it('should translate Windows host path to container path before removal', async () => { process.env.WORKSPACE_ROOT = '/workspace'; process.env.HOST_WORKSPACE_ROOT = 'C:\\Users\\timot\\Documents\\GitHub'; const userPath = 'C:\\Users\\timot\\Documents\\GitHub\\test-project'; // Configure mock for this specific test mockConfigManager.getByPath.mockResolvedValueOnce({ id: 'watch-test', path: '/workspace/test-project', status: 'active' }); const result = await handleRemoveFolder( { path: userPath }, mockDriver, mockWatchManager, mockConfigManager ); // Verify two queries were made (Step 1: relationships, Step 2: path fallback) expect(mockSession.run).toHaveBeenCalledTimes(2); // Step 1: Relationship-based query const step1Params = mockSession.run.mock.calls[0][1]; expect(step1Params.watchConfigId).toBe('watch-test'); // Step 2: Path-based fallback query const step2Params = mockSession.run.mock.calls[1][1]; expect(step2Params.folderPathWithSep).toBe('/workspace/test-project/'); expect(step2Params.exactPath).toBe('/workspace/test-project'); }); it('should return error when path is not being watched', async () => { process.env.WORKSPACE_ROOT = '/workspace'; process.env.HOST_WORKSPACE_ROOT = '/home/user/src'; const userPath = '/home/user/src/my-project'; // Mock config not found mockConfigManager.getByPath.mockResolvedValueOnce(null); const result = await handleRemoveFolder( { path: userPath }, mockDriver, mockWatchManager, mockConfigManager ); expect(result.status).toBe('error'); expect(result.message).toContain('is not being watched'); }); it('should use container path for Neo4j query', async () => { process.env.WORKSPACE_ROOT = '/workspace'; process.env.HOST_WORKSPACE_ROOT = 'C:\\Users\\timot\\Documents\\GitHub'; const userPath = 'C:\\Users\\timot\\Documents\\GitHub\\my-project'; // Configure mock mockConfigManager.getByPath.mockResolvedValueOnce({ id: 'watch-query-test', path: '/workspace/my-project', status: 'active' }); await handleRemoveFolder( { path: userPath }, mockDriver, mockWatchManager, mockConfigManager ); // Verify two queries were made expect(mockSession.run).toHaveBeenCalledTimes(2); // Step 2: Path-based fallback query should use container path const step2Params = mockSession.run.mock.calls[1][1]; expect(step2Params.folderPathWithSep).toBe('/workspace/my-project/'); expect(step2Params.folderPathWithSep).not.toContain('C:\\'); }); it('should return user-provided path in response', async () => { process.env.WORKSPACE_ROOT = '/workspace'; process.env.HOST_WORKSPACE_ROOT = 'C:\\Users\\timot\\Documents\\GitHub'; const userPath = 'C:\\Users\\timot\\Documents\\GitHub\\my-project'; // Configure mock mockConfigManager.getByPath.mockResolvedValueOnce({ id: 'watch-response-test', path: '/workspace/my-project', status: 'active' }); const result = await handleRemoveFolder( { path: userPath }, mockDriver, mockWatchManager, mockConfigManager ); // Result should contain original user path AND container path expect(result.path).toBe(userPath); expect(result.containerPath).toBe('/workspace/my-project'); // Mock returns 5 files from each step, so total is 10 expect(result.files_removed).toBe(10); expect(result.chunks_removed).toBe(100); }); it('should pass correct parameters to Neo4j DELETE query', async () => { process.env.WORKSPACE_ROOT = '/workspace'; const containerPath = '/workspace/my-project'; // Configure mock mockConfigManager.getByPath.mockResolvedValueOnce({ id: 'watch-params-test', path: containerPath, status: 'active' }); await handleRemoveFolder( { path: containerPath }, mockDriver, mockWatchManager, mockConfigManager ); // Verify two queries were made expect(mockSession.run).toHaveBeenCalledTimes(2); // Step 1: Relationship-based query const cypher1 = mockSession.run.mock.calls[0][0]; const params1 = mockSession.run.mock.calls[0][1]; expect(params1.watchConfigId).toBe('watch-params-test'); expect(cypher1).toContain('MATCH (wc:WatchConfig {id: $watchConfigId})-[:WATCHES]->(f:File)'); // Step 2: Path-based fallback query const cypher2 = mockSession.run.mock.calls[1][0]; const params2 = mockSession.run.mock.calls[1][1]; expect(params2.folderPathWithSep).toBe('/workspace/my-project/'); expect(params2.exactPath).toBe('/workspace/my-project'); expect(cypher2).toContain('f.path STARTS WITH $folderPathWithSep'); expect(cypher2).toContain('NOT EXISTS { MATCH (f)<-[:WATCHES]-(:WatchConfig) }'); }); // ======================================================================== // BUG FIX TESTS - Path Separator & WatchConfig Deletion // ======================================================================== describe('Bug Fix: Path Separator Handling', () => { it('should add trailing slash to prevent false path matches', async () => { process.env.WORKSPACE_ROOT = '/workspace'; const containerPath = '/workspace/src'; // Without trailing slash, this would match /workspace/src-other // With trailing slash, it only matches /workspace/src/... const folderPathWithSep = containerPath.endsWith('/') ? containerPath : containerPath + '/'; expect(folderPathWithSep).toBe('/workspace/src/'); // Verify query uses both folderPathWithSep and exactPath const queryParams = { folderPathWithSep: folderPathWithSep, exactPath: containerPath }; expect(queryParams.folderPathWithSep).toBe('/workspace/src/'); expect(queryParams.exactPath).toBe('/workspace/src'); }); it('should not add trailing slash if already present', async () => { const containerPath = '/workspace/src/'; const folderPathWithSep = containerPath.endsWith('/') ? containerPath : containerPath + '/'; expect(folderPathWithSep).toBe('/workspace/src/'); }); it('should use correct query parameters to avoid false matches', async () => { const containerPath = '/workspace/src'; const folderPathWithSep = containerPath + '/'; // Mock the Cypher query const cypher = ` MATCH (f:File) WHERE f.path STARTS WITH $folderPathWithSep OR f.path = $exactPath OPTIONAL MATCH (f)-[:HAS_CHUNK]->(c:FileChunk) WITH f, collect(c) AS chunks, count(c) AS chunk_count FOREACH (chunk IN chunks | DETACH DELETE chunk) DETACH DELETE f RETURN count(f) AS files_deleted, sum(chunk_count) AS chunks_deleted `; expect(cypher).toContain('f.path STARTS WITH $folderPathWithSep'); expect(cypher).toContain('f.path = $exactPath'); expect(cypher).not.toContain('f.path STARTS WITH $pathPrefix'); }); it('should match files under /workspace/src but not /workspace/src-other', async () => { const containerPath = '/workspace/src'; const folderPathWithSep = '/workspace/src/'; // Files that should match const shouldMatch = [ '/workspace/src/file.ts', '/workspace/src/nested/file.ts', '/workspace/src', // Exact match ]; // Files that should NOT match const shouldNotMatch = [ '/workspace/src-other/file.ts', '/workspace/src-backup/file.ts', '/workspace/source/file.ts', ]; shouldMatch.forEach(filePath => { const matches = filePath.startsWith(folderPathWithSep) || filePath === containerPath; expect(matches).toBe(true); }); shouldNotMatch.forEach(filePath => { const matches = filePath.startsWith(folderPathWithSep) || filePath === containerPath; expect(matches).toBe(false); }); }); }); describe('Bug Fix: WatchConfig Deletion', () => { let mockConfigManager: any; beforeEach(() => { mockConfigManager = { getByPath: vi.fn().mockResolvedValue({ id: 'watch-123', path: '/workspace/my-project', status: 'active' }), markInactive: vi.fn().mockResolvedValue(undefined), delete: vi.fn().mockResolvedValue(undefined) }; }); it('should call delete() instead of markInactive()', async () => { const configId = 'watch-123'; // Old behavior (wrong): markInactive // await mockConfigManager.markInactive(configId); // New behavior (correct): delete await mockConfigManager.delete(configId); expect(mockConfigManager.delete).toHaveBeenCalledWith(configId); expect(mockConfigManager.markInactive).not.toHaveBeenCalled(); }); it('should completely remove WatchConfig from database', async () => { const configId = 'watch-123'; // Simulate deletion await mockConfigManager.delete(configId); // After deletion, config should not exist mockConfigManager.getByPath = vi.fn().mockResolvedValue(null); const config = await mockConfigManager.getByPath('/workspace/my-project'); expect(config).toBeNull(); }); it('should delete WatchConfig before deleting files', async () => { const callOrder: string[] = []; mockConfigManager.delete = vi.fn().mockImplementation(async () => { callOrder.push('delete-config'); }); mockSession.run = vi.fn().mockImplementation(async () => { callOrder.push('delete-files'); return { records: [{ get: vi.fn((key: string) => { if (key === 'files_deleted') return { toNumber: () => 5 }; if (key === 'chunks_deleted') return { toNumber: () => 50 }; return null; }) }] }; }); // Simulate the removal flow await mockWatchManager.stopWatch('/workspace/my-project'); await mockConfigManager.delete('watch-123'); await mockSession.run('DELETE query', {}); expect(callOrder).toEqual(['delete-config', 'delete-files']); }); }); describe('Integration: Both Bug Fixes Together', () => { let mockConfigManager: any; beforeEach(() => { mockConfigManager = { getByPath: vi.fn().mockResolvedValue({ id: 'watch-123', path: '/workspace/src', status: 'active' }), delete: vi.fn().mockResolvedValue(undefined) }; mockSession.run = vi.fn().mockResolvedValue({ records: [{ get: vi.fn((key: string) => { if (key === 'files_deleted') return { toNumber: () => 10 }; if (key === 'chunks_deleted') return { toNumber: () => 100 }; return null; }) }] }); }); it('should use correct path separator AND delete WatchConfig', async () => { const containerPath = '/workspace/src'; const folderPathWithSep = containerPath + '/'; // Step 1: Stop watcher await mockWatchManager.stopWatch(containerPath); expect(mockWatchManager.stopWatch).toHaveBeenCalledWith(containerPath); // Step 2: Delete WatchConfig (not markInactive) await mockConfigManager.delete('watch-123'); expect(mockConfigManager.delete).toHaveBeenCalledWith('watch-123'); // Step 3: Delete files with correct path parameters await mockSession.run('DELETE query', { folderPathWithSep: folderPathWithSep, exactPath: containerPath }); const runCall = mockSession.run.mock.calls[0]; expect(runCall[1].folderPathWithSep).toBe('/workspace/src/'); expect(runCall[1].exactPath).toBe('/workspace/src'); }); it('should handle complete removal workflow correctly', async () => { const containerPath = '/workspace/my-project'; const folderPathWithSep = containerPath + '/'; // Simulate complete removal const config = await mockConfigManager.getByPath(containerPath); expect(config).toBeTruthy(); expect(config.id).toBe('watch-123'); await mockWatchManager.stopWatch(containerPath); await mockConfigManager.delete(config.id); const result = await mockSession.run('DELETE query', { folderPathWithSep, exactPath: containerPath }); const filesDeleted = result.records[0].get('files_deleted').toNumber(); const chunksDeleted = result.records[0].get('chunks_deleted').toNumber(); expect(filesDeleted).toBe(10); expect(chunksDeleted).toBe(100); // Verify WatchConfig was deleted expect(mockConfigManager.delete).toHaveBeenCalledWith('watch-123'); }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/orneryd/Mimir'

If you have feedback or need assistance with the MCP directory API, please join our Discord server