mirror of
https://github.com/addyosmani/agent-skills.git
synced 2026-08-12 18:07:26 +02:00
The path fix has no regression guard: nothing in CI resolves references/ links, so all 18 were broken while CI stayed green. validate-artifact- paths.js is scoped to spec/plan/todo artifacts and says in its own header that it is not a general markdown path linter. Add validate-reference-links.js, which resolves every `references/*.md` link in skills/*/SKILL.md against that skill's own directory. This accepts both conventions in CLAUDE.md: shared checklists reached via ../../references/, and a skill's own colocated references/ directory. Scope stays narrow on purpose. Skills legitimately name paths that do not exist yet -- tasks/todo.md, PERF.md, docs/ideas/[idea-name].md -- and a general markdown linter would fail the build on them. A test pins that. Proven against the pre-fix tree: 18 error(s), exit 1, matching the 18 links fixed in the previous commit. After the fix: 0 error(s), exit 0. 7 unit tests cover the regression itself, colocated references/, markdown-link syntax, a renamed target, multiple violations in one skill, and the non-reference paths that must be ignored. Wired into the validate-skills job, alongside the other skill-content checks. Known limitation: fenced code blocks are not stripped, so a SKILL.md that documents the anti-pattern inside a fence would be flagged. Nothing does today. Sharing stripFencedCodeBlocks looks right once #444 lands. Refs #468 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
154 lines
5.5 KiB
JavaScript
154 lines
5.5 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
'use strict';
|
|
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const os = require('node:os');
|
|
const path = require('node:path');
|
|
const { spawnSync } = require('node:child_process');
|
|
const { afterEach, test } = require('node:test');
|
|
|
|
const VALIDATOR = path.join(__dirname, 'validate-reference-links.js');
|
|
const sandboxes = [];
|
|
|
|
function makeSandbox() {
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-skills-validate-reference-links-test-'));
|
|
const scriptsDir = path.join(root, 'scripts');
|
|
fs.mkdirSync(scriptsDir, { recursive: true });
|
|
fs.copyFileSync(VALIDATOR, path.join(scriptsDir, 'validate-reference-links.js'));
|
|
sandboxes.push(root);
|
|
return root;
|
|
}
|
|
|
|
function writeFile(root, relativePath, content) {
|
|
const file = path.join(root, relativePath);
|
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
fs.writeFileSync(file, content);
|
|
}
|
|
|
|
function run(root) {
|
|
return spawnSync(process.execPath, [path.join(root, 'scripts', 'validate-reference-links.js')], {
|
|
cwd: root,
|
|
encoding: 'utf8',
|
|
});
|
|
}
|
|
|
|
afterEach(() => {
|
|
for (const root of sandboxes.splice(0)) {
|
|
fs.rmSync(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('passes when a skill reaches the shared checklist two levels up', () => {
|
|
const root = makeSandbox();
|
|
writeFile(root, 'references/definition-of-done.md', '# Definition of Done\n');
|
|
writeFile(
|
|
root,
|
|
'skills/using-agent-skills/SKILL.md',
|
|
'See `../../references/definition-of-done.md`.\n'
|
|
);
|
|
|
|
const result = run(root);
|
|
|
|
assert.equal(result.status, 0, result.stdout + result.stderr);
|
|
assert.match(result.stdout, /1 skills checked — 0 error\(s\) — PASSED/);
|
|
});
|
|
|
|
test('fails when a skill links the shared checklist as if it were colocated', () => {
|
|
// The regression: references/ lives at the repo root, but the link is
|
|
// resolved from skills/<name>/, so it points two levels too deep.
|
|
const root = makeSandbox();
|
|
writeFile(root, 'references/definition-of-done.md', '# Definition of Done\n');
|
|
writeFile(
|
|
root,
|
|
'skills/using-agent-skills/SKILL.md',
|
|
'See `references/definition-of-done.md`.\n'
|
|
);
|
|
|
|
const result = run(root);
|
|
|
|
assert.equal(result.status, 1, result.stdout + result.stderr);
|
|
assert.match(result.stdout, /1 skills checked — 1 error\(s\) — FAILED/);
|
|
assert.match(
|
|
result.stdout,
|
|
/L1: references\/definition-of-done\.md — resolves to skills\/using-agent-skills\/references\/definition-of-done\.md/
|
|
);
|
|
assert.match(result.stdout, /use `\.\.\/\.\.\/references\/<file>\.md`/);
|
|
});
|
|
|
|
test('checks markdown link syntax, not just backtick mentions', () => {
|
|
const root = makeSandbox();
|
|
writeFile(root, 'references/definition-of-done.md', '# Definition of Done\n');
|
|
writeFile(root, 'skills/using-agent-skills/SKILL.md', 'See [DoD](references/definition-of-done.md).\n');
|
|
|
|
const result = run(root);
|
|
|
|
assert.equal(result.status, 1, result.stdout + result.stderr);
|
|
assert.match(result.stdout, /L1: references\/definition-of-done\.md/);
|
|
});
|
|
|
|
test('passes when a skill colocates its own references directory', () => {
|
|
// CLAUDE.md allows self-contained skills to keep references under
|
|
// skills/<name>/references/. Those links are correct as written.
|
|
const root = makeSandbox();
|
|
writeFile(root, 'skills/dataviz/references/palette.md', '# Palette\n');
|
|
writeFile(root, 'skills/dataviz/SKILL.md', 'See `references/palette.md`.\n');
|
|
|
|
const result = run(root);
|
|
|
|
assert.equal(result.status, 0, result.stdout + result.stderr);
|
|
assert.match(result.stdout, /1 skills checked — 0 error\(s\) — PASSED/);
|
|
});
|
|
|
|
test('fails when a link points at a checklist that no longer exists', () => {
|
|
const root = makeSandbox();
|
|
writeFile(root, 'references/definition-of-done.md', '# Definition of Done\n');
|
|
writeFile(root, 'skills/shipping-and-launch/SKILL.md', 'See `../../references/renamed.md`.\n');
|
|
|
|
const result = run(root);
|
|
|
|
assert.equal(result.status, 1, result.stdout + result.stderr);
|
|
assert.match(result.stdout, /L1: \.\.\/\.\.\/references\/renamed\.md/);
|
|
assert.match(result.stdout, /1 skills checked — 1 error\(s\) — FAILED/);
|
|
});
|
|
|
|
test('ignores paths that are not references/ links', () => {
|
|
// Skills legitimately name artifacts the user has yet to create. Widening
|
|
// this validator into a general markdown linter would fail the build on them.
|
|
const root = makeSandbox();
|
|
writeFile(root, 'references/definition-of-done.md', '# Definition of Done\n');
|
|
writeFile(
|
|
root,
|
|
'skills/planning-and-task-breakdown/SKILL.md',
|
|
[
|
|
'Save the task list to `tasks/todo.md` and the plan to `tasks/plan.md`.',
|
|
'Record findings in `PERF.md` or `docs/ideas/[idea-name].md`.',
|
|
'Related: `skills/incremental-implementation/SKILL.md`.',
|
|
'See `../../references/definition-of-done.md`.',
|
|
'',
|
|
].join('\n')
|
|
);
|
|
|
|
const result = run(root);
|
|
|
|
assert.equal(result.status, 0, result.stdout + result.stderr);
|
|
assert.match(result.stdout, /1 skills checked — 0 error\(s\) — PASSED/);
|
|
});
|
|
|
|
test('reports every unresolvable link, not just the first per skill', () => {
|
|
const root = makeSandbox();
|
|
writeFile(root, 'references/security-checklist.md', '# Security\n');
|
|
writeFile(root, 'references/performance-checklist.md', '# Performance\n');
|
|
writeFile(
|
|
root,
|
|
'skills/code-review-and-quality/SKILL.md',
|
|
['See `references/security-checklist.md`.', 'And `references/performance-checklist.md`.', ''].join('\n')
|
|
);
|
|
|
|
const result = run(root);
|
|
|
|
assert.equal(result.status, 1, result.stdout + result.stderr);
|
|
assert.match(result.stdout, /1 skills checked — 2 error\(s\) — FAILED/);
|
|
});
|