Files
msd-core/tests/feat-1754-cli-skew-detection.test.cjs
Tom Boucher e075a41c86 feat(#1754): CLI version-skew detection — warn when a global install shadows project-local GSD (#1755)
* 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>
2026-06-26 12:19:39 -04:00

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)');
});
});