* fix(3579): ship graphify hook + lib/ helper through build-hooks + install `scripts/build-hooks.js` `HOOKS_TO_COPY` did not include `gsd-graphify-update.sh` (added in #3347 / PR #3557), so it never landed in `hooks/dist/` and `bin/install.js` — which `readdirSync`s the dist — never copied it to `~/.claude/hooks/`. The hook's detached rebuild helper at `hooks/lib/gsd-graphify-rebuild.sh` was also silently dropped because both build-hooks.js (flat allowlist) and bin/install.js (readdir + isFile filter) only walked top-level files. The published tarball gap (Gap 3 in the issue body) is not reproduced on origin/main — `npm pack --dry-run --json` shows both source files are present today. Only Gaps 1 and 2 are in scope. Changes: - Add `gsd-graphify-update.sh` to `HOOKS_TO_COPY`. - Add `HOOKS_SUBDIRS_TO_COPY = ['lib']` and copy whitelisted hook subdirectories (`hooks/<dir>/*` → `hooks/dist/<dir>/*`) in build-hooks.js, with the same syntax-check + atomic-rename path the top-level loop uses. - `bin/install.js`: when copying `hooks/dist/`, recurse one level into any directory entry so subdir files (e.g. `lib/gsd-graphify-rebuild.sh`) land at the mirrored target path the hook's REBUILD_SCRIPT lookup expects. Top-level if/else structure for files is unchanged. Regression test `tests/bug-3579-graphify-hook-publish.test.cjs`: - Drift guard: every top-level `hooks/*.sh` must appear in `HOOKS_TO_COPY`. Generalizes beyond graphify so the next .sh hook added cannot regress. - After build: `hooks/dist/gsd-graphify-update.sh` AND `hooks/dist/lib/gsd-graphify-rebuild.sh` exist. - After install: both files land at the target, and no "Missing expected hook: gsd-graphify-update.sh" warning is emitted. Fixes #3579 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(3579): replace source-grep drift guard with filesystem-behavior assertion The Gap-1 drift guard read scripts/build-hooks.js as text and regex-parsed the HOOKS_TO_COPY literal, which tripped lint-no-source-grep and is brittle under refactors. Replace with a behavior-based assertion: run the build, then for every top-level hooks/*.sh assert hooks/dist/<name> exists. Strictly stronger — catches both the original allowlist gap and any future regression that silently drops a hook for any other reason. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
151 lines
6.5 KiB
JavaScript
151 lines
6.5 KiB
JavaScript
'use strict';
|
|
|
|
/**
|
|
* Regression tests for #3579 — graphify auto-update hook (#3347 / PR #3557)
|
|
* was dead-on-arrival in 1.50.0-canary.x because:
|
|
*
|
|
* Gap 1: scripts/build-hooks.js HOOKS_TO_COPY did not include
|
|
* gsd-graphify-update.sh, so it never landed in hooks/dist/ — the
|
|
* installer's bin/install.js readdir loop then never copied it to
|
|
* ~/.claude/hooks/.
|
|
* Gap 2: build-hooks.js (flat allowlist) and bin/install.js (readdir +
|
|
* isFile filter) never copied hooks/lib/gsd-graphify-rebuild.sh.
|
|
* Without the helper the hook resolves rebuild script → not found →
|
|
* exit 0 — feature silently dead.
|
|
*
|
|
* Beyond these two gaps the issue body lists a Gap 3 (npm tarball missing
|
|
* the source files). Inspection of `npm pack --dry-run --json` on origin/main
|
|
* shows both files are now present in the tarball, so the tarball-side
|
|
* regression is not reproduced; only Gaps 1 & 2 are in scope here.
|
|
*
|
|
* Test strategy — three layers, each independent:
|
|
* 1. build-hooks.js HOOKS_TO_COPY includes every top-level .sh under hooks/
|
|
* (allowlist-coverage drift guard). This generalizes beyond graphify so
|
|
* the next .sh added cannot drift back into the gap.
|
|
* 2. After running scripts/build-hooks.js, hooks/dist/gsd-graphify-update.sh
|
|
* and hooks/dist/lib/gsd-graphify-rebuild.sh both exist.
|
|
* 3. After installing into a temp config dir, both files land at
|
|
* hooks/gsd-graphify-update.sh and hooks/lib/gsd-graphify-rebuild.sh
|
|
* and the installer does not emit the "Missing expected hook" warning
|
|
* for gsd-graphify-update.sh.
|
|
*/
|
|
|
|
const { test, describe, before, after } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
const os = require('node:os');
|
|
const { execFileSync } = require('node:child_process');
|
|
|
|
const REPO_ROOT = path.resolve(__dirname, '..');
|
|
const HOOKS_DIR = path.join(REPO_ROOT, 'hooks');
|
|
const DIST_DIR = path.join(HOOKS_DIR, 'dist');
|
|
const BUILD_SCRIPT = path.join(REPO_ROOT, 'scripts', 'build-hooks.js');
|
|
const INSTALL_SCRIPT = path.join(REPO_ROOT, 'bin', 'install.js');
|
|
|
|
// ─── Coverage guard ─────────────────────────────────────────────────────────
|
|
|
|
describe('#3579 Gap 1: build-hooks.js packages every top-level hooks/*.sh into dist', () => {
|
|
// Behavior-based drift guard: rather than parsing the HOOKS_TO_COPY literal
|
|
// out of scripts/build-hooks.js as text (a source-grep that breaks under
|
|
// harmless refactors and fails to catch any other reason a file might get
|
|
// dropped on the floor), we run the actual build and assert the actual
|
|
// filesystem outcome: every top-level hooks/*.sh has a corresponding file
|
|
// in hooks/dist/. This catches the original gap (missing allowlist entry)
|
|
// AND any future regression that silently drops a hook for any other
|
|
// reason (e.g. a copy that swallows errors, a syntax-validator bug, etc.).
|
|
before(() => {
|
|
execFileSync(process.execPath, [BUILD_SCRIPT], { encoding: 'utf-8', stdio: 'pipe' });
|
|
});
|
|
|
|
test('every top-level hooks/*.sh is emitted to hooks/dist/ by the build', () => {
|
|
const topLevelSh = fs
|
|
.readdirSync(HOOKS_DIR, { withFileTypes: true })
|
|
.filter((e) => e.isFile() && e.name.endsWith('.sh'))
|
|
.map((e) => e.name);
|
|
|
|
assert.ok(topLevelSh.length > 0, 'expected at least one top-level hooks/*.sh in source');
|
|
|
|
const missing = topLevelSh.filter(
|
|
(sh) => !fs.existsSync(path.join(DIST_DIR, sh))
|
|
);
|
|
assert.deepStrictEqual(
|
|
missing,
|
|
[],
|
|
`every top-level hooks/*.sh must be emitted to hooks/dist/ by scripts/build-hooks.js; missing from dist: ${JSON.stringify(missing)}`
|
|
);
|
|
});
|
|
});
|
|
|
|
// ─── build-hooks emits dist/ files ──────────────────────────────────────────
|
|
|
|
describe('#3579 Gap 1 + Gap 2: build-hooks.js populates dist with graphify hook + lib helper', () => {
|
|
before(() => {
|
|
execFileSync(process.execPath, [BUILD_SCRIPT], { encoding: 'utf-8', stdio: 'pipe' });
|
|
});
|
|
|
|
test('hooks/dist/gsd-graphify-update.sh exists after build', () => {
|
|
assert.ok(
|
|
fs.existsSync(path.join(DIST_DIR, 'gsd-graphify-update.sh')),
|
|
'expected hooks/dist/gsd-graphify-update.sh to exist after build (Gap 1)'
|
|
);
|
|
});
|
|
|
|
test('hooks/dist/lib/gsd-graphify-rebuild.sh exists after build', () => {
|
|
assert.ok(
|
|
fs.existsSync(path.join(DIST_DIR, 'lib', 'gsd-graphify-rebuild.sh')),
|
|
'expected hooks/dist/lib/gsd-graphify-rebuild.sh to exist after build (Gap 2)'
|
|
);
|
|
});
|
|
});
|
|
|
|
// ─── install lands the files at the target ──────────────────────────────────
|
|
|
|
describe('#3579: installer deploys graphify hook + lib helper to target', () => {
|
|
let tmpDir;
|
|
let installStdout;
|
|
|
|
before(() => {
|
|
execFileSync(process.execPath, [BUILD_SCRIPT], { encoding: 'utf-8', stdio: 'pipe' });
|
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-3579-install-'));
|
|
installStdout = execFileSync(
|
|
process.execPath,
|
|
[INSTALL_SCRIPT, '--claude', '--global', '--yes', '--no-sdk'],
|
|
{
|
|
encoding: 'utf-8',
|
|
stdio: 'pipe',
|
|
env: { ...process.env, CLAUDE_CONFIG_DIR: tmpDir },
|
|
}
|
|
);
|
|
});
|
|
|
|
after(() => {
|
|
if (tmpDir) {
|
|
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
}
|
|
});
|
|
|
|
test('hooks/gsd-graphify-update.sh present at install target', () => {
|
|
const dest = path.join(tmpDir, 'hooks', 'gsd-graphify-update.sh');
|
|
assert.ok(fs.existsSync(dest), `expected ${dest} to exist after install`);
|
|
});
|
|
|
|
test('hooks/lib/gsd-graphify-rebuild.sh present at install target', () => {
|
|
const dest = path.join(tmpDir, 'hooks', 'lib', 'gsd-graphify-rebuild.sh');
|
|
assert.ok(fs.existsSync(dest), `expected ${dest} to exist after install`);
|
|
});
|
|
|
|
test('installer does not warn about missing gsd-graphify-update.sh', () => {
|
|
assert.ok(
|
|
!installStdout.includes('Missing expected hook: gsd-graphify-update.sh'),
|
|
`installer output must not warn about missing graphify hook; got:\n${installStdout}`
|
|
);
|
|
assert.ok(
|
|
!installStdout.includes(
|
|
'Skipped graphify auto-update hook — gsd-graphify-update.sh not found'
|
|
),
|
|
`installer must not skip graphify hook configuration; got:\n${installStdout}`
|
|
);
|
|
});
|
|
});
|