bookworm-smart-assistant/scripts/patches/patch-phase1-b-tests.js

331 lines
11 KiB
JavaScript
Raw Normal View History

#!/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();