fix(#378): poll scoped package name in update check (#414)

The update worker queried the unscoped 'get-shit-done-redux' via
`npm view`, which returns E404 — so `latest` stayed null and
`update_available` could never become true. Now derives the name from
package.json (`require('../package.json').name`) so it always matches
the actual published scoped name (@opengsd/get-shit-done-redux).

Fixes #378.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-05-27 21:10:07 -04:00
committed by GitHub
parent 485ea1bd3d
commit 7ea6a06645
3 changed files with 107 additions and 1 deletions

View File

@@ -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.

View File

@@ -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,

View File

@@ -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 <name> 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(' '),
);
});
});