185 lines
5.0 KiB
JavaScript
185 lines
5.0 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* PostToolUse Hook: 源代码修改后提醒运行测试
|
|
* 匹配器: Edit|Write|NotebookEdit
|
|
* 触发: 修改了源代码文件(非测试文件)时
|
|
* 退出码: 0=静默通过, 2=提醒(非阻断,continue=true)
|
|
*
|
|
* 行为:
|
|
* - 修改测试文件本身 → 静默
|
|
* - 修改配置/文档文件 → 静默
|
|
* - 修改源代码文件 → 检测对应测试文件是否存在 → 提醒运行
|
|
*/
|
|
|
|
const path = require('path');
|
|
const fs = require('fs');
|
|
|
|
// 源代码文件扩展名
|
|
const SOURCE_EXTENSIONS = [
|
|
'.ts', '.tsx', '.js', '.jsx',
|
|
'.py',
|
|
'.go',
|
|
'.rs'
|
|
];
|
|
|
|
// 测试文件模式 (这些文件本身是测试,修改时不提醒)
|
|
const TEST_PATTERNS = [
|
|
/\.test\.[tj]sx?$/,
|
|
/\.spec\.[tj]sx?$/,
|
|
/__tests__\//,
|
|
/\/tests?\//,
|
|
/test_[^/]+\.py$/,
|
|
/_test\.go$/,
|
|
/\.e2e\./,
|
|
/\.integration\./
|
|
];
|
|
|
|
// 配置/文档文件 (不提醒)
|
|
const SKIP_PATTERNS = [
|
|
/\.md$/,
|
|
/\.json$/,
|
|
/\.yaml$/,
|
|
/\.yml$/,
|
|
/\.toml$/,
|
|
/\.env($|\.)/,
|
|
/\.css$/,
|
|
/\.scss$/,
|
|
/\.html$/,
|
|
/\.svg$/,
|
|
/\.png$/,
|
|
/\.jpg$/,
|
|
/Dockerfile/,
|
|
/docker-compose/,
|
|
/\.gitignore/,
|
|
/\.eslintrc/,
|
|
/tsconfig/,
|
|
/package\.json/
|
|
];
|
|
|
|
const KNOWN_TEST_COMMANDS = {
|
|
'.ts': 'pnpm vitest run',
|
|
'.tsx': 'pnpm vitest run',
|
|
'.js': 'pnpm vitest run',
|
|
'.jsx': 'pnpm vitest run',
|
|
'.py': 'python -m pytest',
|
|
'.go': 'go test ./...',
|
|
'.rs': 'cargo test'
|
|
};
|
|
|
|
const readStdin = require('./lib/read-stdin.js');
|
|
|
|
function findTestFile(filePath) {
|
|
const dir = path.dirname(filePath);
|
|
const ext = path.extname(filePath);
|
|
const base = path.basename(filePath, ext);
|
|
|
|
// 常见测试文件路径模式
|
|
const candidates = [
|
|
// 同目录: foo.test.ts
|
|
path.join(dir, `${base}.test${ext}`),
|
|
path.join(dir, `${base}.spec${ext}`),
|
|
// __tests__ 子目录
|
|
path.join(dir, '__tests__', `${base}.test${ext}`),
|
|
path.join(dir, '__tests__', `${base}.spec${ext}`),
|
|
// tests 兄弟目录
|
|
path.join(dir, '..', 'tests', `${base}.test${ext}`),
|
|
path.join(dir, '..', 'tests', `test_${base}${ext}`),
|
|
// Python 风格
|
|
path.join(dir, `test_${base}${ext}`),
|
|
// Go 风格
|
|
path.join(dir, `${base}_test${ext}`)
|
|
];
|
|
|
|
for (const candidate of candidates) {
|
|
try {
|
|
if (fs.existsSync(candidate)) {
|
|
return candidate;
|
|
}
|
|
} catch {
|
|
// 文件系统错误,跳过
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function main() {
|
|
readStdin({ maxSize: 1024 * 1024 }).then(input => {
|
|
const toolName = input.tool_name;
|
|
if (toolName !== 'Edit' && toolName !== 'Write' && toolName !== 'NotebookEdit') {
|
|
process.exit(0);
|
|
}
|
|
|
|
const filePath = (input.tool_input && (input.tool_input.file_path || input.tool_input.filePath || input.tool_input.notebook_path)) || '';
|
|
if (!filePath) {
|
|
process.exit(0);
|
|
}
|
|
|
|
const ext = path.extname(filePath).toLowerCase();
|
|
|
|
// 不是源代码文件 → 静默
|
|
if (!SOURCE_EXTENSIONS.includes(ext)) {
|
|
process.exit(0);
|
|
}
|
|
|
|
// 是测试文件本身 → 静默
|
|
if (TEST_PATTERNS.some(p => p.test(filePath))) {
|
|
process.exit(0);
|
|
}
|
|
|
|
// 是配置/文档 → 静默
|
|
if (SKIP_PATTERNS.some(p => p.test(filePath))) {
|
|
process.exit(0);
|
|
}
|
|
|
|
// 查找对应测试文件
|
|
const testFile = findTestFile(filePath);
|
|
const testCmd = KNOWN_TEST_COMMANDS[ext] || 'test';
|
|
const baseName = path.basename(filePath);
|
|
|
|
let suggestion;
|
|
if (testFile) {
|
|
const relTest = path.basename(testFile);
|
|
suggestion = `[test-hint] 已修改 \`${baseName}\`,对应测试文件 \`${relTest}\` 存在。建议后续运行: \`${testCmd} ${path.basename(testFile)}\``;
|
|
} else {
|
|
suggestion = `[test-hint] 已修改 \`${baseName}\`,未找到对应测试文件。如果是核心逻辑,建议补充测试。`;
|
|
}
|
|
|
|
const output = {
|
|
continue: true,
|
|
suppressOutput: false,
|
|
systemMessage: suggestion
|
|
};
|
|
|
|
process.stderr.write(JSON.stringify(output));
|
|
process.exit(2);
|
|
}).catch(() => process.exit(0));
|
|
}
|
|
|
|
/**
|
|
* 内联检查函数 (供 post-edit-dispatcher 委托调用)
|
|
*/
|
|
function inlineCheck(filePath, ext, toolName) {
|
|
try {
|
|
if (!SOURCE_EXTENSIONS.includes(ext)) return null;
|
|
if (TEST_PATTERNS.some(p => p.test(filePath))) return null;
|
|
if (SKIP_PATTERNS.some(p => p.test(filePath))) return null;
|
|
|
|
const testFile = findTestFile(filePath);
|
|
const testCmd = KNOWN_TEST_COMMANDS[ext] || 'test';
|
|
const baseName = path.basename(filePath);
|
|
|
|
if (testFile) {
|
|
const relTest = path.basename(testFile);
|
|
return `[test-hint] 已修改 \`${baseName}\`,对应测试文件 \`${relTest}\` 存在。建议运行: \`${testCmd} ${path.basename(testFile)}\``;
|
|
}
|
|
return `[test-hint] 已修改 \`${baseName}\`,未找到对应测试文件。如果是核心逻辑,建议补充测试。`;
|
|
} catch { return null; }
|
|
}
|
|
|
|
// 模块导出 (供 post-edit-dispatcher 委托调用)
|
|
if (typeof module !== 'undefined') {
|
|
module.exports = { findTestFile, inlineCheck, SOURCE_EXTENSIONS, TEST_PATTERNS, SKIP_PATTERNS };
|
|
}
|
|
|
|
if (require.main === module) { main(); }
|