* 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>
194 lines
6.9 KiB
JavaScript
194 lines
6.9 KiB
JavaScript
/**
|
|
* Cursor conversion regression tests.
|
|
*
|
|
* Ensures Cursor frontmatter names are emitted as plain identifiers
|
|
* (without surrounding quotes), so Cursor does not treat quotes as
|
|
* literal parts of skill/subagent names.
|
|
*/
|
|
|
|
process.env.GSD_TEST_MODE = '1';
|
|
|
|
const { describe, test } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
|
|
const {
|
|
convertClaudeCommandToCursorSkill,
|
|
convertClaudeAgentToCursorAgent,
|
|
} = require('../bin/install.js');
|
|
const {
|
|
_applyRuntimeRewrites,
|
|
} = require('../gsd-core/bin/lib/runtime-artifact-conversion.cjs');
|
|
|
|
describe('convertClaudeCommandToCursorSkill', () => {
|
|
test('writes unquoted Cursor skill name in frontmatter', () => {
|
|
const input = `---
|
|
name: quick
|
|
description: Execute a quick task
|
|
---
|
|
|
|
<objective>
|
|
Test body
|
|
</objective>
|
|
`;
|
|
|
|
const result = convertClaudeCommandToCursorSkill(input, 'gsd-quick');
|
|
const nameMatch = result.match(/^name:\s*(.+)$/m);
|
|
|
|
assert.ok(nameMatch, 'frontmatter contains name field');
|
|
assert.strictEqual(nameMatch[1], 'gsd-quick', 'skill name is plain scalar');
|
|
assert.ok(!result.includes('name: "gsd-quick"'), 'quoted skill name is not emitted');
|
|
});
|
|
|
|
test('preserves slash for slash commands in markdown body', () => {
|
|
const input = `---
|
|
name: gsd:plan-phase
|
|
description: Plan a phase
|
|
---
|
|
|
|
Next:
|
|
/gsd:execute-phase 17
|
|
/gsd-help
|
|
gsd:progress
|
|
`;
|
|
|
|
const result = convertClaudeCommandToCursorSkill(input, 'gsd-plan-phase');
|
|
|
|
assert.ok(result.includes('/gsd-execute-phase 17'), 'slash command remains slash-prefixed');
|
|
assert.ok(result.includes('/gsd-help'), 'existing slash command is preserved');
|
|
assert.ok(result.includes('gsd-progress'), 'non-slash gsd: references still normalize');
|
|
assert.ok(!result.includes('/gsd:execute-phase'), 'legacy colon command form is removed');
|
|
});
|
|
});
|
|
|
|
describe('convertClaudeAgentToCursorAgent', () => {
|
|
test('writes unquoted Cursor agent name in frontmatter', () => {
|
|
const input = `---
|
|
name: gsd-planner
|
|
description: Planner agent
|
|
tools: Read, Write
|
|
color: green
|
|
---
|
|
|
|
<role>
|
|
Planner body
|
|
</role>
|
|
`;
|
|
|
|
const result = convertClaudeAgentToCursorAgent(input);
|
|
const nameMatch = result.match(/^name:\s*(.+)$/m);
|
|
|
|
assert.ok(nameMatch, 'frontmatter contains name field');
|
|
assert.strictEqual(nameMatch[1], 'gsd-planner', 'agent name is plain scalar');
|
|
assert.ok(!result.includes('name: "gsd-planner"'), 'quoted agent name is not emitted');
|
|
});
|
|
});
|
|
|
|
// ─── _applyRuntimeRewrites(cursor) — bare-form regression (#1356) ────────────
|
|
//
|
|
// Prior to this fix the cursor branch only rewrote trailing-slash ~/.claude/
|
|
// and $HOME/.claude/ forms. Bare end-of-token references (end of line,
|
|
// inside backtick spans, before punctuation) survived and triggered the
|
|
// post-install audit "Found N unreplaced .claude path reference(s)".
|
|
//
|
|
// Fix: add three bare-form rewrites (mirroring augment/windsurf/trae) using
|
|
// (?![\w-]) to avoid corrupting .claude-plugin / .claudeignore.
|
|
//
|
|
// TDD proof: these assertions FAIL before the fix and PASS after.
|
|
|
|
describe('_applyRuntimeRewrites(cursor) — bare-form ~/.claude regression (#1356)', () => {
|
|
const CURSOR_PATH_PREFIX = '~/.cursor/';
|
|
|
|
// Compound input that exercises every bare and slash ~/.claude / $HOME/.claude
|
|
// form the fix must handle. .claude-plugin is intentionally excluded here
|
|
// because \b fires before the hyphen — its preservation is tested separately.
|
|
const COMPOUND_INPUT = [
|
|
'Config dir: ~/.claude',
|
|
'Also: $HOME/.claude',
|
|
'Slash form: ~/.claude/gsd-core/foo.md',
|
|
'Inline: paths `~/.claude`, `~/.cursor`',
|
|
].join('\n');
|
|
|
|
// Input for the preservation test only — includes .claude-plugin.
|
|
const COMPOUND_INPUT_WITH_PLUGIN = COMPOUND_INPUT + '\nPlugin installed at: ~/.claude-plugin/plugin.json';
|
|
|
|
test('bare ~/.claude at end of line is rewritten (no trailing slash)', () => {
|
|
const result = _applyRuntimeRewrites(COMPOUND_INPUT, 'cursor', CURSOR_PATH_PREFIX);
|
|
assert.ok(
|
|
!/~\/\.claude\b/.test(result),
|
|
`bare ~/.claude must be gone; got:\n${result}`,
|
|
);
|
|
assert.ok(result.includes('~/.cursor'), `cursor prefix must appear; got:\n${result}`);
|
|
});
|
|
|
|
test('bare $HOME/.claude at end of line is rewritten', () => {
|
|
const result = _applyRuntimeRewrites(COMPOUND_INPUT, 'cursor', CURSOR_PATH_PREFIX);
|
|
assert.ok(
|
|
!/\$HOME\/\.claude\b/.test(result),
|
|
`bare $HOME/.claude must be gone; got:\n${result}`,
|
|
);
|
|
});
|
|
|
|
test('bare ~/.claude inside a code span (before punctuation) is rewritten', () => {
|
|
const input = 'paths: `~/.claude`, `~/.cursor`';
|
|
const result = _applyRuntimeRewrites(input, 'cursor', CURSOR_PATH_PREFIX);
|
|
assert.ok(
|
|
!/~\/\.claude\b/.test(result),
|
|
`bare ~/.claude before punctuation must be gone; got:\n${result}`,
|
|
);
|
|
});
|
|
|
|
test('trailing-slash ~/.claude/gsd-core/foo.md is rewritten exactly once (no doubling)', () => {
|
|
const input = '~/.claude/gsd-core/foo.md';
|
|
const result = _applyRuntimeRewrites(input, 'cursor', CURSOR_PATH_PREFIX);
|
|
assert.ok(result.includes('~/.cursor/gsd-core/foo.md'), `slash form must be rewritten; got: ${result}`);
|
|
assert.ok(!result.includes('cursor/cursor'), `path must not be doubled; got: ${result}`);
|
|
assert.ok(!result.includes('.claude'), `no .claude must survive; got: ${result}`);
|
|
});
|
|
|
|
test('zero surviving bare ~/.claude or $HOME/.claude refs in compound input', () => {
|
|
const result = _applyRuntimeRewrites(COMPOUND_INPUT, 'cursor', CURSOR_PATH_PREFIX);
|
|
const bareClaudePattern = /(?:~|\$HOME)\/\.claude\b/;
|
|
assert.ok(
|
|
!bareClaudePattern.test(result),
|
|
`no bare ~/.claude / $HOME/.claude must survive; got:\n${result}`,
|
|
);
|
|
});
|
|
|
|
test('~/.claude-plugin is NOT corrupted — (?![\\w-]) lookahead preserves it', () => {
|
|
const result = _applyRuntimeRewrites(COMPOUND_INPUT_WITH_PLUGIN, 'cursor', CURSOR_PATH_PREFIX);
|
|
assert.ok(
|
|
result.includes('~/.claude-plugin'),
|
|
`~/.claude-plugin must be preserved; got:\n${result}`,
|
|
);
|
|
assert.ok(
|
|
!result.includes('~/.cursor-plugin'),
|
|
`~/.cursor-plugin must NOT appear; got:\n${result}`,
|
|
);
|
|
});
|
|
|
|
test('bare relative ./.claude (end-of-token) is rewritten to ./.cursor', () => {
|
|
// The relative bare form ./.claude (no trailing slash) must also be caught.
|
|
// End-of-line, inside a code span, and before punctuation variants.
|
|
// The cursor dir name derived from pathPrefix '~/.cursor/' is '.cursor',
|
|
// so ./.claude → ./.cursor (the ./${dotDirName} form).
|
|
const pathPrefix = '~/.cursor/';
|
|
const isGlobal = false;
|
|
|
|
const input = [
|
|
'see ./.claude for config',
|
|
'also `./.claude` in a code span',
|
|
].join('\n');
|
|
|
|
const result = _applyRuntimeRewrites(input, 'cursor', pathPrefix, isGlobal);
|
|
|
|
assert.ok(
|
|
result.includes('./.cursor'),
|
|
`bare ./.claude must be rewritten to ./.cursor; got:\n${result}`,
|
|
);
|
|
assert.ok(
|
|
!/\/\.claude\b/.test(result),
|
|
`no bare ./.claude (end-of-token) must survive; got:\n${result}`,
|
|
);
|
|
});
|
|
});
|