From b6199460ed0cad9150ce9ae096a26dd7bb89e4c5 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Mon, 8 Jun 2026 16:05:59 -0400 Subject: [PATCH] refactor(#887): consolidate duplicated CHILD_ROUTER test maps into shared helper (#889) Three test files copy-pasted the concrete-skill->namespace-router CHILD_ROUTER map verbatim, and two more duplicated an identical local parseRouterRequires regex. Introduce tests/helpers/nested-layout.cjs that derives the child->router map from the authoritative commands/gsd/ns-*.md requires: lists once, reusing the production parseRequires (now exported from install-profiles) plus a nestedSkillPath(skillsRoot, prefix, stem) helper. Refactor all five test files to import from it. Test-only + a single internal export addition; no production behavior change. Closes #887 Co-authored-by: Claude Opus 4.8 --- src/install-profiles.cts | 1 + tests/bug-782-cline-skills-emission.test.cjs | 55 ++-------------- tests/enh-2792-namespace-skills.test.cjs | 14 +--- ...h-769-context-fork-effort.install.test.cjs | 63 +++--------------- tests/helpers/nested-layout.cjs | 66 +++++++++++++++++++ tests/install-nested-layout.test.cjs | 26 +------- tests/install.test.cjs | 49 ++------------ 7 files changed, 90 insertions(+), 184 deletions(-) create mode 100644 tests/helpers/nested-layout.cjs diff --git a/src/install-profiles.cts b/src/install-profiles.cts index 44e1cd8f9..fcf5afb10 100644 --- a/src/install-profiles.cts +++ b/src/install-profiles.cts @@ -720,6 +720,7 @@ export = { readActiveProfile, writeActiveProfile, // Shared internals + parseRequires, cleanupStagedSkills, // Back-compat / deprecated MINIMAL_SKILL_ALLOWLIST, diff --git a/tests/bug-782-cline-skills-emission.test.cjs b/tests/bug-782-cline-skills-emission.test.cjs index cf3d5789e..952beb95b 100644 --- a/tests/bug-782-cline-skills-emission.test.cjs +++ b/tests/bug-782-cline-skills-emission.test.cjs @@ -37,57 +37,12 @@ const { resolveProfile, } = require('../gsd-core/bin/lib/install-profiles.cjs'); +const { nestedSkillPath } = require('./helpers/nested-layout.cjs'); + const REAL_COMMANDS_DIR = path.join(__dirname, '..', 'commands', 'gsd'); const MANIFEST = loadSkillsManifest(REAL_COMMANDS_DIR); const RESOLVED_CORE = resolveProfile({ modes: ['core'], manifest: MANIFEST }); -/** - * Map from concrete skill stem → ns-* router stem. - * Used for nesting runtimes (claude, cline, qwen, etc.) when the full profile - * is installed: concrete skills live at /gsd-/skills//SKILL.md. - */ -const CHILD_ROUTER = { - // ns-workflow - 'discuss-phase': 'ns-workflow', 'spec-phase': 'ns-workflow', 'plan-phase': 'ns-workflow', - 'execute-phase': 'ns-workflow', 'verify-work': 'ns-workflow', 'phase': 'ns-workflow', - 'progress': 'ns-workflow', 'ultraplan-phase': 'ns-workflow', - 'plan-review-convergence': 'ns-workflow', 'add-tests': 'ns-workflow', - 'ai-integration-phase': 'ns-workflow', 'autonomous': 'ns-workflow', - 'fast': 'ns-workflow', 'mvp-phase': 'ns-workflow', 'quick': 'ns-workflow', - // ns-project - 'new-project': 'ns-project', 'new-milestone': 'ns-project', 'complete-milestone': 'ns-project', - 'audit-milestone': 'ns-project', 'milestone-summary': 'ns-project', 'import': 'ns-project', - 'ingest-docs': 'ns-project', 'profile-user': 'ns-project', 'review-backlog': 'ns-project', - // ns-review - 'code-review': 'ns-review', 'audit-uat': 'ns-review', 'secure-phase': 'ns-review', - 'eval-review': 'ns-review', 'ui-review': 'ns-review', 'validate-phase': 'ns-review', - 'debug': 'ns-review', 'forensics': 'ns-review', 'audit-fix': 'ns-review', - 'review': 'ns-review', 'ui-phase': 'ns-review', - // ns-context - 'map-codebase': 'ns-context', 'graphify': 'ns-context', 'docs-update': 'ns-context', - 'extract-learnings': 'ns-context', - // ns-ideate - 'capture': 'ns-ideate', 'explore': 'ns-ideate', 'sketch': 'ns-ideate', - 'spike': 'ns-ideate', - // ns-manage - 'config': 'ns-manage', 'workspace': 'ns-manage', 'workstreams': 'ns-manage', - 'thread': 'ns-manage', 'pause-work': 'ns-manage', 'resume-work': 'ns-manage', - 'update': 'ns-manage', 'ship': 'ns-manage', 'inbox': 'ns-manage', - 'pr-branch': 'ns-manage', 'undo': 'ns-manage', 'cleanup': 'ns-manage', - 'health': 'ns-manage', 'manager': 'ns-manage', 'settings': 'ns-manage', - 'stats': 'ns-manage', 'surface': 'ns-manage', 'help': 'ns-manage', -}; - -/** - * Returns the nested SKILL.md path for a concrete skill stem on cline - * (prefix='gsd-'): /gsd-/skills//SKILL.md - */ -function nestedClineSkillPath(skillsRoot, stem) { - const router = CHILD_ROUTER[stem]; - if (!router) throw new Error(`No router mapping for stem: ${stem}`); - return path.join(skillsRoot, 'gsd-' + router, 'skills', stem, 'SKILL.md'); -} - // ─── (a) Converter unit test ───────────────────────────────────────────────── const SAMPLE_COMMAND = `--- @@ -395,7 +350,7 @@ describe('install() global cline — coexistence: skills AND .clinerules', () => ); // full profile: gsd-help is nested under gsd-ns-manage/skills/help/SKILL.md - const helpSkillFile = nestedClineSkillPath(skillsDir, 'help'); + const helpSkillFile = nestedSkillPath(skillsDir, 'gsd-', 'help'); assert.ok( fs.existsSync(helpSkillFile), `${path.relative(tmpGlobalDir, helpSkillFile)} must exist under ${tmpGlobalDir} — skills emission broken for global cline` @@ -486,7 +441,7 @@ describe('convertClaudeToCliineMarkdown — bare ~/.claude and CLAUDE_CONFIG_DIR installRuntimeArtifacts('cline', configDir, 'global', RESOLVED_FULL); // full profile: surface is nested under gsd-ns-manage/skills/surface/SKILL.md - const surfaceSkill = nestedClineSkillPath(path.join(configDir, 'skills'), 'surface'); + const surfaceSkill = nestedSkillPath(path.join(configDir, 'skills'), 'gsd-', 'surface'); assert.ok(fs.existsSync(surfaceSkill), `${path.relative(configDir, surfaceSkill)} must exist for full profile`); const content = fs.readFileSync(surfaceSkill, 'utf8'); @@ -546,7 +501,7 @@ describe('_applyRuntimeRewrites — cline custom-dir embedded path (Fix 1)', () // gsd-surface SKILL.md references config paths; with a custom configDir // (not under $HOME), pathPrefix will be the absolute custom path. // full profile: surface is nested under gsd-ns-manage/skills/surface/SKILL.md - const surfaceSkill = nestedClineSkillPath(path.join(configDir, 'skills'), 'surface'); + const surfaceSkill = nestedSkillPath(path.join(configDir, 'skills'), 'gsd-', 'surface'); assert.ok(fs.existsSync(surfaceSkill), `${path.relative(configDir, surfaceSkill)} must exist`); const content = fs.readFileSync(surfaceSkill, 'utf8'); diff --git a/tests/enh-2792-namespace-skills.test.cjs b/tests/enh-2792-namespace-skills.test.cjs index acb194622..464fde31c 100644 --- a/tests/enh-2792-namespace-skills.test.cjs +++ b/tests/enh-2792-namespace-skills.test.cjs @@ -9,6 +9,8 @@ const assert = require('node:assert/strict'); const fs = require('node:fs'); const path = require('node:path'); +const { parseRequires } = require('./helpers/nested-layout.cjs'); + const COMMANDS_DIR = path.join(__dirname, '..', 'commands', 'gsd'); const NAMESPACE_SKILLS = [ @@ -216,16 +218,6 @@ describe('gsd-health --context flag is wired into command + workflow', () => { const NS_FILES = NAMESPACE_SKILLS.map((ns) => ns.file); -/** - * Parse the `requires:` flow-style array from a router file's raw content. - * Matches `requires: [a, b, c]` anywhere (frontmatter or body — always in fm). - */ -function parseRouterRequires(content) { - const m = content.match(/^requires:\s*\[([^\]]*)\]/m); - if (!m) return []; - return m[1].split(',').map((s) => s.trim()).filter(Boolean); -} - describe('namespace nesting completeness (#69)', () => { // Build the concrete-skill set once (all *.md minus ns-*.md) const allFiles = fs.readdirSync(COMMANDS_DIR).filter((f) => f.endsWith('.md')); @@ -240,7 +232,7 @@ describe('namespace nesting completeness (#69)', () => { for (const f of NS_FILES) { const stem = f.replace(/\.md$/, ''); const content = fs.readFileSync(path.join(COMMANDS_DIR, f), 'utf-8'); - routerRequires.set(stem, parseRouterRequires(content)); + routerRequires.set(stem, parseRequires(content)); } const allRoutedStems = new Set([...routerRequires.values()].flat()); diff --git a/tests/enh-769-context-fork-effort.install.test.cjs b/tests/enh-769-context-fork-effort.install.test.cjs index c192914e2..422128ec7 100644 --- a/tests/enh-769-context-fork-effort.install.test.cjs +++ b/tests/enh-769-context-fork-effort.install.test.cjs @@ -33,6 +33,7 @@ const os = require('node:os'); const { install, convertClaudeCommandToClaudeSkill } = require('../bin/install.js'); const { cleanup } = require('./helpers.cjs'); +const { nestedSkillPath } = require('./helpers/nested-layout.cjs'); const REPO_ROOT = path.resolve(__dirname, '..'); const SOURCE_COMMANDS_DIR = path.join(REPO_ROOT, 'commands', 'gsd'); @@ -43,52 +44,6 @@ function makeTmpDir(prefix) { return fs.mkdtempSync(path.join(os.tmpdir(), prefix)); } -/** - * Map from concrete skill stem → ns-* router stem for nesting runtimes. - * Derived from the authoritative ns-*.md `requires:` lists. - */ -const CHILD_ROUTER = { - // ns-workflow - 'discuss-phase': 'ns-workflow', 'spec-phase': 'ns-workflow', 'plan-phase': 'ns-workflow', - 'execute-phase': 'ns-workflow', 'verify-work': 'ns-workflow', 'phase': 'ns-workflow', - 'progress': 'ns-workflow', 'ultraplan-phase': 'ns-workflow', - 'plan-review-convergence': 'ns-workflow', 'add-tests': 'ns-workflow', - 'ai-integration-phase': 'ns-workflow', 'autonomous': 'ns-workflow', - 'fast': 'ns-workflow', 'mvp-phase': 'ns-workflow', 'quick': 'ns-workflow', - // ns-project - 'new-project': 'ns-project', 'new-milestone': 'ns-project', 'complete-milestone': 'ns-project', - 'audit-milestone': 'ns-project', 'milestone-summary': 'ns-project', 'import': 'ns-project', - 'ingest-docs': 'ns-project', 'profile-user': 'ns-project', 'review-backlog': 'ns-project', - // ns-review - 'code-review': 'ns-review', 'audit-uat': 'ns-review', 'secure-phase': 'ns-review', - 'eval-review': 'ns-review', 'ui-review': 'ns-review', 'validate-phase': 'ns-review', - 'debug': 'ns-review', 'forensics': 'ns-review', 'audit-fix': 'ns-review', - 'review': 'ns-review', 'ui-phase': 'ns-review', - // ns-context - 'map-codebase': 'ns-context', 'graphify': 'ns-context', 'docs-update': 'ns-context', - 'extract-learnings': 'ns-context', - // ns-ideate - 'capture': 'ns-ideate', 'explore': 'ns-ideate', 'sketch': 'ns-ideate', - 'spike': 'ns-ideate', - // ns-manage - 'config': 'ns-manage', 'workspace': 'ns-manage', 'workstreams': 'ns-manage', - 'thread': 'ns-manage', 'pause-work': 'ns-manage', 'resume-work': 'ns-manage', - 'update': 'ns-manage', 'ship': 'ns-manage', 'inbox': 'ns-manage', - 'pr-branch': 'ns-manage', 'undo': 'ns-manage', 'cleanup': 'ns-manage', - 'health': 'ns-manage', 'manager': 'ns-manage', 'settings': 'ns-manage', - 'stats': 'ns-manage', 'surface': 'ns-manage', 'help': 'ns-manage', -}; - -/** - * Returns the nested SKILL.md path for a concrete skill stem on Claude - * (prefix='gsd-'): /gsd-/skills//SKILL.md - */ -function nestedClaudeSkillPath(skillsRoot, stem) { - const router = CHILD_ROUTER[stem]; - if (!router) throw new Error(`No router mapping for stem: ${stem}`); - return path.join(skillsRoot, 'gsd-' + router, 'skills', stem, 'SKILL.md'); -} - function readFrontmatter(mdPath) { const content = fs.readFileSync(mdPath, 'utf8'); if (!content.startsWith('---')) return ''; @@ -299,7 +254,7 @@ describe('#769 Claude global install: SKILL.md files preserve context: fork and test('gsd-autonomous SKILL.md has context: fork after global install', () => { runClaudeGlobalInstall(claudeHome); - const skillPath = nestedClaudeSkillPath(path.join(claudeHome, 'skills'), 'autonomous'); + const skillPath = nestedSkillPath(path.join(claudeHome, 'skills'), 'gsd-', 'autonomous'); const fm = readFrontmatter(skillPath); assert.match(fm, /^context:[ \t]*fork$/m, `gsd-autonomous SKILL.md must have context: fork\nActual:\n${fm}`); @@ -307,7 +262,7 @@ describe('#769 Claude global install: SKILL.md files preserve context: fork and test('gsd-autonomous SKILL.md has effort: xhigh after global install', () => { runClaudeGlobalInstall(claudeHome); - const skillPath = nestedClaudeSkillPath(path.join(claudeHome, 'skills'), 'autonomous'); + const skillPath = nestedSkillPath(path.join(claudeHome, 'skills'), 'gsd-', 'autonomous'); const fm = readFrontmatter(skillPath); assert.match(fm, /^effort:[ \t]*xhigh$/m, `gsd-autonomous SKILL.md must have effort: xhigh\nActual:\n${fm}`); @@ -315,7 +270,7 @@ describe('#769 Claude global install: SKILL.md files preserve context: fork and test('gsd-execute-phase SKILL.md has context: fork after global install', () => { runClaudeGlobalInstall(claudeHome); - const skillPath = nestedClaudeSkillPath(path.join(claudeHome, 'skills'), 'execute-phase'); + const skillPath = nestedSkillPath(path.join(claudeHome, 'skills'), 'gsd-', 'execute-phase'); const fm = readFrontmatter(skillPath); assert.match(fm, /^context:[ \t]*fork$/m, `gsd-execute-phase SKILL.md must have context: fork\nActual:\n${fm}`); @@ -323,7 +278,7 @@ describe('#769 Claude global install: SKILL.md files preserve context: fork and test('gsd-execute-phase SKILL.md has effort: xhigh after global install', () => { runClaudeGlobalInstall(claudeHome); - const skillPath = nestedClaudeSkillPath(path.join(claudeHome, 'skills'), 'execute-phase'); + const skillPath = nestedSkillPath(path.join(claudeHome, 'skills'), 'gsd-', 'execute-phase'); const fm = readFrontmatter(skillPath); assert.match(fm, /^effort:[ \t]*xhigh$/m, `gsd-execute-phase SKILL.md must have effort: xhigh\nActual:\n${fm}`); @@ -331,7 +286,7 @@ describe('#769 Claude global install: SKILL.md files preserve context: fork and test('gsd-plan-phase SKILL.md has context: fork after global install', () => { runClaudeGlobalInstall(claudeHome); - const skillPath = nestedClaudeSkillPath(path.join(claudeHome, 'skills'), 'plan-phase'); + const skillPath = nestedSkillPath(path.join(claudeHome, 'skills'), 'gsd-', 'plan-phase'); const fm = readFrontmatter(skillPath); assert.match(fm, /^context:[ \t]*fork$/m, `gsd-plan-phase SKILL.md must have context: fork\nActual:\n${fm}`); @@ -339,7 +294,7 @@ describe('#769 Claude global install: SKILL.md files preserve context: fork and test('gsd-plan-phase SKILL.md has effort: xhigh after global install', () => { runClaudeGlobalInstall(claudeHome); - const skillPath = nestedClaudeSkillPath(path.join(claudeHome, 'skills'), 'plan-phase'); + const skillPath = nestedSkillPath(path.join(claudeHome, 'skills'), 'gsd-', 'plan-phase'); const fm = readFrontmatter(skillPath); assert.match(fm, /^effort:[ \t]*xhigh$/m, `gsd-plan-phase SKILL.md must have effort: xhigh\nActual:\n${fm}`); @@ -347,7 +302,7 @@ describe('#769 Claude global install: SKILL.md files preserve context: fork and test('gsd-progress SKILL.md has effort: low after global install', () => { runClaudeGlobalInstall(claudeHome); - const skillPath = nestedClaudeSkillPath(path.join(claudeHome, 'skills'), 'progress'); + const skillPath = nestedSkillPath(path.join(claudeHome, 'skills'), 'gsd-', 'progress'); const fm = readFrontmatter(skillPath); assert.match(fm, /^effort:[ \t]*low$/m, `gsd-progress SKILL.md must have effort: low\nActual:\n${fm}`); @@ -355,7 +310,7 @@ describe('#769 Claude global install: SKILL.md files preserve context: fork and test('gsd-stats SKILL.md has effort: low after global install', () => { runClaudeGlobalInstall(claudeHome); - const skillPath = nestedClaudeSkillPath(path.join(claudeHome, 'skills'), 'stats'); + const skillPath = nestedSkillPath(path.join(claudeHome, 'skills'), 'gsd-', 'stats'); const fm = readFrontmatter(skillPath); assert.match(fm, /^effort:[ \t]*low$/m, `gsd-stats SKILL.md must have effort: low\nActual:\n${fm}`); diff --git a/tests/helpers/nested-layout.cjs b/tests/helpers/nested-layout.cjs new file mode 100644 index 000000000..52fa85c80 --- /dev/null +++ b/tests/helpers/nested-layout.cjs @@ -0,0 +1,66 @@ +'use strict'; +/** + * Shared helpers for the namespace nested-skill install-layout tests (#69). + * + * Single source of truth for the concrete-skill → namespace-router map and the + * nested SKILL.md path layout. The map is DERIVED at require-time by parsing + * each commands/gsd/ns-*.md router's `requires:` frontmatter list with the + * production parser (install-profiles.parseRequires) — never hand-maintained. + */ + +const fs = require('node:fs'); +const path = require('node:path'); + +const { parseRequires } = require('../../gsd-core/bin/lib/install-profiles.cjs'); + +const COMMANDS_GSD = path.join(__dirname, '..', '..', 'commands', 'gsd'); + +// Router stems (ns-*.md basenames), discovered from disk and sorted. +const ROUTER_STEMS = fs + .readdirSync(COMMANDS_GSD) + .filter((f) => f.startsWith('ns-') && f.endsWith('.md')) + .map((f) => f.replace(/\.md$/, '')) + .sort(); + +/** + * Read a router's `requires:` child list from commands/gsd/.md + * using the production frontmatter parser. + */ +function routerChildren(routerStem) { + const srcFile = path.join(COMMANDS_GSD, `${routerStem}.md`); + return parseRequires(fs.readFileSync(srcFile, 'utf8')); +} + +// concrete-skill stem -> router stem (e.g. 'plan-phase' -> 'ns-workflow'). +// A few skills are intentionally multi-owner (e.g. 'spec-phase' is required by +// both ns-ideate and ns-workflow). ROUTER_STEMS is sorted, so the +// alphabetically-last owner wins — which reproduces the prior hand-maintained +// maps (spec-phase -> ns-workflow). The enh-2792 router-completeness tests +// guard the requires: lists themselves. +const CHILD_ROUTER = {}; +for (const routerStem of ROUTER_STEMS) { + for (const child of routerChildren(routerStem)) { + CHILD_ROUTER[child] = routerStem; + } +} + +/** + * Nested SKILL.md path for a concrete skill stem: + * //skills//SKILL.md + * prefix is '' for runtimes that nest under a skills/gsd parent dir (hermes), + * or 'gsd-' for flat-prefixed runtimes (claude, cline, qwen, …). + */ +function nestedSkillPath(skillsRoot, prefix, stem) { + const router = CHILD_ROUTER[stem]; + if (!router) throw new Error(`No router mapping for stem: ${stem}`); + return path.join(skillsRoot, prefix + router, 'skills', stem, 'SKILL.md'); +} + +module.exports = { + COMMANDS_GSD, + ROUTER_STEMS, + CHILD_ROUTER, + routerChildren, + nestedSkillPath, + parseRequires, +}; diff --git a/tests/install-nested-layout.test.cjs b/tests/install-nested-layout.test.cjs index e59b96e7d..f3a9fd50f 100644 --- a/tests/install-nested-layout.test.cjs +++ b/tests/install-nested-layout.test.cjs @@ -14,9 +14,6 @@ const fs = require('node:fs'); const path = require('node:path'); const os = require('node:os'); -const ROOT = path.join(__dirname, '..'); -const COMMANDS_GSD = path.join(ROOT, 'commands', 'gsd'); - const { installRuntimeArtifacts, } = require('../bin/install.js'); @@ -28,6 +25,8 @@ const { resolveProfile, } = require('../gsd-core/bin/lib/install-profiles.cjs'); +const { COMMANDS_GSD, ROUTER_STEMS, routerChildren } = require('./helpers/nested-layout.cjs'); + // --------------------------------------------------------------------------- // Runtime parity decision matrix (#69) // --------------------------------------------------------------------------- @@ -52,31 +51,10 @@ const FLAT = [ { runtime: 'kilo', scope: 'global', skillsSub: 'skills' }, ]; -const ROUTER_STEMS = ['ns-context', 'ns-ideate', 'ns-manage', 'ns-project', 'ns-review', 'ns-workflow']; - // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- -/** - * Parse the `requires:` flow-style array from a router file's raw content. - * Matches `requires: [a, b, c]` (inline array form). - */ -function parseRouterRequires(content) { - const m = content.match(/^requires:\s*\[([^\]]*)\]/m); - if (!m) return []; - return m[1].split(',').map((s) => s.trim()).filter(Boolean); -} - -/** - * Read the requires list for a router stem from the source commands/gsd dir. - */ -function routerChildren(routerStem) { - const srcFile = path.join(COMMANDS_GSD, `${routerStem}.md`); - const content = fs.readFileSync(srcFile, 'utf-8'); - return parseRouterRequires(content); -} - /** * Create a fresh temp dir, run installRuntimeArtifacts into it, and return * the tmpDir path. Caller must cleanup in finally. diff --git a/tests/install.test.cjs b/tests/install.test.cjs index ce52a4e61..b43e91000 100644 --- a/tests/install.test.cjs +++ b/tests/install.test.cjs @@ -53,42 +53,7 @@ const { walk, } = require('./helpers/install-shared.cjs'); -/** - * Map from concrete skill stem → ns-* router stem for nesting runtimes. - * These runtimes nest concrete skills at //skills//SKILL.md - * (claude/cline/qwen/trae/augment/antigravity: prefix='gsd-'; hermes: prefix=''). - */ -const CHILD_ROUTER = { - // ns-workflow - 'discuss-phase': 'ns-workflow', 'spec-phase': 'ns-workflow', 'plan-phase': 'ns-workflow', - 'execute-phase': 'ns-workflow', 'verify-work': 'ns-workflow', 'phase': 'ns-workflow', - 'progress': 'ns-workflow', 'ultraplan-phase': 'ns-workflow', - 'plan-review-convergence': 'ns-workflow', 'add-tests': 'ns-workflow', - 'ai-integration-phase': 'ns-workflow', 'autonomous': 'ns-workflow', - 'fast': 'ns-workflow', 'mvp-phase': 'ns-workflow', 'quick': 'ns-workflow', - // ns-project - 'new-project': 'ns-project', 'new-milestone': 'ns-project', 'complete-milestone': 'ns-project', - 'audit-milestone': 'ns-project', 'milestone-summary': 'ns-project', 'import': 'ns-project', - 'ingest-docs': 'ns-project', 'profile-user': 'ns-project', 'review-backlog': 'ns-project', - // ns-review - 'code-review': 'ns-review', 'audit-uat': 'ns-review', 'secure-phase': 'ns-review', - 'eval-review': 'ns-review', 'ui-review': 'ns-review', 'validate-phase': 'ns-review', - 'debug': 'ns-review', 'forensics': 'ns-review', 'audit-fix': 'ns-review', - 'review': 'ns-review', 'ui-phase': 'ns-review', - // ns-context - 'map-codebase': 'ns-context', 'graphify': 'ns-context', 'docs-update': 'ns-context', - 'extract-learnings': 'ns-context', - // ns-ideate - 'capture': 'ns-ideate', 'explore': 'ns-ideate', 'sketch': 'ns-ideate', - 'spike': 'ns-ideate', - // ns-manage - 'config': 'ns-manage', 'workspace': 'ns-manage', 'workstreams': 'ns-manage', - 'thread': 'ns-manage', 'pause-work': 'ns-manage', 'resume-work': 'ns-manage', - 'update': 'ns-manage', 'ship': 'ns-manage', 'inbox': 'ns-manage', - 'pr-branch': 'ns-manage', 'undo': 'ns-manage', 'cleanup': 'ns-manage', - 'health': 'ns-manage', 'manager': 'ns-manage', 'settings': 'ns-manage', - 'stats': 'ns-manage', 'surface': 'ns-manage', 'help': 'ns-manage', -}; +const { CHILD_ROUTER, nestedSkillPath } = require('./helpers/nested-layout.cjs'); // ─── Section 1: getDirName / getGlobalConfigDir / getConfigDirFromHome ────────── @@ -346,9 +311,7 @@ describe('install/uninstall — hermes (nested skills/gsd//skills/ assert.strictEqual(result.configDir, fs.realpathSync(targetDir)); // hermes nests: skills/gsd//skills//SKILL.md - const hermesHelpPath = path.join( - targetDir, 'skills', 'gsd', CHILD_ROUTER['help'], 'skills', 'help', 'SKILL.md' - ); + const hermesHelpPath = nestedSkillPath(path.join(targetDir, 'skills', 'gsd'), '', 'help'); assert.ok(fs.existsSync(hermesHelpPath), `help SKILL.md must exist at nested path: ${path.relative(targetDir, hermesHelpPath)}`); assert.ok(fs.existsSync(path.join(targetDir, 'skills', 'gsd', 'DESCRIPTION.md')), @@ -446,9 +409,7 @@ describe('install/uninstall — qwen (nested skills/gsd-/skills// assert.strictEqual(result.configDir, fs.realpathSync(targetDir)); // qwen nests: skills/gsd-/skills//SKILL.md - const qwenHelpPath = path.join( - targetDir, 'skills', 'gsd-' + CHILD_ROUTER['help'], 'skills', 'help', 'SKILL.md' - ); + const qwenHelpPath = nestedSkillPath(path.join(targetDir, 'skills'), 'gsd-', 'help'); assert.ok(fs.existsSync(qwenHelpPath), `help SKILL.md must exist at nested path: ${path.relative(targetDir, qwenHelpPath)}`); assert.ok(fs.existsSync(path.join(targetDir, 'gsd-core', 'VERSION'))); @@ -496,9 +457,7 @@ describe('install/uninstall — trae (nested skills/gsd-/skills// }); // trae nests: skills/gsd-/skills//SKILL.md - const traeHelpPath = path.join( - targetDir, 'skills', 'gsd-' + CHILD_ROUTER['help'], 'skills', 'help', 'SKILL.md' - ); + const traeHelpPath = nestedSkillPath(path.join(targetDir, 'skills'), 'gsd-', 'help'); assert.ok(fs.existsSync(traeHelpPath), `help SKILL.md must exist at nested path: ${path.relative(targetDir, traeHelpPath)}`); assert.ok(fs.existsSync(path.join(targetDir, 'gsd-core', 'VERSION')));