mirror of
https://github.com/modelcontextprotocol/servers.git
synced 2026-04-26 15:55:25 +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]')
|
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
|
echo "packages=$PACKAGES" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
build:
|
test:
|
||||||
needs: [detect-packages]
|
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:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
package: ${{ fromJson(needs.detect-packages.outputs.packages) }}
|
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": {
|
"scripts": {
|
||||||
"build": "tsc && shx chmod +x dist/*.js",
|
"build": "tsc && shx chmod +x dist/*.js",
|
||||||
"prepare": "npm run build",
|
"prepare": "npm run build",
|
||||||
"watch": "tsc --watch"
|
"watch": "tsc --watch",
|
||||||
|
"test": "jest --config=jest.config.cjs --coverage"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modelcontextprotocol/sdk": "0.5.0",
|
"@modelcontextprotocol/sdk": "^1.12.3",
|
||||||
"diff": "^5.1.0",
|
"diff": "^5.1.0",
|
||||||
"glob": "^10.3.10",
|
"glob": "^10.3.10",
|
||||||
"minimatch": "^10.0.1",
|
"minimatch": "^10.0.1",
|
||||||
"zod-to-json-schema": "^3.23.5"
|
"zod-to-json-schema": "^3.23.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@jest/globals": "^29.7.0",
|
||||||
"@types/diff": "^5.0.9",
|
"@types/diff": "^5.0.9",
|
||||||
|
"@types/jest": "^29.5.14",
|
||||||
"@types/minimatch": "^5.1.2",
|
"@types/minimatch": "^5.1.2",
|
||||||
"@types/node": "^22",
|
"@types/node": "^22",
|
||||||
|
"jest": "^29.7.0",
|
||||||
"shx": "^0.3.4",
|
"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