* feat(#1754): CLI version-skew detection — warn when a global install shadows project-local GSD Addresses #1754 (approved-enhancement). Detects when the running gsd-tools.cjs is outside the project root while a project-local install exists — the shadowing scenario from #1748 where a stale global canary CLI (retired @gsd-build/sdk) silently overrides project-local GSD. Implementation (Node CLI entry-point, not shell snippet — avoids bloating 93 workflow files past their size caps): - src/cli-skew-check.cts: pure function checkCliSkew({resolvedPath, projectRoot, projectLocalExists}) → string|null. Compares paths via path.relative; returns a warning when the resolved CLI is outside the project root AND a project-local install exists. Includes @gsd-build/sdk removal hint when the path matches. No I/O (pure), no gsd-sdk literal (avoids bug-2801 lint). - gsd-core/bin/gsd-tools.cjs: wired at startup via the existing findProjectRoot resolver. Non-blocking (try/catch; advisory stderr warning, never gates). - eslint.config.mjs: registers the new ADR-457 generated artifact in the ignores. - tests: 6-case suite (skew/no-skew/legacy/normalization); all green. - Golden fixtures regenerated (UPDATE_GOLDEN=1) for the new compiled artifact. - docs/how-to/update-gsd.md: Diátaxis reference note for the skew warning. Full suite: 3354 pass, 0 regressions (1 pre-existing local AGENTS.md failure). lint:ci green. Closes #1754 * chore(#1754): backfill changeset pr placeholder * chore(#1754): regenerate INVENTORY-MANIFEST for the new cli-skew-check source module --------- Co-authored-by: review-bot <review-bot@gsd>
86 lines
3.7 KiB
JavaScript
86 lines
3.7 KiB
JavaScript
'use strict';
|
|
|
|
/**
|
|
* feat-1754-cli-skew-detection.test.cjs
|
|
*
|
|
* Tests for the CLI version-skew detection module (src/cli-skew-check.cts).
|
|
*
|
|
* The check warns (returns a string) when the running gsd-tools.cjs is NOT the
|
|
* project-local install while a project-local install EXISTS — the shadowing
|
|
* scenario from #1748 (a stale global canary from @gsd-build/sdk shadowing
|
|
* project-local 1.6.0).
|
|
*
|
|
* DEFECT class: environment / version skew (enhancement #1754)
|
|
*
|
|
* The function is PURE (no I/O — the caller provides paths + existence flags),
|
|
* making it trivially testable without filesystem setup.
|
|
*/
|
|
|
|
const { test, describe } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const path = require('node:path');
|
|
|
|
const { checkCliSkew } = require('../gsd-core/bin/lib/cli-skew-check.cjs');
|
|
|
|
describe('#1754: checkCliSkew — pure path-comparison skew detection', () => {
|
|
test('SKEW: resolved CLI outside project root + project-local exists → returns warning', () => {
|
|
const warning = checkCliSkew({
|
|
resolvedPath: '/opt/homebrew/bin/gsd-tools',
|
|
projectRoot: '/home/user/my-project',
|
|
projectLocalExists: true,
|
|
});
|
|
assert.ok(warning, 'Expected a warning string when resolved CLI is outside project root and project-local exists');
|
|
assert.ok(warning.includes('shadow') || warning.includes('outside') || warning.includes('may'),
|
|
`Warning should mention the shadowing/outside nature, got: "${warning}"`);
|
|
});
|
|
|
|
test('NO-SKEW: resolved CLI is the project-local install → returns null', () => {
|
|
const warning = checkCliSkew({
|
|
resolvedPath: '/home/user/my-project/.claude/gsd-core/bin/gsd-tools.cjs',
|
|
projectRoot: '/home/user/my-project',
|
|
projectLocalExists: true,
|
|
});
|
|
assert.strictEqual(warning, null, 'No warning expected when resolved CLI IS the project-local install');
|
|
});
|
|
|
|
test('NO-SKEW: resolved CLI outside project root but NO project-local install → returns null', () => {
|
|
const warning = checkCliSkew({
|
|
resolvedPath: '/usr/local/bin/gsd-tools',
|
|
projectRoot: '/home/user/my-project',
|
|
projectLocalExists: false,
|
|
});
|
|
assert.strictEqual(warning, null, 'No warning expected when no project-local install exists (legitimate global-only)');
|
|
});
|
|
|
|
test('NO-SKEW: projectRoot is null (no project context) → returns null', () => {
|
|
const warning = checkCliSkew({
|
|
resolvedPath: '/usr/local/bin/gsd-tools',
|
|
projectRoot: null,
|
|
projectLocalExists: false,
|
|
});
|
|
assert.strictEqual(warning, null, 'No warning expected when there is no project root');
|
|
});
|
|
|
|
test('LEGACY-SDK: resolved path contains @gsd-build → warning includes removal instructions', () => {
|
|
const warning = checkCliSkew({
|
|
resolvedPath: '/opt/homebrew/lib/node_modules/@gsd-build/sdk/bin/gsd-tools',
|
|
projectRoot: '/home/user/my-project',
|
|
projectLocalExists: true,
|
|
});
|
|
assert.ok(warning, 'Expected a warning for @gsd-build/sdk paths');
|
|
assert.ok(warning.includes('@gsd-build/sdk') || warning.includes('npm uninstall'),
|
|
`Warning should include @gsd-build/sdk removal instructions, got: "${warning}"`);
|
|
});
|
|
|
|
test('PATH-NORMALIZATION: resolved under project root via realpath → no false positive', () => {
|
|
// Even if the resolved path differs in symlink resolution, if it's under the
|
|
// project root, it's not a skew. The caller normalizes paths before calling.
|
|
const warning = checkCliSkew({
|
|
resolvedPath: path.resolve('/home/user/my-project/.claude/gsd-core/bin/gsd-tools.cjs'),
|
|
projectRoot: path.resolve('/home/user/my-project'),
|
|
projectLocalExists: true,
|
|
});
|
|
assert.strictEqual(warning, null, 'No warning when resolved path is under project root (even with realpath normalization)');
|
|
});
|
|
});
|