Files
msd-core/tests/issue-498-package-identity.test.cjs
Tom Boucher 91f5bd7cb1 feat(#607): rebuild get-shit-done-cc → gsd-core migration (per-package cache + installer auto-cleanup + --dry-run) (#611)
* feat(#607): rebuild get-shit-done-cc → gsd-core migration

Leftover get-shit-done-cc installs poisoned the shared update cache,
causing a permanent false "update available". Rebuild the migration so a
stale old install is both harmless and actively removed.

- Per-package update cache filename (gsd-update-check-<slug>.json) in the
  shared ~/.cache/gsd dir, single-sourced via package-identity; writers
  stamp package_name and readers reject foreign/absent lineage. Multi-
  runtime visibility preserved (same shared dir + filename across runtimes).
- New get-shit-done/bin/lib/legacy-cleanup.cjs seam: detects code-file
  references to the old package + the legacy fixed-name cache across home
  runtime dirs; installer auto-cleans on every install; --dry-run previews
  and mutates nothing. User hooks and dev-preferences are never touched.
- update.md cache-clear globs gsd-update-check*.json across ALL supported
  runtimes (adds cursor/windsurf/augment/trae/qwen/hermes/codebuddy/cline).
- Fix worker MODULE_NOT_FOUND post-install (ship managed-hooks-registry.cjs
  + degrade gracefully) so the per-package cache is always written.
- Diataxis how-to: docs/cleanup-get-shit-done-cc.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore(#607): add changeset for PR #611

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(#607): set USERPROFILE alongside HOME in dry-run install test for Windows

os.homedir() reads USERPROFILE on win32, so HOME-only isolation let the
spawned installer scan the real runner home on windows-latest. Set both.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 12:19:57 -04:00

147 lines
6.0 KiB
JavaScript

'use strict';
process.env.GSD_TEST_MODE = '1';
// Issue #498: single Package Identity seam.
// The package coordinates (npm name, bin name, repo slug, changelog URL) are
// DERIVED from package.json, not re-typed. deriveIdentity is the pure core;
// the generated runtime module get-shit-done/bin/lib/package-identity.cjs
// bakes those values at build time so it survives the install layout where
// the only package.json present is the synthetic {"type":"commonjs"} marker.
const { test, describe } = require('node:test');
const assert = require('node:assert/strict');
const path = require('node:path');
const fs = require('node:fs');
const ROOT = path.join(__dirname, '..');
const { deriveIdentity, formatManualInstall, render, slugifyPackageName } = require(
path.join(ROOT, 'scripts', 'generate-package-identity.cjs'),
);
const GENERATED = path.join(ROOT, 'get-shit-done', 'bin', 'lib', 'package-identity.cjs');
describe('Issue #498: deriveIdentity (pure, package.json -> coordinates)', () => {
const FAKE_PKG = {
name: '@scope/example-pkg',
bin: { 'example-pkg': 'bin/install.js', 'extra-tool': 'x.cjs' },
repository: { type: 'git', url: 'git+https://github.com/acme/example-pkg.git' },
};
test('packageName is package.json .name', () => {
assert.equal(deriveIdentity(FAKE_PKG).packageName, '@scope/example-pkg');
});
test('binName is the FIRST bin key (primary launcher)', () => {
assert.equal(deriveIdentity(FAKE_PKG).binName, 'example-pkg');
});
test('repoSlug is owner/name parsed from repository.url (git+ and .git stripped)', () => {
assert.equal(deriveIdentity(FAKE_PKG).repoSlug, 'acme/example-pkg');
});
test('repoUrl is the cleaned https github url', () => {
assert.equal(deriveIdentity(FAKE_PKG).repoUrl, 'https://github.com/acme/example-pkg');
});
test('changelogRawUrl points at raw.githubusercontent main CHANGELOG', () => {
assert.equal(
deriveIdentity(FAKE_PKG).changelogRawUrl,
'https://raw.githubusercontent.com/acme/example-pkg/main/CHANGELOG.md',
);
});
test('derives the real GSD coordinates from the repo package.json', () => {
const real = require(path.join(ROOT, 'package.json'));
const id = deriveIdentity(real);
assert.equal(id.packageName, '@opengsd/gsd-core');
assert.equal(id.binName, 'gsd-core');
assert.equal(id.repoSlug, 'open-gsd/gsd-core');
});
test('deriveIdentity returns cacheSlug for @opengsd/gsd-core', () => {
const real = require(path.join(ROOT, 'package.json'));
const id = deriveIdentity(real);
assert.equal(id.cacheSlug, 'opengsd-gsd-core');
});
test('deriveIdentity returns updateCacheFileName for @opengsd/gsd-core', () => {
const real = require(path.join(ROOT, 'package.json'));
const id = deriveIdentity(real);
assert.equal(id.updateCacheFileName, 'gsd-update-check-opengsd-gsd-core.json');
});
});
describe('Issue #498: slugifyPackageName (pure helper for cache filename)', () => {
test('slugifyPackageName strips leading @, replaces / with -, for @opengsd/gsd-core', () => {
assert.equal(slugifyPackageName('@opengsd/gsd-core'), 'opengsd-gsd-core');
});
test('slugifyPackageName returns empty string for empty input', () => {
assert.equal(slugifyPackageName(''), '');
});
});
describe('Issue #498: formatManualInstall (the npx fallback command)', () => {
test('global scope, no runtime -> npx with --global only', () => {
assert.equal(
formatManualInstall({ packageName: '@scope/example-pkg', binName: 'example-pkg', scope: 'global' }),
'npx -y --package=@scope/example-pkg@latest -- example-pkg --global',
);
});
test('local scope with runtime -> --<runtime> before --<scope>', () => {
assert.equal(
formatManualInstall({ packageName: '@scope/example-pkg', binName: 'example-pkg', scope: 'local', runtime: 'claude' }),
'npx -y --package=@scope/example-pkg@latest -- example-pkg --claude --local',
);
});
test('matches the literal update.md uses for the real package (global+claude)', () => {
const id = deriveIdentity(require(path.join(ROOT, 'package.json')));
assert.equal(
formatManualInstall({ packageName: id.packageName, binName: id.binName, scope: 'global', runtime: 'claude' }),
'npx -y --package=@opengsd/gsd-core@latest -- gsd-core --claude --global',
);
});
});
describe('Issue #498: generated runtime module (baked, drift-checked)', () => {
test('the committed generated file is in sync with package.json (no drift)', () => {
// Normalize line endings: on Windows the file is checked out with CRLF
// (no .gitattributes eol rule), while render() emits LF. The repo's
// convention is to compare normalized content (see autonomous-decomposition,
// bug-3707). The sync check is about content, not the checkout's eol.
const norm = (s) => s.replace(/\r\n/g, '\n');
const expected = render(deriveIdentity(require(path.join(ROOT, 'package.json'))));
// allow-test-rule: architectural-invariant
const actual = fs.readFileSync(GENERATED, 'utf8');
assert.equal(norm(actual), norm(expected),
'package-identity.cjs is stale — run `node scripts/generate-package-identity.cjs`');
});
test('requiring the generated module exposes the real coordinates', () => {
const id = require(GENERATED);
assert.equal(id.packageName, '@opengsd/gsd-core');
assert.equal(id.binName, 'gsd-core');
assert.equal(id.repoSlug, 'open-gsd/gsd-core');
});
test('generated module exports cacheSlug matching @opengsd/gsd-core', () => {
const id = require(GENERATED);
assert.equal(id.cacheSlug, 'opengsd-gsd-core');
});
test('generated module exports updateCacheFileName matching @opengsd/gsd-core', () => {
const id = require(GENERATED);
assert.equal(id.updateCacheFileName, 'gsd-update-check-opengsd-gsd-core.json');
});
test('generated manualInstallCommand closes over the baked coordinates', () => {
const id = require(GENERATED);
assert.equal(
id.manualInstallCommand({ scope: 'global', runtime: 'claude' }),
'npx -y --package=@opengsd/gsd-core@latest -- gsd-core --claude --global',
);
});
});