mirror of
https://github.com/glittercowboy/get-shit-done
synced 2026-04-25 17:25:23 +02:00
refactor: replace try/finally with beforeEach/afterEach + add CONTRIBUTING.md
Test suite modernization: - Converted all try/finally cleanup patterns to beforeEach/afterEach hooks across 11 test files (core, copilot-install, config, workstream, milestone-summary, forensics, state, antigravity, profile-pipeline, workspace) - Consolidated 40 inline mkdtempSync calls to use centralized helpers - Added createTempDir() helper for bare temp directories - Added optional prefix parameter to createTempProject/createTempGitProject - Fixed config test HOME sandboxing (was reading global defaults.json) New CONTRIBUTING.md: - Test standards: hooks over try/finally, centralized helpers, HOME sandboxing - Node 22/24 compatibility requirements with Node 26 forward-compat - Code style, PR guidelines, security practices - File structure overview All 1382 tests pass, 0 failures. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -51,54 +51,41 @@ describe('getDirName (Copilot)', () => {
|
||||
// ─── getGlobalDir ───────────────────────────────────────────────────────────────
|
||||
|
||||
describe('getGlobalDir (Copilot)', () => {
|
||||
test('returns ~/.copilot with no env var or explicit dir', () => {
|
||||
const original = process.env.COPILOT_CONFIG_DIR;
|
||||
try {
|
||||
let originalCopilotConfigDir;
|
||||
|
||||
beforeEach(() => {
|
||||
originalCopilotConfigDir = process.env.COPILOT_CONFIG_DIR;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (originalCopilotConfigDir !== undefined) {
|
||||
process.env.COPILOT_CONFIG_DIR = originalCopilotConfigDir;
|
||||
} else {
|
||||
delete process.env.COPILOT_CONFIG_DIR;
|
||||
const result = getGlobalDir('copilot');
|
||||
assert.strictEqual(result, path.join(os.homedir(), '.copilot'));
|
||||
} finally {
|
||||
if (original !== undefined) {
|
||||
process.env.COPILOT_CONFIG_DIR = original;
|
||||
} else {
|
||||
delete process.env.COPILOT_CONFIG_DIR;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('returns ~/.copilot with no env var or explicit dir', () => {
|
||||
delete process.env.COPILOT_CONFIG_DIR;
|
||||
const result = getGlobalDir('copilot');
|
||||
assert.strictEqual(result, path.join(os.homedir(), '.copilot'));
|
||||
});
|
||||
|
||||
test('returns explicit dir when provided', () => {
|
||||
const result = getGlobalDir('copilot', '/custom/path');
|
||||
assert.strictEqual(result, '/custom/path');
|
||||
});
|
||||
|
||||
test('respects COPILOT_CONFIG_DIR env var', () => {
|
||||
const original = process.env.COPILOT_CONFIG_DIR;
|
||||
try {
|
||||
process.env.COPILOT_CONFIG_DIR = '~/custom-copilot';
|
||||
const result = getGlobalDir('copilot');
|
||||
assert.strictEqual(result, path.join(os.homedir(), 'custom-copilot'));
|
||||
} finally {
|
||||
if (original !== undefined) {
|
||||
process.env.COPILOT_CONFIG_DIR = original;
|
||||
} else {
|
||||
delete process.env.COPILOT_CONFIG_DIR;
|
||||
}
|
||||
}
|
||||
process.env.COPILOT_CONFIG_DIR = '~/custom-copilot';
|
||||
const result = getGlobalDir('copilot');
|
||||
assert.strictEqual(result, path.join(os.homedir(), 'custom-copilot'));
|
||||
});
|
||||
|
||||
test('explicit dir takes priority over COPILOT_CONFIG_DIR', () => {
|
||||
const original = process.env.COPILOT_CONFIG_DIR;
|
||||
try {
|
||||
process.env.COPILOT_CONFIG_DIR = '~/env-path';
|
||||
const result = getGlobalDir('copilot', '/explicit/path');
|
||||
assert.strictEqual(result, '/explicit/path');
|
||||
} finally {
|
||||
if (original !== undefined) {
|
||||
process.env.COPILOT_CONFIG_DIR = original;
|
||||
} else {
|
||||
delete process.env.COPILOT_CONFIG_DIR;
|
||||
}
|
||||
}
|
||||
process.env.COPILOT_CONFIG_DIR = '~/env-path';
|
||||
const result = getGlobalDir('copilot', '/explicit/path');
|
||||
assert.strictEqual(result, '/explicit/path');
|
||||
});
|
||||
|
||||
test('does not break existing runtimes', () => {
|
||||
@@ -605,46 +592,45 @@ Check ~/.claude/settings and run gsd:health.`;
|
||||
|
||||
describe('copyCommandsAsCopilotSkills', () => {
|
||||
const srcDir = path.join(__dirname, '..', 'commands', 'gsd');
|
||||
let tempDir;
|
||||
|
||||
beforeEach(() => {
|
||||
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-copilot-skills-'));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test('creates skill folders from source commands', () => {
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-copilot-skills-'));
|
||||
try {
|
||||
copyCommandsAsCopilotSkills(srcDir, tempDir, 'gsd');
|
||||
copyCommandsAsCopilotSkills(srcDir, tempDir, 'gsd');
|
||||
|
||||
// Check specific folders exist
|
||||
assert.ok(fs.existsSync(path.join(tempDir, 'gsd-health')), 'gsd-health folder exists');
|
||||
assert.ok(fs.existsSync(path.join(tempDir, 'gsd-health', 'SKILL.md')), 'gsd-health/SKILL.md exists');
|
||||
assert.ok(fs.existsSync(path.join(tempDir, 'gsd-help')), 'gsd-help folder exists');
|
||||
assert.ok(fs.existsSync(path.join(tempDir, 'gsd-progress')), 'gsd-progress folder exists');
|
||||
// Check specific folders exist
|
||||
assert.ok(fs.existsSync(path.join(tempDir, 'gsd-health')), 'gsd-health folder exists');
|
||||
assert.ok(fs.existsSync(path.join(tempDir, 'gsd-health', 'SKILL.md')), 'gsd-health/SKILL.md exists');
|
||||
assert.ok(fs.existsSync(path.join(tempDir, 'gsd-help')), 'gsd-help folder exists');
|
||||
assert.ok(fs.existsSync(path.join(tempDir, 'gsd-progress')), 'gsd-progress folder exists');
|
||||
|
||||
// Count gsd-* directories — should match number of source command files
|
||||
const dirs = fs.readdirSync(tempDir, { withFileTypes: true })
|
||||
.filter(e => e.isDirectory() && e.name.startsWith('gsd-'));
|
||||
const expectedSkillCount = fs.readdirSync(path.join(__dirname, '..', 'commands', 'gsd'))
|
||||
.filter(f => f.endsWith('.md')).length;
|
||||
assert.strictEqual(dirs.length, expectedSkillCount, `expected ${expectedSkillCount} skill folders, got ${dirs.length}`);
|
||||
} finally {
|
||||
fs.rmSync(tempDir, { recursive: true });
|
||||
}
|
||||
// Count gsd-* directories — should match number of source command files
|
||||
const dirs = fs.readdirSync(tempDir, { withFileTypes: true })
|
||||
.filter(e => e.isDirectory() && e.name.startsWith('gsd-'));
|
||||
const expectedSkillCount = fs.readdirSync(path.join(__dirname, '..', 'commands', 'gsd'))
|
||||
.filter(f => f.endsWith('.md')).length;
|
||||
assert.strictEqual(dirs.length, expectedSkillCount, `expected ${expectedSkillCount} skill folders, got ${dirs.length}`);
|
||||
});
|
||||
|
||||
test('skill content has Copilot frontmatter format', () => {
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-copilot-skills-'));
|
||||
try {
|
||||
copyCommandsAsCopilotSkills(srcDir, tempDir, 'gsd');
|
||||
copyCommandsAsCopilotSkills(srcDir, tempDir, 'gsd');
|
||||
|
||||
const skillContent = fs.readFileSync(path.join(tempDir, 'gsd-health', 'SKILL.md'), 'utf8');
|
||||
// Frontmatter format checks
|
||||
assert.ok(skillContent.startsWith('---\nname: gsd-health\n'), 'starts with name: gsd-health');
|
||||
assert.ok(skillContent.includes('allowed-tools: Read, Bash, Write, AskUserQuestion'),
|
||||
'allowed-tools is comma-separated');
|
||||
assert.ok(!skillContent.includes('allowed-tools:\n -'), 'NOT YAML multiline format');
|
||||
// CONV-06/07 applied
|
||||
assert.ok(!skillContent.includes('~/.claude/'), 'no ~/.claude/ references');
|
||||
assert.ok(!skillContent.match(/gsd:[a-z]/), 'no gsd: command references');
|
||||
} finally {
|
||||
fs.rmSync(tempDir, { recursive: true });
|
||||
}
|
||||
const skillContent = fs.readFileSync(path.join(tempDir, 'gsd-health', 'SKILL.md'), 'utf8');
|
||||
// Frontmatter format checks
|
||||
assert.ok(skillContent.startsWith('---\nname: gsd-health\n'), 'starts with name: gsd-health');
|
||||
assert.ok(skillContent.includes('allowed-tools: Read, Bash, Write, AskUserQuestion'),
|
||||
'allowed-tools is comma-separated');
|
||||
assert.ok(!skillContent.includes('allowed-tools:\n -'), 'NOT YAML multiline format');
|
||||
// CONV-06/07 applied
|
||||
assert.ok(!skillContent.includes('~/.claude/'), 'no ~/.claude/ references');
|
||||
assert.ok(!skillContent.match(/gsd:[a-z]/), 'no gsd: command references');
|
||||
});
|
||||
|
||||
test('generates gsd-autonomous skill from autonomous.md command', () => {
|
||||
@@ -652,31 +638,26 @@ describe('copyCommandsAsCopilotSkills', () => {
|
||||
const srcFile = path.join(srcDir, 'autonomous.md');
|
||||
assert.ok(fs.existsSync(srcFile), 'commands/gsd/autonomous.md must exist as source');
|
||||
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-copilot-skills-'));
|
||||
try {
|
||||
copyCommandsAsCopilotSkills(srcDir, tempDir, 'gsd');
|
||||
copyCommandsAsCopilotSkills(srcDir, tempDir, 'gsd');
|
||||
|
||||
// Skill folder and file created
|
||||
assert.ok(fs.existsSync(path.join(tempDir, 'gsd-autonomous')), 'gsd-autonomous folder exists');
|
||||
assert.ok(fs.existsSync(path.join(tempDir, 'gsd-autonomous', 'SKILL.md')), 'gsd-autonomous/SKILL.md exists');
|
||||
// Skill folder and file created
|
||||
assert.ok(fs.existsSync(path.join(tempDir, 'gsd-autonomous')), 'gsd-autonomous folder exists');
|
||||
assert.ok(fs.existsSync(path.join(tempDir, 'gsd-autonomous', 'SKILL.md')), 'gsd-autonomous/SKILL.md exists');
|
||||
|
||||
const skillContent = fs.readFileSync(path.join(tempDir, 'gsd-autonomous', 'SKILL.md'), 'utf8');
|
||||
const skillContent = fs.readFileSync(path.join(tempDir, 'gsd-autonomous', 'SKILL.md'), 'utf8');
|
||||
|
||||
// Frontmatter: name converted from gsd:autonomous to gsd-autonomous
|
||||
assert.ok(skillContent.startsWith('---\nname: gsd-autonomous\n'), 'name is gsd-autonomous');
|
||||
assert.ok(skillContent.includes('description: Run all remaining phases autonomously'),
|
||||
'description preserved');
|
||||
// argument-hint present and double-quoted
|
||||
assert.ok(skillContent.includes('argument-hint: "[--from N]"'), 'argument-hint present and quoted');
|
||||
// allowed-tools comma-separated
|
||||
assert.ok(skillContent.includes('allowed-tools: Read, Write, Bash, Glob, Grep, AskUserQuestion, Task'),
|
||||
'allowed-tools is comma-separated');
|
||||
// No Claude-format remnants
|
||||
assert.ok(!skillContent.includes('allowed-tools:\n -'), 'NOT YAML multiline format');
|
||||
assert.ok(!skillContent.includes('~/.claude/'), 'no ~/.claude/ references in body');
|
||||
} finally {
|
||||
fs.rmSync(tempDir, { recursive: true });
|
||||
}
|
||||
// Frontmatter: name converted from gsd:autonomous to gsd-autonomous
|
||||
assert.ok(skillContent.startsWith('---\nname: gsd-autonomous\n'), 'name is gsd-autonomous');
|
||||
assert.ok(skillContent.includes('description: Run all remaining phases autonomously'),
|
||||
'description preserved');
|
||||
// argument-hint present and double-quoted
|
||||
assert.ok(skillContent.includes('argument-hint: "[--from N]"'), 'argument-hint present and quoted');
|
||||
// allowed-tools comma-separated
|
||||
assert.ok(skillContent.includes('allowed-tools: Read, Write, Bash, Glob, Grep, AskUserQuestion, Task'),
|
||||
'allowed-tools is comma-separated');
|
||||
// No Claude-format remnants
|
||||
assert.ok(!skillContent.includes('allowed-tools:\n -'), 'NOT YAML multiline format');
|
||||
assert.ok(!skillContent.includes('~/.claude/'), 'no ~/.claude/ references in body');
|
||||
});
|
||||
|
||||
test('autonomous skill body converts gsd: to gsd- (CONV-07)', () => {
|
||||
@@ -697,21 +678,16 @@ describe('copyCommandsAsCopilotSkills', () => {
|
||||
});
|
||||
|
||||
test('cleans up old skill directories on re-run', () => {
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-copilot-skills-'));
|
||||
try {
|
||||
// Create a fake old directory
|
||||
fs.mkdirSync(path.join(tempDir, 'gsd-fake-old'), { recursive: true });
|
||||
fs.writeFileSync(path.join(tempDir, 'gsd-fake-old', 'SKILL.md'), 'old');
|
||||
assert.ok(fs.existsSync(path.join(tempDir, 'gsd-fake-old')), 'fake old dir exists before');
|
||||
// Create a fake old directory
|
||||
fs.mkdirSync(path.join(tempDir, 'gsd-fake-old'), { recursive: true });
|
||||
fs.writeFileSync(path.join(tempDir, 'gsd-fake-old', 'SKILL.md'), 'old');
|
||||
assert.ok(fs.existsSync(path.join(tempDir, 'gsd-fake-old')), 'fake old dir exists before');
|
||||
|
||||
// Run copy — should clean up old dirs
|
||||
copyCommandsAsCopilotSkills(srcDir, tempDir, 'gsd');
|
||||
// Run copy — should clean up old dirs
|
||||
copyCommandsAsCopilotSkills(srcDir, tempDir, 'gsd');
|
||||
|
||||
assert.ok(!fs.existsSync(path.join(tempDir, 'gsd-fake-old')), 'fake old dir removed');
|
||||
assert.ok(fs.existsSync(path.join(tempDir, 'gsd-health')), 'real dirs still exist');
|
||||
} finally {
|
||||
fs.rmSync(tempDir, { recursive: true });
|
||||
}
|
||||
assert.ok(!fs.existsSync(path.join(tempDir, 'gsd-fake-old')), 'fake old dir removed');
|
||||
assert.ok(fs.existsSync(path.join(tempDir, 'gsd-health')), 'real dirs still exist');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1058,55 +1034,52 @@ describe('Copilot manifest and patches fixes', () => {
|
||||
assert.ok(data.files[skillKey].length === 64, 'hash is SHA-256 (64 hex chars)');
|
||||
});
|
||||
|
||||
test('reportLocalPatches shows /gsd-reapply-patches for Copilot', () => {
|
||||
// Create patches directory with metadata
|
||||
const patchesDir = path.join(tmpDir, 'gsd-local-patches');
|
||||
fs.mkdirSync(patchesDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(patchesDir, 'backup-meta.json'), JSON.stringify({
|
||||
from_version: '1.0',
|
||||
files: ['skills/gsd-test/SKILL.md']
|
||||
}));
|
||||
describe('reportLocalPatches', () => {
|
||||
let originalLog;
|
||||
let logs;
|
||||
|
||||
// Capture console output
|
||||
const logs = [];
|
||||
const originalLog = console.log;
|
||||
console.log = (...args) => logs.push(args.join(' '));
|
||||
beforeEach(() => {
|
||||
originalLog = console.log;
|
||||
logs = [];
|
||||
console.log = (...args) => logs.push(args.join(' '));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
console.log = originalLog;
|
||||
});
|
||||
|
||||
test('reportLocalPatches shows /gsd-reapply-patches for Copilot', () => {
|
||||
// Create patches directory with metadata
|
||||
const patchesDir = path.join(tmpDir, 'gsd-local-patches');
|
||||
fs.mkdirSync(patchesDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(patchesDir, 'backup-meta.json'), JSON.stringify({
|
||||
from_version: '1.0',
|
||||
files: ['skills/gsd-test/SKILL.md']
|
||||
}));
|
||||
|
||||
try {
|
||||
const result = reportLocalPatches(tmpDir, 'copilot');
|
||||
|
||||
assert.ok(result.length > 0, 'returns patched files list');
|
||||
const output = logs.join('\n');
|
||||
assert.ok(output.includes('/gsd-reapply-patches'), 'uses dash format for Copilot');
|
||||
assert.ok(!output.includes('/gsd:reapply-patches'), 'does not use colon format');
|
||||
} finally {
|
||||
console.log = originalLog;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test('reportLocalPatches shows /gsd:reapply-patches for Claude (unchanged)', () => {
|
||||
// Create patches directory with metadata
|
||||
const patchesDir = path.join(tmpDir, 'gsd-local-patches');
|
||||
fs.mkdirSync(patchesDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(patchesDir, 'backup-meta.json'), JSON.stringify({
|
||||
from_version: '1.0',
|
||||
files: ['get-shit-done/bin/verify.cjs']
|
||||
}));
|
||||
test('reportLocalPatches shows /gsd:reapply-patches for Claude (unchanged)', () => {
|
||||
// Create patches directory with metadata
|
||||
const patchesDir = path.join(tmpDir, 'gsd-local-patches');
|
||||
fs.mkdirSync(patchesDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(patchesDir, 'backup-meta.json'), JSON.stringify({
|
||||
from_version: '1.0',
|
||||
files: ['get-shit-done/bin/verify.cjs']
|
||||
}));
|
||||
|
||||
// Capture console output
|
||||
const logs = [];
|
||||
const originalLog = console.log;
|
||||
console.log = (...args) => logs.push(args.join(' '));
|
||||
|
||||
try {
|
||||
const result = reportLocalPatches(tmpDir, 'claude');
|
||||
|
||||
assert.ok(result.length > 0, 'returns patched files list');
|
||||
const output = logs.join('\n');
|
||||
assert.ok(output.includes('/gsd:reapply-patches'), 'uses colon format for Claude');
|
||||
} finally {
|
||||
console.log = originalLog;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1329,11 +1302,19 @@ describe('E2E: Copilot uninstall verification', () => {
|
||||
}
|
||||
});
|
||||
|
||||
test('preserves non-GSD content in skills directory', () => {
|
||||
// Standalone lifecycle: install → add custom content → uninstall → verify
|
||||
const td = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-e2e-preserve-skill-'));
|
||||
try {
|
||||
describe('preserves non-GSD content', () => {
|
||||
let td;
|
||||
|
||||
beforeEach(() => {
|
||||
td = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-e2e-preserve-'));
|
||||
runCopilotInstall(td);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(td, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test('preserves non-GSD content in skills directory', () => {
|
||||
// Add non-GSD custom skill
|
||||
const customSkillDir = path.join(td, '.github', 'skills', 'my-custom-skill');
|
||||
fs.mkdirSync(customSkillDir, { recursive: true });
|
||||
@@ -1343,16 +1324,9 @@ describe('E2E: Copilot uninstall verification', () => {
|
||||
// Verify custom content preserved
|
||||
assert.ok(fs.existsSync(path.join(customSkillDir, 'SKILL.md')),
|
||||
'Non-GSD skill directory and SKILL.md should be preserved after uninstall');
|
||||
} finally {
|
||||
fs.rmSync(td, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test('preserves non-GSD content in agents directory', () => {
|
||||
// Standalone lifecycle: install → add custom content → uninstall → verify
|
||||
const td = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-e2e-preserve-agent-'));
|
||||
try {
|
||||
runCopilotInstall(td);
|
||||
test('preserves non-GSD content in agents directory', () => {
|
||||
// Add non-GSD custom agent
|
||||
const customAgentPath = path.join(td, '.github', 'agents', 'my-agent.md');
|
||||
fs.writeFileSync(customAgentPath, '# My Custom Agent\n');
|
||||
@@ -1361,8 +1335,6 @@ describe('E2E: Copilot uninstall verification', () => {
|
||||
// Verify custom content preserved
|
||||
assert.ok(fs.existsSync(customAgentPath),
|
||||
'Non-GSD agent file should be preserved after uninstall');
|
||||
} finally {
|
||||
fs.rmSync(td, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user