#!/usr/bin/env node 'use strict'; /** * Phase 1 B · 补测试 —— 3 份 vitest 测试文件落地 * * 产出: * - hooks/__tests__/mcp-usage-tracker.test.js * - hooks/__tests__/mcp-prune.test.js * - hooks/__tests__/session-start-mcp-probe.test.js * * sentinel: PHASE1_B_TESTS_2026_04_24 * 幂等: 已存在且一致则跳过;不同则备份 */ const fs = require('fs'); const path = require('path'); const CLAUDE_ROOT = path.join(__dirname, '..', '..'); const TESTS_DIR = path.join(CLAUDE_ROOT, 'hooks', '__tests__'); const SENTINEL = 'PHASE1_B_TESTS_2026_04_24'; const TRACKER_TEST = `/** * ${SENTINEL} — mcp-usage-tracker 单元测试 * 覆盖: parseDetail / aggregate / identifyPruneCandidates */ import { describe, it, expect } from 'vitest'; import { createRequire } from 'node:module'; const require = createRequire(import.meta.url); const { parseDetail, aggregate, identifyPruneCandidates } = require('../../scripts/mcp-usage-tracker.js'); describe('parseDetail', () => { it('处理 null / undefined / 空串', () => { expect(parseDetail(null)).toEqual({ server: null, tool: null }); expect(parseDetail(undefined)).toEqual({ server: null, tool: null }); expect(parseDetail('')).toEqual({ server: null, tool: null }); }); it('处理无斜杠: server 裸名', () => { expect(parseDetail('github')).toEqual({ server: 'github', tool: null }); }); it('处理标准 server/tool', () => { expect(parseDetail('github/create_issue')).toEqual({ server: 'github', tool: 'create_issue' }); }); it('处理多层 slash: 第一个 / 为分界', () => { expect(parseDetail('server/tool/with/nested')).toEqual({ server: 'server', tool: 'tool/with/nested' }); }); it('处理非字符串类型 (防 NPE)', () => { expect(parseDetail(123)).toEqual({ server: null, tool: null }); expect(parseDetail({})).toEqual({ server: null, tool: null }); }); }); describe('aggregate', () => { it('空事件 + 已知 server → 全 0 骨架', () => { const stats = aggregate([], ['github', 'context7']); expect(Object.keys(stats).sort()).toEqual(['context7', 'github']); expect(stats.github.totalCalls).toBe(0); expect(stats.github.firstUsed).toBe(null); expect(stats.github.lastUsed).toBe(null); }); it('事件统计 success/error 分桶', () => { const events = [ { ts: '2026-04-20T10:00:00Z', detail: 'github/issue_create', success: true }, { ts: '2026-04-21T10:00:00Z', detail: 'github/issue_create', success: false }, { ts: '2026-04-22T10:00:00Z', detail: 'github/pr_list', success: true }, ]; const stats = aggregate(events, ['github']); expect(stats.github.totalCalls).toBe(3); expect(stats.github.successCount).toBe(2); expect(stats.github.errorCount).toBe(1); expect(stats.github.firstUsed).toBe('2026-04-20T10:00:00Z'); expect(stats.github.lastUsed).toBe('2026-04-22T10:00:00Z'); expect(stats.github.tools.issue_create.count).toBe(2); expect(stats.github.tools.issue_create.errorCount).toBe(1); expect(stats.github.tools.pr_list.count).toBe(1); }); it('未知 server (不在 allServers) 也入表', () => { const events = [ { ts: '2026-04-20T10:00:00Z', detail: 'unknown-server/x', success: true }, ]; const stats = aggregate(events, []); expect(stats['unknown-server']).toBeDefined(); expect(stats['unknown-server'].totalCalls).toBe(1); }); it('detail 无效事件被忽略', () => { const events = [ { ts: '2026-04-20T10:00:00Z', detail: null, success: true }, { ts: '2026-04-20T10:00:00Z', detail: '', success: true }, ]; const stats = aggregate(events, ['github']); expect(stats.github.totalCalls).toBe(0); }); }); describe('identifyPruneCandidates', () => { it('critical 名单永不候选', () => { const stats = { github: { server: 'github', totalCalls: 0 }, other: { server: 'other', totalCalls: 0 }, }; const critical = new Set(['github']); const candidates = identifyPruneCandidates(stats, critical); expect(candidates.map(c => c.server)).toEqual(['other']); }); it('有调用的 server 不候选', () => { const stats = { active: { server: 'active', totalCalls: 5 }, idle: { server: 'idle', totalCalls: 0 }, }; const candidates = identifyPruneCandidates(stats, new Set()); expect(candidates).toHaveLength(1); expect(candidates[0].server).toBe('idle'); expect(candidates[0].reason).toBe('zero-calls-in-window'); }); it('空 stats 返回空数组', () => { expect(identifyPruneCandidates({}, new Set())).toEqual([]); }); }); `; const PRUNE_TEST = `/** * ${SENTINEL} — mcp-prune CLI 集成测试 * 覆盖: 默认报告 / --plan / --help / 退出码 */ import { describe, it, expect } from 'vitest'; import { execFileSync } from 'node:child_process'; import { existsSync, readFileSync, unlinkSync } from 'node:fs'; import { join } from 'node:path'; import os from 'node:os'; const HOME = process.env.USERPROFILE || process.env.HOME || os.homedir(); const CLAUDE_ROOT = join(HOME, '.claude'); const SCRIPT = join(CLAUDE_ROOT, 'scripts', 'mcp-prune.js'); function run(args) { return execFileSync(process.execPath, [SCRIPT, ...args], { encoding: 'utf8', stdio: 'pipe', }); } describe('mcp-prune CLI', () => { it('--help 打印用法且 exit 0', () => { const out = run(['--help']); expect(out).toContain('用法:'); expect(out).toContain('--plan'); expect(out).toContain('--confirm'); }); it('默认模式输出包含报告标题', () => { const out = run(['--days', '7']); expect(out).toContain('/mcp-prune'); expect(out).toContain('剪枝分析'); expect(out).toContain('7天窗口'); }); it('报告含总数 / critical / 活跃 / 候选 四个统计', () => { const out = run(['--days', '30']); expect(out).toMatch(/总 MCP 数:/); expect(out).toMatch(/critical/); expect(out).toMatch(/活跃 MCP/); expect(out).toMatch(/剪枝候选/); }); it('--plan 写入 plan 文件', () => { const today = new Date().toISOString().slice(0, 10); const planFile = join(CLAUDE_ROOT, 'mcp-prune-plan-' + today + '.json'); // 清理旧 plan 避免干扰断言 if (existsSync(planFile)) unlinkSync(planFile); const out = run(['--days', '30', '--plan']); // 若候选为 0 则不会生成 plan 文件 (正常);有候选才检查 if (out.includes('Plan 写入:')) { expect(existsSync(planFile)).toBe(true); const plan = JSON.parse(readFileSync(planFile, 'utf8')); expect(plan.schema_version).toBe(1); expect(plan.tool).toBe('mcp-prune'); expect(Array.isArray(plan.candidates)).toBe(true); expect(Array.isArray(plan.critical)).toBe(true); expect(plan.note).toMatch(/不自动修改/); } }); }); `; const PROBE_TEST = `/** * ${SENTINEL} — session-start-mcp-probe 单元测试 * 覆盖: commandPlausible / probeMcp (http+sse+stdio) */ import { describe, it, expect } from 'vitest'; import { createRequire } from 'node:module'; const require = createRequire(import.meta.url); const { commandPlausible, probeMcp } = require('../../hooks/session-start-mcp-probe.js'); describe('commandPlausible', () => { it('空 / null / 非字符串 → false', () => { expect(commandPlausible(null)).toBe(false); expect(commandPlausible(undefined)).toBe(false); expect(commandPlausible('')).toBe(false); expect(commandPlausible(123)).toBe(false); }); it('well-known 命令裸名 → true', () => { expect(commandPlausible('npx')).toBe(true); expect(commandPlausible('node')).toBe(true); expect(commandPlausible('python')).toBe(true); }); it('Windows .cmd 后缀 → true (剥离后匹配)', () => { expect(commandPlausible('npx.cmd')).toBe(true); expect(commandPlausible('node.exe')).toBe(true); expect(commandPlausible('python.bat')).toBe(true); }); it('绝对路径 + 不存在 → false', () => { expect(commandPlausible('C:/definitely-not-exists/xyz.exe')).toBe(false); }); it('未知命令 → PATH 回退扫描 (通常 false 除非 PATH 里真有)', () => { expect(commandPlausible('__definitely_not_installed_cmd_xyz__')).toBe(false); }); }); describe('probeMcp', () => { it('http 类型 + 合法 URL → reachable true', () => { const r = probeMcp('x', { type: 'http', url: 'https://example.com/mcp' }); expect(r.kind).toBe('http'); expect(r.urlValid).toBe(true); expect(r.reachable).toBe(true); }); it('http 类型 + 非法 URL → reachable false', () => { const r = probeMcp('x', { type: 'http', url: 'not-a-url' }); expect(r.urlValid).toBe(false); expect(r.reachable).toBe(false); }); it('sse 类型 + 无 url → reachable false', () => { const r = probeMcp('x', { type: 'sse' }); expect(r.reachable).toBe(false); }); it('stdio 类型 + 已知命令 → reachable true', () => { const r = probeMcp('x', { command: 'npx' }); expect(r.kind).toBe('stdio'); expect(r.commandExists).toBe(true); expect(r.reachable).toBe(true); }); it('stdio 类型 + 空命令 → reachable false', () => { const r = probeMcp('x', { command: '' }); expect(r.reachable).toBe(false); }); it('cfg 为 null → 不抛异常', () => { const r = probeMcp('x', null); expect(r.reachable).toBe(false); }); }); `; const FILES = [ { name: 'mcp-usage-tracker.test.js', content: TRACKER_TEST }, { name: 'mcp-prune.test.js', content: PRUNE_TEST }, { name: 'session-start-mcp-probe.test.js', content: PROBE_TEST }, ]; function writeIfDifferent(target, content, label) { if (fs.existsSync(target)) { const current = fs.readFileSync(target, 'utf8'); if (current === content) { console.log('[patch-phase1-B] ' + label + ' 已一致,跳过'); return 'skipped'; } const bak = target + '.bak.phase1-b'; fs.copyFileSync(target, bak); console.log('[patch-phase1-B] 已备份:', bak); } const tmp = target + '.tmp.js'; fs.writeFileSync(tmp, content, 'utf8'); try { const { execFileSync } = require('child_process'); execFileSync(process.execPath, ['--check', tmp], { stdio: 'pipe' }); } catch (e) { try { fs.unlinkSync(tmp); } catch {} console.error('[patch-phase1-B] ' + label + ' 语法检查失败:', (e.stderr || e.message || '').toString().slice(0, 500)); process.exit(3); } fs.renameSync(tmp, target); console.log('[patch-phase1-B] 已写入 ' + label + ':', target); return 'written'; } function main() { if (!fs.existsSync(TESTS_DIR)) { console.error('[patch-phase1-B] 测试目录不存在:', TESTS_DIR); process.exit(1); } const results = []; for (const f of FILES) { const r = writeIfDifferent(path.join(TESTS_DIR, f.name), f.content, f.name); results.push({ file: f.name, result: r }); } console.log(''); console.log('[patch-phase1-B] sentinel:', SENTINEL); console.log('[patch-phase1-B] 结果:', JSON.stringify(results)); console.log('[patch-phase1-B] 完成。'); console.log(''); console.log('验证: pnpm test -- mcp-usage-tracker mcp-prune session-start-mcp-probe'); process.exit(0); } main();