fix: treat research containers as scopes, recover them from children#98
Open
DavertMik wants to merge 1 commit into
Open
fix: treat research containers as scopes, recover them from children#98DavertMik wants to merge 1 commit into
DavertMik wants to merge 1 commit into
Conversation
Two of four containers in trace d9fd5d5a14aba27bc86cf925f57dc8a1 were
declared broken by testLocators, cascading `container broken` onto every
child locator without testing it and handing a mechanical problem to a
13s AI repair.
A container is never clicked -- it only scopes child lookups, and
Playwright searches inside the union of all its matches. So a container
matching N >= 1 elements is valid: children still prove themselves
individually with strict count === 1, and a child duplicated across
instances fails honestly. testLocators takes { scope: true } for
containers; element call sites stay strict. This also aligns stage 2
with validateContainers, which already accepted count >= 1.
A container matching 0 elements is now recovered without AI: the
section's elements carry eidx, so resolve them to live nodes, take their
lowest common ancestor, and derive a selector from its real attributes
(non-dynamic id, then filtered classes), preferring a unique match. Only
if that fails does the old cascade + fixBrokenSections path apply, and
the all-containers-broken retry now fires only when every container
matches nothing and none is recoverable.
tests/unit/web-element.test.ts also restores the window/document globals
it replaces. It was leaking its JSDOM fixture into tests/unit/explorer.test.ts,
whose mocked page.evaluate reads the global document -- it was silently
asserting against the leaked fixture rather than its own page.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
DenysKuchma
requested changes
Jul 17, 2026
| const containerLocs = result.containerLocators; | ||
| await this.testLocators(containerLocs, { scope: true }); | ||
| for (const loc of containerLocs.filter((l) => l.valid === false)) { | ||
| if (await this.recoverContainerFromChildren(result, loc.locator)) loc.valid = true; |
Collaborator
There was a problem hiding this comment.
Returns true when only one section sharing this CSS is recovered, marking the container valid for all sections. Track recovery per section or return true only when every matching section is recovered
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements
plans/024-container-scope-validation.md. Follow-up to #97 — based on that branch, so this PR shows only its own commit. It retargets tomainautomatically once #97 merges.Problem
In trace
d9fd5d5a14aba27bc86cf925f57dc8a1two of four containers were declared broken bytestLocators:'nav'matched 2 elements, andtestLocatorsrequiredcount === 1— a perfectly usable scope marked broken'.detail.detail-view-resizable'matched 0 at test time (it matched 1 in the live DOM later)Both cascaded
container brokenonto every child locator without testing it, and only the AI (fixBrokenSections, 13s) repaired what was mostly a mechanical problem.Design rule
A container is never clicked — it only scopes child lookups. Playwright searches inside the union of all matches (
page.locator('nav').locator(child)looks in both navs), and children are still validated with strictcount === 1.2 elementsand flows tofixBrokenSectionsas before.eidx, so resolve them to live nodes, take their lowest common ancestor, derive a stable CSS selector from its real attributes.fixBrokenSections. The all-containers-broken retry now fires only when every container matches nothing and none is recoverable — genuine hallucination.This also aligns stage 2 with
validateContainers(stage 5), which already acceptedcount >= 1. One rule everywhere.Explicitly out of scope: compound-selector decomposition, child-hit-count instance narrowing, any AI involvement.
Changes
src/utils/web-element.ts— newWebElement.commonAncestor(page, eidxList)besidefromEidxList, same evaluate/extractor pattern. Returns null when fewer than 2 distinct elements resolve, or when the walk up reachesbody/html.src/ai/researcher/locators.ts—testLocatorstakes{ scope?: boolean }; only the validity line changes. New publicresolveContainersis the single entry point for stage-2 container handling; new privaterecoverContainerFromChildrenbuilds candidates from the ancestor's real attributes (non-dynamic#id, thenfilteredClasses), prefers a unique match, and calls the existingupdateSectionContainerso recovered sections skip the cascade automatically.src/ai/researcher.ts— stage 2 callsresolveContainers; stage 4 passes{ scope: true }so visual annotation uses the same rule.Effect on the observed trace
'nav'(2 matches) → valid scope; children tested against both navs, unique ones pass immediately — no cascade, no AI fix for that section'.detail.detail-view-resizable'(0 matches) → children's eidx resolve to live nodes, LCA is the detail panel, recovered mechanically — the exact correction the regenerated AI report madefixBrokenSectionsshrinks to genuinely dead locators or is skipped entirelyTest plan
WebElement.commonAncestor: nested case returns the wrapper, single-element and unresolvable cases return null, body-level case returns nullbun test tests/unit/— 811 passbun test tests/integration/— 63 passbun run check:fix— cleanDrive-by fix worth a look
tests/unit/web-element.test.tsnow restores thewindow/documentglobals it replaces. It was already leaking its JSDOM fixture intotests/unit/explorer.test.ts, whose mockedpage.evaluateruns functions against the global document — so that file was silently asserting against the leaked fixture instead of its own page, and only passed because of file ordering and which fixture happened to leak last. Adding fixtures here surfaced it. Both files now pass in either order.🤖 Generated with Claude Code