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.
115 lines
5.6 KiB
JavaScript
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 };
|