Files
msd-core/tests/enh-769-context-fork-effort.install.test.cjs
Tom Boucher b6199460ed 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 <noreply@anthropic.com>
2026-06-08 16:05:59 -04:00

319 lines
12 KiB
JavaScript

// allow-test-rule: integration-test-input
// Exercises install() as a black-box by inspecting produced SKILL.md output
// in a temp dir. Source command .md files are inputs whose installed
// transformation is asserted — not inspected for string presence.
/**
* #769 — context:fork + effort: frontmatter on heavy workflow skills.
*
* Verifies:
* 1. Source commands/gsd/autonomous.md has context: fork and effort: xhigh
* 2. Source commands/gsd/execute-phase.md has context: fork and effort: xhigh
* 3. Source commands/gsd/plan-phase.md has context: fork and effort: xhigh
* 4. Source commands/gsd/progress.md has effort: low
* 5. Source commands/gsd/stats.md has effort: low
* 6. Claude global install: SKILL.md for autonomous has context: fork and effort: xhigh
* 7. Claude global install: SKILL.md for execute-phase has context: fork and effort: xhigh
* 8. Claude global install: SKILL.md for plan-phase has context: fork and effort: xhigh
* 9. Claude global install: SKILL.md for progress has effort: low
* 10. Claude global install: SKILL.md for stats has effort: low
* 11. convertClaudeCommandToClaudeSkill preserves context: fork field
* 12. convertClaudeCommandToClaudeSkill preserves effort: field
*/
'use strict';
process.env.GSD_TEST_MODE = '1';
const { describe, test, beforeEach, afterEach } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
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');
// ─── helpers ──────────────────────────────────────────────────────────────────
function makeTmpDir(prefix) {
return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
}
function readFrontmatter(mdPath) {
const content = fs.readFileSync(mdPath, 'utf8');
if (!content.startsWith('---')) return '';
const end = content.indexOf('---', 3);
if (end === -1) return '';
return content.substring(3, end);
}
/**
* Run a global install for Claude, redirecting its home dir to tmpHome.
* Returns the tmpHome for inspection.
*/
function runClaudeGlobalInstall(claudeHome) {
const isolatedHome = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-769-home-'));
const prevCwd = process.cwd();
const prevClaudeConfigDir = process.env.CLAUDE_CONFIG_DIR;
const prevHome = process.env.HOME;
const prevUserProfile = process.env.USERPROFILE;
const prevSkipStale = process.env.GSD_SKIP_STALE_SDK_CHECK;
process.env.CLAUDE_CONFIG_DIR = claudeHome;
process.env.HOME = isolatedHome;
process.env.USERPROFILE = isolatedHome;
process.env.GSD_SKIP_STALE_SDK_CHECK = '1';
process.chdir(REPO_ROOT);
try {
install(true, 'claude');
} finally {
process.chdir(prevCwd);
if (prevClaudeConfigDir === undefined) delete process.env.CLAUDE_CONFIG_DIR;
else process.env.CLAUDE_CONFIG_DIR = prevClaudeConfigDir;
if (prevHome === undefined) delete process.env.HOME;
else process.env.HOME = prevHome;
if (prevUserProfile === undefined) delete process.env.USERPROFILE;
else process.env.USERPROFILE = prevUserProfile;
if (prevSkipStale === undefined) delete process.env.GSD_SKIP_STALE_SDK_CHECK;
else process.env.GSD_SKIP_STALE_SDK_CHECK = prevSkipStale;
cleanup(isolatedHome);
}
return claudeHome;
}
// ─── describe 1: Source command files have correct frontmatter ────────────────
describe('#769 source commands: heavy skills have context: fork and effort: xhigh', () => {
test('commands/gsd/autonomous.md has context: fork', () => {
const fm = readFrontmatter(path.join(SOURCE_COMMANDS_DIR, 'autonomous.md'));
assert.match(fm, /^context:[ \t]*fork$/m,
`autonomous.md frontmatter must have context: fork\nActual:\n${fm}`);
});
test('commands/gsd/autonomous.md has effort: xhigh', () => {
const fm = readFrontmatter(path.join(SOURCE_COMMANDS_DIR, 'autonomous.md'));
assert.match(fm, /^effort:[ \t]*xhigh$/m,
`autonomous.md frontmatter must have effort: xhigh\nActual:\n${fm}`);
});
test('commands/gsd/execute-phase.md has context: fork', () => {
const fm = readFrontmatter(path.join(SOURCE_COMMANDS_DIR, 'execute-phase.md'));
assert.match(fm, /^context:[ \t]*fork$/m,
`execute-phase.md frontmatter must have context: fork\nActual:\n${fm}`);
});
test('commands/gsd/execute-phase.md has effort: xhigh', () => {
const fm = readFrontmatter(path.join(SOURCE_COMMANDS_DIR, 'execute-phase.md'));
assert.match(fm, /^effort:[ \t]*xhigh$/m,
`execute-phase.md frontmatter must have effort: xhigh\nActual:\n${fm}`);
});
test('commands/gsd/plan-phase.md has context: fork', () => {
const fm = readFrontmatter(path.join(SOURCE_COMMANDS_DIR, 'plan-phase.md'));
assert.match(fm, /^context:[ \t]*fork$/m,
`plan-phase.md frontmatter must have context: fork\nActual:\n${fm}`);
});
test('commands/gsd/plan-phase.md has effort: xhigh', () => {
const fm = readFrontmatter(path.join(SOURCE_COMMANDS_DIR, 'plan-phase.md'));
assert.match(fm, /^effort:[ \t]*xhigh$/m,
`plan-phase.md frontmatter must have effort: xhigh\nActual:\n${fm}`);
});
});
describe('#769 source commands: quick-status skills have effort: low', () => {
test('commands/gsd/progress.md has effort: low', () => {
const fm = readFrontmatter(path.join(SOURCE_COMMANDS_DIR, 'progress.md'));
assert.match(fm, /^effort:[ \t]*low$/m,
`progress.md frontmatter must have effort: low\nActual:\n${fm}`);
});
test('commands/gsd/stats.md has effort: low', () => {
const fm = readFrontmatter(path.join(SOURCE_COMMANDS_DIR, 'stats.md'));
assert.match(fm, /^effort:[ \t]*low$/m,
`stats.md frontmatter must have effort: low\nActual:\n${fm}`);
});
});
// ─── describe 2: convertClaudeCommandToClaudeSkill preserves new fields ───────
describe('#769 convertClaudeCommandToClaudeSkill: preserves context and effort fields', () => {
test('preserves context: fork in emitted SKILL.md frontmatter', () => {
const input = [
'---',
'name: gsd:test-heavy',
'description: Test heavy skill',
'context: fork',
'effort: xhigh',
'allowed-tools:',
' - Read',
' - Bash',
'---',
'',
'Heavy skill body.',
].join('\n');
const result = convertClaudeCommandToClaudeSkill(input, 'test-heavy');
const end = result.indexOf('---', 3);
const fm = result.substring(3, end);
assert.match(fm, /^context:[ \t]*fork$/m,
`SKILL.md frontmatter must include context: fork\nActual frontmatter:\n${fm}`);
});
test('preserves effort: xhigh in emitted SKILL.md frontmatter', () => {
const input = [
'---',
'name: gsd:test-heavy',
'description: Test heavy skill',
'context: fork',
'effort: xhigh',
'allowed-tools:',
' - Read',
' - Bash',
'---',
'',
'Heavy skill body.',
].join('\n');
const result = convertClaudeCommandToClaudeSkill(input, 'test-heavy');
const end = result.indexOf('---', 3);
const fm = result.substring(3, end);
assert.match(fm, /^effort:[ \t]*xhigh$/m,
`SKILL.md frontmatter must include effort: xhigh\nActual frontmatter:\n${fm}`);
});
test('preserves effort: low in emitted SKILL.md frontmatter', () => {
const input = [
'---',
'name: gsd:test-light',
'description: Test light skill',
'effort: low',
'allowed-tools:',
' - Read',
'---',
'',
'Light skill body.',
].join('\n');
const result = convertClaudeCommandToClaudeSkill(input, 'test-light');
const end = result.indexOf('---', 3);
const fm = result.substring(3, end);
assert.match(fm, /^effort:[ \t]*low$/m,
`SKILL.md frontmatter must include effort: low\nActual frontmatter:\n${fm}`);
});
test('does NOT emit context: or effort: when absent from source', () => {
const input = [
'---',
'name: gsd:test-plain',
'description: Plain skill without context or effort',
'allowed-tools:',
' - Read',
'---',
'',
'Plain skill body.',
].join('\n');
const result = convertClaudeCommandToClaudeSkill(input, 'test-plain');
const end = result.indexOf('---', 3);
const fm = result.substring(3, end);
assert.doesNotMatch(fm, /^context:/m,
`SKILL.md must not emit context: when absent from source\nActual:\n${fm}`);
assert.doesNotMatch(fm, /^effort:/m,
`SKILL.md must not emit effort: when absent from source\nActual:\n${fm}`);
});
});
// ─── describe 3: Claude global install — SKILL.md files include new fields ────
describe('#769 Claude global install: SKILL.md files preserve context: fork and effort:', () => {
let tmpDir;
let claudeHome;
beforeEach(() => {
tmpDir = makeTmpDir('gsd-769-claude-');
claudeHome = path.join(tmpDir, 'claude-home');
fs.mkdirSync(claudeHome, { recursive: true });
});
afterEach(() => {
cleanup(tmpDir);
});
test('gsd-autonomous SKILL.md has context: fork after global install', () => {
runClaudeGlobalInstall(claudeHome);
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}`);
});
test('gsd-autonomous SKILL.md has effort: xhigh after global install', () => {
runClaudeGlobalInstall(claudeHome);
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}`);
});
test('gsd-execute-phase SKILL.md has context: fork after global install', () => {
runClaudeGlobalInstall(claudeHome);
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}`);
});
test('gsd-execute-phase SKILL.md has effort: xhigh after global install', () => {
runClaudeGlobalInstall(claudeHome);
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}`);
});
test('gsd-plan-phase SKILL.md has context: fork after global install', () => {
runClaudeGlobalInstall(claudeHome);
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}`);
});
test('gsd-plan-phase SKILL.md has effort: xhigh after global install', () => {
runClaudeGlobalInstall(claudeHome);
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}`);
});
test('gsd-progress SKILL.md has effort: low after global install', () => {
runClaudeGlobalInstall(claudeHome);
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}`);
});
test('gsd-stats SKILL.md has effort: low after global install', () => {
runClaudeGlobalInstall(claudeHome);
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}`);
});
});