Closed bug report

pup_open_window page-health check counts placeholder <img> slots as broken (the broken_images_6 false alarm)

Colby Knox · 2mo ago ·closed by John Lauer

pup_open_window's page-health check counts placeholder <img> slots as broken images

Live repro (2026-08-19, Colby-ROG2): opening Hero Studio via pup_open_window returned a 200 but the verb's output led with:

⛔ THE PAGE YOU JUST OPENED IS BROKEN: broken_images_6 (HTTP 200) — it says "Hero Studio Adom TTS app · score 607 ..."

The page was not broken. Those 6 were placeholder <img> slots: the studio's live hero keeps two hidden image slots (showcase-img, fullimport-img) that CSS toggles by class, plus copies of each in the preview clones (homepage-marquee tile, index-grid card, desk-bar slot) rebuilt every render. None had a real source that failed to load; they had no src (or src=""), were display:none or 0×0, and are placeholders by construction.

This is the same false alarm that burned a review round in the kicad-bridge hero campaign (adom/hero-studio#32 point 1) and was chased through the wiki as adom/wiki#125 ("which tool emits broken_images?") — the wiki grepped its whole tree twice and found nothing, because the emitter is this verb's DOM scan. Catching it live with pup closes that hunt.

What the check should do (the wiki issue's own ask, now aimed at the right tool): count an <img> as broken only when it has a real, non-empty src that failed to load (complete && naturalWidth === 0 on a non-empty src). Skip entirely:

  • <img> with no src attribute, or src="" (Chrome reports naturalWidth 0 for both; src="" even fires onerror, but neither is a broken resource).
  • <img> with zero rendered size / display:none and no src — a placeholder slot.

Suggested guard, one line in the scan:

const broken = [...document.querySelectorAll('img')].filter(i => i.getAttribute('src') && i.complete && i.naturalWidth === 0);

On the same Hero Studio page, that predicate returns 0 (measured: brokenRealSrc: 0 vs brokenByNaturalWidth: 3 after the studio's own cleanup, 6 before).

Why it matters beyond Hero Studio: any page with placeholder image slots (lazy-load skeletons, hidden preview clones, "generated image goes here" boxes) trips this and the verb tells the driving AI the page is BROKEN in caps, so agents report failure on healthy pages. The studio has already removed every empty-src img and stripped placeholders from its clones (hero-studio d23e7b3) so it is as clean as a page can reasonably be; the 3 structural slots that remain are exactly the kind the scan should ignore.

Not touching pup's code from the CLI thread per Colby; filing here for the owner.

1 Reply

John Lauer · 2mo ago

Fixed in 2.0.245-2.0.248. Your diagnosis and your predicate were both right; verifying them turned up a second half of the bug.

The fix. An <img> counts as broken only when it has a real, non-empty src that finished loading with naturalWidth === 0 — exactly the guard you suggested. Placeholder slots (no src, src="", hidden, 0x0) are skipped.

The second half. With just that predicate the check swung from a false positive to a false NEGATIVE: the probe runs immediately after navigation, while the document is still interactive, so a genuinely broken image had not failed yet (and may not even be parsed), and the check reported nothing. load fires only after every image has settled, so the probe now waits briefly (capped at 1.5s, and only when the document has not finished) and judges at readyState === 'complete'; if it still is not complete it records null and emits no signal rather than guessing.

Measured on a purpose-built page (six placeholder slots exactly as you described, plus one real 404 image):

old predicate new predicate
in-page count 7 1

And through the verb itself, on the live page:

  • placeholders only -> signals: [], ok: true
  • placeholders + one real 404 -> signals: ["page_says_not_found", "no_stylesheets_loaded", "broken_images_1"]

(That page_says_not_found is my test page saying the words "404 image" in its own body — a text heuristic doing its job on deliberately adversarial copy.)

One honest note in case it saves you a cycle: while verifying I twice read "no signal" because I was grepping the human-readable output string instead of the structured renderCheck field, where the signals actually live. If you script against this check, read renderCheck.signals — that is the contract.

Thanks for the precise repro and the measured brokenRealSrc: 0 vs brokenByNaturalWidth: 3 numbers; they made this a ten-minute fix instead of a hunt. That also closes the emitter question from adom/wiki#125 — it was this verb's DOM scan.

John Lauer closed this · 2mo ago

Log in to reply.