mirror of
https://github.com/glittercowboy/get-shit-done
synced 2026-04-25 17:25:23 +02:00
* fix(hooks): stamp gsd-hook-version in .sh hooks and fix stale detection regex (#2136, #2206) Three-part fix for the persistent "⚠ stale hooks — run /gsd-update" false positive that appeared on every session after a fresh install. Root cause: the stale-hook detector (gsd-check-update.js) could only match the JS comment syntax // in its version regex — never the bash # syntax used in .sh hooks. And the bash hooks had no version header at all, so they always landed in the "unknown / stale" branch regardless. Neither partial fix (PR #2207 regex only, PR #2215 install stamping only) was sufficient alone: - Regex fix without install stamping: hooks install with literal "{{GSD_VERSION}}", the {{-guard silently skips them, bash hook staleness permanently undetectable after future updates. - Install stamping without regex fix: hooks are stamped correctly with "# gsd-hook-version: 1.36.0" but the detector's // regex can't read it; still falls to the unknown/stale branch on every session. Fix: 1. Add "# gsd-hook-version: {{GSD_VERSION}}" header to gsd-phase-boundary.sh, gsd-session-state.sh, gsd-validate-commit.sh 2. Extend install.js (both bundled and Codex paths) to substitute {{GSD_VERSION}} in .sh files at install time (same as .js hooks) 3. Extend gsd-check-update.js versionMatch regex to handle bash "#" comment syntax: /(?:\/\/|#) gsd-hook-version:\s*(.+)/ Tests: 11 new assertions across 5 describe blocks covering all three fix parts independently plus an E2E install+detect round-trip. 3885/3885 pass. Approach credit: PR #2207 (j2h4u / Maxim Brashenko) for the regex fix; PR #2215 (nitsan2dots) for the install.js substitution approach. Closes #2136, #2206, #2209, #2210, #2212 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor(hooks): extract check-update worker to dedicated file, eliminating template-literal regex escaping Move stale-hook detection logic from inline `node -e '<template literal>'` subprocess to a standalone gsd-check-update-worker.js. Benefits: - Regex is plain JS with no double-escaping (root cause of the (?:\\/\\/|#) confusion) - Worker is independently testable and can be read directly by tests - Uses execFileSync (array args) to satisfy security hook that blocks execSync - MANAGED_HOOKS now includes gsd-check-update-worker.js itself Update tests to read worker file instead of main hook for regex/configDir assertions. All 3886 tests pass. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
77 lines
2.8 KiB
JavaScript
77 lines
2.8 KiB
JavaScript
/**
|
|
* Regression tests for bug #2136
|
|
*
|
|
* gsd-check-update.js contains a MANAGED_HOOKS array used to detect stale
|
|
* hooks after a GSD update. It must list every hook file that GSD ships so
|
|
* that all deployed hooks are checked for staleness — not just the .js ones.
|
|
*
|
|
* The original bug: the 3 bash hooks (gsd-phase-boundary.sh,
|
|
* gsd-session-state.sh, gsd-validate-commit.sh) were missing from
|
|
* MANAGED_HOOKS, so they would never be detected as stale after an update.
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
const { describe, test } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const HOOKS_DIR = path.join(__dirname, '..', 'hooks');
|
|
// MANAGED_HOOKS now lives in the worker script (extracted from inline -e code
|
|
// to avoid template-literal regex-escaping concerns). The test reads the worker.
|
|
const MANAGED_HOOKS_FILE = path.join(HOOKS_DIR, 'gsd-check-update-worker.js');
|
|
|
|
describe('bug #2136: MANAGED_HOOKS must include all shipped hook files', () => {
|
|
let src;
|
|
let managedHooks;
|
|
let shippedHooks;
|
|
|
|
// Read once — all tests share the same source snapshot
|
|
src = fs.readFileSync(MANAGED_HOOKS_FILE, 'utf-8');
|
|
|
|
// Extract the MANAGED_HOOKS array entries from the source
|
|
// The array is defined as a multi-line array literal of quoted strings
|
|
const match = src.match(/const MANAGED_HOOKS\s*=\s*\[([\s\S]*?)\]/);
|
|
assert.ok(match, 'MANAGED_HOOKS array not found in gsd-check-update-worker.js');
|
|
|
|
managedHooks = match[1]
|
|
.split('\n')
|
|
.map(line => line.trim().replace(/^['"]|['"],?$/g, ''))
|
|
.filter(s => s.length > 0 && !s.startsWith('//'));
|
|
|
|
// List all GSD-managed hook files in hooks/ (names starting with "gsd-")
|
|
shippedHooks = fs.readdirSync(HOOKS_DIR)
|
|
.filter(f => f.startsWith('gsd-') && (f.endsWith('.js') || f.endsWith('.sh')));
|
|
|
|
test('every shipped gsd-*.js hook is in MANAGED_HOOKS', () => {
|
|
const jsHooks = shippedHooks.filter(f => f.endsWith('.js'));
|
|
for (const hookFile of jsHooks) {
|
|
assert.ok(
|
|
managedHooks.includes(hookFile),
|
|
`${hookFile} is shipped in hooks/ but missing from MANAGED_HOOKS in gsd-check-update-worker.js`
|
|
);
|
|
}
|
|
});
|
|
|
|
test('every shipped gsd-*.sh hook is in MANAGED_HOOKS', () => {
|
|
const shHooks = shippedHooks.filter(f => f.endsWith('.sh'));
|
|
for (const hookFile of shHooks) {
|
|
assert.ok(
|
|
managedHooks.includes(hookFile),
|
|
`${hookFile} is shipped in hooks/ but missing from MANAGED_HOOKS in gsd-check-update-worker.js`
|
|
);
|
|
}
|
|
});
|
|
|
|
test('MANAGED_HOOKS contains no entries for hooks that do not exist', () => {
|
|
for (const entry of managedHooks) {
|
|
const exists = fs.existsSync(path.join(HOOKS_DIR, entry));
|
|
assert.ok(
|
|
exists,
|
|
`MANAGED_HOOKS entry '${entry}' has no corresponding file in hooks/ — remove stale entry`
|
|
);
|
|
}
|
|
});
|
|
});
|