explorbot 0.1.0 → 0.1.2
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 +93 -36
- package/dist/bin/explorbot-cli.js +71 -16
- package/dist/rules/rerunner/healing-approach.md +19 -0
- package/dist/src/action.js +8 -10
- package/dist/src/ai/historian.js +34 -3
- package/dist/src/ai/navigator.js +35 -28
- package/dist/src/ai/pilot.js +33 -9
- package/dist/src/ai/planner/session-dedup.js +3 -0
- package/dist/src/ai/planner/styles.js +3 -0
- package/dist/src/ai/planner.js +29 -10
- package/dist/src/ai/rerunner.js +472 -0
- package/dist/src/ai/researcher/cache.js +4 -3
- package/dist/src/ai/researcher/fingerprint-worker.js +7 -6
- package/dist/src/ai/researcher.js +3 -4
- package/dist/src/ai/rules.js +2 -2
- package/dist/src/ai/tools.js +2 -2
- package/dist/src/commands/add-rule-command.js +1 -2
- package/dist/src/commands/base-command.js +12 -0
- package/dist/src/commands/context-command.js +12 -5
- package/dist/src/commands/drill-command.js +0 -1
- package/dist/src/commands/explore-command.js +20 -5
- package/dist/src/commands/freesail-command.js +8 -22
- package/dist/src/commands/index.js +4 -0
- package/dist/src/commands/init-command.js +3 -3
- package/dist/src/commands/path-command.js +2 -1
- package/dist/src/commands/plan-command.js +37 -15
- package/dist/src/commands/rerun-command.js +42 -0
- package/dist/src/commands/research-command.js +10 -4
- package/dist/src/commands/runs-command.js +22 -0
- package/dist/src/commands/start-command.js +0 -1
- package/dist/src/commands/test-command.js +3 -3
- package/dist/src/components/App.js +8 -0
- package/dist/src/config.js +3 -0
- package/dist/src/explorbot.js +19 -0
- package/dist/src/explorer.js +2 -1
- package/dist/src/suite.js +115 -0
- package/dist/src/utils/html.js +2 -5
- package/dist/src/utils/rules-loader.js +33 -17
- package/dist/src/utils/test-files.js +103 -0
- package/package.json +3 -1
- package/rules/rerunner/healing-approach.md +19 -0
- package/src/action.ts +7 -9
- package/src/ai/historian.ts +37 -3
- package/src/ai/navigator.ts +35 -28
- package/src/ai/pilot.ts +33 -9
- package/src/ai/planner/session-dedup.ts +4 -0
- package/src/ai/planner/styles.ts +4 -0
- package/src/ai/planner.ts +28 -9
- package/src/ai/rerunner.ts +532 -0
- package/src/ai/researcher/cache.ts +4 -3
- package/src/ai/researcher/fingerprint-worker.ts +7 -13
- package/src/ai/researcher.ts +3 -4
- package/src/ai/rules.ts +2 -2
- package/src/ai/tools.ts +2 -2
- package/src/commands/add-rule-command.ts +1 -2
- package/src/commands/base-command.ts +13 -0
- package/src/commands/context-command.ts +12 -5
- package/src/commands/drill-command.ts +0 -1
- package/src/commands/explore-command.ts +21 -5
- package/src/commands/freesail-command.ts +6 -23
- package/src/commands/index.ts +4 -0
- package/src/commands/init-command.ts +3 -3
- package/src/commands/path-command.ts +2 -1
- package/src/commands/plan-command.ts +45 -16
- package/src/commands/rerun-command.ts +46 -0
- package/src/commands/research-command.ts +10 -4
- package/src/commands/runs-command.ts +27 -0
- package/src/commands/start-command.ts +0 -1
- package/src/commands/test-command.ts +3 -3
- package/src/components/App.tsx +8 -0
- package/src/config.ts +23 -0
- package/src/explorbot.ts +21 -0
- package/src/explorer.ts +3 -2
- package/src/suite.ts +135 -0
- package/src/utils/html.ts +1 -5
- package/src/utils/rules-loader.ts +35 -17
- package/src/utils/test-files.ts +122 -0
package/src/ai/researcher.ts
CHANGED
|
@@ -131,9 +131,9 @@ export class Researcher extends ResearcherBase implements Agent {
|
|
|
131
131
|
await this.ensureNavigated(state.url, screenshot && this.provider.hasVision());
|
|
132
132
|
await this.hooksRunner.runBeforeHook('researcher', state.url);
|
|
133
133
|
|
|
134
|
-
const
|
|
134
|
+
const annotatedElements = await this.explorer.annotateElements();
|
|
135
135
|
debugLog(`Annotated ${annotatedElements.length} interactive elements with eidx`);
|
|
136
|
-
this.actionResult = await this.explorer.createAction().capturePageState({ includeScreenshot: screenshot && this.provider.hasVision()
|
|
136
|
+
this.actionResult = await this.explorer.createAction().capturePageState({ includeScreenshot: screenshot && this.provider.hasVision() });
|
|
137
137
|
|
|
138
138
|
if (isErrorPage(this.actionResult!)) {
|
|
139
139
|
const recovered = await this.waitForPageLoad(screenshot);
|
|
@@ -385,10 +385,9 @@ export class Researcher extends ResearcherBase implements Agent {
|
|
|
385
385
|
try {
|
|
386
386
|
await withRetry(
|
|
387
387
|
async () => {
|
|
388
|
-
|
|
388
|
+
await this.explorer.annotateElements();
|
|
389
389
|
this.actionResult = await this.explorer.createAction().capturePageState({
|
|
390
390
|
includeScreenshot: screenshot && this.provider.hasVision(),
|
|
391
|
-
ariaSnapshot,
|
|
392
391
|
});
|
|
393
392
|
if (isErrorPage(this.actionResult!)) throw new Error('Error page detected');
|
|
394
393
|
},
|
package/src/ai/rules.ts
CHANGED
|
@@ -266,7 +266,7 @@ export const actionRule = dedent`
|
|
|
266
266
|
I.fillField('Username', 'John', '.login-form'); // fills Username inside .login-form
|
|
267
267
|
I.fillField('Username', 'John'); // fills the field located by name or placeholder or label "Username" with the text "John"
|
|
268
268
|
I.fillField('//user/input', 'John'); // fills the field located by XPath "//user/input" with the text "John"
|
|
269
|
-
</example>
|
|
269
|
+
</example>
|
|
270
270
|
|
|
271
271
|
### I.type
|
|
272
272
|
|
|
@@ -303,7 +303,7 @@ export const actionRule = dedent`
|
|
|
303
303
|
</example>
|
|
304
304
|
|
|
305
305
|
IMPORTANT: Requires an active/focused element for most keys.
|
|
306
|
-
Commonly used after I.type() to submit forms or navigate dropdowns.
|
|
306
|
+
Commonly used after I.type() or I.fillField() to submit forms or navigate dropdowns.
|
|
307
307
|
|
|
308
308
|
### I.switchTo
|
|
309
309
|
|
package/src/ai/tools.ts
CHANGED
|
@@ -310,7 +310,7 @@ export function createCodeceptJSTools(explorer: Explorer, task: Task) {
|
|
|
310
310
|
I.selectOption({"role":"combobox","text":"Category"}, 'Technology')
|
|
311
311
|
|
|
312
312
|
Do not submit form - use verify() first to check fields were filled correctly, then click() to submit.
|
|
313
|
-
Do not use: wait functions, amOnPage, reloadPage, saveScreenshot
|
|
313
|
+
Do not use: wait functions, amOnPage, reloadPage, saveScreenshot
|
|
314
314
|
`,
|
|
315
315
|
inputSchema: z.object({
|
|
316
316
|
codeBlock: z.string().describe('Valid CodeceptJS code starting with I. Can contain multiple commands separated by newlines.'),
|
|
@@ -385,7 +385,7 @@ export function createCodeceptJSTools(explorer: Explorer, task: Task) {
|
|
|
385
385
|
message: `Form completed successfully with ${lines.length} commands.`,
|
|
386
386
|
commandsExecuted: lines.length,
|
|
387
387
|
code: codeBlock,
|
|
388
|
-
suggestion: 'Verify the form was filled in correctly using see() tool.
|
|
388
|
+
suggestion: 'Verify the form was filled in correctly using see() tool. If needed to submit: try click() tool or form() with I.pressKey("Enter").',
|
|
389
389
|
});
|
|
390
390
|
} catch (error) {
|
|
391
391
|
activeNote.commit(TestResult.FAILED);
|
|
@@ -6,8 +6,7 @@ import { tag } from '../utils/logger.js';
|
|
|
6
6
|
import { BaseCommand } from './base-command.js';
|
|
7
7
|
|
|
8
8
|
export class AddRuleCommand extends BaseCommand {
|
|
9
|
-
name = '
|
|
10
|
-
aliases = ['add-rule'];
|
|
9
|
+
name = 'add-rule';
|
|
11
10
|
description = 'Create a rule file for an agent';
|
|
12
11
|
suggestions = ['/add-rule researcher check-tooltips'];
|
|
13
12
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
1
2
|
import type { ExplorBot } from '../explorbot.js';
|
|
2
3
|
|
|
3
4
|
export interface CommandOption {
|
|
@@ -24,4 +25,16 @@ export abstract class BaseCommand {
|
|
|
24
25
|
matches(commandName: string): boolean {
|
|
25
26
|
return this.name === commandName || this.aliases.includes(commandName);
|
|
26
27
|
}
|
|
28
|
+
|
|
29
|
+
protected parseArgs(args: string): { opts: Record<string, string | boolean>; args: string[] } {
|
|
30
|
+
const cmd = new Command();
|
|
31
|
+
cmd.exitOverride();
|
|
32
|
+
for (const opt of this.options) {
|
|
33
|
+
cmd.option(opt.flags, opt.description);
|
|
34
|
+
}
|
|
35
|
+
cmd.argument('[args...]');
|
|
36
|
+
const argv = (args.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) || []).map((s) => s.replace(/^["']|["']$/g, ''));
|
|
37
|
+
cmd.parse(argv, { from: 'user' });
|
|
38
|
+
return { opts: cmd.opts(), args: cmd.args };
|
|
39
|
+
}
|
|
27
40
|
}
|
|
@@ -10,6 +10,12 @@ export class ContextCommand extends BaseCommand {
|
|
|
10
10
|
name = 'context';
|
|
11
11
|
description = 'Show page context summary (URL, headings, experience, knowledge, ARIA, HTML, research)';
|
|
12
12
|
suggestions = ['context:aria', 'context:html', 'context:knowledge', 'context:experience', 'context:data'];
|
|
13
|
+
options = [
|
|
14
|
+
{ flags: '--visual', description: 'Include annotated screenshot' },
|
|
15
|
+
{ flags: '--screenshot', description: 'Include annotated screenshot' },
|
|
16
|
+
{ flags: '--full', description: 'Show full context with HTML' },
|
|
17
|
+
{ flags: '--attached', description: 'Show attached context mode' },
|
|
18
|
+
];
|
|
13
19
|
|
|
14
20
|
async execute(args: string): Promise<void> {
|
|
15
21
|
const explorer = this.explorBot.getExplorer();
|
|
@@ -19,9 +25,10 @@ export class ContextCommand extends BaseCommand {
|
|
|
19
25
|
throw new Error('No active page to show context for');
|
|
20
26
|
}
|
|
21
27
|
|
|
22
|
-
const
|
|
28
|
+
const { opts } = this.parseArgs(args);
|
|
29
|
+
const isVisual = !!(opts.visual || opts.screenshot);
|
|
23
30
|
|
|
24
|
-
|
|
31
|
+
await explorer.annotateElements();
|
|
25
32
|
|
|
26
33
|
if (isVisual) {
|
|
27
34
|
const cachedResearch = Researcher.getCachedResearch(state);
|
|
@@ -29,14 +36,14 @@ export class ContextCommand extends BaseCommand {
|
|
|
29
36
|
await explorer.visuallyAnnotateElements({ containers });
|
|
30
37
|
}
|
|
31
38
|
|
|
32
|
-
const actionResult = await explorer.createAction().capturePageState({ includeScreenshot: isVisual
|
|
39
|
+
const actionResult = await explorer.createAction().capturePageState({ includeScreenshot: isVisual });
|
|
33
40
|
const experienceTracker = explorer.getStateManager().getExperienceTracker();
|
|
34
41
|
const knowledgeTracker = this.explorBot.getKnowledgeTracker();
|
|
35
42
|
|
|
36
43
|
let mode: ContextMode = 'compact';
|
|
37
|
-
if (
|
|
44
|
+
if (opts.full) {
|
|
38
45
|
mode = 'full';
|
|
39
|
-
} else if (
|
|
46
|
+
} else if (opts.attached) {
|
|
40
47
|
mode = 'attached';
|
|
41
48
|
}
|
|
42
49
|
|
|
@@ -3,7 +3,6 @@ import { BaseCommand } from './base-command.js';
|
|
|
3
3
|
export class DrillCommand extends BaseCommand {
|
|
4
4
|
name = 'drill';
|
|
5
5
|
description = 'Drill all components on current page to learn interactions';
|
|
6
|
-
aliases = ['bosun'];
|
|
7
6
|
suggestions = ['/research - to see UI map first', '/navigate <page> - to go to another page'];
|
|
8
7
|
|
|
9
8
|
async execute(args: string): Promise<void> {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { existsSync, readdirSync } from 'node:fs';
|
|
1
2
|
import figureSet from 'figures';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import { getStyles } from '../ai/planner/styles.js';
|
|
5
|
+
import { ConfigParser } from '../config.ts';
|
|
4
6
|
import { getCliName } from '../utils/cli-name.ts';
|
|
5
7
|
import type { Plan } from '../test-plan.js';
|
|
6
8
|
import { jsonToTable } from '../utils/markdown-parser.js';
|
|
@@ -18,13 +20,12 @@ export class ExploreCommand extends BaseCommand {
|
|
|
18
20
|
private completedPlans: Plan[] = [];
|
|
19
21
|
|
|
20
22
|
async execute(args: string): Promise<void> {
|
|
21
|
-
const
|
|
22
|
-
if (
|
|
23
|
-
this.maxTests = Number.parseInt(
|
|
24
|
-
args = args.replace(/--max-tests\s+\d+/, '').trim();
|
|
23
|
+
const { opts, args: remaining } = this.parseArgs(args);
|
|
24
|
+
if (opts.maxTests) {
|
|
25
|
+
this.maxTests = Number.parseInt(opts.maxTests as string, 10);
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
const feature =
|
|
28
|
+
const feature = remaining.join(' ') || undefined;
|
|
28
29
|
const mainUrl = this.explorBot.getExplorer().getStateManager().getCurrentState()?.url;
|
|
29
30
|
|
|
30
31
|
await this.runAllStyles(mainUrl, feature);
|
|
@@ -61,6 +62,7 @@ export class ExploreCommand extends BaseCommand {
|
|
|
61
62
|
if (mainUrl) await this.explorBot.visit(mainUrl);
|
|
62
63
|
const savedPath = this.explorBot.savePlans(this.completedPlans);
|
|
63
64
|
this.printResults(savedPath);
|
|
65
|
+
this.printRerunSuggestions();
|
|
64
66
|
}
|
|
65
67
|
|
|
66
68
|
private async runAllStyles(pageUrl?: string, feature?: string, parentPlan?: Plan, completedPlans?: Plan[]): Promise<void> {
|
|
@@ -113,6 +115,20 @@ export class ExploreCommand extends BaseCommand {
|
|
|
113
115
|
}
|
|
114
116
|
}
|
|
115
117
|
|
|
118
|
+
private printRerunSuggestions(): void {
|
|
119
|
+
const testsDir = ConfigParser.getInstance().getTestsDir();
|
|
120
|
+
if (!existsSync(testsDir)) return;
|
|
121
|
+
|
|
122
|
+
const testFiles = readdirSync(testsDir).filter((f) => f.endsWith('.js'));
|
|
123
|
+
if (testFiles.length === 0) return;
|
|
124
|
+
|
|
125
|
+
for (const file of testFiles) {
|
|
126
|
+
tag('info').log(`Generated: ${file}`);
|
|
127
|
+
}
|
|
128
|
+
tag('info').log(`List tests: ${getCliName()} runs`);
|
|
129
|
+
tag('info').log(`Re-run with healing: ${getCliName()} rerun <filename> [index]`);
|
|
130
|
+
}
|
|
131
|
+
|
|
116
132
|
private isLimitReached(): boolean {
|
|
117
133
|
return this.maxTests != null && this.testsRun >= this.maxTests;
|
|
118
134
|
}
|
|
@@ -18,7 +18,12 @@ export class FreesailCommand extends BaseCommand {
|
|
|
18
18
|
];
|
|
19
19
|
|
|
20
20
|
async execute(args: string): Promise<void> {
|
|
21
|
-
const {
|
|
21
|
+
const { opts } = this.parseArgs(args);
|
|
22
|
+
let strategy: 'deep' | 'shallow' | undefined;
|
|
23
|
+
if (opts.deep) strategy = 'deep';
|
|
24
|
+
if (opts.shallow) strategy = 'shallow';
|
|
25
|
+
const scope = opts.scope as string | undefined;
|
|
26
|
+
const maxTests = opts.maxTests ? Number.parseInt(opts.maxTests as string, 10) : undefined;
|
|
22
27
|
|
|
23
28
|
await this.explorBot.visitInitialState();
|
|
24
29
|
|
|
@@ -71,25 +76,3 @@ export class FreesailCommand extends BaseCommand {
|
|
|
71
76
|
);
|
|
72
77
|
}
|
|
73
78
|
}
|
|
74
|
-
|
|
75
|
-
function parseArgs(args: string): { strategy: 'deep' | 'shallow' | undefined; scope: string | undefined; maxTests: number | undefined } {
|
|
76
|
-
const parts = args.trim().split(/\s+/);
|
|
77
|
-
let strategy: 'deep' | 'shallow' | undefined;
|
|
78
|
-
let scope: string | undefined;
|
|
79
|
-
let maxTests: number | undefined;
|
|
80
|
-
|
|
81
|
-
for (let i = 0; i < parts.length; i++) {
|
|
82
|
-
if (parts[i] === '--deep') strategy = 'deep';
|
|
83
|
-
if (parts[i] === '--shallow') strategy = 'shallow';
|
|
84
|
-
if (parts[i] === '--scope' && parts[i + 1]) {
|
|
85
|
-
scope = parts[i + 1];
|
|
86
|
-
i++;
|
|
87
|
-
}
|
|
88
|
-
if (parts[i] === '--max-tests' && parts[i + 1]) {
|
|
89
|
-
maxTests = Number.parseInt(parts[i + 1], 10);
|
|
90
|
-
i++;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
return { strategy, scope, maxTests };
|
|
95
|
-
}
|
package/src/commands/index.ts
CHANGED
|
@@ -24,7 +24,9 @@ import { PlanEditCommand } from './plan-edit-command.js';
|
|
|
24
24
|
import { PlanLoadCommand } from './plan-load-command.js';
|
|
25
25
|
import { PlanReloadCommand } from './plan-reload-command.js';
|
|
26
26
|
import { PlanSaveCommand } from './plan-save-command.js';
|
|
27
|
+
import { RerunCommand } from './rerun-command.js';
|
|
27
28
|
import { ResearchCommand } from './research-command.js';
|
|
29
|
+
import { RunsCommand } from './runs-command.js';
|
|
28
30
|
import { StartCommand } from './start-command.js';
|
|
29
31
|
import { StatusCommand } from './status-command.tsx';
|
|
30
32
|
import { TestCommand } from './test-command.js';
|
|
@@ -59,6 +61,8 @@ const commandClasses: CommandClass[] = [
|
|
|
59
61
|
ContextExperienceCommand,
|
|
60
62
|
ContextDataCommand,
|
|
61
63
|
TestCommand,
|
|
64
|
+
RunsCommand,
|
|
65
|
+
RerunCommand,
|
|
62
66
|
StatusCommand,
|
|
63
67
|
DebugCommand,
|
|
64
68
|
ExitCommand,
|
|
@@ -23,11 +23,11 @@ const config = {
|
|
|
23
23
|
|
|
24
24
|
ai: {
|
|
25
25
|
// fast model with tool calling capabilities
|
|
26
|
-
model: openrouter('
|
|
26
|
+
model: openrouter('openai/gpt-oss-20b:nitro'),
|
|
27
27
|
// vision model for screenshot analysis
|
|
28
|
-
visionModel: openrouter('
|
|
28
|
+
visionModel: openrouter('meta-llama/llama-4-scout-17b-16e-instruct'),
|
|
29
29
|
// agentic model for decision making
|
|
30
|
-
agenticModel: openrouter('
|
|
30
|
+
agenticModel: openrouter('minimax/minimax-m2.5:nitro'),
|
|
31
31
|
},
|
|
32
32
|
};
|
|
33
33
|
|
|
@@ -14,7 +14,8 @@ export class PathCommand extends BaseCommand {
|
|
|
14
14
|
options = [{ flags: '--links', description: 'Show outgoing links from each page' }];
|
|
15
15
|
|
|
16
16
|
async execute(args: string): Promise<void> {
|
|
17
|
-
const
|
|
17
|
+
const { opts } = this.parseArgs(args);
|
|
18
|
+
const showLinks = !!opts.links;
|
|
18
19
|
const stateManager = this.explorBot.getExplorer().getStateManager();
|
|
19
20
|
const history = stateManager.getStateHistory();
|
|
20
21
|
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import figureSet from 'figures';
|
|
1
4
|
import { tag } from '../utils/logger.js';
|
|
2
5
|
import { BaseCommand } from './base-command.js';
|
|
3
6
|
|
|
@@ -13,21 +16,10 @@ export class PlanCommand extends BaseCommand {
|
|
|
13
16
|
];
|
|
14
17
|
|
|
15
18
|
async execute(args: string): Promise<void> {
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const focusMatch = args.match(/--focus\s+("[^"]+"|'[^']+'|\S+)/);
|
|
21
|
-
const focusFromFlag = focusMatch?.[1]?.replace(/^["']|["']$/g, '');
|
|
22
|
-
const focusFromText = args
|
|
23
|
-
.replace('--clear', '')
|
|
24
|
-
.replace('--fresh', '')
|
|
25
|
-
.replace(/--style\s+\S+/, '')
|
|
26
|
-
.replace(/--focus\s+("[^"]+"|'[^']+'|\S+)/, '')
|
|
27
|
-
.trim();
|
|
28
|
-
const focus = focusFromFlag || focusFromText;
|
|
29
|
-
|
|
30
|
-
if (clear) {
|
|
19
|
+
const { opts, args: remaining } = this.parseArgs(args);
|
|
20
|
+
const focus = (opts.focus as string) || remaining.join(' ') || undefined;
|
|
21
|
+
|
|
22
|
+
if (opts.clear) {
|
|
31
23
|
this.explorBot.clearPlan();
|
|
32
24
|
tag('success').log('Plan cleared');
|
|
33
25
|
}
|
|
@@ -36,11 +28,48 @@ export class PlanCommand extends BaseCommand {
|
|
|
36
28
|
tag('info').log(`Planning focus: ${focus}`);
|
|
37
29
|
}
|
|
38
30
|
|
|
39
|
-
await this.explorBot.plan(focus
|
|
31
|
+
await this.explorBot.plan(focus, { fresh: !!(opts.fresh || opts.clear), style: opts.style as string });
|
|
40
32
|
|
|
41
33
|
const plan = this.explorBot.getCurrentPlan();
|
|
42
34
|
if (!plan?.tests.length) {
|
|
43
35
|
throw new Error('No test scenarios in the current plan.');
|
|
44
36
|
}
|
|
37
|
+
|
|
38
|
+
this.printPlanSummary();
|
|
39
|
+
this.updateSuggestions();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private printPlanSummary(): void {
|
|
43
|
+
const suite = this.explorBot.getSuite();
|
|
44
|
+
const plan = this.explorBot.getCurrentPlan();
|
|
45
|
+
|
|
46
|
+
if (suite && suite.automatedTestCount > 0) {
|
|
47
|
+
const names = suite.getAutomatedTestNames();
|
|
48
|
+
console.log(`\n${chalk.bold.cyan(`Already implemented (${names.length} tests)`)}`);
|
|
49
|
+
for (let i = 0; i < names.length; i++) {
|
|
50
|
+
console.log(` ${chalk.dim(`${i + 1}.`)} ${chalk.green(figureSet.pointer)} ${names[i]}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (plan?.tests.length) {
|
|
55
|
+
console.log(`\n${chalk.bold.cyan(`New test scenarios (${plan.tests.length})`)}`);
|
|
56
|
+
for (let i = 0; i < plan.tests.length; i++) {
|
|
57
|
+
const t = plan.tests[i];
|
|
58
|
+
console.log(` ${chalk.dim(`${i + 1}.`)} ${chalk.green(figureSet.pointer)} ${t.scenario} ${chalk.dim(`[${t.priority}]`)}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private updateSuggestions(): void {
|
|
64
|
+
this.suggestions = ['/test - to launch first test', '/test * - to launch all tests'];
|
|
65
|
+
|
|
66
|
+
const suite = this.explorBot.getSuite();
|
|
67
|
+
if (suite && suite.automatedTestCount > 0) {
|
|
68
|
+
for (const f of suite.getAutomatedTestFiles()) {
|
|
69
|
+
this.suggestions.push(`/rerun ${path.relative(process.cwd(), f)} - re-run automated tests`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
this.suggestions.push('Edit the plan in file and call /plan:reload to update it');
|
|
45
74
|
}
|
|
46
75
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { ConfigParser } from '../config.ts';
|
|
4
|
+
import { tag } from '../utils/logger.ts';
|
|
5
|
+
import { BaseCommand } from './base-command.js';
|
|
6
|
+
|
|
7
|
+
export class RerunCommand extends BaseCommand {
|
|
8
|
+
name = 'rerun';
|
|
9
|
+
description = 'Re-run generated tests with AI auto-healing';
|
|
10
|
+
tuiEnabled = true;
|
|
11
|
+
|
|
12
|
+
async execute(args: string): Promise<void> {
|
|
13
|
+
const { args: remaining } = this.parseArgs(args);
|
|
14
|
+
const filename = remaining[0];
|
|
15
|
+
const indexArg = remaining[1];
|
|
16
|
+
|
|
17
|
+
if (!filename) {
|
|
18
|
+
tag('error').log('Usage: /rerun <filename> [index]');
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let filePath = resolve(filename);
|
|
23
|
+
if (!existsSync(filePath)) {
|
|
24
|
+
filePath = resolve(ConfigParser.getInstance().getTestsDir(), filename);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const testIndices = indexArg ? parseTestIndices(indexArg) : undefined;
|
|
28
|
+
await this.explorBot.agentRerunner().rerun(filePath, { testIndices });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function parseTestIndices(input: string): number[] {
|
|
33
|
+
if (input === '*' || input === 'all') return [];
|
|
34
|
+
|
|
35
|
+
const indices = new Set<number>();
|
|
36
|
+
for (const part of input.split(',')) {
|
|
37
|
+
const trimmed = part.trim();
|
|
38
|
+
const range = trimmed.match(/^(\d+)-(\d+)$/);
|
|
39
|
+
if (range) {
|
|
40
|
+
for (let i = Number.parseInt(range[1]); i <= Number.parseInt(range[2]); i++) indices.add(i - 1);
|
|
41
|
+
} else {
|
|
42
|
+
indices.add(Number.parseInt(trimmed) - 1);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return [...indices].sort((a, b) => a - b);
|
|
46
|
+
}
|
|
@@ -7,12 +7,18 @@ export class ResearchCommand extends BaseCommand {
|
|
|
7
7
|
name = 'research';
|
|
8
8
|
description = 'Research current page or navigate to URI and research. Use --deep to explore interactive elements by clicking them. Use --data to include page data.';
|
|
9
9
|
suggestions = ['/navigate <page> - to go to another page', '/plan <feature> - to plan testing'];
|
|
10
|
+
options = [
|
|
11
|
+
{ flags: '--data', description: 'Include page data' },
|
|
12
|
+
{ flags: '--deep', description: 'Explore interactive elements by clicking them' },
|
|
13
|
+
{ flags: '--no-fix', description: 'Skip fixing research issues' },
|
|
14
|
+
];
|
|
10
15
|
|
|
11
16
|
async execute(args: string): Promise<void> {
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
const
|
|
17
|
+
const { opts, args: remaining } = this.parseArgs(args);
|
|
18
|
+
const includeData = !!opts.data;
|
|
19
|
+
const enableDeep = !!opts.deep;
|
|
20
|
+
const noFix = !!opts.noFix;
|
|
21
|
+
const target = remaining.join(' ');
|
|
16
22
|
|
|
17
23
|
if (target) {
|
|
18
24
|
await this.explorBot.agentNavigator().visit(target);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import * as codeceptjs from 'codeceptjs';
|
|
2
|
+
import { ConfigParser } from '../config.ts';
|
|
3
|
+
import { dryRunTestFile, loadTestSuites, printTestList } from '../utils/test-files.ts';
|
|
4
|
+
import { BaseCommand } from './base-command.js';
|
|
5
|
+
|
|
6
|
+
export class RunsCommand extends BaseCommand {
|
|
7
|
+
name = 'runs';
|
|
8
|
+
description = 'List generated test files and their scenarios';
|
|
9
|
+
tuiEnabled = true;
|
|
10
|
+
|
|
11
|
+
async execute(args: string): Promise<void> {
|
|
12
|
+
if (!this.explorBot.isExploring) {
|
|
13
|
+
codeceptjs.container.create({ helpers: {} }, {});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const { args: remaining } = this.parseArgs(args);
|
|
17
|
+
const filePath = remaining[0];
|
|
18
|
+
|
|
19
|
+
if (filePath) {
|
|
20
|
+
await dryRunTestFile(filePath);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const suites = loadTestSuites(ConfigParser.getInstance().getTestsDir());
|
|
25
|
+
printTestList(suites);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -3,7 +3,6 @@ import { ExploreCommand } from './explore-command.js';
|
|
|
3
3
|
|
|
4
4
|
export class StartCommand extends BaseCommand {
|
|
5
5
|
name = 'start';
|
|
6
|
-
aliases = ['sail'];
|
|
7
6
|
description = 'Start web exploration';
|
|
8
7
|
suggestions = ['/navigate <page> - to go to another page', '/research - to analyze', '/plan <feature> - to plan testing'];
|
|
9
8
|
|
|
@@ -25,10 +25,10 @@ export class TestCommand extends BaseCommand {
|
|
|
25
25
|
} else if (args === '*' || args === 'all') {
|
|
26
26
|
toExecute.push(...requirePlan().getPendingTests());
|
|
27
27
|
} else if (args.match(/^[\d,\-\s]+$/)) {
|
|
28
|
-
const
|
|
29
|
-
const indices = parseTestIndices(args,
|
|
28
|
+
const visible = requirePlan().tests.filter((t) => t.enabled);
|
|
29
|
+
const indices = parseTestIndices(args, visible.length);
|
|
30
30
|
for (const idx of indices) {
|
|
31
|
-
toExecute.push(
|
|
31
|
+
toExecute.push(visible[idx]);
|
|
32
32
|
}
|
|
33
33
|
} else {
|
|
34
34
|
const matching = plan?.getPendingTests().filter((test) => test.scenario.toLowerCase().includes(args.toLowerCase())) || [];
|
package/src/components/App.tsx
CHANGED
|
@@ -242,6 +242,14 @@ export function App({ explorBot, initialShowInput = false, exitOnEmptyInput = fa
|
|
|
242
242
|
setShowPlanEditor(true);
|
|
243
243
|
return;
|
|
244
244
|
}
|
|
245
|
+
if (key.upArrow) {
|
|
246
|
+
setTaskScrollOffset((prev) => Math.max(0, prev - 1));
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
if (key.downArrow) {
|
|
250
|
+
setTaskScrollOffset((prev) => prev + 1);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
245
253
|
}
|
|
246
254
|
|
|
247
255
|
if (!showInput && !showPlanEditor) {
|
package/src/config.ts
CHANGED
|
@@ -85,6 +85,22 @@ interface NavigatorAgentConfig extends AgentConfig {
|
|
|
85
85
|
maxAttempts?: number;
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
type HealFn = (ctx: { I: any }) => Promise<void> | void;
|
|
89
|
+
|
|
90
|
+
interface HealRecipe {
|
|
91
|
+
priority?: number;
|
|
92
|
+
steps?: string[];
|
|
93
|
+
fn: (context: { step: any; error: Error; prevSteps?: any[] }) => HealFn | Promise<HealFn | null> | null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
interface RerunnerAgentConfig extends AgentConfig {
|
|
97
|
+
healLimit?: number;
|
|
98
|
+
ariaSnapshotLimit?: number;
|
|
99
|
+
retryFailedStep?: Record<string, any>;
|
|
100
|
+
screenshotOnFail?: Record<string, any>;
|
|
101
|
+
recipes?: Record<string, HealRecipe>;
|
|
102
|
+
}
|
|
103
|
+
|
|
88
104
|
interface PlannerAgentConfig extends AgentConfig {
|
|
89
105
|
styles?: string[];
|
|
90
106
|
stylesDir?: string;
|
|
@@ -103,6 +119,7 @@ interface AgentsConfig {
|
|
|
103
119
|
fisherman?: AgentConfig;
|
|
104
120
|
chief?: AgentConfig;
|
|
105
121
|
curler?: AgentConfig;
|
|
122
|
+
rerunner?: RerunnerAgentConfig;
|
|
106
123
|
}
|
|
107
124
|
|
|
108
125
|
interface AIConfig {
|
|
@@ -214,6 +231,8 @@ export type {
|
|
|
214
231
|
ResearcherAgentConfig,
|
|
215
232
|
NavigatorAgentConfig,
|
|
216
233
|
PlannerAgentConfig,
|
|
234
|
+
RerunnerAgentConfig,
|
|
235
|
+
HealRecipe,
|
|
217
236
|
Hook,
|
|
218
237
|
HookConfig,
|
|
219
238
|
HooksConfig,
|
|
@@ -328,6 +347,10 @@ export class ConfigParser {
|
|
|
328
347
|
return outputPath('plans');
|
|
329
348
|
}
|
|
330
349
|
|
|
350
|
+
public getTestsDir(): string {
|
|
351
|
+
return outputPath('tests');
|
|
352
|
+
}
|
|
353
|
+
|
|
331
354
|
// For testing purposes only
|
|
332
355
|
public static resetForTesting(): void {
|
|
333
356
|
if (ConfigParser.instance) {
|
package/src/explorbot.ts
CHANGED
|
@@ -14,11 +14,13 @@ import { Planner } from './ai/planner.ts';
|
|
|
14
14
|
import { AIProvider } from './ai/provider.ts';
|
|
15
15
|
import { Quartermaster } from './ai/quartermaster.ts';
|
|
16
16
|
import { Researcher } from './ai/researcher.ts';
|
|
17
|
+
import { Rerunner } from './ai/rerunner.ts';
|
|
17
18
|
import { Tester } from './ai/tester.ts';
|
|
18
19
|
import { createAgentTools } from './ai/tools.ts';
|
|
19
20
|
import type { ExplorbotConfig } from './config.js';
|
|
20
21
|
import { ConfigParser } from './config.ts';
|
|
21
22
|
import Explorer from './explorer.ts';
|
|
23
|
+
import type { Suite } from './suite.ts';
|
|
22
24
|
import { KnowledgeTracker } from './knowledge-tracker.ts';
|
|
23
25
|
import { WebPageState } from './state-manager.ts';
|
|
24
26
|
import { Plan } from './test-plan.ts';
|
|
@@ -245,6 +247,21 @@ export class ExplorBot {
|
|
|
245
247
|
}));
|
|
246
248
|
}
|
|
247
249
|
|
|
250
|
+
agentRerunner(): Rerunner {
|
|
251
|
+
if (!this.agents.rerunner) {
|
|
252
|
+
this.agents.rerunner = this.createAgent(({ ai, explorer }) => {
|
|
253
|
+
const researcher = this.agentResearcher();
|
|
254
|
+
const navigator = this.agentNavigator();
|
|
255
|
+
const tools = createAgentTools({ explorer, researcher, navigator });
|
|
256
|
+
return new Rerunner(explorer, ai, tools);
|
|
257
|
+
});
|
|
258
|
+
const qm = this.agentQuartermaster();
|
|
259
|
+
if (qm) this.agents.rerunner.setQuartermaster(qm);
|
|
260
|
+
this.agents.rerunner.setHistorian(this.agentHistorian());
|
|
261
|
+
}
|
|
262
|
+
return this.agents.rerunner;
|
|
263
|
+
}
|
|
264
|
+
|
|
248
265
|
agentBosun(): Bosun {
|
|
249
266
|
return (this.agents.bosun ||= this.createAgent(({ ai, explorer }) => {
|
|
250
267
|
const researcher = this.agentResearcher();
|
|
@@ -291,6 +308,10 @@ export class ExplorBot {
|
|
|
291
308
|
return this.currentPlan;
|
|
292
309
|
}
|
|
293
310
|
|
|
311
|
+
getSuite(): Suite | null {
|
|
312
|
+
return this.agentPlanner().getSuite();
|
|
313
|
+
}
|
|
314
|
+
|
|
294
315
|
getPlanFeature(): string | undefined {
|
|
295
316
|
return this.planFeature;
|
|
296
317
|
}
|
package/src/explorer.ts
CHANGED
|
@@ -309,8 +309,9 @@ class Explorer {
|
|
|
309
309
|
return action;
|
|
310
310
|
}
|
|
311
311
|
|
|
312
|
-
async annotateElements(): Promise<
|
|
313
|
-
|
|
312
|
+
async annotateElements(): Promise<WebElement[]> {
|
|
313
|
+
const { elements } = await annotatePageElements(this.playwrightHelper.page);
|
|
314
|
+
return elements;
|
|
314
315
|
}
|
|
315
316
|
|
|
316
317
|
async visuallyAnnotateElements(opts?: { containers?: Array<{ css: string; label: string }> }): Promise<number> {
|