/**
* Batería de tests para verificar el funcionamiento de las herramientas MCP
* Sistema CV Recruitment Assistant - Brayan Smith Cordova Tasayco
*/
const candidateData = require('../src/candidate-data.json');
const { generateKeyPairSync, createHash, sign, verify } = require('node:crypto');
// Generar claves para testing
const { publicKey, privateKey } = generateKeyPairSync('rsa', {
modulusLength: 2048,
publicKeyEncoding: { type: 'spki', format: 'pem' },
privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
});
// Funciones de firma digital para testing
function createDataHash(data) {
const jsonString = JSON.stringify(data, null, 0);
return createHash('sha256').update(jsonString).digest('hex');
}
function signHash(hash, privateKey) {
const signature = sign('sha256', Buffer.from(hash, 'hex'), {
key: privateKey,
format: 'pem'
});
return signature.toString('base64');
}
function signResponse(data) {
const hash = createDataHash(data);
const signature = signHash(hash, privateKey);
return {
data,
hash,
signature,
timestamp: new Date().toISOString(),
publicKey,
serverId: 'test-cv-server',
version: '1.0'
};
}
function verifySignedResponse(signedResponse) {
try {
if (!signedResponse.data || !signedResponse.hash || !signedResponse.signature) {
return {
isValid: false,
error: 'Respuesta firmada incompleta',
details: { hashMatch: false, signatureValid: false }
};
}
const computedHash = createDataHash(signedResponse.data);
const hashMatch = computedHash === signedResponse.hash;
const signatureValid = verify(
'sha256',
Buffer.from(signedResponse.hash, 'hex'),
{ key: signedResponse.publicKey, format: 'pem' },
Buffer.from(signedResponse.signature, 'base64')
);
return {
isValid: hashMatch && signatureValid,
error: (hashMatch && signatureValid) ? undefined : 'Hash o firma inválidos',
details: { hashMatch, signatureValid }
};
} catch (error) {
return {
isValid: false,
error: `Error durante verificación: ${error}`,
details: { hashMatch: false, signatureValid: false }
};
}
}
// Mock del servidor MCP
class TestMCPServer {
constructor() {
this.tools = new Map();
}
tool(name, schema, handler) {
this.tools.set(name, { schema, handler });
}
async callTool(name, params = {}) {
const tool = this.tools.get(name);
if (!tool) {
throw new Error(`Tool ${name} not found`);
}
return await tool.handler(params);
}
}
// Clase de test con todas las herramientas MCP
class TestMCPAgent {
constructor() {
this.server = new TestMCPServer();
}
async init() {
// === CORE CANDIDATE INFORMATION TOOLS ===
// Get candidate's personal profile and summary
this.server.tool("get_candidate_profile", {}, async () => {
const data = {
name: candidateData.personal.fullName,
location: candidateData.personal.location,
contact: {
phone: candidateData.personal.phone,
linkedin: candidateData.personal.linkedin,
portfolio: candidateData.personal.portfolio,
},
professionalSummary: candidateData.personal.professionalSummary,
totalExperience: candidateData.experienceYears.totalExperience,
};
const signedResponse = signResponse(data);
return {
content: [{
type: "text",
text: JSON.stringify(signedResponse, null, 2),
}],
};
});
// Get detailed work experience
this.server.tool("get_work_experience", {}, async () => ({
content: [{
type: "text",
text: JSON.stringify(candidateData.workExperience, null, 2),
}],
}));
// Get education background
this.server.tool("get_education_background", {}, async () => ({
content: [{
type: "text",
text: JSON.stringify(candidateData.education, null, 2),
}],
}));
// Get certifications
this.server.tool("get_certifications", {}, async () => ({
content: [{
type: "text",
text: JSON.stringify(candidateData.certifications, null, 2),
}],
}));
// Get technical skills categorized
this.server.tool("get_technical_skills", {}, async () => ({
content: [{
type: "text",
text: JSON.stringify({
technical: candidateData.skills.technical,
design: candidateData.skills.design,
productivity: candidateData.skills.productivity,
}, null, 2),
}],
}));
// Get soft skills and competencies
this.server.tool("get_soft_skills", {}, async () => ({
content: [{
type: "text",
text: JSON.stringify({
softSkills: candidateData.skills.soft,
languages: candidateData.languages,
}, null, 2),
}],
}));
// Get languages and interests
this.server.tool("get_languages_interests", {}, async () => ({
content: [{
type: "text",
text: JSON.stringify({
languages: candidateData.languages,
interests: candidateData.interests,
}, null, 2),
}],
}));
// Search candidate information by keywords
this.server.tool("search_candidate_info", {}, async ({ keyword }) => {
const searchTerm = keyword.toLowerCase();
const results = [];
// Search in all text fields
const searchableData = JSON.stringify(candidateData).toLowerCase();
if (searchableData.includes(searchTerm)) {
// Search in specific sections
if (JSON.stringify(candidateData.skills.technical).toLowerCase().includes(searchTerm)) {
results.push({ section: "Technical Skills", match: "Found in technical skills" });
}
if (JSON.stringify(candidateData.workExperience).toLowerCase().includes(searchTerm)) {
results.push({ section: "Work Experience", match: "Found in work experience" });
}
if (JSON.stringify(candidateData.skills.soft).toLowerCase().includes(searchTerm)) {
results.push({ section: "Soft Skills", match: "Found in soft skills" });
}
if (JSON.stringify(candidateData.interests).toLowerCase().includes(searchTerm)) {
results.push({ section: "Interests", match: "Found in interests" });
}
}
return {
content: [{
type: "text",
text: results.length > 0
? JSON.stringify({ keyword: keyword, results: results }, null, 2)
: `No matches found for "${keyword}"`
}]
};
});
// === ONLINE PRESENCE TOOLS ===
// Get candidate's portfolio/personal website
this.server.tool("get_portfolio_website", {}, async () => ({
content: [{
type: "text",
text: JSON.stringify({
portfolio: candidateData.personal.portfolio,
description: "Portfolio profesional con proyectos y experiencia",
status: "available",
note: "Sitio web personal donde se muestran proyectos y experiencia profesional"
}, null, 2),
}],
}));
// Get candidate's GitHub profile information
this.server.tool("get_github_profile", {}, async () => {
const githubCertifications = candidateData.certifications.filter(cert =>
cert.name.toLowerCase().includes('github') || cert.issuer.toLowerCase().includes('github')
);
return {
content: [{
type: "text",
text: JSON.stringify({
github_certifications: githubCertifications,
profile_url: "not_specified_in_cv",
note: "Candidato certificado en GitHub pero URL específica del perfil no disponible en CV",
skills_related: candidateData.skills.technical.tools.filter(tool =>
tool.toLowerCase().includes('git')
)
}, null, 2),
}],
};
});
// Get candidate's social networks and online presence
this.server.tool("get_social_networks", {}, async () => ({
content: [{
type: "text",
text: JSON.stringify({
available: {
linkedin: candidateData.personal.linkedin,
portfolio: candidateData.personal.portfolio
},
interests: candidateData.interests.filter(interest =>
interest.toLowerCase().includes('red') ||
interest.toLowerCase().includes('social') ||
interest.toLowerCase().includes('conexión')
),
note: "LinkedIn y portfolio son las principales redes profesionales especificadas en CV",
contact_preferences: "Disponible para conexión profesional a través de LinkedIn"
}, null, 2),
}],
}));
// === ADVANCED ANALYSIS TOOLS ===
// Evaluate tech stack compatibility
this.server.tool("evaluate_tech_stack", {}, async ({ requiredTechnologies }) => {
const candidateTechStack = [
...candidateData.skills.technical.programming,
...candidateData.skills.technical.frameworks,
...candidateData.skills.technical.styling,
...candidateData.skills.technical.databases,
...candidateData.skills.technical.tools,
...candidateData.skills.technical.deployment,
];
const matches = requiredTechnologies.filter((tech) =>
candidateTechStack.some((candidateTech) =>
candidateTech.toLowerCase().includes(tech.toLowerCase()) ||
tech.toLowerCase().includes(candidateTech.toLowerCase()),
),
);
const missing = requiredTechnologies.filter((tech) =>
!candidateTechStack.some((candidateTech) =>
candidateTech.toLowerCase().includes(tech.toLowerCase()) ||
tech.toLowerCase().includes(candidateTech.toLowerCase()),
),
);
const compatibilityScore = (matches.length / requiredTechnologies.length) * 100;
return {
content: [{
type: "text",
text: JSON.stringify({
compatibilityScore: `${compatibilityScore.toFixed(1)}%`,
matchingTechnologies: matches,
missingTechnologies: missing,
candidateStrengths: candidateTechStack,
}, null, 2),
}],
};
});
// Assess leadership experience
this.server.tool("assess_leadership_experience", {}, async () => {
const leadershipExperience = candidateData.workExperience.filter((exp) =>
exp.responsibilities.includes("Team Leadership") ||
exp.position.toLowerCase().includes("lead"),
);
const leadershipSkills = candidateData.skills.soft.filter((skill) =>
skill.toLowerCase().includes("liderazgo") ||
skill.toLowerCase().includes("equipo") ||
skill.toLowerCase().includes("gestión"),
);
return {
content: [{
type: "text",
text: JSON.stringify({
hasLeadershipExperience: leadershipExperience.length > 0,
leadershipRoles: leadershipExperience,
leadershipDuration: candidateData.experienceYears.leadership,
relevantSoftSkills: leadershipSkills,
leadershipAchievements: leadershipExperience.flatMap((exp) => exp.achievements),
}, null, 2),
}],
};
});
// Calculate experience years by technology
this.server.tool("calculate_experience_years", {}, async ({ technology }) => {
const techLower = technology.toLowerCase();
let experienceInfo = "No specific experience found";
// Check predefined experience years
const experienceYearsMap = candidateData.experienceYears;
if (experienceYearsMap[techLower]) {
experienceInfo = experienceYearsMap[techLower];
} else {
// Check if technology is in skill set
const allTechSkills = [
...candidateData.skills.technical.programming,
...candidateData.skills.technical.frameworks,
...candidateData.skills.technical.styling,
...candidateData.skills.technical.tools,
];
const hasTech = allTechSkills.some((skill) =>
skill.toLowerCase().includes(techLower),
);
if (hasTech) {
experienceInfo = "Present in skill set - likely 2+ years based on academic training since 2022";
}
}
return {
content: [{
type: "text",
text: JSON.stringify({
technology: technology,
experience: experienceInfo,
isCore: candidateData.skills.technical.programming.some((skill) =>
skill.toLowerCase().includes(techLower),
) ||
candidateData.skills.technical.frameworks.some((skill) =>
skill.toLowerCase().includes(techLower),
),
}, null, 2),
}],
};
});
// Match job requirements
this.server.tool("match_job_requirements", {}, async ({ jobTitle, requiredSkills, preferredSkills = [] }) => {
const allCandidateSkills = [
...candidateData.skills.technical.programming,
...candidateData.skills.technical.frameworks,
...candidateData.skills.technical.styling,
...candidateData.skills.technical.tools,
...candidateData.skills.soft,
];
const requiredMatches = requiredSkills.filter((skill) =>
allCandidateSkills.some((candidateSkill) =>
candidateSkill.toLowerCase().includes(skill.toLowerCase()) ||
skill.toLowerCase().includes(candidateSkill.toLowerCase()),
),
);
const preferredMatches = preferredSkills.filter((skill) =>
allCandidateSkills.some((candidateSkill) =>
candidateSkill.toLowerCase().includes(skill.toLowerCase()) ||
skill.toLowerCase().includes(candidateSkill.toLowerCase()),
),
);
const requiredScore = (requiredMatches.length / requiredSkills.length) * 100;
const preferredScore = preferredSkills.length > 0
? (preferredMatches.length / preferredSkills.length) * 100
: 0;
return {
content: [{
type: "text",
text: JSON.stringify({
jobTitle: jobTitle,
matchAnalysis: {
requiredSkillsMatch: `${requiredScore.toFixed(1)}%`,
preferredSkillsMatch: `${preferredScore.toFixed(1)}%`,
overallFit: requiredScore >= 70 ? "High" : requiredScore >= 50 ? "Medium" : "Low",
},
skillsMatched: {
required: requiredMatches,
preferred: preferredMatches,
},
skillsGap: {
required: requiredSkills.filter((skill) => !requiredMatches.includes(skill)),
preferred: preferredSkills.filter((skill) => !preferredMatches.includes(skill)),
},
candidateStrengths: {
leadership: candidateData.experienceYears.leadership,
frontend: candidateData.experienceYears.frontend,
totalExperience: candidateData.experienceYears.totalExperience,
},
}, null, 2),
}],
};
});
// === DEBUGGING AND VERIFICATION TOOL ===
this.server.tool("verify_response_signature", { signedResponse: {} }, async ({ signedResponse }) => {
try {
const parsedResponse = JSON.parse(signedResponse);
const verificationResult = verifySignedResponse(parsedResponse);
const data = {
verificationStatus: verificationResult.isValid ? "VALID" : "INVALID",
result: verificationResult,
verifiedBy: 'test-cv-server',
verificationTimestamp: new Date().toISOString(),
};
const signedVerificationResult = signResponse(data);
return {
content: [{
type: "text",
text: JSON.stringify(signedVerificationResult, null, 2),
}],
};
} catch (error) {
const errorData = {
verificationStatus: "ERROR",
error: `Failed to parse or verify response: ${error}`,
verifiedBy: 'test-cv-server',
verificationTimestamp: new Date().toISOString(),
};
const signedErrorResult = signResponse(errorData);
return {
content: [{
type: "text",
text: JSON.stringify(signedErrorResult, null, 2),
}],
};
}
});
this.server.tool("get_server_public_key", {}, async () => {
const data = {
publicKey: publicKey,
serverId: 'test-cv-server',
purpose: "Use this public key to verify signatures from this MCP server",
algorithm: "RSA-SHA256",
keyFormat: "PEM",
};
const signedResponse = signResponse(data);
return {
content: [{
type: "text",
text: JSON.stringify(signedResponse, null, 2),
}],
};
});
}
}
// === TESTS ===
async function runTests() {
console.log('🧪 Iniciando batería de tests para CV MCP Tools...\n');
const agent = new TestMCPAgent();
await agent.init();
let passedTests = 0;
let totalTests = 0;
// Helper para ejecutar tests
async function test(testName, testFn) {
totalTests++;
try {
await testFn();
console.log(`✅ ${testName}`);
passedTests++;
} catch (error) {
console.log(`❌ ${testName}: ${error.message}`);
}
}
// === TESTS CORE TOOLS ===
console.log('📋 Testing Core Information Tools...');
await test('get_candidate_profile should return candidate profile', async () => {
const result = await agent.server.callTool('get_candidate_profile');
const signedData = JSON.parse(result.content[0].text);
// Verify it's a signed response
if (!signedData.data || !signedData.hash || !signedData.signature) {
throw new Error('Response should be digitally signed');
}
const data = signedData.data;
if (!data.name || data.name !== "Brayan Smith Cordova Tasayco") {
throw new Error('Profile name incorrect');
}
if (!data.contact || !data.contact.linkedin) {
throw new Error('Contact information missing');
}
});
await test('get_work_experience should return work history', async () => {
const result = await agent.server.callTool('get_work_experience');
const data = JSON.parse(result.content[0].text);
if (!Array.isArray(data) || data.length === 0) {
throw new Error('Work experience should be non-empty array');
}
if (!data[0].company || !data[0].position) {
throw new Error('Work experience missing required fields');
}
});
await test('get_education_background should return education', async () => {
const result = await agent.server.callTool('get_education_background');
const data = JSON.parse(result.content[0].text);
if (!Array.isArray(data) || data.length === 0) {
throw new Error('Education should be non-empty array');
}
if (!data[0].institution || !data[0].degree) {
throw new Error('Education missing required fields');
}
});
await test('get_certifications should return certifications', async () => {
const result = await agent.server.callTool('get_certifications');
const data = JSON.parse(result.content[0].text);
if (!Array.isArray(data) || data.length === 0) {
throw new Error('Certifications should be non-empty array');
}
if (!data[0].name || !data[0].issuer) {
throw new Error('Certifications missing required fields');
}
});
await test('get_technical_skills should return categorized skills', async () => {
const result = await agent.server.callTool('get_technical_skills');
const data = JSON.parse(result.content[0].text);
if (!data.technical || !data.technical.programming) {
throw new Error('Technical skills missing programming category');
}
if (!Array.isArray(data.technical.programming) || data.technical.programming.length === 0) {
throw new Error('Programming skills should be non-empty array');
}
});
await test('get_soft_skills should return soft skills and languages', async () => {
const result = await agent.server.callTool('get_soft_skills');
const data = JSON.parse(result.content[0].text);
if (!data.softSkills || !Array.isArray(data.softSkills)) {
throw new Error('Soft skills should be array');
}
if (!data.languages || !Array.isArray(data.languages)) {
throw new Error('Languages should be array');
}
});
await test('get_languages_interests should return languages and interests', async () => {
const result = await agent.server.callTool('get_languages_interests');
const data = JSON.parse(result.content[0].text);
if (!data.languages || !data.interests) {
throw new Error('Missing languages or interests');
}
if (!Array.isArray(data.interests) || data.interests.length === 0) {
throw new Error('Interests should be non-empty array');
}
});
await test('search_candidate_info should find React', async () => {
const result = await agent.server.callTool('search_candidate_info', { keyword: 'React' });
const text = result.content[0].text;
if (text.includes('No matches found')) {
throw new Error('Should find React in candidate data');
}
const data = JSON.parse(text);
if (!data.results || data.results.length === 0) {
throw new Error('Search results should not be empty');
}
});
// === TESTS ONLINE PRESENCE TOOLS ===
console.log('\n🌐 Testing Online Presence Tools...');
await test('get_portfolio_website should return portfolio URL and info', async () => {
const result = await agent.server.callTool('get_portfolio_website');
const data = JSON.parse(result.content[0].text);
if (!data.portfolio || !data.portfolio.includes('https://')) {
throw new Error('Portfolio should contain valid URL');
}
if (!data.description || !data.status) {
throw new Error('Portfolio info missing description or status');
}
if (data.status !== 'available') {
throw new Error('Portfolio status should be available');
}
});
await test('get_github_profile should return GitHub certifications and note', async () => {
const result = await agent.server.callTool('get_github_profile');
const data = JSON.parse(result.content[0].text);
if (!data.github_certifications || !Array.isArray(data.github_certifications)) {
throw new Error('GitHub certifications should be array');
}
if (data.profile_url !== 'not_specified_in_cv') {
throw new Error('Should indicate URL not specified in CV');
}
if (!data.note || !data.note.includes('certificado')) {
throw new Error('Should include explanatory note about certification');
}
// Should find GitHub Foundation certification
const hasGithubCert = data.github_certifications.some(cert =>
cert.name.toLowerCase().includes('github')
);
if (!hasGithubCert) {
throw new Error('Should find GitHub certification');
}
});
await test('get_social_networks should return available networks and interests', async () => {
const result = await agent.server.callTool('get_social_networks');
const data = JSON.parse(result.content[0].text);
if (!data.available || !data.available.linkedin) {
throw new Error('Should include LinkedIn in available networks');
}
if (!data.available.portfolio) {
throw new Error('Should include portfolio in available networks');
}
if (!data.interests || !Array.isArray(data.interests)) {
throw new Error('Interests should be array');
}
if (!data.note || !data.contact_preferences) {
throw new Error('Should include explanatory note and contact preferences');
}
// Should find social-related interests
const hasSocialInterest = data.interests.some(interest =>
interest.toLowerCase().includes('social') ||
interest.toLowerCase().includes('red') ||
interest.toLowerCase().includes('conexión')
);
if (!hasSocialInterest) {
throw new Error('Should find social-related interests');
}
});
// === TESTS ADVANCED ANALYSIS TOOLS ===
console.log('\n🔍 Testing Advanced Analysis Tools...');
await test('evaluate_tech_stack should calculate compatibility score', async () => {
const result = await agent.server.callTool('evaluate_tech_stack', {
requiredTechnologies: ['React', 'JavaScript', 'TypeScript', 'Python']
});
const data = JSON.parse(result.content[0].text);
if (!data.compatibilityScore) {
throw new Error('Missing compatibility score');
}
if (!data.matchingTechnologies || !data.missingTechnologies) {
throw new Error('Missing matching/missing technologies');
}
// Should match React, JavaScript, TypeScript but not Python
if (data.matchingTechnologies.length < 2) {
throw new Error('Should find at least 2 matching technologies');
}
});
await test('assess_leadership_experience should identify leadership', async () => {
const result = await agent.server.callTool('assess_leadership_experience');
const data = JSON.parse(result.content[0].text);
if (typeof data.hasLeadershipExperience !== 'boolean') {
throw new Error('Missing hasLeadershipExperience boolean');
}
if (!data.leadershipDuration) {
throw new Error('Missing leadership duration');
}
// Should find leadership experience (Lead Frontend developer position)
if (!data.hasLeadershipExperience) {
throw new Error('Should identify leadership experience');
}
});
await test('calculate_experience_years should return React experience', async () => {
const result = await agent.server.callTool('calculate_experience_years', {
technology: 'React'
});
const data = JSON.parse(result.content[0].text);
if (data.technology !== 'React') {
throw new Error('Technology name should match input');
}
if (!data.experience || data.experience === 'No specific experience found') {
throw new Error('Should find React experience');
}
});
await test('calculate_experience_years should handle unknown technology', async () => {
const result = await agent.server.callTool('calculate_experience_years', {
technology: 'Rust'
});
const data = JSON.parse(result.content[0].text);
if (data.experience !== 'No specific experience found') {
throw new Error('Should return no experience for unknown tech');
}
});
await test('match_job_requirements should calculate job fit', async () => {
const result = await agent.server.callTool('match_job_requirements', {
jobTitle: 'Frontend Developer',
requiredSkills: ['React', 'JavaScript', 'CSS'],
preferredSkills: ['TypeScript', 'Next.js']
});
const data = JSON.parse(result.content[0].text);
if (!data.matchAnalysis || !data.matchAnalysis.overallFit) {
throw new Error('Missing match analysis');
}
if (!data.skillsMatched || !data.skillsGap) {
throw new Error('Missing skills matched/gap analysis');
}
// Should have high compatibility for frontend role
if (data.matchAnalysis.overallFit === 'Low') {
throw new Error('Should have higher fit for frontend developer role');
}
});
// === TESTS EDGE CASES ===
console.log('\n🚨 Testing Edge Cases...');
await test('search_candidate_info should handle non-existent keyword', async () => {
const result = await agent.server.callTool('search_candidate_info', { keyword: 'blockchain' });
const text = result.content[0].text;
if (!text.includes('No matches found')) {
throw new Error('Should return no matches for non-existent keyword');
}
});
await test('evaluate_tech_stack should handle empty requirements', async () => {
const result = await agent.server.callTool('evaluate_tech_stack', {
requiredTechnologies: []
});
const data = JSON.parse(result.content[0].text);
// Should handle edge case gracefully (might cause division by zero)
if (!data.compatibilityScore) {
throw new Error('Should handle empty requirements gracefully');
}
});
// === RESULTS ===
console.log(`\n📊 Resultados del Testing:`);
console.log(`✅ Tests pasados: ${passedTests}/${totalTests}`);
console.log(`📈 Porcentaje de éxito: ${((passedTests/totalTests) * 100).toFixed(1)}%`);
if (passedTests === totalTests) {
console.log('\n🎉 ¡Todos los tests pasaron exitosamente!');
console.log('✨ El sistema MCP está funcionando correctamente.');
} else {
console.log(`\n⚠️ ${totalTests - passedTests} tests fallaron.`);
console.log('🔧 Revisa los errores anteriores para corregir los problemas.');
}
console.log('\n🏁 Testing completo.');
}
// Ejecutar tests si se ejecuta directamente
if (require.main === module) {
runTests().catch(console.error);
}
module.exports = { TestMCPAgent, runTests };