Files
msd-core/tests/helpers/hooks-dist.cjs
Jakub Zych a9a7a328e6 refactor: hard-fork GSD -> MSD (Make Software Done)
Mechanical rename produced by scripts/msd-rename.cjs: gsd/Gsd/GSD -> msd/Msd/MSD
across contents and paths, upstream package/repo coordinates -> @golem15/msd-core
and golem15com/msd-core. Deep links into upstream history, sibling upstream
packages, the GSD-2 import feature, CHANGELOG.md and .changeset/ are kept as-is.

Hand edits on top: MSD block-letter banner and logos, LICENSE copyright line,
package/plugin identity, regenerated lockfile, install-tree fixtures, derived
registries and benchmark baseline; migration checksum baseline re-locked
(MSD keeps its own install state, so no install had applied the old sums);
sort-order and regex-escaped expectations in tests adjusted.
2026-10-06 01:47:40 +02:00

115 lines
5.6 KiB
JavaScript

'use strict';
/**
* Ensure hooks/dist is populated before any suite that reads it.
* hooks/dist/ is gitignored and only produced by `npm run build:hooks`.
* In CI the scoped/windows test jobs do NOT run build:hooks before running
* tests, so the first test that needs hooks/dist would fail. This mirrors
* the pattern used in bug-3357-codex-legacy-hooks-json-migration.test.cjs.
*
* Idempotent: `isHooksDistStale` rebuilds only when the directory is absent
* or missing an entry from the EXPECTED SET — `scripts/build-hooks.js`'s
* exported `HOOKS_TO_COPY` list. This replaces a former `.js`-extension-count
* heuristic ("populated" if at least one `.js` file exists), which could not
* see a dist missing exactly the file class that caused #3108: build-hooks
* also ships `.sh` files (e.g. `msd-session-state.sh`), so a dist with every
* `.js` present and every `.sh` absent read as fully populated and the
* rebuild was silently skipped. The set-membership check has no such blind
* spot: any expected entry missing, of any extension, is stale. It does NOT
* flag extra/unexpected files as stale — hooks/dist legitimately accumulates
* output the list does not name (subdirectory output, hooks/lib) — and it
* stays cheap (one `readdirSync` into a `Set`, no per-entry `existsSync`, no
* `statSync`) since it runs once per suite.
*
* Extracted from six behaviorally-identical copies that had accumulated
* across tests/install.test.cjs (x2) and tests/install-minimal-hooks.test.cjs
* (x4) — see #2704's Failure B, where a seventh suite
* (tests/mcp-catalog-parity.install.test.cjs) needed the same guard but had
* no copy of its own, and so failed only on lanes where no other suite
* happened to build hooks/dist first.
*/
const fs = require('node:fs');
const path = require('node:path');
// Required as a module object (not destructured) so tests can monkeypatch
// `processSeam.runNode` in place and have `ensureHooksDist` observe the
// replacement — a destructured `const { runNode } = require(...)` would
// bind a local reference at require-time that a later patch to the
// process-seam module's exports could never reach.
const processSeam = require('./process-seam.cjs');
const { throwIfFailed } = require('./git-fixture.cjs');
const { BUILD_TIMEOUT_MS } = require('./timeouts.cjs');
const { HOOKS_TO_COPY, HOOKS_SUBDIRS_TO_COPY } = require('../../scripts/build-hooks.js');
const REPO_ROOT = path.resolve(__dirname, '..', '..');
const HOOKS_DIST_DIR = path.join(REPO_ROOT, 'hooks', 'dist');
const BUILD_HOOKS_SCRIPT = path.join(REPO_ROOT, 'scripts', 'build-hooks.js');
/**
* True when `dir` is missing, or missing any entry `scripts/build-hooks.js`
* expects to have copied there (`HOOKS_TO_COPY`, bare top-level filenames,
* and `HOOKS_SUBDIRS_TO_COPY`, bare subdirectory names such as `lib`). A dist
* with every top-level file present but no `lib/` (e.g. missing
* `msd-graphify-rebuild.sh`) is exactly the same blindness the old
* `.js`-count heuristic had, one level down — so each subdir entry is first
* checked against the same top-level readdir Set, THEN additionally required
* to be a readable, non-empty directory (one extra `readdirSync` per subdir
* entry — there is exactly one, `lib` — never a per-expected-file
* `existsSync`/`statSync`). Presence alone is not enough: a stray regular
* file named `lib`, or an empty `lib/`, would otherwise read as fresh, which
* is the same blind spot one level down again. Extra/unexpected entries never
* count as stale — this is a "is everything expected present and populated"
* check, not an exact-set check.
*
* @param {string} dir
* @returns {boolean}
*/
function isHooksDistStale(dir) {
if (!fs.existsSync(dir)) return true;
let present;
try {
present = new Set(fs.readdirSync(dir));
} catch (e) {
// dir exists (existsSync passed above) but readdirSync still threw —
// e.g. dir is actually a regular file (ENOTDIR), unreadable (EACCES),
// or was removed in the race between the two calls. Unreadable is
// indistinguishable from unusable here, and treating it as stale is
// both safe (rebuilding is idempotent) and the likely remedy — whereas
// throwing would fail every suite's before() on a diagnosis it cannot
// act on.
return true;
}
if (HOOKS_TO_COPY.some((name) => !present.has(name))) return true;
for (const name of HOOKS_SUBDIRS_TO_COPY) {
if (!present.has(name)) return true;
// Presence in the top-level Set only proves an entry named `lib`
// exists — not that it is a directory, nor that it is populated. A
// stray regular file named `lib`, or an empty `lib/` missing e.g.
// `msd-graphify-rebuild.sh`, would otherwise read as fresh: exactly
// the blind spot the subdir check exists to close. One `readdirSync`
// per subdir entry (there is exactly one, `lib`) — no per-expected-file
// `existsSync`/`statSync`, keeping the added cost to one syscall total.
try {
if (fs.readdirSync(path.join(dir, name)).length === 0) return true;
} catch (e) {
// ENOTDIR (regular file), EACCES, or a race where it vanished —
// unreadable/unusable is indistinguishable from absent here, and
// treating it as stale is safe (idempotent rebuild) per the same
// posture as the top-level readdirSync catch above.
return true;
}
}
return false;
}
function ensureHooksDist() {
if (isHooksDistStale(HOOKS_DIST_DIR)) {
throwIfFailed(
processSeam.runNode([BUILD_HOOKS_SCRIPT], { timeoutMs: BUILD_TIMEOUT_MS }),
`node ${BUILD_HOOKS_SCRIPT}`,
);
}
}
module.exports = { ensureHooksDist, isHooksDistStale, HOOKS_DIST_DIR, BUILD_HOOKS_SCRIPT };