test: migrate from Jest to Vitest (Phase 1 complete)

- Remove Jest and all related packages
- Install Vitest with coverage support
- Create vitest.config.ts with path aliases
- Set up global test configuration
- Migrate all 6 test files to Vitest syntax
- Update TypeScript configuration for better Vitest support
- Create separate tsconfig.build.json for clean builds
- Fix all import/module issues in tests
- All 68 tests passing successfully
- Current coverage baseline: 2.45%

Phase 1 of testing suite improvement complete.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
czlonkowski
2025-07-28 13:05:38 +02:00
parent d44ec49814
commit aa3b2a8460
18 changed files with 2565 additions and 365 deletions

View File

@@ -4,12 +4,22 @@
- [x] ~~Fix failing tests (Phase 0)~~ ✅ COMPLETED - [x] ~~Fix failing tests (Phase 0)~~ ✅ COMPLETED
- [x] ~~Create GitHub Actions workflow file~~ ✅ COMPLETED - [x] ~~Create GitHub Actions workflow file~~ ✅ COMPLETED
- [ ] Install Vitest and remove Jest - [x] ~~Install Vitest and remove Jest~~ ✅ COMPLETED
- [ ] Create vitest.config.ts - [x] ~~Create vitest.config.ts~~ ✅ COMPLETED
- [ ] Setup global test configuration - [x] ~~Setup global test configuration~~ ✅ COMPLETED
- [ ] Migrate existing tests to Vitest syntax - [x] ~~Migrate existing tests to Vitest syntax~~ ✅ COMPLETED
- [ ] Setup coverage reporting with Codecov - [ ] Setup coverage reporting with Codecov
## Phase 1: Vitest Migration ✅ COMPLETED
All tests have been successfully migrated from Jest to Vitest:
- ✅ Removed Jest and installed Vitest
- ✅ Created vitest.config.ts with path aliases
- ✅ Set up global test configuration
- ✅ Migrated all 6 test files (68 tests passing)
- ✅ Updated TypeScript configuration
- ✅ Cleaned up Jest configuration files
## Week 1: Foundation ## Week 1: Foundation
### Testing Infrastructure ### Testing Infrastructure

View File

@@ -81,7 +81,7 @@ git push
# Check Actions tab on GitHub - should see workflow running # Check Actions tab on GitHub - should see workflow running
``` ```
## Phase 1: Vitest Migration (Week 1) ## Phase 1: Vitest Migration (Week 1) ✅ COMPLETED
### Task 1.1: Install Vitest ### Task 1.1: Install Vitest

View File

@@ -1,16 +0,0 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['<rootDir>/src', '<rootDir>/tests'],
testMatch: ['**/__tests__/**/*.ts', '**/?(*.)+(spec|test).ts'],
transform: {
'^.+\\.ts$': 'ts-jest',
},
collectCoverageFrom: [
'src/**/*.ts',
'!src/**/*.d.ts',
'!src/**/*.test.ts',
],
coverageDirectory: 'coverage',
coverageReporters: ['text', 'lcov', 'html'],
};

2512
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@
"n8n-mcp": "./dist/mcp/index.js" "n8n-mcp": "./dist/mcp/index.js"
}, },
"scripts": { "scripts": {
"build": "tsc", "build": "tsc -p tsconfig.build.json",
"rebuild": "node dist/scripts/rebuild.js", "rebuild": "node dist/scripts/rebuild.js",
"rebuild:optimized": "node dist/scripts/rebuild-optimized.js", "rebuild:optimized": "node dist/scripts/rebuild-optimized.js",
"validate": "node dist/scripts/validate.js", "validate": "node dist/scripts/validate.js",
@@ -19,7 +19,14 @@
"dev": "npm run build && npm run rebuild && npm run validate", "dev": "npm run build && npm run rebuild && npm run validate",
"dev:http": "MCP_MODE=http nodemon --watch src --ext ts --exec 'npm run build && npm run start:http'", "dev:http": "MCP_MODE=http nodemon --watch src --ext ts --exec 'npm run build && npm run start:http'",
"test:single-session": "./scripts/test-single-session.sh", "test:single-session": "./scripts/test-single-session.sh",
"test": "jest", "test": "vitest",
"test:ui": "vitest --ui",
"test:run": "vitest run",
"test:coverage": "vitest run --coverage",
"test:watch": "vitest watch",
"test:unit": "vitest run tests/unit",
"test:integration": "vitest run tests/integration",
"test:e2e": "vitest run tests/e2e",
"lint": "tsc --noEmit", "lint": "tsc --noEmit",
"typecheck": "tsc --noEmit", "typecheck": "tsc --noEmit",
"update:n8n": "node scripts/update-n8n-deps.js", "update:n8n": "node scripts/update-n8n-deps.js",
@@ -83,21 +90,24 @@
"package.runtime.json" "package.runtime.json"
], ],
"devDependencies": { "devDependencies": {
"@faker-js/faker": "^9.9.0",
"@testing-library/jest-dom": "^6.6.4",
"@types/express": "^5.0.3", "@types/express": "^5.0.3",
"@types/jest": "^29.5.14",
"@types/node": "^22.15.30", "@types/node": "^22.15.30",
"@types/ws": "^8.18.1", "@types/ws": "^8.18.1",
"jest": "^29.7.0", "@vitest/coverage-v8": "^3.2.4",
"@vitest/ui": "^3.2.4",
"fishery": "^2.3.1",
"msw": "^2.10.4",
"nodemon": "^3.1.10", "nodemon": "^3.1.10",
"ts-jest": "^29.3.4",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"typescript": "^5.8.3" "typescript": "^5.8.3",
"vitest": "^3.2.4"
}, },
"dependencies": { "dependencies": {
"@modelcontextprotocol/sdk": "^1.13.2", "@modelcontextprotocol/sdk": "^1.13.2",
"@n8n/n8n-nodes-langchain": "^1.102.1", "@n8n/n8n-nodes-langchain": "^1.102.1",
"axios": "^1.10.0", "axios": "^1.10.0",
"better-sqlite3": "^11.10.0",
"dotenv": "^16.5.0", "dotenv": "^16.5.0",
"express": "^5.1.0", "express": "^5.1.0",
"n8n": "^1.103.2", "n8n": "^1.103.2",

View File

@@ -556,7 +556,10 @@ declare module './mcp/server' {
} }
// Start if called directly // Start if called directly
if (require.main === module) { // Check if this file is being run directly (not imported)
// In ES modules, we check import.meta.url against process.argv[1]
// But since we're transpiling to CommonJS, we use the require.main check
if (typeof require !== 'undefined' && require.main === module) {
startFixedHTTPServer().catch(error => { startFixedHTTPServer().catch(error => {
logger.error('Failed to start Fixed HTTP server:', error); logger.error('Failed to start Fixed HTTP server:', error);
console.error('Failed to start Fixed HTTP server:', error); console.error('Failed to start Fixed HTTP server:', error);

View File

@@ -1,3 +1,4 @@
import { describe, it, expect, vi, beforeEach, afterEach, beforeAll } from 'vitest';
import { SingleSessionHTTPServer } from '../http-server-single-session'; import { SingleSessionHTTPServer } from '../http-server-single-session';
import express from 'express'; import express from 'express';
import { ConsoleManager } from '../utils/console-manager'; import { ConsoleManager } from '../utils/console-manager';

View File

@@ -1,3 +1,4 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { AuthManager } from '../src/utils/auth'; import { AuthManager } from '../src/utils/auth';
describe('AuthManager', () => { describe('AuthManager', () => {
@@ -28,7 +29,7 @@ describe('AuthManager', () => {
}); });
it('should reject expired tokens', () => { it('should reject expired tokens', () => {
jest.useFakeTimers(); vi.useFakeTimers();
const token = authManager.generateToken(1); // 1 hour expiry const token = authManager.generateToken(1); // 1 hour expiry
@@ -36,12 +37,12 @@ describe('AuthManager', () => {
expect(authManager.validateToken(token, 'expected-token')).toBe(true); expect(authManager.validateToken(token, 'expected-token')).toBe(true);
// Fast forward 2 hours // Fast forward 2 hours
jest.advanceTimersByTime(2 * 60 * 60 * 1000); vi.advanceTimersByTime(2 * 60 * 60 * 1000);
// Token should be expired // Token should be expired
expect(authManager.validateToken(token, 'expected-token')).toBe(false); expect(authManager.validateToken(token, 'expected-token')).toBe(false);
jest.useRealTimers(); vi.useRealTimers();
}); });
}); });
@@ -55,19 +56,19 @@ describe('AuthManager', () => {
}); });
it('should set custom expiry time', () => { it('should set custom expiry time', () => {
jest.useFakeTimers(); vi.useFakeTimers();
const token = authManager.generateToken(24); // 24 hours const token = authManager.generateToken(24); // 24 hours
// Token should be valid after 23 hours // Token should be valid after 23 hours
jest.advanceTimersByTime(23 * 60 * 60 * 1000); vi.advanceTimersByTime(23 * 60 * 60 * 1000);
expect(authManager.validateToken(token, 'expected')).toBe(true); expect(authManager.validateToken(token, 'expected')).toBe(true);
// Token should expire after 25 hours // Token should expire after 25 hours
jest.advanceTimersByTime(2 * 60 * 60 * 1000); vi.advanceTimersByTime(2 * 60 * 60 * 1000);
expect(authManager.validateToken(token, 'expected')).toBe(false); expect(authManager.validateToken(token, 'expected')).toBe(false);
jest.useRealTimers(); vi.useRealTimers();
}); });
}); });

View File

@@ -1,3 +1,4 @@
import { describe, it, expect } from 'vitest';
import { N8NMCPBridge } from '../src/utils/bridge'; import { N8NMCPBridge } from '../src/utils/bridge';
describe('N8NMCPBridge', () => { describe('N8NMCPBridge', () => {

View File

@@ -1,3 +1,4 @@
import { describe, it, expect, vi } from 'vitest';
import { import {
MCPError, MCPError,
N8NConnectionError, N8NConnectionError,
@@ -11,9 +12,9 @@ import {
import { logger } from '../src/utils/logger'; import { logger } from '../src/utils/logger';
// Mock the logger // Mock the logger
jest.mock('../src/utils/logger', () => ({ vi.mock('../src/utils/logger', () => ({
logger: { logger: {
error: jest.fn(), error: vi.fn(),
}, },
})); }));
@@ -158,7 +159,7 @@ describe('handleError', () => {
describe('withErrorHandling', () => { describe('withErrorHandling', () => {
it('should execute operation successfully', async () => { it('should execute operation successfully', async () => {
const operation = jest.fn().mockResolvedValue('success'); const operation = vi.fn().mockResolvedValue('success');
const result = await withErrorHandling(operation, 'test operation'); const result = await withErrorHandling(operation, 'test operation');
@@ -168,7 +169,7 @@ describe('withErrorHandling', () => {
it('should handle and log errors', async () => { it('should handle and log errors', async () => {
const error = new Error('Operation failed'); const error = new Error('Operation failed');
const operation = jest.fn().mockRejectedValue(error); const operation = vi.fn().mockRejectedValue(error);
await expect(withErrorHandling(operation, 'test operation')).rejects.toThrow(); await expect(withErrorHandling(operation, 'test operation')).rejects.toThrow();
@@ -177,7 +178,7 @@ describe('withErrorHandling', () => {
it('should transform errors using handleError', async () => { it('should transform errors using handleError', async () => {
const error = { code: 'ECONNREFUSED' }; const error = { code: 'ECONNREFUSED' };
const operation = jest.fn().mockRejectedValue(error); const operation = vi.fn().mockRejectedValue(error);
try { try {
await withErrorHandling(operation, 'test operation'); await withErrorHandling(operation, 'test operation');

View File

@@ -1,20 +1,25 @@
import { readFileSync, writeFileSync, mkdirSync, rmSync } from 'fs'; import { readFileSync, writeFileSync, mkdirSync, rmSync } from 'fs';
import { join } from 'path'; import { join } from 'path';
import { tmpdir } from 'os'; import { tmpdir } from 'os';
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import type { MockedFunction } from 'vitest';
// Import the actual functions we'll be testing
import { loadAuthToken, startFixedHTTPServer } from '../src/http-server';
// Mock dependencies // Mock dependencies
jest.mock('../src/utils/logger', () => ({ vi.mock('../src/utils/logger', () => ({
logger: { logger: {
info: jest.fn(), info: vi.fn(),
error: jest.fn(), error: vi.fn(),
warn: jest.fn(), warn: vi.fn(),
debug: jest.fn() debug: vi.fn()
}, },
Logger: jest.fn().mockImplementation(() => ({ Logger: vi.fn().mockImplementation(() => ({
info: jest.fn(), info: vi.fn(),
error: jest.fn(), error: vi.fn(),
warn: jest.fn(), warn: vi.fn(),
debug: jest.fn() debug: vi.fn()
})), })),
LogLevel: { LogLevel: {
ERROR: 0, ERROR: 0,
@@ -24,49 +29,68 @@ jest.mock('../src/utils/logger', () => ({
} }
})); }));
jest.mock('dotenv'); vi.mock('dotenv');
// Mock other dependencies to prevent side effects // Mock other dependencies to prevent side effects
jest.mock('../src/mcp/server', () => ({ vi.mock('../src/mcp/server', () => ({
N8NDocumentationMCPServer: jest.fn().mockImplementation(() => ({ N8NDocumentationMCPServer: vi.fn().mockImplementation(() => ({
executeTool: jest.fn() executeTool: vi.fn()
})) }))
})); }));
jest.mock('../src/mcp/tools', () => ({ vi.mock('../src/mcp/tools', () => ({
n8nDocumentationToolsFinal: [] n8nDocumentationToolsFinal: []
})); }));
jest.mock('../src/mcp/tools-n8n-manager', () => ({ vi.mock('../src/mcp/tools-n8n-manager', () => ({
n8nManagementTools: [] n8nManagementTools: []
})); }));
jest.mock('../src/utils/version', () => ({ vi.mock('../src/utils/version', () => ({
PROJECT_VERSION: '2.7.4' PROJECT_VERSION: '2.7.4'
})); }));
jest.mock('../src/config/n8n-api', () => ({ vi.mock('../src/config/n8n-api', () => ({
isN8nApiConfigured: jest.fn().mockReturnValue(false) isN8nApiConfigured: vi.fn().mockReturnValue(false)
})); }));
vi.mock('../src/utils/url-detector', () => ({
getStartupBaseUrl: vi.fn().mockReturnValue('http://localhost:3000'),
formatEndpointUrls: vi.fn().mockReturnValue({
health: 'http://localhost:3000/health',
mcp: 'http://localhost:3000/mcp'
}),
detectBaseUrl: vi.fn().mockReturnValue('http://localhost:3000')
}));
// Create mock server instance
const mockServer = {
on: vi.fn(),
close: vi.fn((callback) => callback())
};
// Mock Express to prevent server from starting // Mock Express to prevent server from starting
jest.mock('express', () => { const mockExpressApp = {
const mockApp = { use: vi.fn(),
use: jest.fn(), get: vi.fn(),
get: jest.fn(), post: vi.fn(),
post: jest.fn(), listen: vi.fn((port: any, host: any, callback: any) => {
listen: jest.fn().mockReturnValue({ // Call the callback immediately to simulate server start
on: jest.fn() if (callback) callback();
}) return mockServer;
}; }),
const express: any = jest.fn(() => mockApp); set: vi.fn()
express.json = jest.fn(); };
express.urlencoded = jest.fn();
express.static = jest.fn(); vi.mock('express', () => {
const express: any = vi.fn(() => mockExpressApp);
express.json = vi.fn();
express.urlencoded = vi.fn();
express.static = vi.fn();
express.Request = {}; express.Request = {};
express.Response = {}; express.Response = {};
express.NextFunction = {}; express.NextFunction = {};
return express; return { default: express };
}); });
describe('HTTP Server Authentication', () => { describe('HTTP Server Authentication', () => {
@@ -76,8 +100,8 @@ describe('HTTP Server Authentication', () => {
beforeEach(() => { beforeEach(() => {
// Reset modules and environment // Reset modules and environment
jest.clearAllMocks(); vi.clearAllMocks();
jest.resetModules(); vi.resetModules();
process.env = { ...originalEnv }; process.env = { ...originalEnv };
// Create temporary directory for test files // Create temporary directory for test files
@@ -99,26 +123,10 @@ describe('HTTP Server Authentication', () => {
}); });
describe('loadAuthToken', () => { describe('loadAuthToken', () => {
let loadAuthToken: () => string | null;
beforeEach(() => {
// Set a default token to prevent validateEnvironment from exiting
process.env.AUTH_TOKEN = 'test-token-for-module-load';
// Import the function after environment is set up
const httpServerModule = require('../src/http-server');
// Access the loadAuthToken function (we'll need to export it)
loadAuthToken = httpServerModule.loadAuthToken || (() => null);
});
it('should load token from AUTH_TOKEN environment variable', () => { it('should load token from AUTH_TOKEN environment variable', () => {
process.env.AUTH_TOKEN = 'test-token-from-env'; process.env.AUTH_TOKEN = 'test-token-from-env';
delete process.env.AUTH_TOKEN_FILE; delete process.env.AUTH_TOKEN_FILE;
// Re-import to get fresh module with new env
jest.resetModules();
const { loadAuthToken } = require('../src/http-server');
const token = loadAuthToken(); const token = loadAuthToken();
expect(token).toBe('test-token-from-env'); expect(token).toBe('test-token-from-env');
}); });
@@ -130,10 +138,6 @@ describe('HTTP Server Authentication', () => {
// Write test token to file // Write test token to file
writeFileSync(authTokenFile, 'test-token-from-file\n'); writeFileSync(authTokenFile, 'test-token-from-file\n');
// Re-import to get fresh module with new env
jest.resetModules();
const { loadAuthToken } = require('../src/http-server');
const token = loadAuthToken(); const token = loadAuthToken();
expect(token).toBe('test-token-from-file'); expect(token).toBe('test-token-from-file');
}); });
@@ -145,9 +149,6 @@ describe('HTTP Server Authentication', () => {
// Write token with whitespace // Write token with whitespace
writeFileSync(authTokenFile, ' test-token-with-spaces \n\n'); writeFileSync(authTokenFile, ' test-token-with-spaces \n\n');
jest.resetModules();
const { loadAuthToken } = require('../src/http-server');
const token = loadAuthToken(); const token = loadAuthToken();
expect(token).toBe('test-token-with-spaces'); expect(token).toBe('test-token-with-spaces');
}); });
@@ -157,28 +158,24 @@ describe('HTTP Server Authentication', () => {
process.env.AUTH_TOKEN_FILE = authTokenFile; process.env.AUTH_TOKEN_FILE = authTokenFile;
writeFileSync(authTokenFile, 'file-token'); writeFileSync(authTokenFile, 'file-token');
jest.resetModules();
const { loadAuthToken } = require('../src/http-server');
const token = loadAuthToken(); const token = loadAuthToken();
expect(token).toBe('env-token'); expect(token).toBe('env-token');
}); });
it('should return null when AUTH_TOKEN_FILE points to non-existent file', () => { it('should return null when AUTH_TOKEN_FILE points to non-existent file', async () => {
delete process.env.AUTH_TOKEN; delete process.env.AUTH_TOKEN;
process.env.AUTH_TOKEN_FILE = join(tempDir, 'non-existent-file'); process.env.AUTH_TOKEN_FILE = join(tempDir, 'non-existent-file');
jest.resetModules(); // Import logger to check calls
const { loadAuthToken } = require('../src/http-server'); const { logger } = await import('../src/utils/logger');
const { logger } = require('../src/utils/logger');
// Clear any previous mock calls // Clear any previous mock calls
jest.clearAllMocks(); vi.clearAllMocks();
const token = loadAuthToken(); const token = loadAuthToken();
expect(token).toBeNull(); expect(token).toBeNull();
expect(logger.error).toHaveBeenCalled(); expect(logger.error).toHaveBeenCalled();
const errorCall = logger.error.mock.calls[0]; const errorCall = (logger.error as MockedFunction<any>).mock.calls[0];
expect(errorCall[0]).toContain('Failed to read AUTH_TOKEN_FILE'); expect(errorCall[0]).toContain('Failed to read AUTH_TOKEN_FILE');
// Check that the second argument exists and is truthy (the error object) // Check that the second argument exists and is truthy (the error object)
expect(errorCall[1]).toBeTruthy(); expect(errorCall[1]).toBeTruthy();
@@ -188,9 +185,6 @@ describe('HTTP Server Authentication', () => {
delete process.env.AUTH_TOKEN; delete process.env.AUTH_TOKEN;
delete process.env.AUTH_TOKEN_FILE; delete process.env.AUTH_TOKEN_FILE;
jest.resetModules();
const { loadAuthToken } = require('../src/http-server');
const token = loadAuthToken(); const token = loadAuthToken();
expect(token).toBeNull(); expect(token).toBeNull();
}); });
@@ -201,13 +195,10 @@ describe('HTTP Server Authentication', () => {
delete process.env.AUTH_TOKEN; delete process.env.AUTH_TOKEN;
delete process.env.AUTH_TOKEN_FILE; delete process.env.AUTH_TOKEN_FILE;
const mockExit = jest.spyOn(process, 'exit').mockImplementation((code?: string | number | null | undefined) => { const mockExit = vi.spyOn(process, 'exit').mockImplementation((code?: string | number | null | undefined) => {
throw new Error('Process exited'); throw new Error('Process exited');
}); });
jest.resetModules();
const { startFixedHTTPServer } = require('../src/http-server');
// validateEnvironment is called when starting the server // validateEnvironment is called when starting the server
await expect(async () => { await expect(async () => {
await startFixedHTTPServer(); await startFixedHTTPServer();
@@ -220,28 +211,15 @@ describe('HTTP Server Authentication', () => {
it('should warn when token is less than 32 characters', async () => { it('should warn when token is less than 32 characters', async () => {
process.env.AUTH_TOKEN = 'short-token'; process.env.AUTH_TOKEN = 'short-token';
// Mock express to prevent actual server start // Import logger to check calls
const mockListen = jest.fn().mockReturnValue({ on: jest.fn() }); const { logger } = await import('../src/utils/logger');
jest.doMock('express', () => {
const mockApp = {
use: jest.fn(),
get: jest.fn(),
post: jest.fn(),
listen: mockListen,
set: jest.fn()
};
const express: any = jest.fn(() => mockApp);
express.json = jest.fn();
express.urlencoded = jest.fn();
express.static = jest.fn();
return express;
});
jest.resetModules();
jest.clearAllMocks();
const { startFixedHTTPServer } = require('../src/http-server'); // Clear any previous mock calls
const { logger } = require('../src/utils/logger'); vi.clearAllMocks();
// Ensure the mock server is properly configured
mockExpressApp.listen.mockReturnValue(mockServer);
mockServer.on.mockReturnValue(undefined);
// Start the server which will trigger validateEnvironment // Start the server which will trigger validateEnvironment
await startFixedHTTPServer(); await startFixedHTTPServer();
@@ -261,9 +239,6 @@ describe('HTTP Server Authentication', () => {
process.env.AUTH_TOKEN_FILE = authTokenFile; process.env.AUTH_TOKEN_FILE = authTokenFile;
delete process.env.AUTH_TOKEN; delete process.env.AUTH_TOKEN;
jest.resetModules();
const { loadAuthToken } = require('../src/http-server');
const token = loadAuthToken(); const token = loadAuthToken();
expect(token).toBe('very-secure-token-with-more-than-32-characters'); expect(token).toBe('very-secure-token-with-more-than-32-characters');
}); });
@@ -277,9 +252,6 @@ describe('HTTP Server Authentication', () => {
process.env.AUTH_TOKEN_FILE = dockerSecretPath; process.env.AUTH_TOKEN_FILE = dockerSecretPath;
delete process.env.AUTH_TOKEN; delete process.env.AUTH_TOKEN;
jest.resetModules();
const { loadAuthToken } = require('../src/http-server');
const token = loadAuthToken(); const token = loadAuthToken();
expect(token).toBe('docker-secret-token'); expect(token).toBe('docker-secret-token');
}); });

View File

@@ -42,8 +42,8 @@ async function testAIAgentExtraction() {
console.log('2. Listing available tools...'); console.log('2. Listing available tools...');
const toolsResponse = await client.request( const toolsResponse = await client.request(
{ method: 'tools/list' }, { method: 'tools/list' },
{} {} as any
); ) as any;
console.log(`✓ Found ${toolsResponse.tools.length} tools`); console.log(`✓ Found ${toolsResponse.tools.length} tools`);
const hasNodeSourceTool = toolsResponse.tools.some( const hasNodeSourceTool = toolsResponse.tools.some(
@@ -63,8 +63,8 @@ async function testAIAgentExtraction() {
}, },
}, },
}, },
{} {} as any
); ) as any;
console.log(`✓ Found nodes matching 'agent':`); console.log(`✓ Found nodes matching 'agent':`);
const content = JSON.parse(listNodesResponse.content[0].text); const content = JSON.parse(listNodesResponse.content[0].text);
content.nodes.forEach((node: any) => { content.nodes.forEach((node: any) => {
@@ -85,8 +85,8 @@ async function testAIAgentExtraction() {
}, },
}, },
}, },
{} {} as any
); ) as any;
const result = JSON.parse(aiAgentResponse.content[0].text); const result = JSON.parse(aiAgentResponse.content[0].text);
console.log('✓ Successfully extracted AI Agent node:'); console.log('✓ Successfully extracted AI Agent node:');
@@ -114,8 +114,8 @@ async function testAIAgentExtraction() {
uri: 'nodes://source/@n8n/n8n-nodes-langchain.Agent', uri: 'nodes://source/@n8n/n8n-nodes-langchain.Agent',
}, },
}, },
{} {} as any
); ) as any;
console.log('✓ Successfully read node source via resource endpoint\n'); console.log('✓ Successfully read node source via resource endpoint\n');
console.log('=== Test Completed Successfully ==='); console.log('=== Test Completed Successfully ===');

View File

@@ -1,20 +1,21 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { Logger, LogLevel } from '../src/utils/logger'; import { Logger, LogLevel } from '../src/utils/logger';
describe('Logger', () => { describe('Logger', () => {
let logger: Logger; let logger: Logger;
let consoleErrorSpy: jest.SpyInstance; let consoleErrorSpy: ReturnType<typeof vi.spyOn>;
let consoleWarnSpy: jest.SpyInstance; let consoleWarnSpy: ReturnType<typeof vi.spyOn>;
let consoleLogSpy: jest.SpyInstance; let consoleLogSpy: ReturnType<typeof vi.spyOn>;
beforeEach(() => { beforeEach(() => {
logger = new Logger({ timestamp: false, prefix: 'test' }); logger = new Logger({ timestamp: false, prefix: 'test' });
consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(); consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(); consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(); consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
}); });
afterEach(() => { afterEach(() => {
jest.restoreAllMocks(); vi.restoreAllMocks();
}); });
describe('log levels', () => { describe('log levels', () => {
@@ -80,7 +81,7 @@ describe('Logger', () => {
it('should include timestamp when enabled', () => { it('should include timestamp when enabled', () => {
const timestampLogger = new Logger({ timestamp: true, prefix: 'test' }); const timestampLogger = new Logger({ timestamp: true, prefix: 'test' });
const dateSpy = jest.spyOn(Date.prototype, 'toISOString').mockReturnValue('2024-01-01T00:00:00.000Z'); const dateSpy = vi.spyOn(Date.prototype, 'toISOString').mockReturnValue('2024-01-01T00:00:00.000Z');
timestampLogger.info('test message'); timestampLogger.info('test message');

View File

@@ -0,0 +1,26 @@
import { beforeEach, afterEach, vi } from 'vitest';
// Reset mocks between tests
beforeEach(() => {
vi.clearAllMocks();
});
// Clean up after each test
afterEach(() => {
vi.restoreAllMocks();
});
// Global test timeout
vi.setConfig({ testTimeout: 10000 });
// Silence console during tests unless DEBUG=true
if (process.env.DEBUG !== 'true') {
global.console = {
...console,
log: vi.fn(),
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
};
}

8
tsconfig.build.json Normal file
View File

@@ -0,0 +1,8 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"rootDir": "./src"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts", "tests"]
}

View File

@@ -3,8 +3,9 @@
"target": "ES2020", "target": "ES2020",
"module": "commonjs", "module": "commonjs",
"lib": ["ES2020"], "lib": ["ES2020"],
"types": ["node", "vitest/globals"],
"outDir": "./dist", "outDir": "./dist",
"rootDir": "./src", "rootDir": "./",
"strict": true, "strict": true,
"esModuleInterop": true, "esModuleInterop": true,
"skipLibCheck": true, "skipLibCheck": true,
@@ -25,8 +26,14 @@
"noUnusedParameters": false, "noUnusedParameters": false,
"noImplicitReturns": true, "noImplicitReturns": true,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"moduleResolution": "node" "moduleResolution": "node",
"allowJs": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@tests/*": ["tests/*"]
}
}, },
"include": ["src/**/*"], "include": ["src/**/*", "tests/**/*", "vitest.config.ts", "types/**/*"],
"exclude": ["node_modules", "dist", "**/*.test.ts"] "exclude": ["node_modules", "dist"]
} }

35
types/mcp.d.ts vendored Normal file
View File

@@ -0,0 +1,35 @@
// Type declarations for MCP SDK responses
declare module '@modelcontextprotocol/sdk/client/index.js' {
export * from '@modelcontextprotocol/sdk/client/index';
export interface ToolsListResponse {
tools: Array<{
name: string;
description?: string;
inputSchema?: any;
}>;
}
export interface CallToolResponse {
content: Array<{
type: string;
text?: string;
}>;
}
}
declare module '@modelcontextprotocol/sdk/server/index.js' {
export * from '@modelcontextprotocol/sdk/server/index';
}
declare module '@modelcontextprotocol/sdk/server/stdio.js' {
export * from '@modelcontextprotocol/sdk/server/stdio';
}
declare module '@modelcontextprotocol/sdk/client/stdio.js' {
export * from '@modelcontextprotocol/sdk/client/stdio';
}
declare module '@modelcontextprotocol/sdk/types.js' {
export * from '@modelcontextprotocol/sdk/types';
}

34
vitest.config.ts Normal file
View File

@@ -0,0 +1,34 @@
import { defineConfig } from 'vitest/config';
import path from 'path';
export default defineConfig({
test: {
globals: true,
environment: 'node',
setupFiles: ['./tests/setup/global-setup.ts'],
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html', 'lcov'],
exclude: [
'node_modules/',
'tests/',
'**/*.d.ts',
'**/*.test.ts',
'scripts/',
'dist/'
],
thresholds: {
lines: 80,
functions: 80,
branches: 75,
statements: 80
}
}
},
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
'@tests': path.resolve(__dirname, './tests')
}
}
});