From 97db9b43b8dc2fb20e841dec981469fe4e4d9cc3 Mon Sep 17 00:00:00 2001 From: DavertMik Date: Fri, 6 Nov 2026 02:36:28 +0200 Subject: [PATCH 1/2] Close popup walls before research and stop testing them A cookie consent or marketing modal was treated as the page's main content: Researcher described it and Planner targeted it first. Researcher now asks the oracle whether a detected overlay is a wall, has Navigator close it, and researches only once it is gone. A wall that stays open raises ResearchBlockedError, so Captain takes over planning. Regular modals stay open and are researched as before. Planner no longer plans tests for such popups, and the shared popup rule tells Tester, Navigator, Captain and Rerunner to dismiss or ignore them. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 7 +++ rules/planner/ignored-pages.md | 1 + src/ai/researcher.ts | 34 ++++++++++-- src/ai/rules.ts | 4 +- src/explorbot.ts | 2 +- src/utils/error-page.ts | 15 ++++++ tests/integration/researcher.test.ts | 81 ++++++++++++++++++++++++++++ 7 files changed, 139 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b7092bab..c05a89f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 2026-11-06 + +### Changes +- [Researcher] Closes cookie consent, paywall, marketing and similar popups before researching a page, so research describes the page itself. Regular dialogs stay open and are researched as before. A page whose popup cannot be closed is not researched, and during planning Captain takes over. +- [Planner] No longer plans tests for cookie consent, marketing, announcement or notification popups, unless you plan that feature explicitly. +- [Tester] Dismisses popups unrelated to the application when they block the page and ignores them otherwise, including popups that can only be closed with an answer such as Accept or Decline. Navigator, Captain and test re-runs follow the same rule. + ## 2026-11-05 ### New CLI Options diff --git a/rules/planner/ignored-pages.md b/rules/planner/ignored-pages.md index a2f38500..4bb2ef6a 100644 --- a/rules/planner/ignored-pages.md +++ b/rules/planner/ignored-pages.md @@ -4,4 +4,5 @@ Do not plan tests for these pages, return an empty scenarios list: - sign up - password recovery If the page shows a sign-in form instead of the content its URL is for, also set prerequisite. +Never plan tests for popups unrelated to the application's features (cookie consent, marketing, announcement, notification), even when marked as Focused or active. Plan for the rest of the page as if they were absent. diff --git a/src/ai/researcher.ts b/src/ai/researcher.ts index 734711d8..a87dccee 100644 --- a/src/ai/researcher.ts +++ b/src/ai/researcher.ts @@ -10,7 +10,7 @@ import type { StateManager } from '../state-manager.js'; import { WebPageState } from '../state-manager.js'; import { Stats } from '../stats.ts'; import { diffAriaSnapshots } from '../utils/aria.ts'; -import { ErrorPageError, type PageCondition, detectPageCondition } from '../utils/error-page.ts'; +import { ErrorPageError, type PageCondition, ResearchBlockedError, detectPageCondition } from '../utils/error-page.ts'; import { isBodyEmpty } from '../utils/html.ts'; import { createDebug, tag } from '../utils/logger.js'; import { mdq } from '../utils/markdown-query.ts'; @@ -37,6 +37,8 @@ export type { Locator } from './researcher/locators.ts'; const debugLog = createDebug('explorbot:researcher'); const HIDDEN_CONTENT_STATEMENT = 'The page has collapsed or hidden content behind controls that open or expand it.'; +const WALL_STATEMENT = "The open overlay is unrelated to the application's features, such as cookie consent, a paywall, marketing or an announcement. A sign-in form is not such an overlay."; +const CLOSE_WALL_INSTRUCTION = "Close the overlay that is unrelated to the application's features and stay on this page."; export const POSSIBLE_SECTIONS = { overlay: 'dialog, modal, drawer, popup, or active form overlay', @@ -63,7 +65,10 @@ export class Researcher extends ResearcherBase implements Agent { private hasScreenshotToAnalyze = false; declare actionResult: ActionResult | undefined; - constructor(deps: AgentDeps) { + constructor( + deps: AgentDeps, + private navigator: Navigator + ) { super(deps); this.experienceTracker = deps.stateManager.getExperienceTracker(); this.settings.reasoning ??= 'low'; @@ -86,7 +91,7 @@ export class Researcher extends ResearcherBase implements Agent { } protected getNavigator(): Navigator { - throw new Error('not implemented'); + return this.navigator; } static getCachedResearch(state: WebPageState): string { @@ -158,6 +163,7 @@ export class Researcher extends ResearcherBase implements Agent { tag('warning').log(`Page at ${state.url} did not finish loading within timeout, continuing with best-effort research`); } } + if (!retrying && this.actionResult!.overlay.isModal) await this.closeWall(state.url, screenshot); debugLog('Researching web page:', this.actionResult!.url); @@ -401,6 +407,28 @@ export class Researcher extends ResearcherBase implements Agent { return conditions[decision.value ?? ''] || 'ok'; } + private async closeWall(url: string, screenshot: boolean): Promise { + if (!(await this.isWallOpen())) return; + + const wall = this.actionResult!.overlay.describe(); + tag('step').log(`Closing ${wall} before research`); + await this.navigator.resolveState(CLOSE_WALL_INSTRUCTION, this.actionResult!); + if (await this.isWallOpen()) throw new ResearchBlockedError(url, wall); + + await this.explorer.withPage(annotatePageElements); + this.actionResult = await this.explorer.capture({ screenshot: screenshot && this.provider.hasVision() }); + } + + private async isWallOpen(): Promise { + const captured = await this.explorer.capture({ screenshot: true }); + if (!captured.overlay.isModal) return false; + + const evidence = { ...pageEvidence(captured), overlay: captured.overlay.describe(), overlayContent: captured.getRegionARIA() }; + const open = await this.provider.oracle(WALL_STATEMENT, evidence, { agentName: 'researcher', screenshot: captured.screenshot, telemetry: { functionId: 'researcher.isWallOpen' } }); + if (open === null) tag('warning').log('Could not check whether the open overlay blocks research, continuing'); + return open === true; + } + private async hasHiddenContent(): Promise { const captured = await this.explorer.capture({ screenshot: true }); const hidden = await this.provider.oracle(HIDDEN_CONTENT_STATEMENT, pageEvidence(captured), { agentName: 'researcher', screenshot: captured.screenshot, telemetry: { functionId: 'researcher.hasHiddenContent' } }); diff --git a/src/ai/rules.ts b/src/ai/rules.ts index b996f977..0f4e2086 100644 --- a/src/ai/rules.ts +++ b/src/ai/rules.ts @@ -243,6 +243,7 @@ export const focusedElementRule = dedent` export const unexpectedPopupRule = dedent` If a modal/popup appeared that you didn't expect, dismiss it first before continuing with original task. + A popup unrelated to the application's features (cookie consent, paywall, marketing, announcement, notification) is never the task, even when it is the current overlay, unless the task is about it. Dismiss it if it blocks the page, otherwise ignore it. If elements become hidden or unclickable (timeout errors on visible elements), a dialog or overlay may have appeared on top. If a click error mentions "intercepts pointer events", another element is covering the target — dismiss it first. If buttons are disabled unexpectedly, trust any explanation the app gives (tooltip, hint text) over your own guess; otherwise check for a blocking popup or empty required fields. @@ -251,7 +252,8 @@ export const unexpectedPopupRule = dedent` 1. I.pressKey('Escape') — press Escape to dismiss 2. I.click('Cancel') — click Cancel button if present 3. I.click({ role: 'button', text: 'Close' }) — click X/close button if present - 4. I.clickXY(0, 0) via form() tool and check if page diff changed + 4. Click the popup's own answer button (Reject, Decline, Accept) if it has no close control + 5. I.clickXY(0, 0) via form() tool and check if page diff changed `; diff --git a/src/explorbot.ts b/src/explorbot.ts index 32b94a5d..e2bd0fa3 100644 --- a/src/explorbot.ts +++ b/src/explorbot.ts @@ -237,7 +237,7 @@ export class ExplorBot { } agentResearcher(): Researcher { - return (this.agents.researcher ||= this.createAgent((deps) => new Researcher(deps))); + return (this.agents.researcher ||= this.createAgent((deps) => new Researcher(deps, this.agentNavigator()))); } agentNavigator(): Navigator { diff --git a/src/utils/error-page.ts b/src/utils/error-page.ts index 898459e2..736e438d 100644 --- a/src/utils/error-page.ts +++ b/src/utils/error-page.ts @@ -115,6 +115,21 @@ export class NavigationBlockedError extends BlockedError { } } +export class ResearchBlockedError extends BlockedError { + constructor(url: string, overlay: string) { + super( + `Research of ${url} is blocked: ${overlay}`, + url, + overlay, + dedent` + Researcher was asked to research ${url} but could not close a popup unrelated to the application's features: ${overlay}. + Look at the current page to find out what keeps it open, resolve it, then call plan() to re-plan this page. If it cannot be resolved, finish without planning. + ` + ); + this.name = 'ResearchBlockedError'; + } +} + export class TestStartBlockedError extends BlockedError { constructor(scenario: string, url: string, currentUrl: string) { super( diff --git a/tests/integration/researcher.test.ts b/tests/integration/researcher.test.ts index fa75a302..674d7032 100644 --- a/tests/integration/researcher.test.ts +++ b/tests/integration/researcher.test.ts @@ -10,6 +10,7 @@ import { Researcher } from '../../src/ai/researcher.ts'; import { clearResearchCache, getCachedResearch, saveResearch } from '../../src/ai/researcher/cache.ts'; import { parseResearchSections } from '../../src/ai/researcher/parser.ts'; import { ConfigParser } from '../../src/config.ts'; +import { ResearchBlockedError } from '../../src/utils/error-page.ts'; const UI_MAPS_DIR = join(process.cwd(), 'test-data', 'ui-maps'); const taskBoardResearch = readFileSync(join(UI_MAPS_DIR, 'task-board-research.md'), 'utf8'); @@ -284,6 +285,86 @@ describe('Researcher with aimock', () => { expect(deepRuns).toBe(1); }); + describe('closing a wall before research', () => { + const wallState = { ...fakeState, ariaSnapshot: `${fakeState.ariaSnapshot}\n- dialog "Cookie settings":\n - button "Accept"` }; + const isWallQuestion = (req: any) => JSON.stringify(req.messages || []).includes('open overlay is unrelated'); + const wallQuestions = () => mock.getRequests().filter((r: any) => isWallQuestion(r.body)); + const researchRequests = () => mock.getRequests().filter((r: any) => !isWallQuestion(r.body)); + + function researcherOnWall(closes: boolean) { + const deps = createMockDeps(wallState); + const closeInstructions: string[] = []; + let current = wallState; + deps.explorer.capture = async () => ActionResult.fromState(current); + const navigator = { + resolveState: async (instruction: string) => { + closeInstructions.push(instruction); + if (closes) current = fakeState; + return true; + }, + }; + return { researcher: new Researcher({ ...deps, ai: provider } as any, navigator as any), closeInstructions }; + } + + function answerWall(holds: boolean) { + mock.clearFixtures(); + mock.on({ predicate: isWallQuestion }, { content: JSON.stringify({ holds }) }); + mock.on({}, { content: taskBoardResearch }); + } + + it('skips the oracle when no overlay is detected', async () => { + answerWall(true); + + await researcher.research(fakeState, { force: true, fix: false }); + + expect(wallQuestions()).toHaveLength(0); + }); + + it('closes a wall, then researches the page without it', async () => { + const { researcher, closeInstructions } = researcherOnWall(true); + answerWall(true); + + await researcher.research(wallState, { force: true, fix: false }); + + expect(closeInstructions).toHaveLength(1); + expect(wallQuestions()).toHaveLength(1); + expect(extractPromptText(researchRequests()[0])).not.toContain('Cookie settings'); + }); + + it('leaves an overlay that is not a wall open and researches it', async () => { + const { researcher, closeInstructions } = researcherOnWall(true); + answerWall(false); + + await researcher.research(wallState, { force: true, fix: false }); + + expect(closeInstructions).toHaveLength(0); + expect(researchRequests().length).toBeGreaterThan(0); + }); + + it('does not research a page whose wall stays open', async () => { + const { researcher } = researcherOnWall(false); + answerWall(true); + + await expect(researcher.research(wallState, { force: true, fix: false })).rejects.toBeInstanceOf(ResearchBlockedError); + + expect(wallQuestions()).toHaveLength(2); + expect(researchRequests()).toHaveLength(0); + }); + + it('continues with research when the oracle could not answer', async () => { + const { researcher, closeInstructions } = researcherOnWall(true); + const oracle = provider.oracle; + provider.oracle = async () => null; + answerWall(true); + + await researcher.research(wallState, { force: true, fix: false }); + provider.oracle = oracle; + + expect(closeInstructions).toHaveLength(0); + expect(researchRequests().length).toBeGreaterThan(0); + }); + }); + it('light research accepts a page without locators instead of retrying', async () => { mock.clearFixtures(); mock.on({}, { content: '# Informational page\n\nStatic text without interactive elements.' }); From 94782ec8769301381ec945c7ceede07e892fe864 Mon Sep 17 00:00:00 2001 From: DavertMik Date: Fri, 6 Nov 2026 02:53:04 +0200 Subject: [PATCH 2/2] Simplify wall closing to an oracle check in its own mixin Replace the Navigator call, re-check, ResearchBlockedError and popup rule with one step: when an overlay is detected, ask the oracle whether it is a wall and close it with the existing cancelInUi. The check lives in a WithWalls mixin next to its statement. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 3 +- src/ai/researcher.ts | 40 +++----------- src/ai/researcher/walls.ts | 28 ++++++++++ src/ai/rules.ts | 4 +- src/explorbot.ts | 2 +- src/utils/error-page.ts | 15 ----- tests/integration/researcher.test.ts | 83 +++++----------------------- 7 files changed, 52 insertions(+), 123 deletions(-) create mode 100644 src/ai/researcher/walls.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index c05a89f8..24435bd2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,9 +3,8 @@ ## 2026-11-06 ### Changes -- [Researcher] Closes cookie consent, paywall, marketing and similar popups before researching a page, so research describes the page itself. Regular dialogs stay open and are researched as before. A page whose popup cannot be closed is not researched, and during planning Captain takes over. +- [Researcher] Closes cookie consent, paywall, marketing and similar popups before researching a page. Regular dialogs stay open and are researched as before. - [Planner] No longer plans tests for cookie consent, marketing, announcement or notification popups, unless you plan that feature explicitly. -- [Tester] Dismisses popups unrelated to the application when they block the page and ignores them otherwise, including popups that can only be closed with an answer such as Accept or Decline. Navigator, Captain and test re-runs follow the same rule. ## 2026-11-05 diff --git a/src/ai/researcher.ts b/src/ai/researcher.ts index a87dccee..03c1d28b 100644 --- a/src/ai/researcher.ts +++ b/src/ai/researcher.ts @@ -10,7 +10,7 @@ import type { StateManager } from '../state-manager.js'; import { WebPageState } from '../state-manager.js'; import { Stats } from '../stats.ts'; import { diffAriaSnapshots } from '../utils/aria.ts'; -import { ErrorPageError, type PageCondition, ResearchBlockedError, detectPageCondition } from '../utils/error-page.ts'; +import { ErrorPageError, type PageCondition, detectPageCondition } from '../utils/error-page.ts'; import { isBodyEmpty } from '../utils/html.ts'; import { createDebug, tag } from '../utils/logger.js'; import { mdq } from '../utils/markdown-query.ts'; @@ -30,6 +30,7 @@ import { type PaginationMethods, WithPagination } from './researcher/pagination. import { extractValidContainers, formatResearchSummary, parseResearchSections } from './researcher/parser.ts'; import { ResearchResult } from './researcher/research-result.ts'; import { type SectionMethods, WithSections } from './researcher/sections.ts'; +import { type WallMethods, WithWalls } from './researcher/walls.ts'; import { locatorRule as generalLocatorRuleText } from './rules.js'; import { TaskAgent } from './task-agent.ts'; @@ -37,8 +38,6 @@ export type { Locator } from './researcher/locators.ts'; const debugLog = createDebug('explorbot:researcher'); const HIDDEN_CONTENT_STATEMENT = 'The page has collapsed or hidden content behind controls that open or expand it.'; -const WALL_STATEMENT = "The open overlay is unrelated to the application's features, such as cookie consent, a paywall, marketing or an announcement. A sign-in form is not such an overlay."; -const CLOSE_WALL_INSTRUCTION = "Close the overlay that is unrelated to the application's features and stay on this page."; export const POSSIBLE_SECTIONS = { overlay: 'dialog, modal, drawer, popup, or active form overlay', @@ -50,9 +49,9 @@ export const POSSIBLE_SECTIONS = { navigation: 'main navigation (top bar, sidebar, breadcrumbs)', }; -const ResearcherBase = WithSections(WithPagination(WithDeepAnalysis(WithCoordinates(WithLocators(TaskAgent as unknown as new (...args: any[]) => TaskAgent))))); +const ResearcherBase = WithWalls(WithSections(WithPagination(WithDeepAnalysis(WithCoordinates(WithLocators(TaskAgent as unknown as new (...args: any[]) => TaskAgent)))))); -export interface Researcher extends LocatorMethods, CoordinateMethods, DeepAnalysisMethods, SectionMethods, PaginationMethods {} +export interface Researcher extends LocatorMethods, CoordinateMethods, DeepAnalysisMethods, SectionMethods, PaginationMethods, WallMethods {} export class Researcher extends ResearcherBase implements Agent { protected readonly ACTION_TOOLS = ['click']; @@ -65,10 +64,7 @@ export class Researcher extends ResearcherBase implements Agent { private hasScreenshotToAnalyze = false; declare actionResult: ActionResult | undefined; - constructor( - deps: AgentDeps, - private navigator: Navigator - ) { + constructor(deps: AgentDeps) { super(deps); this.experienceTracker = deps.stateManager.getExperienceTracker(); this.settings.reasoning ??= 'low'; @@ -91,7 +87,7 @@ export class Researcher extends ResearcherBase implements Agent { } protected getNavigator(): Navigator { - return this.navigator; + throw new Error('not implemented'); } static getCachedResearch(state: WebPageState): string { @@ -163,7 +159,7 @@ export class Researcher extends ResearcherBase implements Agent { tag('warning').log(`Page at ${state.url} did not finish loading within timeout, continuing with best-effort research`); } } - if (!retrying && this.actionResult!.overlay.isModal) await this.closeWall(state.url, screenshot); + if (this.actionResult!.overlay.isModal) await this.closeWall(screenshot); debugLog('Researching web page:', this.actionResult!.url); @@ -407,28 +403,6 @@ export class Researcher extends ResearcherBase implements Agent { return conditions[decision.value ?? ''] || 'ok'; } - private async closeWall(url: string, screenshot: boolean): Promise { - if (!(await this.isWallOpen())) return; - - const wall = this.actionResult!.overlay.describe(); - tag('step').log(`Closing ${wall} before research`); - await this.navigator.resolveState(CLOSE_WALL_INSTRUCTION, this.actionResult!); - if (await this.isWallOpen()) throw new ResearchBlockedError(url, wall); - - await this.explorer.withPage(annotatePageElements); - this.actionResult = await this.explorer.capture({ screenshot: screenshot && this.provider.hasVision() }); - } - - private async isWallOpen(): Promise { - const captured = await this.explorer.capture({ screenshot: true }); - if (!captured.overlay.isModal) return false; - - const evidence = { ...pageEvidence(captured), overlay: captured.overlay.describe(), overlayContent: captured.getRegionARIA() }; - const open = await this.provider.oracle(WALL_STATEMENT, evidence, { agentName: 'researcher', screenshot: captured.screenshot, telemetry: { functionId: 'researcher.isWallOpen' } }); - if (open === null) tag('warning').log('Could not check whether the open overlay blocks research, continuing'); - return open === true; - } - private async hasHiddenContent(): Promise { const captured = await this.explorer.capture({ screenshot: true }); const hidden = await this.provider.oracle(HIDDEN_CONTENT_STATEMENT, pageEvidence(captured), { agentName: 'researcher', screenshot: captured.screenshot, telemetry: { functionId: 'researcher.hasHiddenContent' } }); diff --git a/src/ai/researcher/walls.ts b/src/ai/researcher/walls.ts new file mode 100644 index 00000000..a7ec4c67 --- /dev/null +++ b/src/ai/researcher/walls.ts @@ -0,0 +1,28 @@ +import type { ActionResult } from '../../action-result.ts'; +import type Explorer from '../../explorer.ts'; +import { pageEvidence } from '../judge.ts'; +import type { Provider } from '../provider.ts'; +import type { Constructor } from './mixin.ts'; + +const WALL_STATEMENT = "The open overlay is unrelated to the application's features, such as cookie consent, a paywall, marketing or an announcement. A sign-in form is not such an overlay."; + +export function WithWalls(Base: T) { + return class extends Base { + declare explorer: Explorer; + declare provider: Provider; + declare actionResult: ActionResult | undefined; + + async closeWall(screenshot: boolean): Promise { + const page = this.actionResult!; + const isWall = await this.provider.oracle(WALL_STATEMENT, pageEvidence(page), { agentName: 'researcher', screenshot: page.screenshot }); + if (!isWall) return; + + await (this as any).cancelInUi(); + this.actionResult = await this.explorer.capture({ screenshot: screenshot && this.provider.hasVision() }); + } + }; +} + +export interface WallMethods { + closeWall(screenshot: boolean): Promise; +} diff --git a/src/ai/rules.ts b/src/ai/rules.ts index 0f4e2086..b996f977 100644 --- a/src/ai/rules.ts +++ b/src/ai/rules.ts @@ -243,7 +243,6 @@ export const focusedElementRule = dedent` export const unexpectedPopupRule = dedent` If a modal/popup appeared that you didn't expect, dismiss it first before continuing with original task. - A popup unrelated to the application's features (cookie consent, paywall, marketing, announcement, notification) is never the task, even when it is the current overlay, unless the task is about it. Dismiss it if it blocks the page, otherwise ignore it. If elements become hidden or unclickable (timeout errors on visible elements), a dialog or overlay may have appeared on top. If a click error mentions "intercepts pointer events", another element is covering the target — dismiss it first. If buttons are disabled unexpectedly, trust any explanation the app gives (tooltip, hint text) over your own guess; otherwise check for a blocking popup or empty required fields. @@ -252,8 +251,7 @@ export const unexpectedPopupRule = dedent` 1. I.pressKey('Escape') — press Escape to dismiss 2. I.click('Cancel') — click Cancel button if present 3. I.click({ role: 'button', text: 'Close' }) — click X/close button if present - 4. Click the popup's own answer button (Reject, Decline, Accept) if it has no close control - 5. I.clickXY(0, 0) via form() tool and check if page diff changed + 4. I.clickXY(0, 0) via form() tool and check if page diff changed `; diff --git a/src/explorbot.ts b/src/explorbot.ts index e2bd0fa3..32b94a5d 100644 --- a/src/explorbot.ts +++ b/src/explorbot.ts @@ -237,7 +237,7 @@ export class ExplorBot { } agentResearcher(): Researcher { - return (this.agents.researcher ||= this.createAgent((deps) => new Researcher(deps, this.agentNavigator()))); + return (this.agents.researcher ||= this.createAgent((deps) => new Researcher(deps))); } agentNavigator(): Navigator { diff --git a/src/utils/error-page.ts b/src/utils/error-page.ts index 736e438d..898459e2 100644 --- a/src/utils/error-page.ts +++ b/src/utils/error-page.ts @@ -115,21 +115,6 @@ export class NavigationBlockedError extends BlockedError { } } -export class ResearchBlockedError extends BlockedError { - constructor(url: string, overlay: string) { - super( - `Research of ${url} is blocked: ${overlay}`, - url, - overlay, - dedent` - Researcher was asked to research ${url} but could not close a popup unrelated to the application's features: ${overlay}. - Look at the current page to find out what keeps it open, resolve it, then call plan() to re-plan this page. If it cannot be resolved, finish without planning. - ` - ); - this.name = 'ResearchBlockedError'; - } -} - export class TestStartBlockedError extends BlockedError { constructor(scenario: string, url: string, currentUrl: string) { super( diff --git a/tests/integration/researcher.test.ts b/tests/integration/researcher.test.ts index 674d7032..8ade2e7a 100644 --- a/tests/integration/researcher.test.ts +++ b/tests/integration/researcher.test.ts @@ -10,7 +10,6 @@ import { Researcher } from '../../src/ai/researcher.ts'; import { clearResearchCache, getCachedResearch, saveResearch } from '../../src/ai/researcher/cache.ts'; import { parseResearchSections } from '../../src/ai/researcher/parser.ts'; import { ConfigParser } from '../../src/config.ts'; -import { ResearchBlockedError } from '../../src/utils/error-page.ts'; const UI_MAPS_DIR = join(process.cwd(), 'test-data', 'ui-maps'); const taskBoardResearch = readFileSync(join(UI_MAPS_DIR, 'task-board-research.md'), 'utf8'); @@ -285,84 +284,30 @@ describe('Researcher with aimock', () => { expect(deepRuns).toBe(1); }); - describe('closing a wall before research', () => { + it('closes an overlay only when the oracle says it is a wall', async () => { const wallState = { ...fakeState, ariaSnapshot: `${fakeState.ariaSnapshot}\n- dialog "Cookie settings":\n - button "Accept"` }; const isWallQuestion = (req: any) => JSON.stringify(req.messages || []).includes('open overlay is unrelated'); - const wallQuestions = () => mock.getRequests().filter((r: any) => isWallQuestion(r.body)); - const researchRequests = () => mock.getRequests().filter((r: any) => !isWallQuestion(r.body)); - - function researcherOnWall(closes: boolean) { - const deps = createMockDeps(wallState); - const closeInstructions: string[] = []; - let current = wallState; - deps.explorer.capture = async () => ActionResult.fromState(current); - const navigator = { - resolveState: async (instruction: string) => { - closeInstructions.push(instruction); - if (closes) current = fakeState; - return true; - }, - }; - return { researcher: new Researcher({ ...deps, ai: provider } as any, navigator as any), closeInstructions }; - } + const researcher = new Researcher({ ...createMockDeps(wallState), ai: provider } as any); + let closed = 0; + (researcher as any).cancelInUi = async () => { + closed++; + }; - function answerWall(holds: boolean) { + for (const holds of [false, true]) { mock.clearFixtures(); mock.on({ predicate: isWallQuestion }, { content: JSON.stringify({ holds }) }); mock.on({}, { content: taskBoardResearch }); - } - - it('skips the oracle when no overlay is detected', async () => { - answerWall(true); - - await researcher.research(fakeState, { force: true, fix: false }); - - expect(wallQuestions()).toHaveLength(0); - }); - - it('closes a wall, then researches the page without it', async () => { - const { researcher, closeInstructions } = researcherOnWall(true); - answerWall(true); - - await researcher.research(wallState, { force: true, fix: false }); - - expect(closeInstructions).toHaveLength(1); - expect(wallQuestions()).toHaveLength(1); - expect(extractPromptText(researchRequests()[0])).not.toContain('Cookie settings'); - }); - - it('leaves an overlay that is not a wall open and researches it', async () => { - const { researcher, closeInstructions } = researcherOnWall(true); - answerWall(false); - await researcher.research(wallState, { force: true, fix: false }); + } - expect(closeInstructions).toHaveLength(0); - expect(researchRequests().length).toBeGreaterThan(0); - }); - - it('does not research a page whose wall stays open', async () => { - const { researcher } = researcherOnWall(false); - answerWall(true); - - await expect(researcher.research(wallState, { force: true, fix: false })).rejects.toBeInstanceOf(ResearchBlockedError); - - expect(wallQuestions()).toHaveLength(2); - expect(researchRequests()).toHaveLength(0); - }); - - it('continues with research when the oracle could not answer', async () => { - const { researcher, closeInstructions } = researcherOnWall(true); - const oracle = provider.oracle; - provider.oracle = async () => null; - answerWall(true); + expect(mock.getRequests().filter((r: any) => isWallQuestion(r.body))).toHaveLength(2); + expect(closed).toBe(1); + }); - await researcher.research(wallState, { force: true, fix: false }); - provider.oracle = oracle; + it('does not ask the oracle about a wall when no overlay is detected', async () => { + await researcher.research(fakeState, { force: true, fix: false }); - expect(closeInstructions).toHaveLength(0); - expect(researchRequests().length).toBeGreaterThan(0); - }); + expect(JSON.stringify(mock.getRequests().map((r: any) => r.body))).not.toContain('open overlay is unrelated'); }); it('light research accepts a page without locators instead of retrying', async () => {