Files
gstack/test/frontend-scope.test.ts
T
Garry TanandClaude Fable 5.1 da6f0ff2f6 fix(design): run the DOM dump in the page on both engines; align doctrine with the catalog
The DOM-dump script is an arrow function, not a self-calling IIFE: Aside's
`pg.evaluate($_DUMP)` receives the function and runs it in the page (the IIFE
form executed in the repl sandbox, where `document` does not exist), and the
fallback engine calls it with `$B js "($_DUMP)()" --out --raw`. Hygiene widens
to every URL-bearing attribute (src, srcset per candidate, poster, action,
formaction, data, ping, cite lose their query strings and fragments) and to
data: URLs inside existing <style> nodes. The persist and scan blocks restate
REPORT_DIR and RUN_ID literally instead of relying on a shell variable from an
earlier block; the baseline's targetSet is defined per mode (repo-relative
paths in source mode, page slugs in DOM mode) so DOM-mode deltas can match; the
PR-body Detector line lists the states the probe can actually print. The DOM
fixture is re-captured with the new script from outside the repo (the engine
walks up from cwd for DESIGN.md, which the metadata now records).

Doctrine contradictions the design specialist found: the landing-page motion
rule matches the one-authored-moment reflex; the background rule names the
catalog's halo/spotlight/stripe/grid slop instead of asking for gradients; the
universal font rule is scoped to the display voice with the body/UI exceptions;
"two typefaces max" allows the mono; the methodology's banned-font line renders
BANNED_FONTS; Courier New is banned outright; the Brutalist, Retro-Futuristic,
and Playful menu entries stop recommending system stacks, glow, and bounce; the
coherence nudge uses the decoration vocabulary; Path A's gate names the display
voice; font-loading prose points at the source the procedure verified;
centered-everything is MEDIUM (an aggregate heuristic); the mockup guard reads
"Never by default (unless the brief above asks for it)". The checklist's
AUTO-FIX list renders the catalog's auto-fix rules; category 9 and the Hard
Rules pointer count from the same partition helpers (detectorSlopEntries,
judgmentTellEntries); the handoff list renders from HANDOFF_COMMANDS; a missing
catalog id fails gen-skill-docs by name. gstack's own DESIGN.md gains border
tokens and Decisions Log rows for its live-feed pulse and 11px mono labels.
frontend-scope is case-sensitive like the bash arm. gen-skill-docs shares one
emitGenerated helper for sections and lib-derived assets; renderCatalog keeps
the one style with a caller. Tests: shared sliceBetween that fails on a missing
end marker, the slop-gate fixture's real end marker, an isolated browse daemon
for the DOM-mode E2E, the DOM hygiene test gated to CI or opt-in, docs notes
for the two superseded plan sentences.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-08 17:31:30 +00:00

89 lines
3.3 KiB
TypeScript

/**
* lib/frontend-scope.ts mirrors the m_frontend arm of bin/gstack-diff-scope.
* Pure cases run everywhere; the parity case runs the bash script in a temp
* repo (POSIX only) so the two implementations cannot drift silently.
*/
import { describe, test, expect } from 'bun:test';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { spawnSync } from 'child_process';
import { isFrontendPath } from '../lib/frontend-scope';
const ROOT = path.join(import.meta.dir, '..');
const POSIX = process.platform !== 'win32';
const SAMPLES: Array<[string, boolean]> = [
['src/components/Button.tsx', true],
['src/Button.jsx', true],
['pages/index.vue', true],
['app/Widget.svelte', true],
['site/page.astro', true],
['styles/main.css', true],
['css/a.scss', true],
['x/y/theme.less', true],
['x/a.sass', true],
['x/a.pcss', true],
['app/views/users/show.html.erb', true],
['templates/a.haml', true],
['templates/a.slim', true],
['templates/a.hbs', true],
['views/a.ejs', true],
['public/index.html', true],
['tailwind.config.js', true],
['postcss.config.cjs', true],
['app/assets/stylesheets/app.css', true],
['lib/util/components/helper.rb', true],
['lib/server.ts', false],
['src/api/route.js', false],
['README.md', false],
['package.json', false],
['test/foo.test.ts', false],
['components.md', false],
['public/Index.HTML', false], // bash globs are case-sensitive; the mirror must agree
['src/App.TSX', false],
];
describe('isFrontendPath', () => {
test.each(SAMPLES)('%s → %p', (p, expected) => {
expect(isFrontendPath(p)).toBe(expected);
});
test('normalizes leading ./ and backslashes', () => {
expect(isFrontendPath('./styles/a.css')).toBe(true);
expect(isFrontendPath('src\\components\\A.tsx')).toBe(true);
});
});
describe.skipIf(!POSIX)('parity with bin/gstack-diff-scope', () => {
test('SCOPE_FRONTEND agrees with isFrontendPath for every sample, one file per diff', () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-scope-parity-'));
const git = (...a: string[]) => {
const r = spawnSync('git', a, { cwd: dir, encoding: 'utf-8', timeout: 30_000 });
if (r.status !== 0) throw new Error(r.stderr);
};
try {
git('init', '-q', '-b', 'main');
git('config', 'user.email', 't@example.com');
git('config', 'user.name', 't');
fs.writeFileSync(path.join(dir, 'base.txt'), 'x\n');
git('add', '-A'); git('commit', '-q', '-m', 'base');
const mismatches: string[] = [];
for (const [rel, expected] of SAMPLES) {
git('checkout', '-q', '-b', 'probe');
const full = path.join(dir, rel);
fs.mkdirSync(path.dirname(full), { recursive: true });
fs.writeFileSync(full, '/* x */\n');
git('add', '-A'); git('commit', '-q', '-m', rel);
const r = spawnSync('bash', [path.join(ROOT, 'bin', 'gstack-diff-scope'), 'main'], { cwd: dir, encoding: 'utf-8', timeout: 30_000 });
const bashSays = /SCOPE_FRONTEND=true/.test(r.stdout);
if (bashSays !== expected) mismatches.push(`${rel}: bash=${bashSays} ts=${expected}`);
git('checkout', '-q', 'main'); git('branch', '-q', '-D', 'probe');
}
expect(mismatches).toEqual([]);
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
});
});