Files
reverse-skills-max/vendor-new/rev-skills/tests/counts.test.mjs

94 lines
4.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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} 项——增删测试后需同步该计数`,
);
});