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

162 lines
7.8 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 { fileURLToPath } from 'node:url';
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { parseFrontmatter, checkSkillDir, collectSkills, SKILL_PREFIX } from '../validate.mjs';
// Windows 下 URL.pathname 产出 /D:/... 形式,不能直接当本地路径;用 fileURLToPath 统一转换
const FIX = fileURLToPath(new URL('./fixtures/', import.meta.url));
test('parseFrontmatter 解析合法 frontmatter', () => {
const md = `---\nname: re-abc\ndescription: 测试。\n---\n\n# 标题\n\n正文`;
const { name, description, body } = parseFrontmatter(md);
assert.equal(name, 're-abc');
assert.equal(description, '测试。');
assert.match(body, /^# 标题/);
});
test('parseFrontmatter 拒绝缺失 name/description', () => {
assert.throws(() => parseFrontmatter('---\nname: x\n---\n'), /description/);
assert.throws(() => parseFrontmatter('# no frontmatter'), /frontmatter/);
});
// 校验用例共用的能力注册表子集(原子技能必须声明能力,声明值须在注册表内)
const CAPS = { knownCapabilities: new Set(['triage']) };
test('good-skill 通过全部检查', () => {
const { errors } = checkSkillDir(FIX + 'good-skill', CAPS);
assert.deepEqual(errors, []);
});
test('bad-name 报名字不一致错误', () => {
const { errors } = checkSkillDir(FIX + 'bad-name');
assert.ok(errors.some(e => e.includes('name') && e.includes('wrong-name')));
});
test('no-tools 叶子技能报缺工具准备', () => {
const { errors } = checkSkillDir(FIX + 'no-tools');
assert.ok(errors.some(e => e.includes('工具准备')));
});
test('bad-frontmatter 顶格游离行报错(校验闸口不再放过非法 frontmatter)', () => {
const { errors } = checkSkillDir(FIX + 'bad-frontmatter');
assert.ok(errors.some(e => e.includes('frontmatter line')), `未报错:${JSON.stringify(errors)}`);
});
test('链接规则:技能死链与跨技能裸引用报错,本技能 references 放行', () => {
const { errors } = checkSkillDir(FIX + 'broken-link', { knownSkills: ['re-abc'] });
assert.ok(errors.some(e => e.includes('re-does-not-exist')), '技能死链应报错');
assert.ok(errors.some(e => e.includes('platform-tips')), '跨技能裸引用应报错');
assert.ok(!errors.some(e => e.includes('local-note')), '本技能 references 链接应放行');
});
test('跨技能 references 用 [[re-xxx/name]] 限定后放行,指向不存在则报错', () => {
const root = mkdtempSync(join(tmpdir(), 'link-'));
const mkSkill = (skill, body, refs = {}) => {
mkdirSync(join(root, skill, 'references'), { recursive: true });
writeFileSync(join(root, skill, 'SKILL.md'),
`---\nname: ${skill}\ndescription: 测试。test。\ncapabilities: [triage]\n---\n\n# 标题\n\n## 工具准备\n\n${body}`);
for (const [n, c] of Object.entries(refs)) writeFileSync(join(root, skill, 'references', `${n}.md`), c);
};
const opts = { ...CAPS, knownSkills: ['re-a', 're-b'] };
try {
mkSkill('re-a', '跨技能见 [[re-b/shared]]');
mkSkill('re-b', '本技能内见 [[shared]]', { shared: 'x' });
assert.deepEqual(checkSkillDir(join(root, 're-a'), opts).errors, []);
assert.deepEqual(checkSkillDir(join(root, 're-b'), opts).errors, []);
mkSkill('re-a', '跨技能见 [[re-b/nope]]');
assert.ok(checkSkillDir(join(root, 're-a'), opts).errors.some(e => e.includes('re-b/nope')));
} finally { rmSync(root, { recursive: true, force: true }); }
});
test('gateway-skill 豁免工具准备检查', () => {
const { errors } = checkSkillDir(FIX + 'gateway-skill');
assert.ok(!errors.some(e => e.includes('工具准备')));
});
test('collectSkills 收集全部技能目录名', () => {
const names = collectSkills(FIX);
assert.deepEqual([...names].sort(), ['bad-frontmatter', 'bad-name', 'broken-link', 'gateway-skill', 'good-skill', 'no-tools']);
});
test('parseFrontmatter 解析 capabilities list', () => {
const md = `---\nname: re-abc\ndescription: 测试。\ncapabilities: [elf-parser, unpack]\n---\n\n# 标题\n\n正文`;
const { capabilities } = parseFrontmatter(md);
assert.deepEqual(capabilities, ['elf-parser', 'unpack']);
});
test('guard 合法 JSON 通过、结构非法报错', () => {
const dir = mkdtempSync(join(tmpdir(), 'guard-test-'));
const name = dir.split(/[\\/]/).pop();
const body = `---\nname: ${name}\ndescription: 测试。test。\ncapabilities: [triage]\nguard: %GUARD%\n---\n\n# 标题\n\n## 工具准备\n\n正文`;
try {
// 合法
writeFileSync(join(dir, 'SKILL.md'), body.replace('%GUARD%', '{"require_authorization": true, "forbidden": ["unauthorized_api_testing"]}'));
assert.deepEqual(checkSkillDir(dir, CAPS).errors, []);
// 非 JSON
writeFileSync(join(dir, 'SKILL.md'), body.replace('%GUARD%', 'require_authorization: true'));
const { errors } = checkSkillDir(dir, CAPS);
assert.ok(errors.some(e => e.includes('guard must be')));
// 结构缺字段
writeFileSync(join(dir, 'SKILL.md'), body.replace('%GUARD%', '{"require_authorization": "yes"}'));
const { errors: e2 } = checkSkillDir(dir, CAPS);
assert.ok(e2.some(e => e.includes('guard must be')));
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test('description 须中英双含(触发词约定)', () => {
const dir = mkdtempSync(join(tmpdir(), 'desc-i18n-'));
const name = dir.split(/[\\/]/).pop();
const withDesc = (d) => `---\nname: ${name}\ndescription: ${d}\ncapabilities: [triage]\n---\n\n# 标题\n\n## 工具准备\n\n正文`;
try {
// 双语俱全 → 通过
writeFileSync(join(dir, 'SKILL.md'), withDesc('测试技能:test skill 触发词'));
assert.deepEqual(checkSkillDir(dir, CAPS).errors, []);
// 仅中文 → 报错(英文环境命中不了)
writeFileSync(join(dir, 'SKILL.md'), withDesc('只有中文触发词'));
assert.ok(checkSkillDir(dir, CAPS).errors.some(e => e.includes('CJK')));
// 仅英文 → 报错
writeFileSync(join(dir, 'SKILL.md'), withDesc('english triggers only'));
assert.ok(checkSkillDir(dir, CAPS).errors.some(e => e.includes('CJK')));
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test('原子技能未声明能力即报错(能力层完整性)', () => {
const dir = mkdtempSync(join(tmpdir(), 'no-caps-'));
const name = dir.split(/[\\/]/).pop();
try {
writeFileSync(join(dir, 'SKILL.md'), `---\nname: ${name}\ndescription: 测试。test。\n---\n\n# 标题\n\n## 工具准备\n\n正文`);
assert.ok(checkSkillDir(dir, CAPS).errors.some(e => e.includes('must declare capabilities')));
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test('capabilities 合法通过、未知标签与非法写法报错', () => {
const dir = mkdtempSync(join(tmpdir(), 'cap-test-'));
const name = dir.split(/[\\/]/).pop();
const caps = new Set(['elf-parser', 'unpack', 'tracing']);
const body = `---\nname: ${name}\ndescription: 测试。test。\ncapabilities: %CAPS%\n---\n\n# 标题\n\n## 工具准备\n\n正文`;
try {
// 合法 list 通过
writeFileSync(join(dir, 'SKILL.md'), body.replace('%CAPS%', '[elf-parser, unpack]'));
assert.deepEqual(checkSkillDir(dir, { knownCapabilities: caps }).errors, []);
// 未知标签报错
writeFileSync(join(dir, 'SKILL.md'), body.replace('%CAPS%', '[elf-parser, nope-cap]'));
const { errors } = checkSkillDir(dir, { knownCapabilities: caps });
assert.ok(errors.some(e => e.includes("unknown capability 'nope-cap'")));
// 非法写法(非 list)报错
writeFileSync(join(dir, 'SKILL.md'), body.replace('%CAPS%', 'elf-parser'));
const { errors: e2 } = checkSkillDir(dir, { knownCapabilities: caps });
assert.ok(e2.some(e => e.includes('non-empty list')));
} finally {
rmSync(dir, { recursive: true, force: true });
}
});