import { test } from 'node:test'; import assert from 'node:assert/strict'; import { readFileSync, readdirSync, existsSync } from 'node:fs'; // 计数散落多处,历史上漏同步过 5 处(见 CLAUDE.md「同步计数与导航」)。 // 本测试把那份人工清单变成检查:新技能增删后,任何一处没跟上都会红。 const SKILLS_DIR = '.claude/skills'; function actualCounts() { let atomic = 0, gateway = 0, entry = 0; let total = 0; for (const d of readdirSync(SKILLS_DIR)) { if (!d.startsWith('re-') || !existsSync(`${SKILLS_DIR}/${d}/SKILL.md`)) continue; total++; const m = /^type:\s*(\w+)/m.exec(readFileSync(`${SKILLS_DIR}/${d}/SKILL.md`, 'utf8')); const kind = m ? m[1] : 'atomic'; if (kind === 'gateway') gateway++; else if (kind === 'entry') entry++; else atomic++; } return { total, atomic, gateway, entry }; } // file → [正则, 期望口径, 说明];正则第 1 个捕获组是要比对的数字 const SITES = [ ['README.md', /^(\d+) 个逆向工程技能/m, 'total', '首段技能总数'], ['README.md', /^## 技能导航((\d+))/m, 'total', '技能导航标题'], ['README.md', /^入口 → (\d+) 大类网关 → (\d+) 原子技能/m, 'gateway+atomic', '前言'], ['README_EN.md', /^(\d+) reverse engineering skills/m, 'total', '首段技能总数'], ['README_EN.md', /^## Skill map \((\d+)\)/m, 'total', 'Skill map 标题'], ['README_EN.md', /^Entry → (\d+) category gateways → (\d+) atomic skills/m, 'gateway+atomic', '前言'], ['AGENTS.md', /^本仓库是通用逆向工程 AI 技能库((\d+) 个技能)/m, 'total', '首段技能总数'], ['AGENTS.md', /^- 原子技能((\d+))/m, 'atomic', '原子技能行'], ['CLAUDE.md', /^通用逆向工程 AI 技能库((\d+) 个技能)/m, 'total', '仓库性质段'], ['CLAUDE.md', /OK: (\d+) skills validated/, 'total', '校验输出示例'], ['CLAUDE.md', /原子技能(type: atomic,(\d+) 个)/m, 'atomic', '架构图'], ['package.json', /"description":\s*"[^"]*?(\d+) 个技能/, 'total', 'npm 描述'], ['.claude-plugin/marketplace.json', /"description":\s*"[^"]*?(\d+) 个技能/, 'total', '插件市场描述'], ]; test('各处的技能计数与实际目录一致', () => { const c = actualCounts(); const bad = []; for (const [file, re, kind, label] of SITES) { const text = readFileSync(file, 'utf8'); const m = re.exec(text); if (!m) { bad.push(`${file}(${label}):找不到计数,模式可能已失效`); continue; } const nums = m.slice(1).map(Number); const want = kind === 'total' ? [c.total] : kind === 'atomic' ? [c.atomic] : [c.gateway, c.atomic]; nums.forEach((n, i) => { if (n !== want[i]) bad.push(`${file}(${label}):写的是 ${n},实际 ${want[i]}`); }); } assert.deepEqual(bad, [], `计数不同步:\n${bad.join('\n')}`); }); test('README / README_EN 的网关行引用的技能都存在', () => { const missing = []; for (const file of ['README.md', 'README_EN.md']) { const text = readFileSync(file, 'utf8'); for (const m of text.matchAll(/`(re-[a-z0-9-]+)`/g)) { if (!existsSync(`${SKILLS_DIR}/${m[1]}/SKILL.md`)) missing.push(`${file}: ${m[1]}`); } } assert.deepEqual(missing, [], `引用了不存在的技能:\n${missing.join('\n')}`); }); test('每个技能都在 README 中至少出现一次', () => { const readme = readFileSync('README.md', 'utf8'); const absent = readdirSync(SKILLS_DIR) .filter((d) => d.startsWith('re-') && existsSync(`${SKILLS_DIR}/${d}/SKILL.md`)) .filter((d) => !readme.includes(d)); assert.deepEqual(absent, [], `README 未提及:${absent.join(' ')}`); }); // 测试计数也曾漂移过:CLAUDE.md 写「validate + 103 项测试」,实际已到 168。 // 技能计数有上面的 SITES 看住,测试计数此前无人看——补上同样的检查: // 顶层 test( 的静态计数与 `node --test` 报告的 tests 数一致,故可静态比对。 test('CLAUDE.md 的测试计数与实际测试数一致', () => { let n = 0; for (const f of readdirSync('tests')) { if (!f.endsWith('.test.mjs')) continue; n += (readFileSync(`tests/${f}`, 'utf8').match(/^test\(/gm) || []).length; } const m = /`npm test` = validate \+ (\d+) 项测试/.exec(readFileSync('CLAUDE.md', 'utf8')); assert.ok(m, 'CLAUDE.md 的「检查分层」段应写明测试项数'); assert.equal( Number(m[1]), n, `CLAUDE.md 写 ${m[1]} 项,实际 ${n} 项——增删测试后需同步该计数`, ); });