* test(#339): add regression guard for gsd-sdk refs in runtime surfaces Lock in the already-clean runtime surface so a retired `gsd-sdk`/`GSD_SDK` reference cannot creep back into a shipped prompt or hook. Scans gsd-core/workflows, gsd-core/references, commands/gsd, agents, and hooks (excluding the gitignored dist/ build artifact). Complements gsd-tools-path-refs.test.cjs, which only catches the `gsd-sdk query` binary form; this catches any runtime reference. A second test guards against an empty sweep so a future dir rename can't silently turn the guard into a no-op. Intentionally does NOT touch bin/install.js (live stale-package-detection mechanics per #339 triage) or CI/lint scripts (legitimate stale detection). Refs #339 * test(#339): split file content on /\r?\n/ for Windows CRLF parity The windows-test-parity-guard lint requires test files that readFileSync + split to use /\r?\n/, not '\n', so CRLF files don't leave a trailing \r. New test files are not in the PR #3649 allowlist. * test(#339): cover .sh/.json runtime files in workflows surface Address #691 review: the gsd-core/workflows surface scanned only .md, silently skipping two deployed runtime files — _runtime-launcher.snippet.sh (synced into every hook) and discuss-phase/templates/checkpoint.json. Add .sh/.json so the guard covers all 290 deployed runtime files (was 288/290), not just the .md subset. * test(#339): cover templates/contexts surfaces + per-ext empty-sweep guard Address PR #691 review (trek-e): - Add gsd-core/templates and gsd-core/contexts to RUNTIME_SURFACES — both are deep-copied by the installer and runtime-loaded via @~/.claude/gsd-core/templates/*.md anchors, so a reintroduced gsd-sdk ref there would have slipped past the guard. (major) - Reword the bin/install.js exclusion rationale: it has zero gsd-sdk refs today (subsystem removed in #515, shim retired in #522); the real reason it is excluded is that it is installer code, not a deployed prompt/hook surface. (minor) - Make the empty-sweep guard assert coverage per configured extension, not per surface — .md files alone kept gsd-core/workflows green even if .sh/.json were dropped, silently un-covering _runtime-launcher.snippet.sh and discuss-phase/templates/*.json. (low) --------- Co-authored-by: Tom Boucher <trekkie@nomorestars.com>
134 lines
6.0 KiB
JavaScript
134 lines
6.0 KiB
JavaScript
// allow-test-rule: source-text-is-the-product
|
|
// Runtime prompt/hook files are deployed verbatim — their text IS what the
|
|
// runtime loads and executes. Asserting that text carries no retired `gsd-sdk`
|
|
// reference tests the deployed contract, which no behavioral seam can observe
|
|
// (there is no runtime API that enumerates "did any shipped prompt name the
|
|
// removed SDK binary").
|
|
|
|
/**
|
|
* Regression guard: no `gsd-sdk` references in runtime-facing surfaces (#339).
|
|
*
|
|
* The `@opengsd/gsd-sdk` package and its `gsd-sdk` binary were retired (ADR 0174,
|
|
* #191). The bulk runtime cleanup is already done — this test locks it in so a
|
|
* `gsd-sdk` / `GSD_SDK` reference cannot creep back into a shipped prompt or hook
|
|
* and re-introduce drift between the documented surface and the supported
|
|
* `gsd-tools` binary.
|
|
*
|
|
* Scope: runtime surfaces only — the prompts and hooks the installer ships into
|
|
* a user's runtime config dir. Explicitly NOT covered here:
|
|
* - `bin/install.js` — installer code, not a runtime-deployed prompt/hook
|
|
* surface. (It carries zero `gsd-sdk` references today; the SDK-shim
|
|
* verification subsystem was removed in #515 and the shim retired in #522.)
|
|
* - `gsd-core/bin/` — executable library code, not deployed prompt text; it
|
|
* may legitimately reference the SDK retirement in comments.
|
|
* - `tests/`, `docs/`, `.changeset/`, CI/lint scripts — legitimately reference
|
|
* the SDK retirement as history or detect its stale artifacts.
|
|
*
|
|
* Complements `tests/gsd-tools-path-refs.test.cjs`, which only catches the
|
|
* `gsd-sdk query` binary-invocation form; this catches ANY runtime reference.
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
const { describe, test } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
|
|
const REPO_ROOT = path.join(__dirname, '..');
|
|
|
|
// Runtime surfaces the installer ships. Each entry is { dir, exts } — dir is
|
|
// repo-relative, exts is the set of file extensions whose text is deployed.
|
|
const RUNTIME_SURFACES = [
|
|
// .md prompts plus the non-.md runtime artifacts this dir also ships:
|
|
// _runtime-launcher.snippet.sh (the canonical launcher synced into every hook
|
|
// by scripts/sync-runtime-launcher.cjs) and discuss-phase/templates/*.json
|
|
// (loaded at runtime by discuss-phase.md). Scanning only .md left these two
|
|
// deployed files uncovered. (#691 review)
|
|
{ dir: path.join('gsd-core', 'workflows'), exts: ['.md', '.sh', '.json'] },
|
|
{ dir: path.join('gsd-core', 'references'), exts: ['.md'] },
|
|
// Prompt surfaces the installer deep-copies and the runtime loads via
|
|
// `@~/.claude/gsd-core/templates/*.md` anchors in workflows/commands; the
|
|
// lone config.json under templates/ ships too. (#691 review)
|
|
{ dir: path.join('gsd-core', 'templates'), exts: ['.md', '.json'] },
|
|
{ dir: path.join('gsd-core', 'contexts'), exts: ['.md'] },
|
|
{ dir: path.join('commands', 'gsd'), exts: ['.md'] },
|
|
{ dir: 'agents', exts: ['.md'] },
|
|
// Hooks ship as executable text (.js/.cjs/.sh). `hooks/dist/` is a gitignored
|
|
// build artifact regenerated from these sources, so scanning the sources is
|
|
// sufficient and avoids asserting against generated copies.
|
|
{ dir: 'hooks', exts: ['.js', '.cjs', '.sh'], skipDirs: ['dist'] },
|
|
];
|
|
|
|
// Matches every casing/separator variant of the retired SDK token:
|
|
// gsd-sdk, gsd_sdk, GSD-SDK, GSD_SDK, etc.
|
|
const SDK_REF = /gsd[-_]sdk/i;
|
|
|
|
/**
|
|
* Recursively collect files under `absDir` whose extension is in `exts`,
|
|
* skipping any directory name listed in `skipDirs`.
|
|
*/
|
|
function collectFiles(absDir, exts, skipDirs) {
|
|
if (!fs.existsSync(absDir)) return [];
|
|
const out = [];
|
|
for (const entry of fs.readdirSync(absDir, { withFileTypes: true })) {
|
|
if (entry.isDirectory()) {
|
|
if (skipDirs.includes(entry.name)) continue;
|
|
out.push(...collectFiles(path.join(absDir, entry.name), exts, skipDirs));
|
|
} else if (entry.isFile() && exts.includes(path.extname(entry.name))) {
|
|
out.push(path.join(absDir, entry.name));
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function rel(file) {
|
|
return path.relative(REPO_ROOT, file).split(path.sep).join('/');
|
|
}
|
|
|
|
describe('#339 no gsd-sdk references in runtime surfaces', () => {
|
|
test('shipped prompts and hooks carry no retired gsd-sdk reference', () => {
|
|
const violations = [];
|
|
|
|
for (const { dir, exts, skipDirs = [] } of RUNTIME_SURFACES) {
|
|
const files = collectFiles(path.join(REPO_ROOT, dir), exts, skipDirs);
|
|
for (const file of files) {
|
|
const lines = fs.readFileSync(file, 'utf-8').split(/\r?\n/);
|
|
for (let i = 0; i < lines.length; i++) {
|
|
if (SDK_REF.test(lines[i])) {
|
|
violations.push(`${rel(file)}:${i + 1}: ${lines[i].trim()}`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
assert.strictEqual(
|
|
violations.length,
|
|
0,
|
|
'Runtime surfaces must not reference the retired gsd-sdk binary/package — ' +
|
|
'use gsd-tools instead.\nViolations:\n' + violations.join('\n')
|
|
);
|
|
});
|
|
|
|
test('at least one file per configured extension is scanned (guards against an empty sweep)', () => {
|
|
// A path typo, directory rename, or stale extension could silently make
|
|
// collectFiles() return [] for part of a surface, turning the guard above
|
|
// into a no-op that always passes. Checking per-surface isn't enough: for
|
|
// gsd-core/workflows the .md files alone keep a per-surface count > 0, so
|
|
// dropping .sh/.json would stop covering _runtime-launcher.snippet.sh and
|
|
// discuss-phase/templates/*.json while the test stayed green. Assert each
|
|
// configured extension actually resolves to scanned files. (#691 review)
|
|
for (const { dir, exts, skipDirs = [] } of RUNTIME_SURFACES) {
|
|
for (const ext of exts) {
|
|
const count = collectFiles(path.join(REPO_ROOT, dir), [ext], skipDirs).length;
|
|
assert.ok(
|
|
count > 0,
|
|
`Runtime surface "${dir}" resolved to 0 "${ext}" files — the path may ` +
|
|
'have moved or the extension is stale; update RUNTIME_SURFACES so the ' +
|
|
'guard keeps covering it.'
|
|
);
|
|
}
|
|
}
|
|
});
|
|
});
|