- bin/install.js now copies scripts/changeset/ and scripts/lib/ into <configDir>/scripts/ so $GSD_DIR/scripts/changeset/cli.cjs resolves at runtime; aborts install with an explicit failure if the source directory is missing from the package. - gsd-core/workflows/update.md: corrected path from gsd-core/scripts/changeset/cli.cjs to scripts/changeset/cli.cjs; added an explicit [ ! -f ] guard so a missing CLI surfaces a clear message rather than silently swallowing the error; stderr captured via 2>&1 sentinel so node errors are visible in the preview output. - release.yml's changeset-CLI invocations (node scripts/changeset/cli.cjs) remain at the repo-root path and are unaffected by this change. Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -332,10 +332,13 @@ describe('changeset cli extract: version-range changelog extraction (#3496)', ()
|
||||
test('F1: workflows/update.md contains concrete extract subcommand invocation', (_t) => {
|
||||
const workflowPath = path.join(ROOT, 'gsd-core', 'workflows', 'update.md');
|
||||
const workflowText = fs.readFileSync(workflowPath, 'utf8');
|
||||
// The invocation is: node "$GSD_DIR/gsd-core/scripts/changeset/cli.cjs" extract
|
||||
// so the literal substring is 'cli.cjs" extract' (quote between script path and subcommand)
|
||||
// The invocation uses either a direct path or an intermediate variable:
|
||||
// node "$GSD_DIR/scripts/changeset/cli.cjs" extract
|
||||
// node "$GSD_CHANGESET_CLI" extract
|
||||
// Accept either form so future refactors don't immediately trip this anchor.
|
||||
assert.ok(
|
||||
workflowText.includes('cli.cjs" extract') || workflowText.includes('cli.cjs extract'),
|
||||
workflowText.includes('cli.cjs" extract') || workflowText.includes('cli.cjs extract') ||
|
||||
(workflowText.includes('GSD_CHANGESET_CLI') && workflowText.includes('" extract')),
|
||||
'update.md must invoke cli.cjs extract (fix for #3496 BLOCKER 1)',
|
||||
);
|
||||
assert.ok(
|
||||
@@ -351,6 +354,40 @@ describe('changeset cli extract: version-range changelog extraction (#3496)', ()
|
||||
'update.md must capture exit code or JSON output from extract',
|
||||
);
|
||||
});
|
||||
|
||||
// F2: update.md must use the INSTALLED path ($GSD_DIR/scripts/changeset/cli.cjs),
|
||||
// NOT the old broken path ($GSD_DIR/gsd-core/scripts/changeset/cli.cjs).
|
||||
// The installer copies scripts/changeset/ into <configDir>/scripts/changeset/,
|
||||
// so the runtime path is $GSD_DIR/scripts/changeset/cli.cjs (#935).
|
||||
// allow-test-rule: reads a product workflow .md file (not CJS source) to verify
|
||||
// the runtime install path contract; there is no behavioural runtime to invoke.
|
||||
test('F2: update.md CLI path is $GSD_DIR/scripts/changeset/cli.cjs (not gsd-core/scripts/…) (#935)', (_t) => {
|
||||
const workflowPath = path.join(ROOT, 'gsd-core', 'workflows', 'update.md');
|
||||
const workflowText = fs.readFileSync(workflowPath, 'utf8');
|
||||
// The correct installed path must appear somewhere in the update workflow
|
||||
assert.ok(
|
||||
workflowText.includes('scripts/changeset/cli.cjs'),
|
||||
'update.md must reference scripts/changeset/cli.cjs',
|
||||
);
|
||||
// The old broken path ($GSD_DIR/gsd-core/scripts/changeset/cli.cjs) must not appear
|
||||
assert.ok(
|
||||
!workflowText.includes('gsd-core/scripts/changeset/cli.cjs'),
|
||||
'update.md must NOT reference the old gsd-core/scripts/changeset/cli.cjs path (fix for #935)',
|
||||
);
|
||||
});
|
||||
|
||||
// F3: update.md must guard against the CLI being missing (not pure silent-swallow)
|
||||
// allow-test-rule: reads a product workflow .md file (not CJS source) to verify
|
||||
// the guard is present; there is no behavioural runtime to invoke.
|
||||
test('F3: update.md has an explicit guard when changeset CLI is missing (#935)', (_t) => {
|
||||
const workflowPath = path.join(ROOT, 'gsd-core', 'workflows', 'update.md');
|
||||
const workflowText = fs.readFileSync(workflowPath, 'utf8');
|
||||
// The workflow must check for CLI existence before invoking it
|
||||
assert.ok(
|
||||
workflowText.includes('GSD_CHANGESET_CLI') && workflowText.includes('! -f'),
|
||||
'update.md must guard against a missing changeset CLI with [ ! -f "$GSD_CHANGESET_CLI" ] (#935)',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('changeset cli render: file-I/O wrapper (#2975)', () => {
|
||||
|
||||
@@ -707,3 +707,126 @@ describe('Kilo source integration assertions', () => {
|
||||
assert.ok(updateContextSrc.includes('KILO_CONFIG'));
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Section N: changeset CLI install regression (#935) ──────────────────────
|
||||
|
||||
describe('install — changeset CLI lands at scripts/changeset/cli.cjs (#935)', () => {
|
||||
// Regression guard: the changeset CLI must be copied into the runtime config dir
|
||||
// by the installer so $GSD_DIR/scripts/changeset/cli.cjs resolves at runtime.
|
||||
// Before this fix, scripts/ was never copied and /gsd-update changelog preview
|
||||
// silently failed on every real install.
|
||||
let tmpDir;
|
||||
let previousCwd;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = createTempDir('gsd-changeset-install-');
|
||||
previousCwd = process.cwd();
|
||||
process.chdir(tmpDir);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.chdir(previousCwd);
|
||||
cleanup(tmpDir);
|
||||
});
|
||||
|
||||
test('install() copies scripts/changeset/cli.cjs to <configDir>/scripts/changeset/cli.cjs', () => {
|
||||
install(false, 'claude');
|
||||
const claudeDir = path.join(tmpDir, '.claude');
|
||||
const cliPath = path.join(claudeDir, 'scripts', 'changeset', 'cli.cjs');
|
||||
assert.ok(
|
||||
fs.existsSync(cliPath),
|
||||
`scripts/changeset/cli.cjs must exist at ${path.relative(tmpDir, cliPath)} after install (#935)`,
|
||||
);
|
||||
});
|
||||
|
||||
test('install() copies scripts/lib/cli-exit.cjs to <configDir>/scripts/lib/cli-exit.cjs', () => {
|
||||
install(false, 'claude');
|
||||
const claudeDir = path.join(tmpDir, '.claude');
|
||||
const cliExitPath = path.join(claudeDir, 'scripts', 'lib', 'cli-exit.cjs');
|
||||
assert.ok(
|
||||
fs.existsSync(cliExitPath),
|
||||
`scripts/lib/cli-exit.cjs must exist at ${path.relative(tmpDir, cliExitPath)} after install (#935)`,
|
||||
);
|
||||
});
|
||||
|
||||
test('installed cli.cjs executes without module-resolution errors', () => {
|
||||
// Smoke test: node can load the installed changeset CLI without crashing.
|
||||
// This catches path mismatches in require('../lib/cli-exit.cjs') etc.
|
||||
install(false, 'claude');
|
||||
const claudeDir = path.join(tmpDir, '.claude');
|
||||
const cliPath = path.join(claudeDir, 'scripts', 'changeset', 'cli.cjs');
|
||||
const { spawnSync } = require('node:child_process');
|
||||
const result = spawnSync(process.execPath, [cliPath, '--help'], { encoding: 'utf8' });
|
||||
// --help exits with code 1 (usage shown), but must NOT throw a MODULE_NOT_FOUND error
|
||||
assert.ok(
|
||||
!result.stderr.includes('MODULE_NOT_FOUND'),
|
||||
`cli.cjs must not produce MODULE_NOT_FOUND errors; stderr=${result.stderr}`,
|
||||
);
|
||||
assert.ok(
|
||||
!result.stderr.includes('Cannot find module'),
|
||||
`cli.cjs must resolve all modules; stderr=${result.stderr}`,
|
||||
);
|
||||
});
|
||||
|
||||
test('installed cli.cjs can run extract subcommand end-to-end (#935)', () => {
|
||||
// Integration smoke test: the installed CLI's extract path (invoked by update.md)
|
||||
// must actually work — this catches require() path issues that --help wouldn't surface.
|
||||
install(false, 'claude');
|
||||
const claudeDir = path.join(tmpDir, '.claude');
|
||||
const cliPath = path.join(claudeDir, 'scripts', 'changeset', 'cli.cjs');
|
||||
// Use the CHANGELOG.md that was installed into gsd-core/ (installed by the installer)
|
||||
const changelogPath = path.join(claudeDir, 'gsd-core', 'CHANGELOG.md');
|
||||
assert.ok(fs.existsSync(changelogPath), 'CHANGELOG.md must be installed under gsd-core/');
|
||||
const { spawnSync } = require('node:child_process');
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
[cliPath, 'extract', '--from', '0.0.0', '--to', '9999.0.0', '--changelog', changelogPath, '--json'],
|
||||
{ encoding: 'utf8' },
|
||||
);
|
||||
// extract must NOT throw a MODULE_NOT_FOUND or Cannot find module error
|
||||
assert.ok(
|
||||
!result.stderr.includes('MODULE_NOT_FOUND') && !result.stderr.includes('Cannot find module'),
|
||||
`installed cli.cjs extract must resolve all modules; stderr=${result.stderr}`,
|
||||
);
|
||||
// extract exit code 0 (found entries) or 2 (no entries in range) are both valid;
|
||||
// any other exit code is an error
|
||||
assert.ok(
|
||||
result.status === 0 || result.status === 2,
|
||||
`installed cli.cjs extract must exit 0 or 2; got ${result.status}; stderr=${result.stderr}`,
|
||||
);
|
||||
});
|
||||
|
||||
test('writeManifest() tracks scripts/changeset/ and scripts/lib/ files', () => {
|
||||
install(false, 'claude');
|
||||
const claudeDir = path.join(tmpDir, '.claude');
|
||||
const manifest = writeManifest(claudeDir, 'claude');
|
||||
const changesetKeys = Object.keys(manifest.files).filter(k => k.startsWith('scripts/changeset/'));
|
||||
const libKeys = Object.keys(manifest.files).filter(k => k.startsWith('scripts/lib/'));
|
||||
assert.ok(changesetKeys.length > 0, 'manifest must track scripts/changeset/ files');
|
||||
assert.ok(libKeys.length > 0, 'manifest must track scripts/lib/ files');
|
||||
assert.ok(
|
||||
changesetKeys.includes('scripts/changeset/cli.cjs'),
|
||||
'manifest must include scripts/changeset/cli.cjs',
|
||||
);
|
||||
assert.ok(
|
||||
libKeys.includes('scripts/lib/cli-exit.cjs'),
|
||||
'manifest must include scripts/lib/cli-exit.cjs',
|
||||
);
|
||||
});
|
||||
|
||||
test('uninstall() removes scripts/changeset/ and scripts/lib/', () => {
|
||||
install(false, 'claude');
|
||||
const claudeDir = path.join(tmpDir, '.claude');
|
||||
assert.ok(fs.existsSync(path.join(claudeDir, 'scripts', 'changeset', 'cli.cjs')),
|
||||
'pre-condition: cli.cjs must be installed before uninstall');
|
||||
uninstall(false, 'claude');
|
||||
assert.ok(
|
||||
!fs.existsSync(path.join(claudeDir, 'scripts', 'changeset')),
|
||||
'scripts/changeset/ must be removed on uninstall',
|
||||
);
|
||||
assert.ok(
|
||||
!fs.existsSync(path.join(claudeDir, 'scripts', 'lib')),
|
||||
'scripts/lib/ must be removed on uninstall',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user