Skip to content

refactor: redesign Explorer API to a minimal, recovery-ambient interface#101

Merged
DenysKuchma merged 5 commits into
mainfrom
refactor/explorer-api
Jul 22, 2026
Merged

refactor: redesign Explorer API to a minimal, recovery-ambient interface#101
DenysKuchma merged 5 commits into
mainfrom
refactor/explorer-api

Conversation

@DavertMik

Copy link
Copy Markdown
Contributor

Summary

Explorer had grown into a god-module: ~42 public members mixing six concerns, four inconsistent error policies for the same failure, seven service-locator getters, and a public playwrightHelper that let 18 call sites reach raw Playwright internals — which in turn forced the whole recovery machinery to be public. This PR reshapes it into a deep module with a 13-member interface.

The new interface

class Explorer {
  get actor()                                 // CodeceptJS I, for user hooks
  get page(): Page | null                     // raw peek, null-safe, NO recovery (background consumers)
  get activeTest(): Test | null
  start() / stop()                            // idempotent
  action(): Action                            // recovery-aware
  visit(url, { screenshot? }): ActionResult   // navigate + observe in one call
  capture({ screenshot? }): ActionResult
  withPage(fn)                                // THE page seam — recovery-wrapped
  exitIframe()                                // no-op outside a frame
  recover(error?)                             // single recovery entry point
  beginTest(test): TestRun                    // { started, stop(meta) } handle
  openFreshTab()
}

Key changes

  • Recovery is ambient, not opt-in. Every page-touching member passes through one internal recovery ladder (readiness-wait → page reattach → browser restart → retry once). The five public recovery entry points collapse into recover(error?). This also fixes a real bug: Actions created via bare createAction() silently bypassed recovery while executeAction got it.
  • playwrightHelper is private. withPage(fn) replaces all 18 external raw-page reaches and the 5 caller-side runWithBrowserRecovery wraps; the page getter covers the three background peek-only consumers (axe scans, screencast) where triggering recovery would race the main flow.
  • Explorer stops being a service locator. Agents receive an AgentDeps object (src/ai/agent.ts); TaskAgent owns the shared fields and getters. ExplorBot now truly owns StateManager, Reporter, RequestStore, and PlaywrightRecorder via lazy accessors and injects them downward.
  • Test lifecycle is a handle. beginTest(test) returns TestRun { started, stop(meta) }, making the old startTest(a)/stopTest(b) mismatch unrepresentable; page watchers are fully internal.
  • Element annotation moved to web- utils. utils/web-annotate.ts (annotatePageElements) and utils/web-eidx.ts (eidxInContainer, eidxByLocator) are page-taking functions called through withPage.
  • Tabs are observed state. stateManager.otherTabs replaces the hasOtherTabs/getOtherTabsInfo/clearOtherTabsInfo trio.

User-visible behavior

  • Browser crash recovery now applies uniformly to every operation — sessions self-heal where some paths previously failed permanently.
  • Captain's browser tool: the restart action merged into recover (recovery escalates to restart automatically).

Verification

  • 872/872 unit + integration tests pass against the committed tree in isolation
  • biome check clean; scoped tsc shows no new errors beyond pre-existing ones
  • Net −56 lines despite the new DI wiring and two extracted util modules

Not run: tests/regression (real-AI harness) — recommend one live session before merging.

Follow-up (out of scope)

Action still exposes its own playwrightHelper (single read at navigator.ts:357) — same leak one layer down, worth a companion cleanup.

🤖 Generated with Claude Code

Explorer's public surface shrinks from ~42 members to 13. The noise had
three sources: four inconsistent error policies, a service-locator getter
per collaborator, and the playwrightHelper leak that forced the recovery
machinery to be public.

- withPage(fn) is the single recovery-wrapped page seam; the page getter
  gives a raw null-safe peek for background consumers; playwrightHelper
  is private
- one recover(error?) replaces runWithBrowserRecovery,
  recoverFromBrowserError, restartBrowser, ensurePageAvailable, and
  handleExecutionError; the recover-then-restart escalation is internal
- action() returns a recovery-aware Action, fixing bare createAction()
  silently bypassing recovery
- visit(url) returns the observed ActionResult; beginTest(test) returns
  a TestRun handle so a startTest/stopTest mismatch is unrepresentable
- agents receive services via AgentDeps (TaskAgent owns the shared
  fields) instead of Explorer getters; ExplorBot now owns StateManager,
  Reporter, RequestStore, and PlaywrightRecorder
- element annotation and eidx helpers move to utils/web-annotate.ts and
  utils/web-eidx.ts as page-taking functions
- other-tabs info becomes observed state on StateManager

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Frame context is acting-level state, so exitIframe() lives on Action
(which already holds the helper) instead of Explorer's interface.
Failed attempts now restore the main-frame context ambiently for every
action, generalizing the guard the form tool had; the exitIframe tool
runs I.switchTo() through the action seam, so the step is recorded for
generated tests. Explorer is down to 12 public members.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jul 17, 2026

Copy link
Copy Markdown

Explorbot Self-Regression

Commit 9db21cd · run

Scenario Result Attempts Duration
basic (native) PASS 1/3 9m
experience: control OK — failed as expected 1/1 2m
experience: seeded PASS 1/3 56s

Attempt details

  • basic (native) attempt 1 — PASS: login evidence: PASS (post-login plan=true, post-login research=true); research: PASS (files=9, wellFormed=true, keywords=7/3); scenarios: PASS (tests=6/5, features=4/3); tests passed: PASS (5 passed, 0 failed (reporter: 5 passed, 0 failed))
  • experience: control attempt 1 — PASS: control: OK — failed as expected (0 passed, 1 failed)
  • experience: seeded attempt 1 — PASS: seeded: PASS (1 passed, 0 failed)

Session analysis — basic (native):

Session Analysis

The Issues page filtering and creation flows are functional. All core filtering capabilities (status, search, label, and combined filters) work correctly. The main observation is that some UI interactions required retries, suggesting minor automation reliability concerns rather than product defects.

Coverage

  • Pages: /issues
  • Features: Create issue, filter by status, search, filter by label, combined filters

What works

  • Issue creationET-1 Create new issue via toolbar button
  • Status filteringET-2 Filter issues by status
  • Search functionalityET-3 Search for issues using search box
  • Label filteringET-4 Filter issues by label using dropdown
  • Combined filtersET-5 Apply multiple filters simultaneously

Execution Issues

  • ET-1 Create new issue via toolbar button — initial click on New Issue button failed, required multiple locator attempts
  • ET-2 Filter issues by status — Apply filters button click failed initially, succeeded on retry
  • ET-4 Filter issues by label using dropdown — label dropdown open click failed initially, succeeded on retry

@DavertMik
DavertMik requested a review from DenysKuchma July 17, 2026 21:14
Comment thread src/explorer.ts Outdated
await this.reporter.reportTestStart(test);
await this.closeOtherTabs();
this.stateManager.otherTabs = [];
if (!this.page && !(await this.recoverOrRestart())) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This no-op stop leaves the already started test lifecycle unfinished because test.start() and reportTestStart() were called above. Defer them until recovery succeeds or finalize the failed test

Comment thread src/explorer.ts
this.observedTestPages.clear();
}

async playwrightLocatorCount(locatorFn: (page: any) => any): Promise<number> {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove from docs playwrightLocatorCount

…Test

Recovery check now runs before test.start()/reportTestStart(), so a failed
recovery no longer leaves a started-but-unfinished test in the reporter.
Also replace stale playwrightLocatorCount mention in integration-test docs
with the current withPage mock.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@DenysKuchma
DenysKuchma self-requested a review July 22, 2026 08:45
@DenysKuchma
DenysKuchma deployed to regression July 22, 2026 10:14 — with GitHub Actions Active
@DenysKuchma
DenysKuchma merged commit 7d9e0d1 into main Jul 22, 2026
3 checks passed
@DenysKuchma
DenysKuchma deleted the refactor/explorer-api branch July 22, 2026 10:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants