gstack/test/no-stale-gstack-brain-refs....

124 lines
5.2 KiB
TypeScript

/**
* Regression: no stale `gstack-brain-init`, `gbrain_sync_mode`, or
* `~/.gstack-brain-remote.txt` references survive the v1.27.0.0 rename.
*
* Per codex Findings #1 + #8 + #9: the rename's blast radius is wider than
* the obvious bin/ + scripts/ surface. This test grep-scans the broader
* tree (bin, scripts, *.tmpl, generated *.md, test/, docs/) for the
* deprecated identifiers and fails CI if any callers were missed.
*
* Allowlist: the migration script (`gstack-upgrade/migrations/v1.27.0.0.sh`)
* legitimately references the old names — it's the rename actor itself.
* Old migration scripts (v1.17.0.0.sh and similar) reference the old names
* for their own historical context and are also allowlisted.
*
* The test is mechanical: if you find yourself adding a non-historical
* file to the allowlist, you probably need to actually fix the rename
* instead.
*/
import { describe, test, expect } from 'bun:test';
import * as fs from 'fs';
import * as path from 'path';
import { spawnSync } from 'child_process';
const ROOT = path.resolve(import.meta.dir, '..');
const ALLOWLIST = [
// The migration script that performs the rename. Self-references are expected.
'gstack-upgrade/migrations/v1.27.0.0.sh',
// Older migration scripts — historical references; these document past state.
'gstack-upgrade/migrations/v1.17.0.0.sh',
// The migration test itself — it asserts on the migration's behavior.
'test/migrations-v1.27.0.0.test.ts',
// The test for the v1.17.0.0 historical migration.
'test/gstack-upgrade-migration-v1_17_0_0.test.ts',
// CHANGELOG entries describe historical state by their nature.
'CHANGELOG.md',
// TODOS may reference past or future states by name.
'TODOS.md',
// The plan file for v1.27.0.0 documents why we're renaming.
'.context/plans/setup-gbrain-remote-mcp-rename-brain-artifacts.md',
// The bin/gstack-config comment explicitly preserves the rename note.
'bin/gstack-config',
// Detect script's "renamed in v1.27.0.0" comment + brain-remote-fallback path.
'bin/gstack-gbrain-detect',
// brain-restore + source-wireup keep the old file as a migration-window fallback
// (read both, prefer artifacts). brain-uninstall has the same fallback.
'bin/gstack-brain-restore',
'bin/gstack-gbrain-source-wireup',
'bin/gstack-brain-uninstall',
// The preamble resolver reads the legacy file as a fallback during the
// migration window — same pattern.
'scripts/resolvers/preamble/generate-brain-sync-block.ts',
// gstack-upgrade.test.ts may exercise old migration behavior.
'test/gstack-upgrade.test.ts',
// This test itself references the patterns to grep for.
'test/no-stale-gstack-brain-refs.test.ts',
// The v1.36.0.0 doc-config drift guard intentionally defends the rename
// by listing the deprecated keys in its DEPRECATED_KEYS denylist.
'test/docs-config-keys.test.ts',
// memory.md documents the rename context.
'setup-gbrain/memory.md',
// The new init script's header comment intentionally cites the rename.
'bin/gstack-artifacts-init',
// The replacement test mirrors the pattern of the old test (lineage note).
'test/gstack-artifacts-init.test.ts',
// The post-rename-doc-regen test references the patterns it greps for.
'test/post-rename-doc-regen.test.ts',
// The Path 4 structural lint references some legacy names in comments.
'test/setup-gbrain-path4-structure.test.ts',
// Generated docs that include the preamble bash (which has the fallback).
// We grep template sources, not generated output, by limiting scan paths.
];
const FORBIDDEN_PATTERNS = [
'gstack-brain-init',
'gbrain_sync_mode',
];
const SCAN_PATHS = [
'bin/',
'scripts/',
'setup-gbrain/SKILL.md.tmpl',
'sync-gbrain/SKILL.md.tmpl',
'health/SKILL.md.tmpl',
'plan-eng-review/SKILL.md.tmpl',
'plan-ceo-review/SKILL.md.tmpl',
'review/SKILL.md.tmpl',
'ship/SKILL.md.tmpl',
'test/',
];
function grepRefs(pattern: string): string[] {
const args = ['-rn', '--', pattern, ...SCAN_PATHS.map((p) => path.join(ROOT, p))];
const r = spawnSync('grep', args, { encoding: 'utf-8' });
// grep exits 1 when no matches — that's fine for our purposes.
const lines = (r.stdout || '').split('\n').filter((l) => l.trim().length > 0);
return lines
.map((line) => {
// Strip ROOT prefix to get repo-relative path.
const colon = line.indexOf(':');
const file = line.slice(0, colon);
return path.relative(ROOT, file);
})
.filter((file) => !ALLOWLIST.includes(file))
// Filter out any file that's inside a directory we don't actually scan.
.filter((file) => !file.startsWith('node_modules/') && !file.startsWith('.git/'));
}
describe('no stale gstack-brain refs (v1.27.0.0 rename)', () => {
for (const pattern of FORBIDDEN_PATTERNS) {
test(`no non-allowlisted references to "${pattern}"`, () => {
const offenders = [...new Set(grepRefs(pattern))];
if (offenders.length > 0) {
console.error(`Found stale "${pattern}" references in:\n${offenders.map((f) => ` - ${f}`).join('\n')}`);
console.error(
`If a file is intentionally referencing the old name (migration, historical doc, fallback path), add it to ALLOWLIST in this test.`
);
}
expect(offenders).toEqual([]);
});
}
});