Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## 2026-11-06

### Changes
- [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.

## 2026-11-05

### New CLI Options
Expand Down
1 change: 1 addition & 0 deletions rules/planner/ignored-pages.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
</ignored_pages>
6 changes: 4 additions & 2 deletions src/ai/researcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -48,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'];
Expand Down Expand Up @@ -158,6 +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 (this.actionResult!.overlay.isModal) await this.closeWall(screenshot);

debugLog('Researching web page:', this.actionResult!.url);

Expand Down
28 changes: 28 additions & 0 deletions src/ai/researcher/walls.ts
Original file line number Diff line number Diff line change
@@ -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<T extends Constructor>(Base: T) {
return class extends Base {
declare explorer: Explorer;
declare provider: Provider;
declare actionResult: ActionResult | undefined;

async closeWall(screenshot: boolean): Promise<void> {
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<void>;
}
26 changes: 26 additions & 0 deletions tests/integration/researcher.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,32 @@ describe('Researcher with aimock', () => {
expect(deepRuns).toBe(1);
});

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 researcher = new Researcher({ ...createMockDeps(wallState), ai: provider } as any);
let closed = 0;
(researcher as any).cancelInUi = async () => {
closed++;
};

for (const holds of [false, true]) {
mock.clearFixtures();
mock.on({ predicate: isWallQuestion }, { content: JSON.stringify({ holds }) });
mock.on({}, { content: taskBoardResearch });
await researcher.research(wallState, { force: true, fix: false });
}

expect(mock.getRequests().filter((r: any) => isWallQuestion(r.body))).toHaveLength(2);
expect(closed).toBe(1);
});

it('does not ask the oracle about a wall when no overlay is detected', async () => {
await researcher.research(fakeState, { force: true, fix: false });

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 () => {
mock.clearFixtures();
mock.on({}, { content: '# Informational page\n\nStatic text without interactive elements.' });
Expand Down
Loading