Skip to main content
Glama
orneryd

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

by orneryd
path-utils.test.ts12.9 kB
/** * @fileoverview Unit tests for cross-platform path utilities * * Tests path normalization, tilde expansion, and host/container translation * across Windows, macOS, and Linux platforms. * * @since 1.0.0 */ import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'; import os from 'os'; import * as pathUtils from './path-utils.js'; import { normalizeSlashes, expandTilde, normalizeAndResolve, getHostWorkspaceRoot, translateHostToContainer, translateContainerToHost, pathExists, validateAndSanitizePath, } from './path-utils.js'; describe('Path Utilities', () => { const originalEnv = process.env.HOST_WORKSPACE_ROOT; const originalHomedir = os.homedir; beforeEach(() => { // Set HOST_WORKSPACE_ROOT for tests process.env.HOST_WORKSPACE_ROOT = '/Users/testuser/src'; // Mock os.homedir() for consistent tests vi.spyOn(os, 'homedir').mockReturnValue('/Users/testuser'); }); afterEach(() => { // Restore original environment if (originalEnv) { process.env.HOST_WORKSPACE_ROOT = originalEnv; } else { delete process.env.HOST_WORKSPACE_ROOT; } // Restore os.homedir vi.restoreAllMocks(); }); describe('normalizeSlashes', () => { it('should convert Windows backslashes to forward slashes', () => { expect(normalizeSlashes('C:\\Users\\john\\project')).toBe('C:/Users/john/project'); expect(normalizeSlashes('D:\\Documents\\file.txt')).toBe('D:/Documents/file.txt'); }); it('should leave Unix paths unchanged', () => { expect(normalizeSlashes('/home/user/project')).toBe('/home/user/project'); expect(normalizeSlashes('/var/log/app.log')).toBe('/var/log/app.log'); }); it('should handle mixed slashes', () => { expect(normalizeSlashes('C:\\Users/john\\project')).toBe('C:/Users/john/project'); expect(normalizeSlashes('/home\\user/project')).toBe('/home/user/project'); }); it('should handle empty strings', () => { expect(normalizeSlashes('')).toBe(''); }); }); describe('expandTilde', () => { it('should expand ~ to home directory', () => { expect(expandTilde('~')).toBe('/Users/testuser'); }); it('should expand ~/ to home directory with path', () => { // On Windows, path.join uses backslashes, so normalize the result expect(normalizeSlashes(expandTilde('~/Documents'))).toBe('/Users/testuser/Documents'); expect(normalizeSlashes(expandTilde('~/project/file.txt'))).toBe('/Users/testuser/project/file.txt'); }); it('should expand ~\\ (Windows style) to home directory', () => { // On Windows, path.join uses backslashes, so normalize the result expect(normalizeSlashes(expandTilde('~\\Documents'))).toBe('/Users/testuser/Documents'); }); it('should not expand paths that do not start with ~', () => { expect(expandTilde('/absolute/path')).toBe('/absolute/path'); expect(expandTilde('relative/path')).toBe('relative/path'); expect(expandTilde('path~with~tildes')).toBe('path~with~tildes'); }); it('should handle empty strings', () => { expect(expandTilde('')).toBe(''); }); }); describe('normalizeAndResolve', () => { it('should expand tilde and normalize', () => { const result = normalizeAndResolve('~/project'); expect(result).toBe('/Users/testuser/project'); }); it('should resolve relative paths', () => { const result = normalizeAndResolve('../other', '/home/user/project'); // On Windows, path.resolve may add drive letter (C:) to Unix-style paths // The important thing is the relative resolution is correct expect(result).toMatch(/\/?home\/user\/other$/); expect(result).not.toContain('..'); }); it('should normalize Windows paths', () => { const result = normalizeAndResolve('C:\\Users\\john\\file.txt'); expect(result).toBe('C:/Users/john/file.txt'); }); it('should remove trailing slashes', () => { // Test with already absolute paths (no path.resolve needed) const path1 = normalizeAndResolve('/home/user/project/'); const path2 = normalizeAndResolve('/home/user/project///'); expect(path1).toBe('/home/user/project'); expect(path2).toBe('/home/user/project'); expect(path1).not.toMatch(/\/$/); expect(path2).not.toMatch(/\/$/); }); it('should preserve root slash', () => { expect(normalizeAndResolve('/')).toBe('/'); }); it('should handle paths with spaces', () => { const result = normalizeAndResolve('~/My Documents/project'); expect(result).toBe('/Users/testuser/My Documents/project'); }); it('should normalize multiple slashes in absolute paths', () => { const result = normalizeAndResolve('/home//user///project'); // Should not contain consecutive slashes expect(result).not.toMatch(/\/\//); // Should start with single slash expect(result).toMatch(/^\/[^/]/); }); }); describe('getHostWorkspaceRoot', () => { it('should return default ~/src when HOST_WORKSPACE_ROOT not set', () => { delete process.env.HOST_WORKSPACE_ROOT; const result = getHostWorkspaceRoot(); expect(result).toBe('/Users/testuser/src'); }); it('should return normalized HOST_WORKSPACE_ROOT when set', () => { process.env.HOST_WORKSPACE_ROOT = '~/Documents/workspace'; const result = getHostWorkspaceRoot(); expect(result).toBe('/Users/testuser/Documents/workspace'); }); it('should normalize Windows paths in HOST_WORKSPACE_ROOT', () => { process.env.HOST_WORKSPACE_ROOT = 'C:\\Users\\john\\workspace'; const result = getHostWorkspaceRoot(); expect(result).toContain('/'); expect(result).not.toContain('\\'); }); it('should handle absolute Unix paths', () => { process.env.HOST_WORKSPACE_ROOT = '/var/workspace'; const result = getHostWorkspaceRoot(); expect(result).toBe('/var/workspace'); }); }); describe('translateHostToContainer', () => { beforeEach(() => { process.env.HOST_WORKSPACE_ROOT = '/Users/testuser/src'; process.env.WORKSPACE_ROOT = '/workspace'; // Mock isRunningInDocker to return true for these tests vi.spyOn(pathUtils, 'isRunningInDocker').mockReturnValue(true); }); it('should translate host paths to container paths', () => { const result = translateHostToContainer('/Users/testuser/src/project'); expect(result).toBe('/workspace/project'); }); it('should handle tilde paths', () => { const result = translateHostToContainer('~/src/project'); expect(result).toBe('/workspace/project'); }); it('should handle Windows paths', () => { process.env.HOST_WORKSPACE_ROOT = 'C:/Users/testuser/src'; const result = translateHostToContainer('C:\\Users\\testuser\\src\\project'); expect(result).toBe('/workspace/project'); }); it('should be idempotent for container paths', () => { const result = translateHostToContainer('/workspace/project'); expect(result).toBe('/workspace/project'); }); it('should handle nested paths', () => { const result = translateHostToContainer('/Users/testuser/src/sub/deep/project'); expect(result).toBe('/workspace/sub/deep/project'); }); it('should throw for paths outside workspace root', () => { expect(() => { translateHostToContainer('/other/directory/project'); }).toThrow('Path is outside workspace root'); }); it('should handle exact workspace root', () => { const result = translateHostToContainer('/Users/testuser/src'); expect(result).toBe('/workspace'); }); }); describe('translateContainerToHost', () => { beforeEach(() => { process.env.HOST_WORKSPACE_ROOT = '/Users/testuser/src'; process.env.WORKSPACE_ROOT = '/workspace'; // Mock isRunningInDocker to return true for these tests vi.spyOn(pathUtils, 'isRunningInDocker').mockReturnValue(true); }); it('should translate container paths to host paths', () => { const result = translateContainerToHost('/workspace/project'); expect(result).toBe('/Users/testuser/src/project'); }); it('should handle nested container paths', () => { const result = translateContainerToHost('/workspace/sub/deep/project'); expect(result).toBe('/Users/testuser/src/sub/deep/project'); }); it('should handle /workspace root', () => { const result = translateContainerToHost('/workspace'); expect(result).toBe('/Users/testuser/src'); }); it('should return non-workspace paths unchanged', () => { const result = translateContainerToHost('/other/path'); expect(result).toBe('/other/path'); }); it('should handle empty strings', () => { const result = translateContainerToHost(''); expect(result).toBe(''); }); }); describe('validateAndSanitizePath', () => { it('should validate and normalize tilde paths', () => { const result = validateAndSanitizePath('~/project'); expect(result).toBe('/Users/testuser/project'); }); it('should validate and normalize relative paths', () => { const result = validateAndSanitizePath('../project'); expect(result).toContain('/project'); expect(result).not.toContain('..'); }); it('should throw for empty paths', () => { expect(() => validateAndSanitizePath('')).toThrow('Path parameter is required'); }); it('should throw for null paths', () => { expect(() => validateAndSanitizePath(null as any)).toThrow('Path parameter is required'); }); it('should throw for non-string paths', () => { expect(() => validateAndSanitizePath(123 as any)).toThrow('Path parameter is required'); }); it('should throw for paths with control characters', () => { expect(() => validateAndSanitizePath('/path/with\x00null')).toThrow('invalid control characters'); expect(() => validateAndSanitizePath('/path\x01\x02')).toThrow('invalid control characters'); }); it('should handle paths with spaces', () => { const result = validateAndSanitizePath('~/My Documents/project'); expect(result).toBe('/Users/testuser/My Documents/project'); }); }); describe('Round-trip translation', () => { beforeEach(() => { process.env.HOST_WORKSPACE_ROOT = '/Users/testuser/src'; process.env.WORKSPACE_ROOT = '/workspace'; // Mock isRunningInDocker to return true for these tests vi.spyOn(pathUtils, 'isRunningInDocker').mockReturnValue(true); }); it('should maintain path integrity through host->container->host', () => { const original = '/Users/testuser/src/my-project/sub/file.txt'; const containerPath = translateHostToContainer(original); const backToHost = translateContainerToHost(containerPath); expect(backToHost).toBe(original); }); it('should maintain path integrity through container->host->container', () => { const original = '/workspace/my-project/sub/file.txt'; const hostPath = translateContainerToHost(original); const backToContainer = translateHostToContainer(hostPath); expect(backToContainer).toBe(original); }); it('should handle tilde paths in round-trip', () => { // In Docker, tilde paths are expanded and translated const original = '~/src/project/file.txt'; const containerPath = translateHostToContainer(original); // In Docker, path is translated to /workspace expect(containerPath).toBe('/workspace/project/file.txt'); const hostPath = translateContainerToHost(containerPath); expect(hostPath).toBe('/Users/testuser/src/project/file.txt'); }); }); describe('Cross-platform scenarios', () => { beforeEach(() => { process.env.WORKSPACE_ROOT = '/workspace'; // Mock isRunningInDocker to return true for these tests vi.spyOn(pathUtils, 'isRunningInDocker').mockReturnValue(true); }); it('should handle Windows paths with HOST_WORKSPACE_ROOT on Windows', () => { process.env.HOST_WORKSPACE_ROOT = 'C:\\Users\\john\\workspace'; const result = translateHostToContainer('C:\\Users\\john\\workspace\\project'); // In Docker, path is translated to /workspace expect(result).toBe('/workspace/project'); }); it('should handle mixed Windows/Unix style in same path', () => { const result = normalizeAndResolve('C:\\Users/john\\workspace/project'); expect(result).not.toContain('\\'); expect(result).toContain('/'); }); it('should handle UNC paths (Windows network paths)', () => { const result = normalizeSlashes('\\\\server\\share\\folder'); expect(result).toBe('//server/share/folder'); }); }); });

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