mirror of
https://github.com/modelcontextprotocol/servers.git
synced 2026-04-20 12:55:21 +02:00
Merge pull request #1405 from olaservo/add-jest-setup
Add path utils and test config for filesystem
This commit is contained in:
37
.github/workflows/typescript.yml
vendored
37
.github/workflows/typescript.yml
vendored
@@ -22,8 +22,43 @@ jobs:
|
||||
PACKAGES=$(find . -name package.json -not -path "*/node_modules/*" -exec dirname {} \; | sed 's/^\.\///' | jq -R -s -c 'split("\n")[:-1]')
|
||||
echo "packages=$PACKAGES" >> $GITHUB_OUTPUT
|
||||
|
||||
build:
|
||||
test:
|
||||
needs: [detect-packages]
|
||||
strategy:
|
||||
matrix:
|
||||
package: ${{ fromJson(needs.detect-packages.outputs.packages) }}
|
||||
name: Test ${{ matrix.package }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: npm
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: src/${{ matrix.package }}
|
||||
run: npm ci
|
||||
|
||||
- name: Check if tests exist
|
||||
id: check-tests
|
||||
working-directory: src/${{ matrix.package }}
|
||||
run: |
|
||||
if npm run test --silent 2>/dev/null; then
|
||||
echo "has-tests=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "has-tests=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
continue-on-error: true
|
||||
|
||||
- name: Run tests
|
||||
if: steps.check-tests.outputs.has-tests == 'true'
|
||||
working-directory: src/${{ matrix.package }}
|
||||
run: npm test
|
||||
|
||||
build:
|
||||
needs: [detect-packages, test]
|
||||
strategy:
|
||||
matrix:
|
||||
package: ${{ fromJson(needs.detect-packages.outputs.packages) }}
|
||||
|
||||
4255
package-lock.json
generated
4255
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
169
src/filesystem/__tests__/path-utils.test.ts
Normal file
169
src/filesystem/__tests__/path-utils.test.ts
Normal file
@@ -0,0 +1,169 @@
|
||||
import { describe, it, expect } from '@jest/globals';
|
||||
import { normalizePath, expandHome, convertToWindowsPath } from '../path-utils.js';
|
||||
|
||||
describe('Path Utilities', () => {
|
||||
describe('convertToWindowsPath', () => {
|
||||
it('leaves Unix paths unchanged', () => {
|
||||
expect(convertToWindowsPath('/usr/local/bin'))
|
||||
.toBe('/usr/local/bin');
|
||||
expect(convertToWindowsPath('/home/user/some path'))
|
||||
.toBe('/home/user/some path');
|
||||
});
|
||||
|
||||
it('converts WSL paths to Windows format', () => {
|
||||
expect(convertToWindowsPath('/mnt/c/NS/MyKindleContent'))
|
||||
.toBe('C:\\NS\\MyKindleContent');
|
||||
});
|
||||
|
||||
it('converts Unix-style Windows paths to Windows format', () => {
|
||||
expect(convertToWindowsPath('/c/NS/MyKindleContent'))
|
||||
.toBe('C:\\NS\\MyKindleContent');
|
||||
});
|
||||
|
||||
it('leaves Windows paths unchanged but ensures backslashes', () => {
|
||||
expect(convertToWindowsPath('C:\\NS\\MyKindleContent'))
|
||||
.toBe('C:\\NS\\MyKindleContent');
|
||||
expect(convertToWindowsPath('C:/NS/MyKindleContent'))
|
||||
.toBe('C:\\NS\\MyKindleContent');
|
||||
});
|
||||
|
||||
it('handles Windows paths with spaces', () => {
|
||||
expect(convertToWindowsPath('C:\\Program Files\\Some App'))
|
||||
.toBe('C:\\Program Files\\Some App');
|
||||
expect(convertToWindowsPath('C:/Program Files/Some App'))
|
||||
.toBe('C:\\Program Files\\Some App');
|
||||
});
|
||||
|
||||
it('handles uppercase and lowercase drive letters', () => {
|
||||
expect(convertToWindowsPath('/mnt/d/some/path'))
|
||||
.toBe('D:\\some\\path');
|
||||
expect(convertToWindowsPath('/d/some/path'))
|
||||
.toBe('D:\\some\\path');
|
||||
});
|
||||
});
|
||||
|
||||
describe('normalizePath', () => {
|
||||
it('preserves Unix paths', () => {
|
||||
expect(normalizePath('/usr/local/bin'))
|
||||
.toBe('/usr/local/bin');
|
||||
expect(normalizePath('/home/user/some path'))
|
||||
.toBe('/home/user/some path');
|
||||
expect(normalizePath('"/usr/local/some app/"'))
|
||||
.toBe('/usr/local/some app');
|
||||
});
|
||||
|
||||
it('removes surrounding quotes', () => {
|
||||
expect(normalizePath('"C:\\NS\\My Kindle Content"'))
|
||||
.toBe('C:\\NS\\My Kindle Content');
|
||||
});
|
||||
|
||||
it('normalizes backslashes', () => {
|
||||
expect(normalizePath('C:\\\\NS\\\\MyKindleContent'))
|
||||
.toBe('C:\\NS\\MyKindleContent');
|
||||
});
|
||||
|
||||
it('converts forward slashes to backslashes on Windows', () => {
|
||||
expect(normalizePath('C:/NS/MyKindleContent'))
|
||||
.toBe('C:\\NS\\MyKindleContent');
|
||||
});
|
||||
|
||||
it('handles WSL paths', () => {
|
||||
expect(normalizePath('/mnt/c/NS/MyKindleContent'))
|
||||
.toBe('C:\\NS\\MyKindleContent');
|
||||
});
|
||||
|
||||
it('handles Unix-style Windows paths', () => {
|
||||
expect(normalizePath('/c/NS/MyKindleContent'))
|
||||
.toBe('C:\\NS\\MyKindleContent');
|
||||
});
|
||||
|
||||
it('handles paths with spaces and mixed slashes', () => {
|
||||
expect(normalizePath('C:/NS/My Kindle Content'))
|
||||
.toBe('C:\\NS\\My Kindle Content');
|
||||
expect(normalizePath('/mnt/c/NS/My Kindle Content'))
|
||||
.toBe('C:\\NS\\My Kindle Content');
|
||||
expect(normalizePath('C:\\Program Files (x86)\\App Name'))
|
||||
.toBe('C:\\Program Files (x86)\\App Name');
|
||||
expect(normalizePath('"C:\\Program Files\\App Name"'))
|
||||
.toBe('C:\\Program Files\\App Name');
|
||||
expect(normalizePath(' C:\\Program Files\\App Name '))
|
||||
.toBe('C:\\Program Files\\App Name');
|
||||
});
|
||||
|
||||
it('preserves spaces in all path formats', () => {
|
||||
expect(normalizePath('/mnt/c/Program Files/App Name'))
|
||||
.toBe('C:\\Program Files\\App Name');
|
||||
expect(normalizePath('/c/Program Files/App Name'))
|
||||
.toBe('C:\\Program Files\\App Name');
|
||||
expect(normalizePath('C:/Program Files/App Name'))
|
||||
.toBe('C:\\Program Files\\App Name');
|
||||
});
|
||||
|
||||
it('handles special characters in paths', () => {
|
||||
// Test ampersand in path
|
||||
expect(normalizePath('C:\\NS\\Sub&Folder'))
|
||||
.toBe('C:\\NS\\Sub&Folder');
|
||||
expect(normalizePath('C:/NS/Sub&Folder'))
|
||||
.toBe('C:\\NS\\Sub&Folder');
|
||||
expect(normalizePath('/mnt/c/NS/Sub&Folder'))
|
||||
.toBe('C:\\NS\\Sub&Folder');
|
||||
|
||||
// Test tilde in path (short names in Windows)
|
||||
expect(normalizePath('C:\\NS\\MYKIND~1'))
|
||||
.toBe('C:\\NS\\MYKIND~1');
|
||||
expect(normalizePath('/Users/NEMANS~1/FOLDER~2/SUBFO~1/Public/P12PST~1'))
|
||||
.toBe('/Users/NEMANS~1/FOLDER~2/SUBFO~1/Public/P12PST~1');
|
||||
|
||||
// Test other special characters
|
||||
expect(normalizePath('C:\\Path with #hash'))
|
||||
.toBe('C:\\Path with #hash');
|
||||
expect(normalizePath('C:\\Path with (parentheses)'))
|
||||
.toBe('C:\\Path with (parentheses)');
|
||||
expect(normalizePath('C:\\Path with [brackets]'))
|
||||
.toBe('C:\\Path with [brackets]');
|
||||
expect(normalizePath('C:\\Path with @at+plus$dollar%percent'))
|
||||
.toBe('C:\\Path with @at+plus$dollar%percent');
|
||||
});
|
||||
|
||||
it('capitalizes lowercase drive letters for Windows paths', () => {
|
||||
expect(normalizePath('c:/windows/system32'))
|
||||
.toBe('C:\\windows\\system32');
|
||||
expect(normalizePath('/mnt/d/my/folder')) // WSL path with lowercase drive
|
||||
.toBe('D:\\my\\folder');
|
||||
expect(normalizePath('/e/another/folder')) // Unix-style Windows path with lowercase drive
|
||||
.toBe('E:\\another\\folder');
|
||||
});
|
||||
|
||||
it('handles UNC paths correctly', () => {
|
||||
// UNC paths should preserve the leading double backslash
|
||||
const uncPath = '\\\\SERVER\\share\\folder';
|
||||
expect(normalizePath(uncPath)).toBe('\\\\SERVER\\share\\folder');
|
||||
|
||||
// Test UNC path with double backslashes that need normalization
|
||||
const uncPathWithDoubles = '\\\\\\\\SERVER\\\\share\\\\folder';
|
||||
expect(normalizePath(uncPathWithDoubles)).toBe('\\\\SERVER\\share\\folder');
|
||||
});
|
||||
|
||||
it('returns normalized non-Windows/WSL/Unix-style Windows paths as is after basic normalization', () => {
|
||||
// Relative path
|
||||
const relativePath = 'some/relative/path';
|
||||
expect(normalizePath(relativePath)).toBe(relativePath.replace(/\//g, '\\'));
|
||||
|
||||
// A path that looks somewhat absolute but isn't a drive or recognized Unix root for Windows conversion
|
||||
const otherAbsolutePath = '\\someserver\\share\\file';
|
||||
expect(normalizePath(otherAbsolutePath)).toBe(otherAbsolutePath);
|
||||
});
|
||||
});
|
||||
|
||||
describe('expandHome', () => {
|
||||
it('expands ~ to home directory', () => {
|
||||
const result = expandHome('~/test');
|
||||
expect(result).toContain('test');
|
||||
expect(result).not.toContain('~');
|
||||
});
|
||||
|
||||
it('leaves other paths unchanged', () => {
|
||||
expect(expandHome('C:/test')).toBe('C:/test');
|
||||
});
|
||||
});
|
||||
});
|
||||
23
src/filesystem/jest.config.cjs
Normal file
23
src/filesystem/jest.config.cjs
Normal file
@@ -0,0 +1,23 @@
|
||||
/** @type {import('ts-jest').JestConfigWithTsJest} */
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
extensionsToTreatAsEsm: ['.ts'],
|
||||
moduleNameMapper: {
|
||||
'^(\\.{1,2}/.*)\\.js$': '$1',
|
||||
},
|
||||
transform: {
|
||||
'^.+\\.tsx?$': [
|
||||
'ts-jest',
|
||||
{
|
||||
useESM: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
testMatch: ['**/__tests__/**/*.test.ts'],
|
||||
collectCoverageFrom: [
|
||||
'**/*.ts',
|
||||
'!**/__tests__/**',
|
||||
'!**/dist/**',
|
||||
],
|
||||
}
|
||||
@@ -16,20 +16,26 @@
|
||||
"scripts": {
|
||||
"build": "tsc && shx chmod +x dist/*.js",
|
||||
"prepare": "npm run build",
|
||||
"watch": "tsc --watch"
|
||||
"watch": "tsc --watch",
|
||||
"test": "jest --config=jest.config.cjs --coverage"
|
||||
},
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "0.5.0",
|
||||
"@modelcontextprotocol/sdk": "^1.12.3",
|
||||
"diff": "^5.1.0",
|
||||
"glob": "^10.3.10",
|
||||
"minimatch": "^10.0.1",
|
||||
"zod-to-json-schema": "^3.23.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@jest/globals": "^29.7.0",
|
||||
"@types/diff": "^5.0.9",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/minimatch": "^5.1.2",
|
||||
"@types/node": "^22",
|
||||
"jest": "^29.7.0",
|
||||
"shx": "^0.3.4",
|
||||
"typescript": "^5.3.3"
|
||||
"ts-jest": "^29.1.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.8.2"
|
||||
}
|
||||
}
|
||||
104
src/filesystem/path-utils.ts
Normal file
104
src/filesystem/path-utils.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import path from "path";
|
||||
import os from 'os';
|
||||
|
||||
/**
|
||||
* Converts WSL or Unix-style Windows paths to Windows format
|
||||
* @param p The path to convert
|
||||
* @returns Converted Windows path
|
||||
*/
|
||||
export function convertToWindowsPath(p: string): string {
|
||||
// Handle WSL paths (/mnt/c/...)
|
||||
if (p.startsWith('/mnt/')) {
|
||||
const driveLetter = p.charAt(5).toUpperCase();
|
||||
const pathPart = p.slice(6).replace(/\//g, '\\');
|
||||
return `${driveLetter}:${pathPart}`;
|
||||
}
|
||||
|
||||
// Handle Unix-style Windows paths (/c/...)
|
||||
if (p.match(/^\/[a-zA-Z]\//)) {
|
||||
const driveLetter = p.charAt(1).toUpperCase();
|
||||
const pathPart = p.slice(2).replace(/\//g, '\\');
|
||||
return `${driveLetter}:${pathPart}`;
|
||||
}
|
||||
|
||||
// Handle standard Windows paths, ensuring backslashes
|
||||
if (p.match(/^[a-zA-Z]:/)) {
|
||||
return p.replace(/\//g, '\\');
|
||||
}
|
||||
|
||||
// Leave non-Windows paths unchanged
|
||||
return p;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes path by standardizing format while preserving OS-specific behavior
|
||||
* @param p The path to normalize
|
||||
* @returns Normalized path
|
||||
*/
|
||||
export function normalizePath(p: string): string {
|
||||
// Remove any surrounding quotes and whitespace
|
||||
p = p.trim().replace(/^["']|["']$/g, '');
|
||||
|
||||
// Check if this is a Unix path (starts with / but not a Windows or WSL path)
|
||||
const isUnixPath = p.startsWith('/') &&
|
||||
!p.match(/^\/mnt\/[a-z]\//i) &&
|
||||
!p.match(/^\/[a-zA-Z]\//);
|
||||
|
||||
if (isUnixPath) {
|
||||
// For Unix paths, just normalize without converting to Windows format
|
||||
// Replace double slashes with single slashes and remove trailing slashes
|
||||
return p.replace(/\/+/g, '/').replace(/\/+$/, '');
|
||||
}
|
||||
|
||||
// Convert WSL or Unix-style Windows paths to Windows format
|
||||
p = convertToWindowsPath(p);
|
||||
|
||||
// Handle double backslashes, preserving leading UNC \\
|
||||
if (p.startsWith('\\\\')) {
|
||||
// For UNC paths, first normalize any excessive leading backslashes to exactly \\
|
||||
// Then normalize double backslashes in the rest of the path
|
||||
let uncPath = p;
|
||||
// Replace multiple leading backslashes with exactly two
|
||||
uncPath = uncPath.replace(/^\\{2,}/, '\\\\');
|
||||
// Now normalize any remaining double backslashes in the rest of the path
|
||||
const restOfPath = uncPath.substring(2).replace(/\\\\/g, '\\');
|
||||
p = '\\\\' + restOfPath;
|
||||
} else {
|
||||
// For non-UNC paths, normalize all double backslashes
|
||||
p = p.replace(/\\\\/g, '\\');
|
||||
}
|
||||
|
||||
// Use Node's path normalization, which handles . and .. segments
|
||||
let normalized = path.normalize(p);
|
||||
|
||||
// Fix UNC paths after normalization (path.normalize can remove a leading backslash)
|
||||
if (p.startsWith('\\\\') && !normalized.startsWith('\\\\')) {
|
||||
normalized = '\\' + normalized;
|
||||
}
|
||||
|
||||
// Handle Windows paths: convert slashes and ensure drive letter is capitalized
|
||||
if (normalized.match(/^[a-zA-Z]:/)) {
|
||||
let result = normalized.replace(/\//g, '\\');
|
||||
// Capitalize drive letter if present
|
||||
if (/^[a-z]:/.test(result)) {
|
||||
result = result.charAt(0).toUpperCase() + result.slice(1);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// For all other paths (including relative paths), convert forward slashes to backslashes
|
||||
// This ensures relative paths like "some/relative/path" become "some\\relative\\path"
|
||||
return normalized.replace(/\//g, '\\');
|
||||
}
|
||||
|
||||
/**
|
||||
* Expands home directory tildes in paths
|
||||
* @param filepath The path to expand
|
||||
* @returns Expanded path
|
||||
*/
|
||||
export function expandHome(filepath: string): string {
|
||||
if (filepath.startsWith('~/') || filepath === '~') {
|
||||
return path.join(os.homedir(), filepath.slice(1));
|
||||
}
|
||||
return filepath;
|
||||
}
|
||||
Reference in New Issue
Block a user