* 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>
178 lines
6.2 KiB
JavaScript
178 lines
6.2 KiB
JavaScript
/**
|
|
* Tests for cache lineage validation (issue #607).
|
|
*
|
|
* Verifies that per-package cache filenames and package_name lineage guards
|
|
* are correctly enforced across gsd-update-banner.js, gsd-statusline.js,
|
|
* and the worker result shape.
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
const { test, describe } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
|
|
const { PACKAGE_NAME, updateCacheFileName } = require('../get-shit-done/bin/lib/package-identity.cjs');
|
|
const { buildBannerOutput } = require('../hooks/gsd-update-banner.js');
|
|
const { evaluateUpdateCache } = require('../hooks/gsd-statusline.js');
|
|
|
|
// ─── Package identity constants ──────────────────────────────────────────────
|
|
|
|
describe('package-identity exports', () => {
|
|
test('PACKAGE_NAME is @opengsd/gsd-core', () => {
|
|
assert.equal(PACKAGE_NAME, '@opengsd/gsd-core');
|
|
});
|
|
|
|
test('updateCacheFileName is per-package filename', () => {
|
|
assert.equal(updateCacheFileName, 'gsd-update-check-opengsd-gsd-core.json');
|
|
});
|
|
});
|
|
|
|
// ─── Worker result shape: package_name field ─────────────────────────────────
|
|
// The worker writes { ..., package_name: PACKAGE_NAME } to the cache.
|
|
// We assert the documented contract by confirming PACKAGE_NAME is correct
|
|
// and that it equals the value that the worker will embed.
|
|
|
|
describe('worker result shape contract', () => {
|
|
test('PACKAGE_NAME value matches the expected installed package', () => {
|
|
// The worker adds package_name: PACKAGE_NAME to its result object.
|
|
// This test asserts the value that will appear in the cache.
|
|
assert.equal(PACKAGE_NAME, '@opengsd/gsd-core');
|
|
});
|
|
});
|
|
|
|
// ─── buildBannerOutput: lineage guard ────────────────────────────────────────
|
|
|
|
describe('buildBannerOutput lineage guard', () => {
|
|
test('returns null when package_name is present but foreign', () => {
|
|
const out = buildBannerOutput({
|
|
cache: {
|
|
update_available: true,
|
|
installed: '1.2.0',
|
|
latest: '1.42.3',
|
|
package_name: 'get-shit-done-cc',
|
|
},
|
|
parseError: false,
|
|
suppressFailureWarning: false,
|
|
});
|
|
assert.equal(out, null, 'foreign lineage must be rejected');
|
|
});
|
|
|
|
test('returns banner when package_name matches PACKAGE_NAME', () => {
|
|
const out = buildBannerOutput({
|
|
cache: {
|
|
update_available: true,
|
|
installed: '1.2.0',
|
|
latest: '1.3.0',
|
|
package_name: '@opengsd/gsd-core',
|
|
},
|
|
parseError: false,
|
|
suppressFailureWarning: false,
|
|
});
|
|
assert.ok(out, 'expected banner envelope for matching lineage');
|
|
assert.equal(typeof out.systemMessage, 'string');
|
|
assert.ok(out.systemMessage.includes('1.2.0'));
|
|
assert.ok(out.systemMessage.includes('1.3.0'));
|
|
assert.ok(out.systemMessage.includes('/gsd:update'));
|
|
});
|
|
|
|
test('returns null when package_name is absent (untrusted cache)', () => {
|
|
const out = buildBannerOutput({
|
|
cache: {
|
|
update_available: true,
|
|
installed: '1.2.0',
|
|
latest: '1.3.0',
|
|
// no package_name field
|
|
},
|
|
parseError: false,
|
|
suppressFailureWarning: false,
|
|
});
|
|
assert.equal(out, null, 'absent package_name must be treated as untrusted → null');
|
|
});
|
|
});
|
|
|
|
// ─── evaluateUpdateCache: lineage guard in statusline ────────────────────────
|
|
|
|
describe('evaluateUpdateCache lineage guard', () => {
|
|
test('returns showUpdate=false when cache is null', () => {
|
|
const r = evaluateUpdateCache(null);
|
|
assert.equal(r.showUpdate, false);
|
|
assert.equal(r.staleWarning, 'none');
|
|
});
|
|
|
|
test('returns showUpdate=false when package_name is absent (untrusted)', () => {
|
|
const r = evaluateUpdateCache({
|
|
update_available: true,
|
|
installed: '1.2.0',
|
|
latest: '1.3.0',
|
|
});
|
|
assert.equal(r.showUpdate, false);
|
|
assert.equal(r.staleWarning, 'none');
|
|
});
|
|
|
|
test('returns showUpdate=false when package_name is foreign', () => {
|
|
const r = evaluateUpdateCache({
|
|
update_available: true,
|
|
installed: '1.2.0',
|
|
latest: '1.3.0',
|
|
package_name: 'some-other-package',
|
|
});
|
|
assert.equal(r.showUpdate, false);
|
|
assert.equal(r.staleWarning, 'none');
|
|
});
|
|
|
|
test('returns showUpdate=true when update_available and package_name matches', () => {
|
|
const r = evaluateUpdateCache({
|
|
update_available: true,
|
|
installed: '1.2.0',
|
|
latest: '1.3.0',
|
|
package_name: '@opengsd/gsd-core',
|
|
});
|
|
assert.equal(r.showUpdate, true);
|
|
assert.equal(r.staleWarning, 'none');
|
|
});
|
|
|
|
test('returns showUpdate=false when update_available=false', () => {
|
|
const r = evaluateUpdateCache({
|
|
update_available: false,
|
|
installed: '1.3.0',
|
|
latest: '1.3.0',
|
|
package_name: '@opengsd/gsd-core',
|
|
});
|
|
assert.equal(r.showUpdate, false);
|
|
assert.equal(r.staleWarning, 'none');
|
|
});
|
|
|
|
test('returns staleWarning=stale when stale_hooks present and matching package_name', () => {
|
|
const r = evaluateUpdateCache({
|
|
update_available: false,
|
|
installed: '1.3.0',
|
|
latest: '1.3.0',
|
|
package_name: '@opengsd/gsd-core',
|
|
stale_hooks: [{ file: 'gsd-statusline.js', hookVersion: '1.2.0', installedVersion: '1.3.0' }],
|
|
});
|
|
assert.equal(r.staleWarning, 'stale');
|
|
});
|
|
|
|
test('returns staleWarning=dev when installed > latest (dev install) and matching package_name', () => {
|
|
const r = evaluateUpdateCache({
|
|
update_available: false,
|
|
installed: '2.0.0',
|
|
latest: '1.3.0',
|
|
package_name: '@opengsd/gsd-core',
|
|
stale_hooks: [{ file: 'gsd-statusline.js', hookVersion: '1.2.0', installedVersion: '2.0.0' }],
|
|
});
|
|
assert.equal(r.staleWarning, 'dev');
|
|
});
|
|
|
|
test('returns staleWarning=none when stale_hooks present but package_name is foreign', () => {
|
|
const r = evaluateUpdateCache({
|
|
update_available: false,
|
|
installed: '1.3.0',
|
|
latest: '1.2.0',
|
|
package_name: 'foreign-pkg',
|
|
stale_hooks: [{ file: 'gsd-statusline.js', hookVersion: '1.2.0', installedVersion: '1.3.0' }],
|
|
});
|
|
assert.equal(r.staleWarning, 'none');
|
|
});
|
|
});
|