mirror of
https://github.com/addyosmani/agent-skills.git
synced 2026-11-06 20:56:13 +00:00
142 lines
4.5 KiB
JavaScript
142 lines
4.5 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
'use strict';
|
|
|
|
const assert = require('node:assert/strict');
|
|
const { test } = require('node:test');
|
|
|
|
const { lintSkillContent } = require('./skill-lint.js');
|
|
|
|
const KNOWN = new Set(['alpha', 'beta']);
|
|
|
|
/** A SKILL.md body carrying every required section, so tests can isolate frontmatter. */
|
|
function withAllSections(frontmatter) {
|
|
return [
|
|
frontmatter,
|
|
'',
|
|
'## Overview',
|
|
'x',
|
|
'## When to Use',
|
|
'x',
|
|
'## Common Rationalizations',
|
|
'x',
|
|
'## Red Flags',
|
|
'x',
|
|
'## Verification',
|
|
'x',
|
|
'',
|
|
].join('\n');
|
|
}
|
|
|
|
const VALID_FRONTMATTER = [
|
|
'---',
|
|
'name: alpha',
|
|
'description: Designs alphas. Use when building one.',
|
|
'---',
|
|
].join('\n');
|
|
|
|
// ─── Section exemptions ──────────────────────────────────────────────────────
|
|
|
|
test('a directory named after an Object.prototype key is not exempt from section checks', () => {
|
|
// `constructor` satisfies KEBAB_CASE, and `dirName in SECTION_EXEMPT_SKILLS`
|
|
// finds it on the prototype chain — silently skipping every section check.
|
|
const content = [
|
|
'---',
|
|
'name: constructor',
|
|
'description: Does a thing. Use when you need it.',
|
|
'---',
|
|
'',
|
|
'no sections here',
|
|
'',
|
|
].join('\n');
|
|
|
|
const { errors, exempt } = lintSkillContent('constructor', content, KNOWN);
|
|
|
|
assert.equal(exempt, false, 'exemptions must come from the allowlist, not the prototype chain');
|
|
assert.equal(errors.filter(e => /Missing required section/.test(e)).length, 5);
|
|
});
|
|
|
|
test('a genuinely allowlisted skill is still exempt', () => {
|
|
const content = [
|
|
'---',
|
|
'name: using-agent-skills',
|
|
'description: Routes to other skills. Use when choosing one.',
|
|
'---',
|
|
'',
|
|
'no sections here',
|
|
'',
|
|
].join('\n');
|
|
|
|
const { errors, exempt } = lintSkillContent('using-agent-skills', content, KNOWN);
|
|
|
|
assert.equal(exempt, true);
|
|
assert.deepEqual(errors.filter(e => /Missing required section/.test(e)), []);
|
|
});
|
|
|
|
test('a skill claiming its own exemption without being allowlisted fails loud', () => {
|
|
const content = withAllSections(
|
|
['---', 'name: alpha', 'description: Designs alphas. Use when building one.', 'exempt: sections', '---'].join('\n')
|
|
);
|
|
const { errors } = lintSkillContent('alpha', content, KNOWN);
|
|
assert.equal(errors.length, 1);
|
|
assert.match(errors[0], /not in the validator's SECTION_EXEMPT_SKILLS allowlist/);
|
|
});
|
|
|
|
// ─── Guardrails on the rules this change sits beside ─────────────────────────
|
|
// Deliberately narrow: #428 rewrites frontmatter parsing and the cross-reference
|
|
// patterns, so asserting their behaviour here would collide with that work.
|
|
|
|
test('a fully valid skill produces no errors', () => {
|
|
const { errors } = lintSkillContent('alpha', withAllSections(VALID_FRONTMATTER), KNOWN);
|
|
assert.deepEqual(errors, []);
|
|
});
|
|
|
|
test('reports a description with no trigger clause', () => {
|
|
const content = withAllSections(
|
|
['---', 'name: alpha', 'description: Designs alpha things and nothing more.', '---'].join('\n')
|
|
);
|
|
const { errors } = lintSkillContent('alpha', content, KNOWN);
|
|
assert.equal(errors.length, 1);
|
|
assert.match(errors[0], /no 'when to use' trigger/);
|
|
});
|
|
|
|
test('reports frontmatter name that disagrees with the directory', () => {
|
|
const content = withAllSections(
|
|
['---', 'name: beta', 'description: Designs alphas. Use when building one.', '---'].join('\n')
|
|
);
|
|
const { errors } = lintSkillContent('alpha', content, KNOWN);
|
|
assert.equal(errors.length, 1);
|
|
assert.match(errors[0], /does not match directory name/);
|
|
});
|
|
|
|
test('reports a workflow step declared without a matching process section', () => {
|
|
const content = withAllSections(VALID_FRONTMATTER).replace(
|
|
'## Common Rationalizations',
|
|
[
|
|
'## The Optimization Workflow',
|
|
'',
|
|
'```',
|
|
'1. MEASURE → Establish a baseline',
|
|
'2. GUARD → Prevent regression',
|
|
'```',
|
|
'',
|
|
'### Step 1: Measure',
|
|
'',
|
|
'Measure first.',
|
|
'',
|
|
'## Common Rationalizations',
|
|
].join('\n'),
|
|
);
|
|
|
|
const { errors } = lintSkillContent('alpha', content, KNOWN);
|
|
|
|
assert.equal(errors.length, 1);
|
|
assert.match(errors[0], /Workflow declares Step 2 but has no matching process section/);
|
|
});
|
|
|
|
test('reports a missing frontmatter block', () => {
|
|
const { errors } = lintSkillContent('alpha', '## Overview\nx\n', KNOWN);
|
|
assert.equal(errors.length, 1);
|
|
assert.match(errors[0], /Missing or malformed YAML frontmatter/);
|
|
});
|