* enh(#2876): retire the dead and pass-through exports from bin/install.js The installer exported 197 names and had zero production consumers - every non-test require of it repo-wide sits inside a comment. Its interface was shaped by test access, not by callers. Removes 9 dead exports and 61 pass-throughs, repointing their tests onto the extracted modules' own interfaces. 197 down to 127. Every count in the issue was wrong: 197 exports not 188, 9 dead not 12, 61 pass-throughs not 49, 44 test files not 42 - and the audit itself then missed 7 more consumer files. restoreUserArtifacts was on the dead list but ceased to exist in phase 6, and two _GSD_EFFORT_MANIFEST_* names listed as dead are now genuinely asserted, so acting on that list would have deleted live exports. 7 of the 9 dead names collide with an independent declaration that install.js delegates TO. Each removal was justified by which declaration a reference resolves to, never by whether the name appears somewhere. Coverage parity was the gate rather than test greenness: per-file counts were captured before any edit and diffed after. 44 of 45 files are byte-identical; the single delta is one added assertion, not a loss. The sweep for scattered require sites found two forms static grep misses - require(VARIABLE) and multi-line require() - plus tests asserting that install.js re-exports the SAME object, which now assert retirement instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(#2876): close review findings — restore the duplicate-body guard, sweep orphaned code Both review engines found real defects in the first cut. The DEFECT.GENERATIVE-FIX single-owner guard from #1511 had been repointed from a reference-identity check to install.X === undefined. Those are not equivalent: the guard exists to catch a duplicate function body reintroduced into install.js, and the replacement passes cleanly if that duplicate is used internally and never exported. It now walks bin/install.js's real top-level bindings, so it catches a duplicate under either shape, exported or not - strictly stronger than the check it replaced. Proved by injecting a duplicate and watching it go red. That weakening survived the coverage-parity gate because the assertion count never moved. The gate compares counts, so an assertion that changes meaning rather than number is invisible to it. Removing the exports had orphaned their wrapper bodies: 14 dead wrappers, 9 consts and 9 destructure entries, several pre-existing and found by the same sweep. Dead code left in the file this phase exists to shrink. Three more comments claimed re-exports this phase removed, and tests were reading Cursor and Windsurf hook constants from install.js's local copy while calling functions from the hooks surface - equal today, with nothing holding them equal. The local consts now reference the owning module. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore(#2876): backfill changeset pr number Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: sim <sim@local> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
271 lines
9.9 KiB
JavaScript
271 lines
9.9 KiB
JavaScript
// allow-test-rule: source-text-is-the-product
|
|
// Workflow .md / agent .md / command .md / reference .md files — their text
|
|
// IS what the runtime loads. Testing text content tests the deployed contract.
|
|
// Per CONTRIBUTING.md exception matrix.
|
|
|
|
process.env.GSD_TEST_MODE = '1';
|
|
|
|
const { test, describe, 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 { createTempDir, cleanup } = require('./helpers.cjs');
|
|
|
|
const {
|
|
getConfigDirFromHome,
|
|
convertClaudeToCodebuddyMarkdown,
|
|
convertClaudeCommandToCodebuddySkill,
|
|
convertClaudeAgentToCodebuddyAgent,
|
|
install,
|
|
uninstall,
|
|
writeManifest,
|
|
} = require('../bin/install.js');
|
|
|
|
const { installRuntimeArtifacts } = require('../gsd-core/bin/lib/install-engine.cjs');
|
|
const { getDirName } = require('../gsd-core/bin/lib/runtime-name-policy.cjs');
|
|
|
|
const { getGlobalConfigDir } = require('../gsd-core/bin/lib/runtime-homes.cjs');
|
|
|
|
// ─── Profile resolution for installRuntimeArtifacts tests ────────────────────
|
|
const _gsdLibDir = path.join(__dirname, '..', 'gsd-core', 'bin', 'lib');
|
|
const { loadSkillsManifest, resolveProfile } = require(path.join(_gsdLibDir, 'install-profiles.cjs'));
|
|
const _manifest = loadSkillsManifest();
|
|
const resolvedProfileFull = resolveProfile({ modes: [], manifest: _manifest });
|
|
|
|
describe('CodeBuddy runtime directory mapping', () => {
|
|
test('maps CodeBuddy to .codebuddy for local installs', () => {
|
|
assert.strictEqual(getDirName('codebuddy'), '.codebuddy');
|
|
});
|
|
|
|
test('maps CodeBuddy to ~/.codebuddy for global installs', () => {
|
|
assert.strictEqual(getGlobalConfigDir('codebuddy'), path.join(os.homedir(), '.codebuddy'));
|
|
});
|
|
|
|
test('returns .codebuddy config fragments for local and global installs', () => {
|
|
assert.strictEqual(getConfigDirFromHome('codebuddy', false), "'.codebuddy'");
|
|
assert.strictEqual(getConfigDirFromHome('codebuddy', true), "'.codebuddy'");
|
|
});
|
|
});
|
|
|
|
describe('getGlobalConfigDir (CodeBuddy)', () => {
|
|
let originalCodebuddyConfigDir;
|
|
|
|
beforeEach(() => {
|
|
originalCodebuddyConfigDir = process.env.CODEBUDDY_CONFIG_DIR;
|
|
});
|
|
|
|
afterEach(() => {
|
|
if (originalCodebuddyConfigDir !== undefined) {
|
|
process.env.CODEBUDDY_CONFIG_DIR = originalCodebuddyConfigDir;
|
|
} else {
|
|
delete process.env.CODEBUDDY_CONFIG_DIR;
|
|
}
|
|
});
|
|
|
|
test('returns ~/.codebuddy with no env var or explicit dir', () => {
|
|
delete process.env.CODEBUDDY_CONFIG_DIR;
|
|
const result = getGlobalConfigDir('codebuddy');
|
|
assert.strictEqual(result, path.join(os.homedir(), '.codebuddy'));
|
|
});
|
|
|
|
test('returns explicit dir when provided', () => {
|
|
const result = getGlobalConfigDir('codebuddy', '/custom/codebuddy-path');
|
|
assert.strictEqual(result, '/custom/codebuddy-path');
|
|
});
|
|
|
|
test('respects CODEBUDDY_CONFIG_DIR env var', () => {
|
|
process.env.CODEBUDDY_CONFIG_DIR = '~/custom-codebuddy';
|
|
const result = getGlobalConfigDir('codebuddy');
|
|
assert.strictEqual(result, path.join(os.homedir(), 'custom-codebuddy'));
|
|
});
|
|
|
|
test('explicit dir takes priority over CODEBUDDY_CONFIG_DIR', () => {
|
|
process.env.CODEBUDDY_CONFIG_DIR = '~/from-env';
|
|
const result = getGlobalConfigDir('codebuddy', '/explicit/path');
|
|
assert.strictEqual(result, '/explicit/path');
|
|
});
|
|
|
|
test('does not break other runtimes', () => {
|
|
assert.strictEqual(getGlobalConfigDir('claude'), path.join(os.homedir(), '.claude'));
|
|
assert.strictEqual(getGlobalConfigDir('codex'), path.join(os.homedir(), '.codex'));
|
|
});
|
|
});
|
|
|
|
describe('CodeBuddy markdown conversion', () => {
|
|
test('converts Claude-specific references to CodeBuddy equivalents', () => {
|
|
const input = [
|
|
'Claude Code reads CLAUDE.md before using .claude/skills/.',
|
|
'Run /gsd:plan-phase with $ARGUMENTS.',
|
|
'Use Bash(command) and Edit(file).',
|
|
].join('\n');
|
|
|
|
const result = convertClaudeToCodebuddyMarkdown(input);
|
|
|
|
assert.ok(result.includes('CodeBuddy reads CODEBUDDY.md before using .codebuddy/skills/.'), result);
|
|
assert.ok(result.includes('/gsd-plan-phase'), result);
|
|
assert.ok(result.includes('{{GSD_ARGS}}'), result);
|
|
// CodeBuddy uses the same tool names as Claude Code — no conversion needed
|
|
assert.ok(result.includes('Bash('), result);
|
|
assert.ok(result.includes('Edit('), result);
|
|
});
|
|
|
|
test('converts commands and agents to CodeBuddy frontmatter', () => {
|
|
const command = `---
|
|
name: gsd:new-project
|
|
description: Initialize a project
|
|
---
|
|
|
|
Use .claude/skills/ and /gsd:help.
|
|
`;
|
|
const agent = `---
|
|
name: gsd-planner
|
|
description: Planner agent
|
|
tools: Read, Write
|
|
color: blue
|
|
---
|
|
|
|
Read CLAUDE.md before acting.
|
|
`;
|
|
|
|
const convertedCommand = convertClaudeCommandToCodebuddySkill(command, 'gsd-new-project');
|
|
const convertedAgent = convertClaudeAgentToCodebuddyAgent(agent);
|
|
|
|
assert.ok(convertedCommand.includes('name: gsd-new-project'), convertedCommand);
|
|
assert.ok(convertedCommand.includes('.codebuddy/skills/'), convertedCommand);
|
|
assert.ok(convertedCommand.includes('/gsd-help'), convertedCommand);
|
|
|
|
assert.ok(convertedAgent.includes('name: gsd-planner'), convertedAgent);
|
|
assert.ok(!convertedAgent.includes('color:'), convertedAgent);
|
|
assert.ok(convertedAgent.includes('CODEBUDDY.md'), convertedAgent);
|
|
});
|
|
});
|
|
|
|
describe('installRuntimeArtifacts (codebuddy integration)', () => {
|
|
// Pivoted from copyCommandsAsCodebuddySkills(srcDir, skillsDir, 'gsd', '$HOME/.codebuddy/', 'codebuddy')
|
|
// shim to installRuntimeArtifacts('codebuddy', configDir, 'local', resolvedProfileFull).
|
|
// Output layout: <configDir>/skills/gsd-<stem>/SKILL.md (destSubpath='skills', prefix='gsd-').
|
|
let configDir;
|
|
|
|
beforeEach(() => {
|
|
configDir = createTempDir('gsd-codebuddy-copy-');
|
|
});
|
|
|
|
afterEach(() => {
|
|
cleanup(configDir);
|
|
});
|
|
|
|
test('creates one skill directory per GSD command', () => {
|
|
installRuntimeArtifacts('codebuddy', configDir, 'local', resolvedProfileFull);
|
|
|
|
const generated = path.join(configDir, 'skills', 'gsd-help', 'SKILL.md');
|
|
assert.ok(fs.existsSync(generated), generated);
|
|
|
|
const content = fs.readFileSync(generated, 'utf8');
|
|
assert.ok(content.includes('name: gsd-help'), content);
|
|
});
|
|
});
|
|
|
|
describe('CodeBuddy local install/uninstall', () => {
|
|
let tmpDir;
|
|
let previousCwd;
|
|
|
|
beforeEach(() => {
|
|
tmpDir = createTempDir('gsd-codebuddy-install-');
|
|
previousCwd = process.cwd();
|
|
process.chdir(tmpDir);
|
|
});
|
|
|
|
afterEach(() => {
|
|
process.chdir(previousCwd);
|
|
cleanup(tmpDir);
|
|
});
|
|
|
|
test('installs GSD into ./.codebuddy and removes it cleanly', () => {
|
|
const result = install(false, 'codebuddy');
|
|
const targetDir = path.join(tmpDir, '.codebuddy');
|
|
|
|
// CodeBuddy supports settings.json hooks (Claude Code compatible)
|
|
assert.strictEqual(result.runtime, 'codebuddy');
|
|
assert.ok(result.settingsPath, 'should have settingsPath (CodeBuddy supports hooks)');
|
|
|
|
assert.ok(fs.existsSync(path.join(targetDir, 'skills', 'gsd-help', 'SKILL.md')));
|
|
assert.ok(fs.existsSync(path.join(targetDir, 'gsd-core', 'VERSION')));
|
|
assert.ok(fs.existsSync(path.join(targetDir, 'agents')));
|
|
|
|
const manifest = writeManifest(targetDir, 'codebuddy');
|
|
assert.ok(Object.keys(manifest.files).some(file => file.startsWith('skills/gsd-help/')), JSON.stringify(manifest));
|
|
|
|
uninstall(false, 'codebuddy');
|
|
|
|
assert.ok(!fs.existsSync(path.join(targetDir, 'skills', 'gsd-help')), 'CodeBuddy skill directory removed');
|
|
assert.ok(!fs.existsSync(path.join(targetDir, 'gsd-core')), 'gsd-core removed');
|
|
});
|
|
});
|
|
|
|
describe('E2E: CodeBuddy uninstall skills cleanup', () => {
|
|
let tmpDir;
|
|
let previousCwd;
|
|
|
|
beforeEach(() => {
|
|
tmpDir = createTempDir('gsd-codebuddy-uninstall-');
|
|
previousCwd = process.cwd();
|
|
process.chdir(tmpDir);
|
|
});
|
|
|
|
afterEach(() => {
|
|
process.chdir(previousCwd);
|
|
cleanup(tmpDir);
|
|
});
|
|
|
|
test('removes all gsd-* skill directories on --codebuddy --uninstall', () => {
|
|
const targetDir = path.join(tmpDir, '.codebuddy');
|
|
install(false, 'codebuddy');
|
|
|
|
const skillsDir = path.join(targetDir, 'skills');
|
|
assert.ok(fs.existsSync(skillsDir), 'skills dir exists after install');
|
|
|
|
const installedSkills = fs.readdirSync(skillsDir, { withFileTypes: true })
|
|
.filter(e => e.isDirectory() && e.name.startsWith('gsd-'));
|
|
assert.ok(installedSkills.length > 0, `found ${installedSkills.length} gsd-* skill dirs before uninstall`);
|
|
|
|
uninstall(false, 'codebuddy');
|
|
|
|
if (fs.existsSync(skillsDir)) {
|
|
const remainingGsd = fs.readdirSync(skillsDir, { withFileTypes: true })
|
|
.filter(e => e.isDirectory() && e.name.startsWith('gsd-'));
|
|
assert.strictEqual(remainingGsd.length, 0,
|
|
`Expected 0 gsd-* skill dirs after uninstall, found: ${remainingGsd.map(e => e.name).join(', ')}`);
|
|
}
|
|
});
|
|
|
|
test('preserves non-GSD skill directories during --codebuddy --uninstall', () => {
|
|
const targetDir = path.join(tmpDir, '.codebuddy');
|
|
install(false, 'codebuddy');
|
|
|
|
const customSkillDir = path.join(targetDir, 'skills', 'my-custom-skill');
|
|
fs.mkdirSync(customSkillDir, { recursive: true });
|
|
fs.writeFileSync(path.join(customSkillDir, 'SKILL.md'), '# My Custom Skill\n');
|
|
|
|
assert.ok(fs.existsSync(path.join(customSkillDir, 'SKILL.md')), 'custom skill exists before uninstall');
|
|
|
|
uninstall(false, 'codebuddy');
|
|
|
|
assert.ok(fs.existsSync(path.join(customSkillDir, 'SKILL.md')),
|
|
'Non-GSD skill directory should be preserved after CodeBuddy uninstall');
|
|
});
|
|
|
|
test('removes engine directory on --codebuddy --uninstall', () => {
|
|
const targetDir = path.join(tmpDir, '.codebuddy');
|
|
install(false, 'codebuddy');
|
|
|
|
assert.ok(fs.existsSync(path.join(targetDir, 'gsd-core', 'VERSION')),
|
|
'engine exists before uninstall');
|
|
|
|
uninstall(false, 'codebuddy');
|
|
|
|
assert.ok(!fs.existsSync(path.join(targetDir, 'gsd-core')),
|
|
'gsd-core engine should be removed after CodeBuddy uninstall');
|
|
});
|
|
});
|