mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-17 00:26:41 +03:00
Greptile's review found a real bug in the new resolver. When the live config lists pages under more than one folder (src/**/*.html and public/**/*.html), the overlay treated a URL like /foo.html as src/foo.html and public/foo.html at the same time. A waiver written only for src/foo.html could then hide a finding on the page actually served from public/foo.html. That fails in the worst direction: a real finding disappears and nothing says so. The overlay can never look up the right file. The live server does not serve the pages; the project's own dev or static server does, and its URL-to-file mapping is invisible from here. So the fix stops guessing: a file-scoped waiver now applies only when it matches the URL path itself, which is true whichever folder serves the page, or when it matches under every configured folder, so no possible reading disagrees. Anything ambiguous shows the finding, which is also what the CLI reports for the file really being served. With a single configured root, the common case, nothing changes: the new rule reduces to the old behaviour exactly. Multi-root projects keep three ways to write a waiver that still applies: name the file under each folder, use the bare path, or use **/. Two new unit tests pin the ambiguous case and the safe spellings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
229 lines
9.0 KiB
JavaScript
229 lines
9.0 KiB
JavaScript
import { describe, it } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { readFileSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
import vm from 'node:vm';
|
|
|
|
const REPO_ROOT = process.cwd();
|
|
const SCRIPT = join(REPO_ROOT, 'skill/scripts/live-browser-ignores.js');
|
|
|
|
// Evaluated in the test realm (not a vm context) so the arrays the resolver
|
|
// returns share this realm's prototypes and deepEqual compares them plainly.
|
|
function loadIgnoresApi() {
|
|
const source = readFileSync(SCRIPT, 'utf-8');
|
|
const factory = vm.runInThisContext(
|
|
`(function (window) {\n${source}\nreturn window.__IMPECCABLE_LIVE_IGNORES__;\n})`,
|
|
{ filename: SCRIPT },
|
|
);
|
|
return factory({});
|
|
}
|
|
|
|
const resolve = loadIgnoresApi().resolveDetectIgnores;
|
|
|
|
const EMPTY = { disabledRules: [], disabledValues: [] };
|
|
|
|
describe('live-browser-ignores resolver', () => {
|
|
it('registers a versioned API on the root', () => {
|
|
const api = loadIgnoresApi();
|
|
assert.equal(api.version, 1);
|
|
assert.equal(typeof api.resolveDetectIgnores, 'function');
|
|
});
|
|
|
|
it('degrades to an empty filter when the config global is missing or malformed', () => {
|
|
assert.deepEqual(resolve(), EMPTY);
|
|
assert.deepEqual(resolve({ ignores: undefined, pathname: '/index.html' }), EMPTY);
|
|
assert.deepEqual(resolve({ ignores: null, pathname: '/index.html' }), EMPTY);
|
|
assert.deepEqual(resolve({ ignores: 'nonsense', pathname: '/index.html' }), EMPTY);
|
|
assert.deepEqual(resolve({ ignores: {}, pathname: '/index.html' }), EMPTY);
|
|
});
|
|
|
|
it('does not spread a string ignoreRules into characters', () => {
|
|
// `"ignoreRules": "foo"` in a hand-edited config must disable nothing,
|
|
// not look like it disabled three one-letter rules.
|
|
const out = resolve({ ignores: { ignoreRules: 'foo' }, pathname: '/index.html' });
|
|
assert.deepEqual(out, EMPTY);
|
|
});
|
|
|
|
it('forwards ignoreRules normalized and deduplicated', () => {
|
|
const out = resolve({
|
|
ignores: { ignoreRules: ['Dark-Glow', 'dark-glow', ' gradient-text ', '', 42, null] },
|
|
pathname: '/index.html',
|
|
});
|
|
assert.deepEqual(out.disabledRules, ['dark-glow', 'gradient-text']);
|
|
});
|
|
|
|
it('forwards unscoped value entries and drops malformed ones', () => {
|
|
const out = resolve({
|
|
ignores: {
|
|
ignoreValues: [
|
|
{ rule: 'overused-font', value: 'Geist+Mono' },
|
|
null,
|
|
'not-an-entry',
|
|
{ rule: '', value: 'x' },
|
|
{ rule: 'overused-font', value: '' },
|
|
],
|
|
},
|
|
pathname: '/index.html',
|
|
});
|
|
assert.deepEqual(out.disabledValues, [{ rule: 'overused-font', value: 'geist mono' }]);
|
|
});
|
|
|
|
it('applies wildcard entries only on pages their globs name', () => {
|
|
const ignores = {
|
|
roots: ['prototype/'],
|
|
ignoreValues: [
|
|
{ rule: 'dark-glow', value: '*', files: ['prototype/attack-the-soc.html'] },
|
|
],
|
|
};
|
|
const onPage = resolve({ ignores, pathname: '/attack-the-soc.html' });
|
|
assert.deepEqual(onPage.disabledRules, ['dark-glow']);
|
|
const elsewhere = resolve({ ignores, pathname: '/index.html' });
|
|
assert.deepEqual(elsewhere.disabledRules, []);
|
|
});
|
|
|
|
it('never applies an unscoped wildcard entry, matching the CLI', () => {
|
|
// isIgnoredFindingValue in cli/lib/impeccable-config.mjs returns false
|
|
// for a wildcard entry with no files; project-wide suppression is
|
|
// ignoreRules' job.
|
|
const out = resolve({
|
|
ignores: { ignoreValues: [{ rule: 'dark-glow', value: '*' }] },
|
|
pathname: '/index.html',
|
|
});
|
|
assert.deepEqual(out, EMPTY);
|
|
});
|
|
|
|
it('drops scoped value entries on pages outside their globs', () => {
|
|
const ignores = {
|
|
roots: ['prototype/'],
|
|
ignoreValues: [
|
|
{ rule: 'overused-font', value: 'geist mono', files: ['prototype/mgmt-demo.html'] },
|
|
],
|
|
};
|
|
const onPage = resolve({ ignores, pathname: '/mgmt-demo.html' });
|
|
assert.deepEqual(onPage.disabledValues, [{ rule: 'overused-font', value: 'geist mono' }]);
|
|
const elsewhere = resolve({ ignores, pathname: '/index.html' });
|
|
assert.deepEqual(elsewhere.disabledValues, []);
|
|
});
|
|
|
|
it('does not lend one entry\'s glob prefix to other pages', () => {
|
|
// The trap from issue #639: prefixes come only from `roots`, never from
|
|
// the ignore globs themselves. An entry scoped to prototype/library/**
|
|
// must not suppress on prototype/index.html.
|
|
const ignores = {
|
|
roots: ['prototype/'],
|
|
ignoreValues: [
|
|
{ rule: 'em-dash-overuse', value: '*', files: ['prototype/library/**'] },
|
|
],
|
|
};
|
|
const inside = resolve({ ignores, pathname: '/library/buttons.html' });
|
|
assert.deepEqual(inside.disabledRules, ['em-dash-overuse']);
|
|
const outside = resolve({ ignores, pathname: '/index.html' });
|
|
assert.deepEqual(outside.disabledRules, []);
|
|
});
|
|
|
|
it('resolves root and directory URLs to their index file', () => {
|
|
const ignores = {
|
|
roots: ['prototype/'],
|
|
ignoreValues: [
|
|
{ rule: 'dark-glow', value: '*', files: ['prototype/index.html'] },
|
|
{ rule: 'gradient-text', value: '*', files: ['prototype/news/index.html'] },
|
|
],
|
|
};
|
|
assert.deepEqual(resolve({ ignores, pathname: '/' }).disabledRules, ['dark-glow']);
|
|
assert.deepEqual(resolve({ ignores, pathname: '/news/' }).disabledRules, ['gradient-text']);
|
|
});
|
|
|
|
it('matches path suffixes like the CLI scoped-file matcher', () => {
|
|
// findingMatchesScopedIgnoreFile tries every path suffix of the finding's
|
|
// file, so `library/**` written without the prototype/ prefix still
|
|
// scopes to the library pages.
|
|
const ignores = {
|
|
roots: ['prototype/'],
|
|
ignoreValues: [
|
|
{ rule: 'em-dash-overuse', value: '*', files: ['library/**'] },
|
|
{ rule: 'dark-glow', value: '*', files: ['buttons.html'] },
|
|
],
|
|
};
|
|
const out = resolve({ ignores, pathname: '/library/buttons.html' });
|
|
assert.deepEqual(out.disabledRules.sort(), ['dark-glow', 'em-dash-overuse']);
|
|
});
|
|
|
|
it('supports the CLI glob dialect, including alternation', () => {
|
|
const ignores = {
|
|
roots: ['prototype/'],
|
|
ignoreValues: [
|
|
{ rule: 'dark-glow', value: '*', files: ['prototype/{index,about}.html'] },
|
|
{ rule: 'gradient-text', value: '*', files: ['prototype/page-?.html'] },
|
|
],
|
|
};
|
|
assert.deepEqual(resolve({ ignores, pathname: '/about.html' }).disabledRules, ['dark-glow']);
|
|
assert.deepEqual(resolve({ ignores, pathname: '/page-3.html' }).disabledRules, ['gradient-text']);
|
|
assert.deepEqual(resolve({ ignores, pathname: '/page-33.html' }).disabledRules, []);
|
|
});
|
|
|
|
it('treats glob metacharacters in filenames literally', () => {
|
|
const ignores = {
|
|
ignoreValues: [
|
|
{ rule: 'dark-glow', value: '*', files: ['pricing (v2).html'] },
|
|
],
|
|
};
|
|
const out = resolve({ ignores, pathname: '/pricing (v2).html' });
|
|
assert.deepEqual(out.disabledRules, ['dark-glow']);
|
|
const near = resolve({ ignores, pathname: '/pricing xv2y.html' });
|
|
assert.deepEqual(near.disabledRules, []);
|
|
});
|
|
|
|
it('accepts a single `file` string alongside `files`', () => {
|
|
const out = resolve({
|
|
ignores: {
|
|
ignoreValues: [{ rule: 'dark-glow', value: '*', file: 'index.html' }],
|
|
},
|
|
pathname: '/index.html',
|
|
});
|
|
assert.deepEqual(out.disabledRules, ['dark-glow']);
|
|
});
|
|
|
|
it('does not apply a waiver scoped to one root when several roots could serve the URL', () => {
|
|
// With src/**/*.html and public/**/*.html both configured, /foo.html
|
|
// could be served from either root. A waiver naming only src/foo.html
|
|
// must not hide a finding on a page actually served from
|
|
// public/foo.html; ambiguity resolves to showing the finding.
|
|
const ignores = {
|
|
roots: ['src/', 'public/'],
|
|
ignoreValues: [
|
|
{ rule: 'dark-glow', value: '*', files: ['src/foo.html'] },
|
|
{ rule: 'gradient-text', value: 'teal', files: ['src/foo.html'] },
|
|
],
|
|
};
|
|
const out = resolve({ ignores, pathname: '/foo.html' });
|
|
assert.deepEqual(out, EMPTY);
|
|
});
|
|
|
|
it('applies a scoped waiver under several roots when every identity matches', () => {
|
|
const ignores = {
|
|
roots: ['src/', 'public/'],
|
|
ignoreValues: [
|
|
// Two globs covering both identities.
|
|
{ rule: 'dark-glow', value: '*', files: ['src/foo.html', 'public/foo.html'] },
|
|
// A bare-path glob holds whichever root serves the page.
|
|
{ rule: 'em-dash-overuse', value: '*', files: ['foo.html'] },
|
|
{ rule: 'gradient-text', value: '*', files: ['**/foo.html'] },
|
|
],
|
|
};
|
|
const out = resolve({ ignores, pathname: '/foo.html' });
|
|
assert.deepEqual(out.disabledRules.sort(), ['dark-glow', 'em-dash-overuse', 'gradient-text']);
|
|
});
|
|
|
|
it('survives malformed roots and percent-escapes without throwing', () => {
|
|
const out = resolve({
|
|
ignores: {
|
|
roots: 7,
|
|
ignoreRules: ['dark-glow'],
|
|
ignoreValues: [{ rule: 'gradient-text', value: '*', files: ['broken%.html'] }],
|
|
},
|
|
pathname: '/broken%.html',
|
|
});
|
|
assert.deepEqual(out.disabledRules.sort(), ['dark-glow', 'gradient-text']);
|
|
});
|
|
});
|