/** * Regression test for bug #2808 * * All 85 GSD SKILL.md files declared `name: gsd:` (colon), the deprecated * form. Claude Code surfaces the `name:` frontmatter field in autocomplete, so * users saw `/gsd:add-phase` suggestions instead of the canonical `/gsd-add-phase`. * * Root cause: skillFrontmatterName() in bin/install.js converted hyphenated * skill dir names to colon form (gsd-add-phase → gsd:add-phase) because * workflows called Skill(skill="gsd:"). That was the original fix for * #2643. Since then, workflows have been updated to use hyphen form (#2808). * * Fix: skillFrontmatterName() now returns the hyphen form unchanged. * Four workflow Skill() colon calls updated to hyphen. * * This test verifies: * 1. skillFrontmatterName returns hyphen form (not colon). * 2. Installed SKILL.md would emit name: gsd- (not gsd:). * 3. No workflow contains a Skill(skill="gsd:") colon call. */ 'use strict'; process.env.GSD_TEST_MODE = '1'; const { describe, test } = require('node:test'); const assert = require('node:assert/strict'); const fs = require('node:fs'); const path = require('node:path'); const ROOT = path.join(__dirname, '..'); const { convertClaudeCommandToClaudeSkill, skillFrontmatterName } = require(path.join(ROOT, 'bin', 'install.js')); const WORKFLOWS_DIR = path.join(ROOT, 'get-shit-done', 'workflows'); const COMMANDS_DIR = path.join(ROOT, 'commands', 'gsd'); function walkMd(dir) { const files = []; try { for (const e of fs.readdirSync(dir, { withFileTypes: true })) { const full = path.join(dir, e.name); if (e.isDirectory()) files.push(...walkMd(full)); else if (e.name.endsWith('.md')) files.push(full); } } catch (err) { assert.fail(`failed to read markdown files from ${dir}: ${err.message}`); } return files; } describe('bug-2808: SKILL.md name: uses hyphen form', () => { test('skillFrontmatterName returns hyphen form (not colon)', () => { assert.strictEqual(skillFrontmatterName('gsd-add-phase'), 'gsd-add-phase'); assert.strictEqual(skillFrontmatterName('gsd-plan-phase'), 'gsd-plan-phase'); assert.strictEqual(skillFrontmatterName('gsd-autonomous'), 'gsd-autonomous'); }); test('generated SKILL.md contains name: gsd- (not gsd:)', () => { const cmdFiles = fs.readdirSync(COMMANDS_DIR).filter(f => f.endsWith('.md')); assert.ok(cmdFiles.length > 0, 'expected GSD command files'); for (const cmd of cmdFiles) { const base = cmd.replace(/\.md$/, ''); const skillDirName = 'gsd-' + base; const src = fs.readFileSync(path.join(COMMANDS_DIR, cmd), 'utf-8'); const skillContent = convertClaudeCommandToClaudeSkill(src, skillDirName); // Parse frontmatter structurally: extract name: line from the --- block. const fmMatch = skillContent.match(/^---\n([\s\S]*?)\n---/); assert.ok(fmMatch, `${cmd}: generated skill content must have a frontmatter block`); const fmLines = fmMatch[1].split('\n'); const nameEntry = fmLines.find((l) => l.startsWith('name:')); assert.ok(nameEntry, `${cmd}: generated SKILL.md is missing required name: field`); const name = nameEntry.replace(/^name:\s*/, '').trim(); assert.ok( !name.includes(':'), `${cmd}: SKILL.md name should be hyphen form, got "${name}"` ); assert.ok( name.startsWith('gsd-'), `${cmd}: SKILL.md name should start with gsd-, got "${name}"` ); } }); test('no workflow contains Skill(skill="gsd:") colon form', () => { const workflowFiles = walkMd(WORKFLOWS_DIR); assert.ok( workflowFiles.length > 0, `expected workflow markdown files under ${WORKFLOWS_DIR}` ); const colonCalls = []; for (const f of workflowFiles) { const src = fs.readFileSync(f, 'utf-8'); // Strip HTML comments to avoid matching commented-out examples. const stripped = src.replace(//g, ''); // Scan each line for Skill() calls using the colon form. // Parsing line-by-line is more precise than a multi-line regex // and avoids false positives from incidental matches in prose. for (const line of stripped.split('\n')) { const colonCallRe = /Skill\(skill=['"]gsd:([a-z0-9-]+)['"]/gi; let m; while ((m = colonCallRe.exec(line)) !== null) { colonCalls.push(`${path.basename(f)}: Skill(skill="gsd:${m[1]}")`); } } } assert.deepStrictEqual( colonCalls, [], 'deprecated colon-form Skill() calls found — update to gsd-: ' + colonCalls.join(', ') ); }); });