Files
agent-skills/scripts/validate-artifact-paths-test.js
T
ayobamiseun e02b59df4b ci: guard artifact paths on every command surface, not only Claude Code
validate-artifact-paths.js exists to stop the #93 class of drift, where a
producer moves SPEC.md or tasks/plan.md and a consumer keeps reading the
old path. It guarded only the .claude/commands copies plus two skills and
two docs. The same commands ship twice more — .gemini/commands/*.toml and
the root commands/*.toml — each with its own prompt body that names the
same artifacts, and validate-commands.js deliberately compares only
descriptions, so a drift on either TOML surface merged green while the
Claude Code copy stayed canonical. docs/copilot-setup.md was also unguarded
although its prompt-file aliases spell the artifact paths out for users.

Add the six TOML producers and consumers and the Copilot doc to
GUARDED_FILES, grouped by surface. Rule, allowlist and regex are unchanged.
Four new tests cover a Gemini producer drifting alone, a root TOML consumer
drifting alone, all three surfaces agreeing, and the Copilot doc. All seven
newly guarded files already use canonical paths: 14 files checked, 0 errors.
2026-09-25 08:33:12 +01:00

180 lines
7.3 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-artifact-paths.js');
const sandboxes = [];
function makeSandbox() {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-skills-validate-artifact-paths-test-'));
const scriptsDir = path.join(root, 'scripts');
fs.mkdirSync(scriptsDir, { recursive: true });
fs.copyFileSync(VALIDATOR, path.join(scriptsDir, 'validate-artifact-paths.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-artifact-paths.js')], {
cwd: root,
encoding: 'utf8',
});
}
afterEach(() => {
for (const root of sandboxes.splice(0)) {
fs.rmSync(root, { recursive: true, force: true });
}
});
test('passes when producers and consumers use the canonical artifact paths', () => {
const root = makeSandbox();
writeFile(root, '.claude/commands/spec.md', 'Save the spec as `SPEC.md` in the project root.\n');
writeFile(root, '.claude/commands/plan.md', 'Save the plan to `tasks/plan.md` and task list to `tasks/todo.md`.\n');
writeFile(root, '.claude/commands/build.md', 'Look for a spec at `SPEC.md`, `docs/SPEC.md`, or under `spec/`. Require `tasks/plan.md`.\n');
writeFile(root, 'skills/spec-driven-development/SKILL.md', 'Save the plan to `tasks/plan.md` and the task list to `tasks/todo.md`.\n');
writeFile(root, 'skills/planning-and-task-breakdown/SKILL.md', 'Save to `tasks/plan.md` and `tasks/todo.md`.\n');
const result = run(root);
assert.equal(result.status, 0, result.stdout + result.stderr);
assert.match(result.stdout, /5 files checked — 0 error\(s\) — PASSED/);
});
test('fails when a producer drifts to an unapproved artifact path (the #93 regression)', () => {
const root = makeSandbox();
// Only the producers are changed to the per-feature layout; consumers stay on the old paths.
writeFile(root, '.claude/commands/spec.md', 'Save the spec to `docs/features/[feature-name]/spec.md`.\n');
writeFile(root, '.claude/commands/plan.md', 'Save the plan to `docs/features/[feature-name]/plan.md`.\n');
writeFile(root, '.claude/commands/build.md', 'Require a spec at `SPEC.md` and a plan at `tasks/plan.md`.\n');
const result = run(root);
assert.equal(result.status, 1, result.stdout + result.stderr);
assert.match(result.stdout, /docs\/features\/\[feature-name\]\/spec\.md/);
assert.match(result.stdout, /docs\/features\/\[feature-name\]\/plan\.md/);
assert.match(result.stdout, /error\(s\) — FAILED/);
});
test('reports the offending file and line number', () => {
const root = makeSandbox();
writeFile(root, '.claude/commands/plan.md', 'Intro line.\nSave the plan to `plan.md` in the root.\n');
const result = run(root);
assert.equal(result.status, 1, result.stdout + result.stderr);
assert.match(result.stdout, /\.claude\/commands\/plan\.md/);
assert.match(result.stdout, /L2:/);
});
test('accepts the docs/SPEC.md alternate spec location', () => {
const root = makeSandbox();
writeFile(root, '.claude/commands/build.md', 'Look for the spec at `docs/SPEC.md`.\n');
const result = run(root);
assert.equal(result.status, 0, result.stdout + result.stderr);
assert.match(result.stdout, /1 files checked — 0 error\(s\) — PASSED/);
});
test('ignores non-artifact markdown references (no false positives)', () => {
const root = makeSandbox();
writeFile(
root,
'skills/spec-driven-development/SKILL.md',
'See `SKILL.md` and `references/testing-patterns.md`. Save the plan to `tasks/plan.md`.\n',
);
const result = run(root);
assert.equal(result.status, 0, result.stdout + result.stderr);
assert.match(result.stdout, /1 files checked — 0 error\(s\) — PASSED/);
});
test('skips guarded files that do not exist', () => {
const root = makeSandbox();
writeFile(root, '.claude/commands/spec.md', 'Save the spec as `SPEC.md`.\n');
// No other guarded files present.
const result = run(root);
assert.equal(result.status, 0, result.stdout + result.stderr);
assert.match(result.stdout, /1 files checked — 0 error\(s\) — PASSED/);
});
// ─── Every command surface is guarded, not only the Claude Code copy ────────
//
// The Gemini and root TOML command sets carry their own prompt bodies, which
// validate-commands.js deliberately does not compare. A producer can therefore
// drift on one surface while `.claude/commands/` stays correct, and before
// these files were guarded that drift merged silently.
test('fails when a Gemini command drifts while the Claude Code copy stays canonical', () => {
const root = makeSandbox();
writeFile(root, '.claude/commands/spec.md', 'Save the spec as `SPEC.md` in the project root.\n');
writeFile(
root,
'.gemini/commands/spec.toml',
'description = "Start spec-driven development"\n\nprompt = """\nSave the spec to `docs/features/[feature-name]/spec.md`.\n"""\n',
);
const result = run(root);
assert.equal(result.status, 1, result.stdout + result.stderr);
assert.match(result.stdout, /\.gemini\/commands\/spec\.toml/);
assert.match(result.stdout, /L4: docs\/features\/\[feature-name\]\/spec\.md/);
assert.match(result.stdout, /1 error\(s\) — FAILED/);
});
test('fails when a root TOML consumer drifts away from the producers', () => {
const root = makeSandbox();
writeFile(root, 'commands/planning.toml', 'prompt = """\nSave the plan to `tasks/plan.md` and the task list to `tasks/todo.md`.\n"""\n');
writeFile(root, 'commands/build.toml', 'prompt = """\nRead the plan from `planning/plan.md` before starting.\n"""\n');
const result = run(root);
assert.equal(result.status, 1, result.stdout + result.stderr);
assert.match(result.stdout, /commands\/build\.toml/);
assert.match(result.stdout, /planning\/plan\.md/);
});
test('passes when all three command surfaces agree on the canonical paths', () => {
const root = makeSandbox();
for (const dir of ['.claude/commands', '.gemini/commands', 'commands']) {
const ext = dir === '.claude/commands' ? 'md' : 'toml';
const planStem = dir === '.claude/commands' ? 'plan' : 'planning';
writeFile(root, `${dir}/spec.${ext}`, 'Save the spec as `SPEC.md`.\n');
writeFile(root, `${dir}/${planStem}.${ext}`, 'Save to `tasks/plan.md` and `tasks/todo.md`.\n');
writeFile(root, `${dir}/build.${ext}`, 'Require `SPEC.md` or `docs/SPEC.md`, and `tasks/plan.md`.\n');
}
const result = run(root);
assert.equal(result.status, 0, result.stdout + result.stderr);
assert.match(result.stdout, /9 files checked — 0 error\(s\) — PASSED/);
});
test('guards the Copilot setup doc, whose prompt-file aliases name the artifacts', () => {
const root = makeSandbox();
writeFile(root, 'docs/copilot-setup.md', 'Read `tasks/plan.md`, then write the spec to `specs/[name]/spec.md`.\n');
const result = run(root);
assert.equal(result.status, 1, result.stdout + result.stderr);
assert.match(result.stdout, /docs\/copilot-setup\.md/);
assert.match(result.stdout, /specs\/\[name\]\/spec\.md/);
});