Fix: skipScan must cover the visual contrast stage too

Bugbot on PR #665: the skipScan guard emptied only the analytic
collectBrowserFindings pass, and scan()'s detached visual-contrast
stage then repopulated an ignoreFiles-waived page with contrast
markers and a second non-zero results post. Hoist the guard into
skipScanActive() and honor it in scan() and the async collector;
regenerate the browser bundle.

Adds a browser-backed regression test that reproduces the leak
(second results post carrying low-contrast findings) and pins the
zero contract; drops a tautological assert flagged in review.

AI-assisted change: implemented with Claude Code under maintainer
direction.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Abdul Wahab
2026-08-28 12:20:02 +05:00
co-authored by Claude Fable 5
parent f031eb0b9f
commit 281f30e5cc
3 changed files with 112 additions and 15 deletions
+17 -7
View File
@@ -1472,13 +1472,17 @@ if (IS_BROWSER) {
return findings;
}
// A page matched by detector.ignoreFiles is waived wholesale: every scan
// stage answers empty so the badge and toast read zero. Mirrors
// shouldIgnoreDetectionFile in cli/lib/impeccable-config.mjs; the live
// overlay resolves the globs per page (live-browser-ignores.js) and
// forwards the verdict as config.skipScan.
function skipScanActive() {
return EXTENSION_MODE && window.__IMPECCABLE_CONFIG__?.skipScan === true;
}
function collectBrowserFindings() {
// A page matched by detector.ignoreFiles is waived wholesale: answer the
// scan with the empty shape so the badge and toast read zero. Mirrors
// shouldIgnoreDetectionFile in cli/lib/impeccable-config.mjs; the live
// overlay resolves the globs per page (live-browser-ignores.js) and
// forwards the verdict as config.skipScan.
if (EXTENSION_MODE && window.__IMPECCABLE_CONFIG__?.skipScan === true) {
if (skipScanActive()) {
return { groupMap: new Map(), allFindings: [], pageLevelFindings: [] };
}
const groupMap = new Map();
@@ -2013,6 +2017,12 @@ if (IS_BROWSER) {
async function collectBrowserFindingsAsync(options = {}, runtime = {}) {
const collected = collectBrowserFindings();
// The visual pass walks the DOM on its own; on a skipScan page it would
// repopulate the emptied scan, so it is skipped with everything else.
if (skipScanActive()) {
lastVisualContrastAnalyses = [];
return { ...collected, allFindings: [], visualContrastAnalyses: [] };
}
await addVisualContrastFindings(collected.groupMap, options, runtime);
return {
...collected,
@@ -2066,7 +2076,7 @@ if (IS_BROWSER) {
const generation = scanGeneration;
const collected = collectBrowserFindings();
const allFindings = renderBrowserFindings(collected, options);
if (shouldRunVisualContrast(options)) {
if (!skipScanActive() && shouldRunVisualContrast(options)) {
addVisualContrastFindings(collected.groupMap, options, { decorate: true, generation })
.then(() => {
if (generation === scanGeneration) postSerializedFindings(collected.groupMap, options);
+17 -7
View File
@@ -8131,13 +8131,17 @@ if (IS_BROWSER) {
return findings;
}
// A page matched by detector.ignoreFiles is waived wholesale: every scan
// stage answers empty so the badge and toast read zero. Mirrors
// shouldIgnoreDetectionFile in cli/lib/impeccable-config.mjs; the live
// overlay resolves the globs per page (live-browser-ignores.js) and
// forwards the verdict as config.skipScan.
function skipScanActive() {
return EXTENSION_MODE && window.__IMPECCABLE_CONFIG__?.skipScan === true;
}
function collectBrowserFindings() {
// A page matched by detector.ignoreFiles is waived wholesale: answer the
// scan with the empty shape so the badge and toast read zero. Mirrors
// shouldIgnoreDetectionFile in cli/lib/impeccable-config.mjs; the live
// overlay resolves the globs per page (live-browser-ignores.js) and
// forwards the verdict as config.skipScan.
if (EXTENSION_MODE && window.__IMPECCABLE_CONFIG__?.skipScan === true) {
if (skipScanActive()) {
return { groupMap: new Map(), allFindings: [], pageLevelFindings: [] };
}
const groupMap = new Map();
@@ -8672,6 +8676,12 @@ if (IS_BROWSER) {
async function collectBrowserFindingsAsync(options = {}, runtime = {}) {
const collected = collectBrowserFindings();
// The visual pass walks the DOM on its own; on a skipScan page it would
// repopulate the emptied scan, so it is skipped with everything else.
if (skipScanActive()) {
lastVisualContrastAnalyses = [];
return { ...collected, allFindings: [], visualContrastAnalyses: [] };
}
await addVisualContrastFindings(collected.groupMap, options, runtime);
return {
...collected,
@@ -8725,7 +8735,7 @@ if (IS_BROWSER) {
const generation = scanGeneration;
const collected = collectBrowserFindings();
const allFindings = renderBrowserFindings(collected, options);
if (shouldRunVisualContrast(options)) {
if (!skipScanActive() && shouldRunVisualContrast(options)) {
addVisualContrastFindings(collected.groupMap, options, { decorate: true, generation })
.then(() => {
if (generation === scanGeneration) postSerializedFindings(collected.groupMap, options);