test(#606): regression guard — every same-dir require() target of a shipped hook must itself ship (#613)

* test(#606): guard that every same-dir require() target of a shipped hook is shipped

#606: gsd-check-update-worker.js require()'d its sibling managed-hooks-registry.cjs,
but the file was missing from HOOKS_TO_COPY, so the installer never placed it next
to the worker and the background update worker crashed silently with
"Cannot find module". The fix shipped in #611 (added the file to HOOKS_TO_COPY);
this adds the regression guard that was the issue's third acceptance criterion.

The new test scans every JS/CJS hook in HOOKS_TO_COPY for same-directory relative
requires — require('./x') and require('./subdir/x') — and asserts each target is
itself shipped: './x' is in HOOKS_TO_COPY, or './subdir/...' lives under a dir in
HOOKS_SUBDIRS_TO_COPY. require('../...') targets that escape the hooks/ dir are out
of scope (their shipping is governed by package.json "files").

Exports HOOKS_SUBDIRS_TO_COPY from scripts/build-hooks.js so the test reads the real
subdir allowlist instead of hardcoding ['lib'].

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

* chore(#606): add changeset crediting installer registry fix

Fixed-type fragment so the #606 installer fix (managed-hooks-registry.cjs
now shipped) is credited in the release notes. The behavioral change landed
in #611; this records it for the changelog and credits the reporter.

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-06-02 13:00:29 -04:00
committed by GitHub
parent 91f5bd7cb1
commit 9263fa1e46
3 changed files with 49 additions and 2 deletions

View File

@@ -0,0 +1,5 @@
---
type: Fixed
pr: 613
---
**Fresh installs now ship `managed-hooks-registry.cjs` next to `gsd-check-update-worker.js`, so the background update checker no longer crashes silently (#606)** — the worker `require()`s that sibling, but it had been missing from the hooks copy allowlist, so on a clean install the worker threw `Cannot find module` in the background and the update cache was never written. The file is now in the allowlist, and a regression guard asserts that every same-directory `require()` target of a shipped hook is itself shipped. Thanks @baksohyeon for the clean-room reproduction.

View File

@@ -237,4 +237,4 @@ if (require.main === module) {
build();
}
module.exports = { HOOKS_TO_COPY };
module.exports = { HOOKS_TO_COPY, HOOKS_SUBDIRS_TO_COPY };

View File

@@ -26,7 +26,7 @@ const HOOKS_DIR = path.join(__dirname, '..', 'hooks');
// Typed imports — no source-grep needed (#455)
const { MANAGED_HOOKS } = require(path.join(HOOKS_DIR, 'managed-hooks-registry.cjs'));
const { HOOKS_TO_COPY } = require(path.join(__dirname, '..', 'scripts', 'build-hooks.js'));
const { HOOKS_TO_COPY, HOOKS_SUBDIRS_TO_COPY } = require(path.join(__dirname, '..', 'scripts', 'build-hooks.js'));
describe('orphaned hooks stale detection (#1750)', () => {
test('MANAGED_HOOKS is an array and does not use a broad gsd-* wildcard', () => {
@@ -111,4 +111,46 @@ describe('orphaned hooks stale detection (#1750)', () => {
);
}
});
test('every same-dir require() target of a shipped hook is itself shipped (#606)', () => {
// Regression guard for #606: gsd-check-update-worker.js does
// require('./managed-hooks-registry.cjs'), but that sibling was missing from
// HOOKS_TO_COPY, so the installer never placed it next to the worker and the
// background worker crashed at runtime with "Cannot find module". The fix added
// the file to HOOKS_TO_COPY; this guard fails if any shipped hook ever again
// requires a same-directory file that the installer would not ship.
//
// Scope: only *same-directory* relative requires — require('./x') and
// require('./subdir/x'). A require('../...') target reaches out of the hooks/
// dir into sibling package dirs (e.g. get-shit-done/) whose shipping is governed
// by package.json "files", not by this allowlist, so it is out of scope here.
assert.ok(Array.isArray(HOOKS_SUBDIRS_TO_COPY), 'HOOKS_SUBDIRS_TO_COPY must be exported as an array');
const SAME_DIR_REQUIRE = /require\(\s*['"](\.\/[^'"]+)['"]\s*\)/g;
const jsHooks = HOOKS_TO_COPY.filter(h => /\.c?js$/.test(h));
assert.ok(jsHooks.length >= 5, `expected at least 5 JS/CJS hooks in HOOKS_TO_COPY, got ${jsHooks.length}`);
for (const hook of jsHooks) {
const source = fs.readFileSync(path.join(HOOKS_DIR, hook), 'utf8');
for (const match of source.matchAll(SAME_DIR_REQUIRE)) {
const rel = match[1].slice(2); // strip leading './'
const slash = rel.indexOf('/');
if (slash === -1) {
assert.ok(
HOOKS_TO_COPY.includes(rel),
`${hook} requires './${rel}', but '${rel}' is not in HOOKS_TO_COPY — the installer ` +
`would not place it next to ${hook}, so the require would throw at runtime (the #606 class of bug). ` +
`Add '${rel}' to HOOKS_TO_COPY in scripts/build-hooks.js.`
);
} else {
const subdir = rel.slice(0, slash);
assert.ok(
HOOKS_SUBDIRS_TO_COPY.includes(subdir),
`${hook} requires './${rel}', but subdirectory '${subdir}' is not in HOOKS_SUBDIRS_TO_COPY — ` +
`the installer would not ship it. Add '${subdir}' to HOOKS_SUBDIRS_TO_COPY in scripts/build-hooks.js.`
);
}
}
}
});
});