explorbot 0.1.15 → 0.1.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/explorbot-cli.ts +12 -1
- package/dist/bin/explorbot-cli.js +13 -1
- package/dist/package.json +1 -1
- package/dist/src/ai/pilot.js +3 -8
- package/dist/src/ai/researcher/focus.js +51 -10
- package/dist/src/ai/researcher/sections.js +8 -4
- package/dist/src/ai/researcher.js +9 -24
- package/dist/src/ai/tester.js +8 -2
- package/dist/src/commands/explore-command.js +359 -43
- package/dist/src/explorbot.js +19 -5
- package/dist/src/utils/test-plan-markdown.js +8 -1
- package/package.json +1 -1
- package/src/ai/pilot.ts +3 -8
- package/src/ai/researcher/focus.ts +57 -8
- package/src/ai/researcher/sections.ts +7 -3
- package/src/ai/researcher.ts +8 -23
- package/src/ai/tester.ts +8 -2
- package/src/commands/explore-command.ts +362 -42
- package/src/explorbot.ts +21 -7
- package/src/utils/test-plan-markdown.ts +8 -1
package/bin/explorbot-cli.ts
CHANGED
|
@@ -122,7 +122,15 @@ addCommonOptions(program.command('start [path]').description('Start web explorat
|
|
|
122
122
|
await startTUI(explorBot);
|
|
123
123
|
});
|
|
124
124
|
|
|
125
|
-
addCommonOptions(
|
|
125
|
+
addCommonOptions(
|
|
126
|
+
program
|
|
127
|
+
.command('explore <path>')
|
|
128
|
+
.description('Explore a page autonomously and run invented scenarios')
|
|
129
|
+
.option('--max-tests <count>', 'Maximum number of tests to run')
|
|
130
|
+
.option('--focus <feature>', 'Focus area for exploration')
|
|
131
|
+
.option('--configure <spec>', 'Reuse spec: keys new|from|style|subpages|pick_by|priority, e.g. "new:25%;pick_by=random;priority=critical,high"')
|
|
132
|
+
.option('--dry-run', 'Mark picked tests as skipped without executing or generating new ones')
|
|
133
|
+
).action(async (explorePath, options) => {
|
|
126
134
|
try {
|
|
127
135
|
const explorBot = new ExplorBot(buildExplorBotOptions(explorePath, options));
|
|
128
136
|
await explorBot.start();
|
|
@@ -130,8 +138,11 @@ addCommonOptions(program.command('explore <path>').description('Explore a page a
|
|
|
130
138
|
const { ExploreCommand } = await import('../src/commands/explore-command.js');
|
|
131
139
|
const cmd = new ExploreCommand(explorBot);
|
|
132
140
|
if (options.maxTests) cmd.maxTests = Number.parseInt(options.maxTests, 10);
|
|
141
|
+
if (options.dryRun) cmd.dryRun = true;
|
|
133
142
|
const execArgs: string[] = [];
|
|
134
143
|
if (options.focus) execArgs.push('--focus', `"${options.focus}"`);
|
|
144
|
+
if (options.configure) execArgs.push('--configure', `"${options.configure}"`);
|
|
145
|
+
if (options.dryRun) execArgs.push('--dry-run');
|
|
135
146
|
await cmd.execute(execArgs.join(' '));
|
|
136
147
|
await explorBot.stop();
|
|
137
148
|
await showStatsAndExit(0);
|
|
@@ -93,7 +93,13 @@ addCommonOptions(program.command('start [path]').description('Start web explorat
|
|
|
93
93
|
await explorBot.start();
|
|
94
94
|
await startTUI(explorBot);
|
|
95
95
|
});
|
|
96
|
-
addCommonOptions(program
|
|
96
|
+
addCommonOptions(program
|
|
97
|
+
.command('explore <path>')
|
|
98
|
+
.description('Explore a page autonomously and run invented scenarios')
|
|
99
|
+
.option('--max-tests <count>', 'Maximum number of tests to run')
|
|
100
|
+
.option('--focus <feature>', 'Focus area for exploration')
|
|
101
|
+
.option('--configure <spec>', 'Reuse spec: keys new|from|style|subpages|pick_by|priority, e.g. "new:25%;pick_by=random;priority=critical,high"')
|
|
102
|
+
.option('--dry-run', 'Mark picked tests as skipped without executing or generating new ones')).action(async (explorePath, options) => {
|
|
97
103
|
try {
|
|
98
104
|
const explorBot = new ExplorBot(buildExplorBotOptions(explorePath, options));
|
|
99
105
|
await explorBot.start();
|
|
@@ -102,9 +108,15 @@ addCommonOptions(program.command('explore <path>').description('Explore a page a
|
|
|
102
108
|
const cmd = new ExploreCommand(explorBot);
|
|
103
109
|
if (options.maxTests)
|
|
104
110
|
cmd.maxTests = Number.parseInt(options.maxTests, 10);
|
|
111
|
+
if (options.dryRun)
|
|
112
|
+
cmd.dryRun = true;
|
|
105
113
|
const execArgs = [];
|
|
106
114
|
if (options.focus)
|
|
107
115
|
execArgs.push('--focus', `"${options.focus}"`);
|
|
116
|
+
if (options.configure)
|
|
117
|
+
execArgs.push('--configure', `"${options.configure}"`);
|
|
118
|
+
if (options.dryRun)
|
|
119
|
+
execArgs.push('--dry-run');
|
|
108
120
|
await cmd.execute(execArgs.join(' '));
|
|
109
121
|
await explorBot.stop();
|
|
110
122
|
await showStatsAndExit(0);
|
package/dist/package.json
CHANGED
package/dist/src/ai/pilot.js
CHANGED
|
@@ -277,14 +277,9 @@ export class Pilot {
|
|
|
277
277
|
- "Edit X" → updated value must be persisted (visible in list/detail). Opening edit is NOT enough; redirect after save with the new value visible IS enough.
|
|
278
278
|
- Negative tests ("without a name", "invalid", "duplicate", "unauthorized") → success means the system PREVENTED the action with validation/error.
|
|
279
279
|
|
|
280
|
-
PROVENANCE
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
matching the goal by text alone but missing the marker is a stale leftover from a prior
|
|
284
|
-
run — it is NOT evidence the current scenario produced anything. Vote \`fail\`, not \`pass\`.
|
|
285
|
-
This does not apply when the field is restricted (numeric only, enum, etc.) or when the
|
|
286
|
-
session_log shows no fillField/type/select actions were attempted at all (in that case
|
|
287
|
-
the scenario clearly didn't run — also vote \`fail\`).
|
|
280
|
+
PROVENANCE: the entity you cite as proof must appear by name in <notes> or
|
|
281
|
+
<session_log> tool inputs for THIS run. Name absent from tester activity = stale
|
|
282
|
+
coincidence, vote \`fail\`. Same if no fillField/type/select/click on a target ran.
|
|
288
283
|
|
|
289
284
|
Expected results are MILESTONES, not the goal. Never fail because a milestone (toast, icon, styling)
|
|
290
285
|
didn't match if the scenario goal IS accomplished.
|
|
@@ -1,20 +1,61 @@
|
|
|
1
|
-
import { detectFocusArea } from "../../utils/aria.js";
|
|
2
1
|
import { mdq } from "../../utils/markdown-query.js";
|
|
3
2
|
export const FOCUSED_MARKER = '> **Focused**';
|
|
4
3
|
const FOCUS_SKIP_SECTIONS = new Set(['navigation', 'menu']);
|
|
5
4
|
export function hasFocusedSection(text) {
|
|
6
5
|
return text.includes(FOCUSED_MARKER);
|
|
7
6
|
}
|
|
8
|
-
export function
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
if (
|
|
15
|
-
|
|
7
|
+
export async function detectFocusedSection(page, sections) {
|
|
8
|
+
const candidates = [];
|
|
9
|
+
for (const section of sections) {
|
|
10
|
+
if (!section.containerCss)
|
|
11
|
+
continue;
|
|
12
|
+
const key = section.name.toLowerCase().replace(/^section:\s*/, '');
|
|
13
|
+
if (FOCUS_SKIP_SECTIONS.has(key))
|
|
14
|
+
continue;
|
|
15
|
+
try {
|
|
16
|
+
const locator = page.locator(section.containerCss).first();
|
|
17
|
+
if (!(await locator.isVisible()))
|
|
18
|
+
continue;
|
|
19
|
+
const probe = await locator.evaluate((el) => {
|
|
20
|
+
const dialogSelector = '[role="dialog"], [role="alertdialog"], [aria-modal="true"]';
|
|
21
|
+
const isDialog = el.matches(dialogSelector) || !!el.querySelector(dialogSelector);
|
|
22
|
+
let cur = el;
|
|
23
|
+
let maxZ = 0;
|
|
24
|
+
while (cur && cur !== document.body) {
|
|
25
|
+
const cs = window.getComputedStyle(cur);
|
|
26
|
+
if (cs.position !== 'static') {
|
|
27
|
+
const z = Number.parseInt(cs.zIndex, 10);
|
|
28
|
+
if (!Number.isNaN(z) && z > maxZ)
|
|
29
|
+
maxZ = z;
|
|
30
|
+
}
|
|
31
|
+
cur = cur.parentElement;
|
|
32
|
+
}
|
|
33
|
+
const shadow = window.getComputedStyle(el).boxShadow;
|
|
34
|
+
const hasShadow = !!shadow && shadow !== 'none';
|
|
35
|
+
return { isDialog, zIndex: maxZ, hasShadow };
|
|
36
|
+
});
|
|
37
|
+
candidates.push({ name: section.name, ...probe });
|
|
38
|
+
}
|
|
39
|
+
catch { }
|
|
16
40
|
}
|
|
17
|
-
|
|
41
|
+
if (candidates.length === 0)
|
|
42
|
+
return null;
|
|
43
|
+
const dialogs = candidates.filter((c) => c.isDialog);
|
|
44
|
+
const pool = dialogs.length > 0 ? dialogs : candidates;
|
|
45
|
+
const winner = pool.reduce((best, c) => {
|
|
46
|
+
if (!best)
|
|
47
|
+
return c;
|
|
48
|
+
if (c.zIndex !== best.zIndex)
|
|
49
|
+
return c.zIndex > best.zIndex ? c : best;
|
|
50
|
+
if (c.hasShadow !== best.hasShadow)
|
|
51
|
+
return c.hasShadow ? c : best;
|
|
52
|
+
return best;
|
|
53
|
+
}, null);
|
|
54
|
+
if (!winner)
|
|
55
|
+
return null;
|
|
56
|
+
if (dialogs.length === 0 && winner.zIndex === 0 && !winner.hasShadow)
|
|
57
|
+
return null;
|
|
58
|
+
return winner.name;
|
|
18
59
|
}
|
|
19
60
|
export function markSectionAsFocused(result, sectionName) {
|
|
20
61
|
if (hasFocusedSection(result.text))
|
|
@@ -3,6 +3,8 @@ import { executionController } from "../../execution-controller.js";
|
|
|
3
3
|
import { tag } from '../../utils/logger.js';
|
|
4
4
|
import { RulesLoader } from "../../utils/rules-loader.js";
|
|
5
5
|
import { locatorRule as generalLocatorRuleText } from '../rules.js';
|
|
6
|
+
import { markSectionAsFocused } from "./focus.js";
|
|
7
|
+
import { ResearchResult } from "./research-result.js";
|
|
6
8
|
export function WithSections(Base) {
|
|
7
9
|
return class extends Base {
|
|
8
10
|
async researchBySections() {
|
|
@@ -40,10 +42,12 @@ export function WithSections(Base) {
|
|
|
40
42
|
if (parts.length === 0) {
|
|
41
43
|
throw new Error('Per-section research produced no sections — AI responses all empty or NOT_PRESENT');
|
|
42
44
|
}
|
|
43
|
-
|
|
44
|
-
if (focusCss)
|
|
45
|
-
merged
|
|
46
|
-
|
|
45
|
+
const merged = parts.join('\n\n');
|
|
46
|
+
if (!focusCss)
|
|
47
|
+
return merged;
|
|
48
|
+
const focused = new ResearchResult(merged, this.actionResult?.url || '');
|
|
49
|
+
markSectionAsFocused(focused, 'Focus');
|
|
50
|
+
return focused.text;
|
|
47
51
|
}
|
|
48
52
|
async _detectFocusCss() {
|
|
49
53
|
const focusSections = this.explorer.getConfig().ai?.agents?.researcher?.focusSections;
|
|
@@ -16,7 +16,7 @@ import { ContextLengthError } from './provider.js';
|
|
|
16
16
|
import { findSimilarResearch, getCachedResearch, saveResearch } from "./researcher/cache.js";
|
|
17
17
|
import { WithCoordinates } from "./researcher/coordinates.js";
|
|
18
18
|
import { WithDeepAnalysis } from "./researcher/deep-analysis.js";
|
|
19
|
-
import {
|
|
19
|
+
import { detectFocusedSection, hasFocusedSection, markSectionAsFocused, pickDefaultFocusedSection } from "./researcher/focus.js";
|
|
20
20
|
import { WithLocators } from "./researcher/locators.js";
|
|
21
21
|
import { extractValidContainers, formatResearchSummary, parseResearchSections } from "./researcher/parser.js";
|
|
22
22
|
import { ResearchResult } from "./researcher/research-result.js";
|
|
@@ -186,18 +186,13 @@ export class Researcher extends ResearcherBase {
|
|
|
186
186
|
if (!interrupted() && fix && result.locators.some((l) => l.valid === false)) {
|
|
187
187
|
await this.fixBrokenSections(result, activeConversation);
|
|
188
188
|
}
|
|
189
|
-
// Focused section:
|
|
190
|
-
|
|
191
|
-
if (
|
|
192
|
-
result.text = result.text.replace(focusMatch[0], '');
|
|
193
|
-
markSectionAsFocused(result, focusMatch[1].trim());
|
|
194
|
-
}
|
|
195
|
-
if (!hasFocusedSection(result.text)) {
|
|
189
|
+
// Focused section: unified Playwright probe (HTML+CSS+visibility).
|
|
190
|
+
// Must run BEFORE visuallyAnnotateContainers — annotation overlays inject z-index 99998+ which would pollute the scoring.
|
|
191
|
+
if (!interrupted() && this.hasScreenshotToAnalyze) {
|
|
196
192
|
const sections = parseResearchSections(result.text);
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
markSectionAsFocused(result, focusedName);
|
|
193
|
+
const focused = await detectFocusedSection(this.explorer.playwrightHelper.page, sections);
|
|
194
|
+
if (focused)
|
|
195
|
+
markSectionAsFocused(result, focused);
|
|
201
196
|
}
|
|
202
197
|
// Stage 4: Visual analysis
|
|
203
198
|
if (!interrupted() && this.hasScreenshotToAnalyze) {
|
|
@@ -232,8 +227,8 @@ export class Researcher extends ResearcherBase {
|
|
|
232
227
|
await this.backfillCoordinates(result);
|
|
233
228
|
await this.backfillBrokenLocators(result);
|
|
234
229
|
}
|
|
235
|
-
// Focused section: final fallback
|
|
236
|
-
if (!hasFocusedSection(result.text)) {
|
|
230
|
+
// Focused section: final fallback (vision-only — without a screenshot we don't infer focus)
|
|
231
|
+
if (this.hasScreenshotToAnalyze && !hasFocusedSection(result.text)) {
|
|
237
232
|
const sections = parseResearchSections(result.text);
|
|
238
233
|
const fallback = pickDefaultFocusedSection(sections);
|
|
239
234
|
if (fallback)
|
|
@@ -388,16 +383,6 @@ export class Researcher extends ResearcherBase {
|
|
|
388
383
|
|
|
389
384
|
| Element | ARIA | CSS | eidx |
|
|
390
385
|
</section_format>
|
|
391
|
-
|
|
392
|
-
<focused_section>
|
|
393
|
-
At the end of your output, declare the primary focus area on a single line:
|
|
394
|
-
|
|
395
|
-
> Focused: <exact section name>
|
|
396
|
-
|
|
397
|
-
- If a dialog/modal/drawer/overlay exists, it is focused.
|
|
398
|
-
- Otherwise pick the section where the main business action happens (list for catalog, detail for item page, content for article).
|
|
399
|
-
- Navigation and menu/toolbar are never focused.
|
|
400
|
-
</focused_section>
|
|
401
386
|
`;
|
|
402
387
|
}
|
|
403
388
|
async buildResearchPrompt() {
|
package/dist/src/ai/tester.js
CHANGED
|
@@ -329,10 +329,15 @@ export class Tester extends TaskAgent {
|
|
|
329
329
|
: undefined,
|
|
330
330
|
catch: async ({ error, stop }) => {
|
|
331
331
|
tag('error').log(`Test execution error: ${error}`);
|
|
332
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
332
333
|
if (!task.hasFinished) {
|
|
333
|
-
task.addNote(`Execution error: ${
|
|
334
|
+
task.addNote(`Execution error: ${message}`);
|
|
334
335
|
}
|
|
335
|
-
|
|
336
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
337
|
+
stop();
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
conversation.addUserText(`Previous AI call failed: ${message}. Take a different approach on the next step.`);
|
|
336
341
|
},
|
|
337
342
|
});
|
|
338
343
|
if (task.hasFinished)
|
|
@@ -643,6 +648,7 @@ export class Tester extends TaskAgent {
|
|
|
643
648
|
- Use pressKey() for pressing special keys (Enter, Escape, Tab, Arrow keys) or key combinations with modifiers (Ctrl+A, Shift+Delete, etc.)
|
|
644
649
|
- Use container CSS locators from <page_ui_map> to interact with elements inside sections
|
|
645
650
|
- Systematically use record({ notes: ["..."] }) to write your findings, planned actions, observations, etc.
|
|
651
|
+
- When creating/editing/deleting a named entity, include its identifier verbatim in the note — Pilot uses it to confirm provenance.
|
|
646
652
|
- Call record({ notes: ["..."], status: "success" }) when you see success/info message on a page or when expected outcome is achieved
|
|
647
653
|
- Call record({ notes: ["..."], status: "fail" }) when an expected outcome cannot be achieved or has failed or you see error/alert/warning message on a page
|
|
648
654
|
- NEVER call record(status: "success") if your last verify() or see() call FAILED. A failed check means the outcome is NOT confirmed — use record(status: "fail") instead, or retry with a different approach.
|