diff --git a/.changeset/378-update-check-scoped-name.md b/.changeset/378-update-check-scoped-name.md new file mode 100644 index 000000000..768de5691 --- /dev/null +++ b/.changeset/378-update-check-scoped-name.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 378 +--- +Update check now polls the correct scoped package name (@opengsd/get-shit-done-redux) so update_available works. diff --git a/hooks/gsd-check-update-worker.js b/hooks/gsd-check-update-worker.js index fe9769195..a23276817 100644 --- a/hooks/gsd-check-update-worker.js +++ b/hooks/gsd-check-update-worker.js @@ -13,6 +13,9 @@ const fs = require('fs'); const path = require('path'); const { execFileSync } = require('child_process'); const { isSemverNewer } = require('../get-shit-done/bin/lib/semver-compare.cjs'); +// Derive the published package name from package.json so this survives +// future renames and always matches the actual registry entry (#378). +const PACKAGE_NAME = require('../package.json').name; const cacheFile = process.env.GSD_CACHE_FILE; const projectVersionFile = process.env.GSD_PROJECT_VERSION_FILE; @@ -79,7 +82,7 @@ if (configDir) { let latest = null; try { - latest = execFileSync('npm', ['view', 'get-shit-done-redux', 'version'], { + latest = execFileSync('npm', ['view', PACKAGE_NAME, 'version'], { encoding: 'utf8', timeout: 10000, windowsHide: true, diff --git a/tests/bug-378-update-check-scoped-name.test.cjs b/tests/bug-378-update-check-scoped-name.test.cjs new file mode 100644 index 000000000..ac8818e42 --- /dev/null +++ b/tests/bug-378-update-check-scoped-name.test.cjs @@ -0,0 +1,98 @@ +/** + * Regression test for #378: gsd-check-update-worker.js must query + * the SCOPED package name (@opengsd/get-shit-done-redux) when calling + * `npm view version`. + * + * Background: the worker previously hardcoded the unscoped string + * 'get-shit-done-redux', which returns E404 from the npm registry because + * the published package is scoped. This caused `latest` to stay null and + * `update_available` to be permanently false — users never saw update + * notifications. + * + * The fix derives the name from package.json (most robust — survives + * future renames). This test locks the contract in two ways: + * + * 1. Structural: the worker must NOT contain the bare unscoped literal + * 'get-shit-done-redux' as a standalone npm view argument. + * 2. Derived: the worker must read the package name from package.json + * and the package.json name MUST be the scoped string + * '@opengsd/get-shit-done-redux'. + * + * Source-grep policy: this test reads hook source via readFileSync. + * The repo's lint-no-source-grep rule targets bin/lib/get-shit-done — hooks/ + * is out of scope. The shape we need to lock (which string is passed as the + * npm view argument) only manifests at runtime against the live registry; + * a structural assertion is the minimum-cost contract. + */ + +// allow-test-rule: structural assertion on hook npm-view argument; the +// behavior being tested (correct package name → no E404) only manifests at +// runtime against the live npm registry, which CI does not call. + +'use strict'; + +const { test, describe } = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('fs'); +const path = require('path'); + +const WORKER_PATH = path.join(__dirname, '..', 'hooks', 'gsd-check-update-worker.js'); +const PKG_PATH = path.join(__dirname, '..', 'package.json'); + +describe('bug #378: update-check worker uses scoped package name', () => { + test('worker file exists', () => { + assert.ok(fs.existsSync(WORKER_PATH), `worker not found at ${WORKER_PATH}`); + }); + + test('package.json name is the scoped @opengsd/get-shit-done-redux', () => { + const pkg = JSON.parse(fs.readFileSync(PKG_PATH, 'utf8')); + assert.equal( + pkg.name, + '@opengsd/get-shit-done-redux', + 'package.json must declare the scoped name — this is what npm view must query', + ); + }); + + test('worker does NOT hardcode the unscoped get-shit-done-redux as an npm view argument', () => { + const src = fs.readFileSync(WORKER_PATH, 'utf8'); + + // Strip comments so doc-prose mentions don't trigger the check. + const codeOnly = src + .replace(/\/\*[\s\S]*?\*\//g, '') + .replace(/(^|[^:])\/\/[^\n]*/g, '$1'); + + // The unscoped bare string as a string literal used in code. + // A match here means the bug is present: npm view 'get-shit-done-redux' + // → E404 → update_available permanently false. + const unscopedLiteral = /['"]get-shit-done-redux['"]/; + + assert.doesNotMatch( + codeOnly, + unscopedLiteral, + [ + "Worker must not pass the unscoped 'get-shit-done-redux' to `npm view`.", + 'That name returns E404, leaving update_available permanently false.', + 'Use the scoped name from package.json: @opengsd/get-shit-done-redux.', + ].join(' '), + ); + }); + + test('worker derives package name from package.json (require + .name)', () => { + const src = fs.readFileSync(WORKER_PATH, 'utf8'); + + // Structural check: worker must load package.json and read .name from it. + // This is the robust form — survives future renames without code edits. + const requiresPkgJson = /require\s*\(\s*['"][^'"]*package\.json['"]\s*\)\.name/; + + assert.match( + src, + requiresPkgJson, + [ + 'Worker must derive the npm view package name via', + "`require('../package.json').name` (or similar).", + 'Hardcoding the scoped literal is less robust: a future rename', + 'would silently break update checks again.', + ].join(' '), + ); + }); +});