mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-03-30 13:43:26 +08:00
267 lines
8.5 KiB
JavaScript
267 lines
8.5 KiB
JavaScript
/**
|
|
* Tests for scripts/hooks/mcp-health-check.js
|
|
*
|
|
* Run with: node tests/hooks/mcp-health-check.test.js
|
|
*/
|
|
|
|
const assert = require('assert');
|
|
const fs = require('fs');
|
|
const os = require('os');
|
|
const path = require('path');
|
|
const { spawnSync } = require('child_process');
|
|
|
|
const script = path.join(__dirname, '..', '..', 'scripts', 'hooks', 'mcp-health-check.js');
|
|
|
|
function test(name, fn) {
|
|
try {
|
|
fn();
|
|
console.log(` ✓ ${name}`);
|
|
return true;
|
|
} catch (err) {
|
|
console.log(` ✗ ${name}`);
|
|
console.log(` Error: ${err.message}`);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async function asyncTest(name, fn) {
|
|
try {
|
|
await fn();
|
|
console.log(` ✓ ${name}`);
|
|
return true;
|
|
} catch (err) {
|
|
console.log(` ✗ ${name}`);
|
|
console.log(` Error: ${err.message}`);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function createTempDir() {
|
|
return fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-mcp-health-'));
|
|
}
|
|
|
|
function cleanupTempDir(dirPath) {
|
|
fs.rmSync(dirPath, { recursive: true, force: true });
|
|
}
|
|
|
|
function writeConfig(configPath, body) {
|
|
fs.writeFileSync(configPath, JSON.stringify(body, null, 2));
|
|
}
|
|
|
|
function readState(statePath) {
|
|
return JSON.parse(fs.readFileSync(statePath, 'utf8'));
|
|
}
|
|
|
|
function createCommandConfig(scriptPath) {
|
|
return {
|
|
command: process.execPath,
|
|
args: [scriptPath]
|
|
};
|
|
}
|
|
|
|
function runHook(input, env = {}) {
|
|
const result = spawnSync('node', [script], {
|
|
input: JSON.stringify(input),
|
|
encoding: 'utf8',
|
|
env: {
|
|
...process.env,
|
|
ECC_HOOK_PROFILE: 'standard',
|
|
...env
|
|
},
|
|
timeout: 15000,
|
|
stdio: ['pipe', 'pipe', 'pipe']
|
|
});
|
|
|
|
return {
|
|
code: result.status || 0,
|
|
stdout: result.stdout || '',
|
|
stderr: result.stderr || ''
|
|
};
|
|
}
|
|
|
|
async function runTests() {
|
|
console.log('\n=== Testing mcp-health-check.js ===\n');
|
|
|
|
let passed = 0;
|
|
let failed = 0;
|
|
|
|
if (test('passes through non-MCP tools untouched', () => {
|
|
const result = runHook(
|
|
{ tool_name: 'Read', tool_input: { file_path: 'README.md' } },
|
|
{ CLAUDE_HOOK_EVENT_NAME: 'PreToolUse' }
|
|
);
|
|
|
|
assert.strictEqual(result.code, 0, 'Expected non-MCP tool to pass through');
|
|
assert.strictEqual(result.stderr, '', 'Expected no stderr for non-MCP tool');
|
|
})) passed++; else failed++;
|
|
|
|
if (await asyncTest('marks healthy command MCP servers and allows the tool call', async () => {
|
|
const tempDir = createTempDir();
|
|
const configPath = path.join(tempDir, 'claude.json');
|
|
const statePath = path.join(tempDir, 'mcp-health.json');
|
|
const serverScript = path.join(tempDir, 'healthy-server.js');
|
|
|
|
try {
|
|
fs.writeFileSync(serverScript, "setInterval(() => {}, 1000);\n");
|
|
writeConfig(configPath, {
|
|
mcpServers: {
|
|
mock: createCommandConfig(serverScript)
|
|
}
|
|
});
|
|
|
|
const input = { tool_name: 'mcp__mock__list_items', tool_input: {} };
|
|
const result = runHook(input, {
|
|
CLAUDE_HOOK_EVENT_NAME: 'PreToolUse',
|
|
ECC_MCP_CONFIG_PATH: configPath,
|
|
ECC_MCP_HEALTH_STATE_PATH: statePath,
|
|
ECC_MCP_HEALTH_TIMEOUT_MS: '100'
|
|
});
|
|
|
|
assert.strictEqual(result.code, 0, `Expected healthy server to pass, got ${result.code}`);
|
|
assert.strictEqual(result.stdout.trim(), JSON.stringify(input), 'Expected original JSON on stdout');
|
|
|
|
const state = readState(statePath);
|
|
assert.strictEqual(state.servers.mock.status, 'healthy', 'Expected mock server to be marked healthy');
|
|
} finally {
|
|
cleanupTempDir(tempDir);
|
|
}
|
|
})) passed++; else failed++;
|
|
|
|
if (await asyncTest('blocks unhealthy command MCP servers and records backoff state', async () => {
|
|
const tempDir = createTempDir();
|
|
const configPath = path.join(tempDir, 'claude.json');
|
|
const statePath = path.join(tempDir, 'mcp-health.json');
|
|
const serverScript = path.join(tempDir, 'unhealthy-server.js');
|
|
|
|
try {
|
|
fs.writeFileSync(serverScript, "process.exit(1);\n");
|
|
writeConfig(configPath, {
|
|
mcpServers: {
|
|
flaky: createCommandConfig(serverScript)
|
|
}
|
|
});
|
|
|
|
const result = runHook(
|
|
{ tool_name: 'mcp__flaky__search', tool_input: {} },
|
|
{
|
|
CLAUDE_HOOK_EVENT_NAME: 'PreToolUse',
|
|
ECC_MCP_CONFIG_PATH: configPath,
|
|
ECC_MCP_HEALTH_STATE_PATH: statePath,
|
|
ECC_MCP_HEALTH_TIMEOUT_MS: '100'
|
|
}
|
|
);
|
|
|
|
assert.strictEqual(result.code, 2, 'Expected unhealthy server to block the MCP tool');
|
|
assert.ok(result.stderr.includes('Blocking search'), `Expected blocking message, got: ${result.stderr}`);
|
|
|
|
const state = readState(statePath);
|
|
assert.strictEqual(state.servers.flaky.status, 'unhealthy', 'Expected flaky server to be marked unhealthy');
|
|
assert.ok(state.servers.flaky.nextRetryAt > state.servers.flaky.checkedAt, 'Expected retry backoff to be recorded');
|
|
} finally {
|
|
cleanupTempDir(tempDir);
|
|
}
|
|
})) passed++; else failed++;
|
|
|
|
if (await asyncTest('fail-open mode warns but does not block unhealthy MCP servers', async () => {
|
|
const tempDir = createTempDir();
|
|
const configPath = path.join(tempDir, 'claude.json');
|
|
const statePath = path.join(tempDir, 'mcp-health.json');
|
|
const serverScript = path.join(tempDir, 'relaxed-server.js');
|
|
|
|
try {
|
|
fs.writeFileSync(serverScript, "process.exit(1);\n");
|
|
writeConfig(configPath, {
|
|
mcpServers: {
|
|
relaxed: createCommandConfig(serverScript)
|
|
}
|
|
});
|
|
|
|
const result = runHook(
|
|
{ tool_name: 'mcp__relaxed__list', tool_input: {} },
|
|
{
|
|
CLAUDE_HOOK_EVENT_NAME: 'PreToolUse',
|
|
ECC_MCP_CONFIG_PATH: configPath,
|
|
ECC_MCP_HEALTH_STATE_PATH: statePath,
|
|
ECC_MCP_HEALTH_FAIL_OPEN: '1',
|
|
ECC_MCP_HEALTH_TIMEOUT_MS: '100'
|
|
}
|
|
);
|
|
|
|
assert.strictEqual(result.code, 0, 'Expected fail-open mode to allow execution');
|
|
assert.ok(result.stderr.includes('Blocking list') || result.stderr.includes('fall back'), 'Expected warning output in fail-open mode');
|
|
} finally {
|
|
cleanupTempDir(tempDir);
|
|
}
|
|
})) passed++; else failed++;
|
|
|
|
if (await asyncTest('post-failure reconnect command restores server health when a reprobe succeeds', async () => {
|
|
const tempDir = createTempDir();
|
|
const configPath = path.join(tempDir, 'claude.json');
|
|
const statePath = path.join(tempDir, 'mcp-health.json');
|
|
const switchFile = path.join(tempDir, 'server-mode.txt');
|
|
const reconnectFile = path.join(tempDir, 'reconnected.txt');
|
|
const probeScript = path.join(tempDir, 'probe-server.js');
|
|
|
|
fs.writeFileSync(switchFile, 'down');
|
|
fs.writeFileSync(
|
|
probeScript,
|
|
[
|
|
"const fs = require('fs');",
|
|
`const mode = fs.readFileSync(${JSON.stringify(switchFile)}, 'utf8').trim();`,
|
|
"if (mode === 'up') { setInterval(() => {}, 1000); } else { console.error('401 Unauthorized'); process.exit(1); }"
|
|
].join('\n')
|
|
);
|
|
|
|
const reconnectScript = path.join(tempDir, 'reconnect.js');
|
|
fs.writeFileSync(
|
|
reconnectScript,
|
|
[
|
|
"const fs = require('fs');",
|
|
`fs.writeFileSync(${JSON.stringify(switchFile)}, 'up');`,
|
|
`fs.writeFileSync(${JSON.stringify(reconnectFile)}, 'done');`
|
|
].join('\n')
|
|
);
|
|
|
|
try {
|
|
writeConfig(configPath, {
|
|
mcpServers: {
|
|
authy: createCommandConfig(probeScript)
|
|
}
|
|
});
|
|
|
|
const result = runHook(
|
|
{
|
|
tool_name: 'mcp__authy__messages',
|
|
tool_input: {},
|
|
error: '401 Unauthorized'
|
|
},
|
|
{
|
|
CLAUDE_HOOK_EVENT_NAME: 'PostToolUseFailure',
|
|
ECC_MCP_CONFIG_PATH: configPath,
|
|
ECC_MCP_HEALTH_STATE_PATH: statePath,
|
|
ECC_MCP_RECONNECT_COMMAND: `node ${JSON.stringify(reconnectScript)}`,
|
|
ECC_MCP_HEALTH_TIMEOUT_MS: '100'
|
|
}
|
|
);
|
|
|
|
assert.strictEqual(result.code, 0, 'Expected failure hook to remain non-blocking');
|
|
assert.ok(result.stderr.includes('reported 401'), `Expected reconnect log, got: ${result.stderr}`);
|
|
assert.ok(result.stderr.includes('connection restored'), `Expected restored log, got: ${result.stderr}`);
|
|
assert.ok(fs.existsSync(reconnectFile), 'Expected reconnect command to run');
|
|
|
|
const state = readState(statePath);
|
|
assert.strictEqual(state.servers.authy.status, 'healthy', 'Expected authy server to be restored after reconnect');
|
|
} finally {
|
|
cleanupTempDir(tempDir);
|
|
}
|
|
})) passed++; else failed++;
|
|
|
|
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
|
|
process.exit(failed > 0 ? 1 : 0);
|
|
}
|
|
|
|
runTests().catch(error => {
|
|
console.error(error);
|
|
process.exit(1);
|
|
});
|