explorbot 0.1.9 → 0.1.11
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/README.md +27 -1
- package/bin/explorbot-cli.ts +86 -15
- package/boat/api-tester/src/ai/curler-tools.ts +3 -3
- package/boat/api-tester/src/ai/curler.ts +1 -1
- package/boat/api-tester/src/apibot.ts +2 -2
- package/boat/api-tester/src/config.ts +1 -1
- package/dist/bin/explorbot-cli.js +85 -14
- package/dist/boat/api-tester/src/ai/curler-tools.js +2 -2
- package/dist/boat/api-tester/src/apibot.js +2 -2
- package/dist/package.json +2 -2
- package/dist/rules/navigator/output.md +9 -0
- package/dist/rules/navigator/verification-actions.md +2 -0
- package/dist/src/action-result.js +23 -1
- package/dist/src/action.js +46 -38
- package/dist/src/ai/bosun.js +16 -2
- package/dist/src/ai/conversation.js +39 -0
- package/dist/src/ai/experience-compactor.js +235 -50
- package/dist/src/ai/historian/codeceptjs.js +109 -0
- package/dist/src/ai/historian/experience.js +320 -0
- package/dist/src/ai/historian/mixin.js +2 -0
- package/dist/src/ai/historian/playwright.js +145 -0
- package/dist/src/ai/historian/utils.js +18 -0
- package/dist/src/ai/historian.js +19 -398
- package/dist/src/ai/navigator.js +133 -80
- package/dist/src/ai/pilot.js +254 -13
- package/dist/src/ai/planner/subpages.js +1 -30
- package/dist/src/ai/planner.js +33 -13
- package/dist/src/ai/provider.js +55 -18
- package/dist/src/ai/rerunner.js +3 -3
- package/dist/src/ai/researcher/deep-analysis.js +1 -1
- package/dist/src/ai/researcher/fingerprint-worker.js +1 -1
- package/dist/src/ai/researcher/locators.js +1 -1
- package/dist/src/ai/researcher/sections.js +8 -1
- package/dist/src/ai/researcher.js +43 -41
- package/dist/src/ai/rules.js +26 -14
- package/dist/src/ai/tester.js +90 -26
- package/dist/src/ai/tools.js +18 -10
- package/dist/src/api/request-store.js +20 -0
- package/dist/src/api/xhr-capture.js +19 -3
- package/dist/src/browser-server.js +16 -3
- package/dist/src/command-handler.js +1 -1
- package/dist/src/commands/add-rule-command.js +12 -9
- package/dist/src/commands/base-command.js +20 -0
- package/dist/src/commands/clean-command.js +3 -2
- package/dist/src/commands/compact-command.js +138 -0
- package/dist/src/commands/context-command.js +7 -1
- package/dist/src/commands/drill-command.js +4 -1
- package/dist/src/commands/experience-command.js +104 -0
- package/dist/src/commands/explore-command.js +54 -19
- package/dist/src/commands/freesail-command.js +2 -0
- package/dist/src/commands/index.js +7 -3
- package/dist/src/commands/init-command.js +11 -10
- package/dist/src/commands/learn-command.js +1 -1
- package/dist/src/commands/navigate-command.js +4 -1
- package/dist/src/commands/plan-clear-command.js +4 -1
- package/dist/src/commands/plan-command.js +43 -4
- package/dist/src/commands/plan-edit-command.js +1 -1
- package/dist/src/commands/plan-load-command.js +4 -1
- package/dist/src/commands/plan-reload-command.js +4 -1
- package/dist/src/commands/plan-save-command.js +20 -8
- package/dist/src/commands/rerun-command.js +4 -0
- package/dist/src/commands/research-command.js +5 -2
- package/dist/src/commands/start-command.js +5 -1
- package/dist/src/commands/test-command.js +7 -1
- package/dist/src/components/App.js +15 -5
- package/dist/src/execution-controller.js +13 -2
- package/dist/src/experience-tracker.js +174 -83
- package/dist/src/explorbot.js +31 -22
- package/dist/src/explorer.js +12 -5
- package/dist/src/observability.js +50 -99
- package/dist/src/playwright-recorder.js +309 -0
- package/dist/src/reporter.js +17 -2
- package/dist/src/stats.js +2 -0
- package/dist/src/suite.js +1 -1
- package/dist/src/test-plan.js +12 -0
- package/dist/src/utils/aria.js +37 -1
- package/dist/src/utils/error-page.js +30 -7
- package/dist/src/utils/logger.js +1 -1
- package/dist/src/utils/next-steps.js +37 -0
- package/dist/src/utils/rules-loader.js +1 -1
- package/dist/src/utils/test-files.js +1 -1
- package/dist/src/utils/url-matcher.js +50 -0
- package/package.json +2 -2
- package/rules/navigator/output.md +9 -0
- package/rules/navigator/verification-actions.md +2 -0
- package/src/action-result.ts +26 -1
- package/src/action.ts +44 -37
- package/src/ai/bosun.ts +16 -2
- package/src/ai/conversation.ts +37 -0
- package/src/ai/experience-compactor.ts +270 -63
- package/src/ai/historian/codeceptjs.ts +130 -0
- package/src/ai/historian/experience.ts +383 -0
- package/src/ai/historian/mixin.ts +4 -0
- package/src/ai/historian/playwright.ts +169 -0
- package/src/ai/historian/utils.ts +23 -0
- package/src/ai/historian.ts +35 -468
- package/src/ai/navigator.ts +140 -85
- package/src/ai/pilot.ts +259 -14
- package/src/ai/planner/subpages.ts +1 -24
- package/src/ai/planner.ts +34 -14
- package/src/ai/provider.ts +52 -18
- package/src/ai/rerunner.ts +3 -3
- package/src/ai/researcher/deep-analysis.ts +1 -1
- package/src/ai/researcher/fingerprint-worker.ts +1 -1
- package/src/ai/researcher/locators.ts +2 -2
- package/src/ai/researcher/sections.ts +7 -1
- package/src/ai/researcher.ts +47 -42
- package/src/ai/rules.ts +27 -14
- package/src/ai/task-agent.ts +1 -1
- package/src/ai/tester.ts +94 -26
- package/src/ai/tools.ts +53 -29
- package/src/api/request-store.ts +22 -0
- package/src/api/xhr-capture.ts +21 -3
- package/src/browser-server.ts +17 -3
- package/src/command-handler.ts +1 -1
- package/src/commands/add-rule-command.ts +13 -9
- package/src/commands/base-command.ts +26 -1
- package/src/commands/clean-command.ts +4 -3
- package/src/commands/compact-command.ts +156 -0
- package/src/commands/context-command.ts +8 -2
- package/src/commands/drill-command.ts +5 -2
- package/src/commands/experience-command.ts +125 -0
- package/src/commands/explore-command.ts +58 -21
- package/src/commands/freesail-command.ts +2 -0
- package/src/commands/index.ts +7 -3
- package/src/commands/init-command.ts +11 -10
- package/src/commands/learn-command.ts +2 -2
- package/src/commands/navigate-command.ts +5 -2
- package/src/commands/plan-clear-command.ts +5 -2
- package/src/commands/plan-command.ts +47 -5
- package/src/commands/plan-edit-command.ts +2 -2
- package/src/commands/plan-load-command.ts +5 -2
- package/src/commands/plan-reload-command.ts +5 -2
- package/src/commands/plan-save-command.ts +20 -9
- package/src/commands/rerun-command.ts +5 -0
- package/src/commands/research-command.ts +6 -3
- package/src/commands/start-command.ts +6 -2
- package/src/commands/test-command.ts +8 -2
- package/src/components/App.tsx +16 -5
- package/src/config.ts +6 -1
- package/src/execution-controller.ts +14 -3
- package/src/experience-tracker.ts +198 -100
- package/src/explorbot.ts +33 -23
- package/src/explorer.ts +14 -5
- package/src/observability.ts +50 -109
- package/src/playwright-recorder.ts +305 -0
- package/src/reporter.ts +17 -3
- package/src/stats.ts +4 -0
- package/src/suite.ts +1 -1
- package/src/test-plan.ts +12 -0
- package/src/utils/aria.ts +38 -1
- package/src/utils/error-page.ts +32 -7
- package/src/utils/logger.ts +1 -1
- package/src/utils/next-steps.ts +51 -0
- package/src/utils/rules-loader.ts +1 -1
- package/src/utils/test-files.ts +1 -1
- package/src/utils/url-matcher.ts +43 -0
package/README.md
CHANGED
|
@@ -169,10 +169,36 @@ See [docs/commands.md](docs/commands.md) for all commands.
|
|
|
169
169
|
|
|
170
170
|
| Output | Location | Description |
|
|
171
171
|
|--------|----------|-------------|
|
|
172
|
-
| Test files | `output/tests/*.js` |
|
|
172
|
+
| Test files | `output/tests/*.spec.ts` or `*.js` | Runnable Playwright or CodeceptJS tests |
|
|
173
173
|
| Test plans | `output/plans/*.md` | Markdown documentation of scenarios |
|
|
174
174
|
| Experience | `./experience/` | What Explorbot learned about your app |
|
|
175
175
|
|
|
176
|
+
Every run is saved as a real Playwright or CodeceptJS test you can commit and run from CI. Choose the framework in your config:
|
|
177
|
+
|
|
178
|
+
```js
|
|
179
|
+
ai: { agents: { historian: { framework: 'playwright' } } } // or 'codeceptjs' (default)
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Playwright output uses the actual `page.locator(...)` calls executed during the run, with each action wrapped in `test.step` so failures land on a labelled step:
|
|
183
|
+
|
|
184
|
+
```ts
|
|
185
|
+
test('Create a new manual plan', async ({ page }) => {
|
|
186
|
+
await test.step("Click the 'New plan' button in toolbar", async () => {
|
|
187
|
+
await page.getByRole('button', { name: 'New plan' }).first().click();
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
await test.step('Select Manual plan type in modal', async () => {
|
|
191
|
+
await page.locator('#portal-container').getByRole('button', { name: 'Manual' }).click();
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
await test.step('Verification', async () => {
|
|
195
|
+
await expect(page).toContainText('Test Plan UI Creation 001');
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
See [Automated Tests](docs/automated-tests.md) for the CodeceptJS version and how failed or unfinished scenarios are handled.
|
|
201
|
+
|
|
176
202
|
## Two Ways to Run
|
|
177
203
|
|
|
178
204
|
**Interactive mode** — Launch TUI, guide exploration, get real-time feedback:
|
package/bin/explorbot-cli.ts
CHANGED
|
@@ -17,6 +17,7 @@ import { getCliName } from '../src/utils/cli-name.ts';
|
|
|
17
17
|
import { log, setPreserveConsoleLogs } from '../src/utils/logger.js';
|
|
18
18
|
import { jsonToTable } from '../src/utils/markdown-parser.js';
|
|
19
19
|
import { parseMarkdownToTerminal } from '../src/utils/markdown-terminal.js';
|
|
20
|
+
import { type NextStepSection, printNextSteps, relativeToCwd } from '../src/utils/next-steps.ts';
|
|
20
21
|
|
|
21
22
|
const program = new Command();
|
|
22
23
|
const cli = getCliName();
|
|
@@ -25,9 +26,10 @@ const pkgPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../p
|
|
|
25
26
|
const pkgVersion = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')).version as string;
|
|
26
27
|
|
|
27
28
|
program.name(cli).description('AI-powered web exploration tool').version(pkgVersion, '-V, --version');
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
});
|
|
29
|
+
|
|
30
|
+
if (!process.env.EXPLORBOT_NO_BANNER) {
|
|
31
|
+
console.log(`⛵ ${chalk.yellow.bold(`Explorbot v${pkgVersion}`)} ${chalk.dim('Autonomous Testing Agent')}`);
|
|
32
|
+
}
|
|
31
33
|
|
|
32
34
|
interface CLIOptions {
|
|
33
35
|
verbose?: boolean;
|
|
@@ -188,22 +190,32 @@ addCommonOptions(program.command('plan <path>').description('Generate test plan
|
|
|
188
190
|
}
|
|
189
191
|
|
|
190
192
|
const savedPath = explorBot.savePlan();
|
|
191
|
-
const planFile = savedPath ? path.basename(savedPath) : 'plan.md';
|
|
192
193
|
|
|
193
194
|
const cliFlags = [options.path ? `--path ${options.path}` : '', options.session ? '--session' : ''].filter(Boolean).join(' ');
|
|
194
195
|
const cliSuffix = cliFlags ? ` ${cliFlags}` : '';
|
|
195
196
|
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
197
|
+
const sections: NextStepSection[] = [];
|
|
198
|
+
if (savedPath) {
|
|
199
|
+
const relPlan = relativeToCwd(savedPath);
|
|
200
|
+
sections.push({
|
|
201
|
+
label: 'Plan',
|
|
202
|
+
path: savedPath,
|
|
203
|
+
commands: [
|
|
204
|
+
{ label: 'Re-run', command: `${cli} test ${relPlan} 1${cliSuffix}` },
|
|
205
|
+
{ label: 'Run all', command: `${cli} test ${relPlan} *${cliSuffix}` },
|
|
206
|
+
{ label: 'Run range', command: `${cli} test ${relPlan} 1-3${cliSuffix}` },
|
|
207
|
+
],
|
|
208
|
+
});
|
|
204
209
|
}
|
|
205
|
-
|
|
206
|
-
|
|
210
|
+
const automatedFiles = suite && suite.automatedTestCount > 0 ? suite.getAutomatedTestFiles() : [];
|
|
211
|
+
if (automatedFiles.length > 0) {
|
|
212
|
+
const commands = automatedFiles.map((f) => ({ label: '', command: `${cli} rerun ${relativeToCwd(f)}${cliSuffix}` }));
|
|
213
|
+
sections.push({
|
|
214
|
+
label: `Automated tests (${automatedFiles.length})`,
|
|
215
|
+
commands,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
printNextSteps(sections);
|
|
207
219
|
|
|
208
220
|
await explorBot.stop();
|
|
209
221
|
await showStatsAndExit(0);
|
|
@@ -383,7 +395,7 @@ program
|
|
|
383
395
|
|
|
384
396
|
program
|
|
385
397
|
.command('clean [target]')
|
|
386
|
-
.description('Clean files: states, research, plans, experiences, output (default: output + experiences)')
|
|
398
|
+
.description('Clean files: states, research, plans, tests, experiences, output (default: output + experiences)')
|
|
387
399
|
.option('-p, --path <path>', 'Custom path to clean')
|
|
388
400
|
.action(async (target, options) => {
|
|
389
401
|
const customPath = options.path;
|
|
@@ -394,6 +406,7 @@ program
|
|
|
394
406
|
states: { description: 'page states', dir: path.join(basePath, 'output', 'states') },
|
|
395
407
|
research: { description: 'research cache', dir: path.join(basePath, 'output', 'research') },
|
|
396
408
|
plans: { description: 'test plans', dir: path.join(basePath, 'output', 'plans') },
|
|
409
|
+
tests: { description: 'generated tests', dir: path.join(basePath, 'output', 'tests') },
|
|
397
410
|
experiences: { description: 'experience files', dir: path.join(basePath, 'experience') },
|
|
398
411
|
output: { description: 'all output files', dir: path.join(basePath, 'output') },
|
|
399
412
|
};
|
|
@@ -492,6 +505,64 @@ program
|
|
|
492
505
|
}
|
|
493
506
|
});
|
|
494
507
|
|
|
508
|
+
program
|
|
509
|
+
.command('compact [target]')
|
|
510
|
+
.description('Compact stored experience files; pass filename or URL substring to limit scope')
|
|
511
|
+
.option('--dry-run', 'Preview without running AI or writing files')
|
|
512
|
+
.option('--no-merge', 'Skip the cross-URL merge step when compacting all')
|
|
513
|
+
.option('-p, --path <path>', 'Working directory path')
|
|
514
|
+
.option('-c, --config <path>', 'Path to configuration file')
|
|
515
|
+
.option('-v, --verbose', 'Enable verbose logging')
|
|
516
|
+
.action(async (target, options) => {
|
|
517
|
+
try {
|
|
518
|
+
const explorBot = new ExplorBot({
|
|
519
|
+
path: options.path,
|
|
520
|
+
config: options.config,
|
|
521
|
+
verbose: options.verbose,
|
|
522
|
+
});
|
|
523
|
+
await explorBot.startProviderOnly();
|
|
524
|
+
|
|
525
|
+
const { CompactCommand } = await import('../src/commands/compact-command.js');
|
|
526
|
+
const argParts = [target, options.dryRun && '--dry-run', options.merge === false && '--no-merge'].filter(Boolean).join(' ');
|
|
527
|
+
const command = new CompactCommand(explorBot);
|
|
528
|
+
await command.execute(argParts);
|
|
529
|
+
command.printSuggestions();
|
|
530
|
+
|
|
531
|
+
await showStatsAndExit(0);
|
|
532
|
+
} catch (error) {
|
|
533
|
+
console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
|
|
534
|
+
await showStatsAndExit(1);
|
|
535
|
+
}
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
program
|
|
539
|
+
.command('experience [filter] [index]')
|
|
540
|
+
.description('List stored experiences grouped by URL; pass URL substring to filter; pass tag (A.1) or index to expand a section')
|
|
541
|
+
.option('-p, --path <path>', 'Working directory path')
|
|
542
|
+
.option('-c, --config <path>', 'Path to configuration file')
|
|
543
|
+
.option('--recent', 'Only files modified within the last 30 days')
|
|
544
|
+
.option('--old', 'Only files modified more than 30 days ago')
|
|
545
|
+
.action(async (filter, index, options) => {
|
|
546
|
+
try {
|
|
547
|
+
await ConfigParser.getInstance().loadConfig({
|
|
548
|
+
config: options.config,
|
|
549
|
+
path: options.path || process.cwd(),
|
|
550
|
+
});
|
|
551
|
+
const { ExperienceCommand } = await import('../src/commands/experience-command.js');
|
|
552
|
+
const explorBot = new ExplorBot({ path: options.path });
|
|
553
|
+
const command = new ExperienceCommand(explorBot);
|
|
554
|
+
const flags: string[] = [];
|
|
555
|
+
if (options.recent) flags.push('--recent');
|
|
556
|
+
if (options.old) flags.push('--old');
|
|
557
|
+
const args = [...flags, filter, index].filter(Boolean).join(' ');
|
|
558
|
+
await command.execute(args);
|
|
559
|
+
command.printSuggestions();
|
|
560
|
+
} catch (error) {
|
|
561
|
+
console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
|
|
562
|
+
process.exit(1);
|
|
563
|
+
}
|
|
564
|
+
});
|
|
565
|
+
|
|
495
566
|
addCommonOptions(program.command('research <url>').description('Research a page and print UI analysis').option('--data', 'Include data extraction in research').option('--deep', 'Enable deep analysis (expand hidden elements)').option('--no-fix', 'Skip locator fix cycle (for debugging)')).action(
|
|
496
567
|
async (url, options) => {
|
|
497
568
|
try {
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import { tool } from 'ai';
|
|
2
|
-
import { expect } from 'expect';
|
|
3
1
|
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { tool } from 'ai';
|
|
4
3
|
import dedent from 'dedent';
|
|
4
|
+
import { expect } from 'expect';
|
|
5
5
|
import { z } from 'zod';
|
|
6
|
+
import type { RequestStore } from '../../../../src/api/request-store.ts';
|
|
6
7
|
import type { Test, TestResultType } from '../../../../src/test-plan.ts';
|
|
7
8
|
import { TestResult } from '../../../../src/test-plan.ts';
|
|
8
9
|
import { tag } from '../../../../src/utils/logger.ts';
|
|
9
10
|
import type { ApiClient } from '../api-client.ts';
|
|
10
|
-
import type { RequestStore } from '../../../../src/api/request-store.ts';
|
|
11
11
|
|
|
12
12
|
const readResponseData = (responseFile: string) => {
|
|
13
13
|
return JSON.parse(readFileSync(responseFile, 'utf8'));
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import dedent from 'dedent';
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import type { AIProvider } from '../../../../src/ai/provider.ts';
|
|
4
|
+
import type { RequestStore } from '../../../../src/api/request-store.ts';
|
|
4
5
|
import type { Reporter } from '../../../../src/reporter.ts';
|
|
5
6
|
import { type Test, TestResult } from '../../../../src/test-plan.ts';
|
|
6
7
|
import { createDebug, tag } from '../../../../src/utils/logger.ts';
|
|
7
8
|
import { loop } from '../../../../src/utils/loop.ts';
|
|
8
9
|
import type { ApiClient } from '../api-client.ts';
|
|
9
|
-
import type { RequestStore } from '../../../../src/api/request-store.ts';
|
|
10
10
|
import { createCurlerTools } from './curler-tools.ts';
|
|
11
11
|
|
|
12
12
|
const debugLog = createDebug('explorbot:curler');
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { existsSync, mkdirSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { AIProvider } from '../../../src/ai/provider.ts';
|
|
4
|
+
import { RequestStore } from '../../../src/api/request-store.ts';
|
|
5
|
+
import { extractEndpointDefinition, loadSpec, searchEndpoints, validateSpecs } from '../../../src/api/spec-reader.ts';
|
|
4
6
|
import { Reporter } from '../../../src/reporter.ts';
|
|
5
7
|
import { Plan } from '../../../src/test-plan.ts';
|
|
6
8
|
import { setVerboseMode, tag } from '../../../src/utils/logger.ts';
|
|
@@ -8,8 +10,6 @@ import { Chief } from './ai/chief.ts';
|
|
|
8
10
|
import { Curler } from './ai/curler.ts';
|
|
9
11
|
import { ApiClient } from './api-client.ts';
|
|
10
12
|
import { type ApibotConfig, ApibotConfigParser } from './config.ts';
|
|
11
|
-
import { RequestStore } from '../../../src/api/request-store.ts';
|
|
12
|
-
import { extractEndpointDefinition, loadSpec, searchEndpoints, validateSpecs } from '../../../src/api/spec-reader.ts';
|
|
13
13
|
|
|
14
14
|
export class ApiBot {
|
|
15
15
|
private configParser: ApibotConfigParser;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
2
2
|
import path, { resolve } from 'node:path';
|
|
3
3
|
import { parseEnv } from 'node:util';
|
|
4
|
-
import {
|
|
4
|
+
import { type AIConfig, type ApiHookFn, type ApiConfig as BaseApiConfig, EXPLORBOT_CONFIG_PATHS } from '../../../src/config.ts';
|
|
5
5
|
|
|
6
6
|
export type { AIConfig };
|
|
7
7
|
|
|
@@ -17,14 +17,15 @@ import { getCliName } from "../src/utils/cli-name.js";
|
|
|
17
17
|
import { log, setPreserveConsoleLogs } from '../src/utils/logger.js';
|
|
18
18
|
import { jsonToTable } from '../src/utils/markdown-parser.js';
|
|
19
19
|
import { parseMarkdownToTerminal } from '../src/utils/markdown-terminal.js';
|
|
20
|
+
import { printNextSteps, relativeToCwd } from "../src/utils/next-steps.js";
|
|
20
21
|
const program = new Command();
|
|
21
22
|
const cli = getCliName();
|
|
22
23
|
const pkgPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../package.json');
|
|
23
24
|
const pkgVersion = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')).version;
|
|
24
25
|
program.name(cli).description('AI-powered web exploration tool').version(pkgVersion, '-V, --version');
|
|
25
|
-
|
|
26
|
-
console.log(chalk.
|
|
27
|
-
}
|
|
26
|
+
if (!process.env.EXPLORBOT_NO_BANNER) {
|
|
27
|
+
console.log(`⛵ ${chalk.yellow.bold(`Explorbot v${pkgVersion}`)} ${chalk.dim('Autonomous Testing Agent')}`);
|
|
28
|
+
}
|
|
28
29
|
function buildExplorBotOptions(from, options) {
|
|
29
30
|
return {
|
|
30
31
|
from,
|
|
@@ -155,19 +156,30 @@ addCommonOptions(program.command('plan <path>').description('Generate test plan
|
|
|
155
156
|
}
|
|
156
157
|
}
|
|
157
158
|
const savedPath = explorBot.savePlan();
|
|
158
|
-
const planFile = savedPath ? path.basename(savedPath) : 'plan.md';
|
|
159
159
|
const cliFlags = [options.path ? `--path ${options.path}` : '', options.session ? '--session' : ''].filter(Boolean).join(' ');
|
|
160
160
|
const cliSuffix = cliFlags ? ` ${cliFlags}` : '';
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
161
|
+
const sections = [];
|
|
162
|
+
if (savedPath) {
|
|
163
|
+
const relPlan = relativeToCwd(savedPath);
|
|
164
|
+
sections.push({
|
|
165
|
+
label: 'Plan',
|
|
166
|
+
path: savedPath,
|
|
167
|
+
commands: [
|
|
168
|
+
{ label: 'Re-run', command: `${cli} test ${relPlan} 1${cliSuffix}` },
|
|
169
|
+
{ label: 'Run all', command: `${cli} test ${relPlan} *${cliSuffix}` },
|
|
170
|
+
{ label: 'Run range', command: `${cli} test ${relPlan} 1-3${cliSuffix}` },
|
|
171
|
+
],
|
|
172
|
+
});
|
|
169
173
|
}
|
|
170
|
-
|
|
174
|
+
const automatedFiles = suite && suite.automatedTestCount > 0 ? suite.getAutomatedTestFiles() : [];
|
|
175
|
+
if (automatedFiles.length > 0) {
|
|
176
|
+
const commands = automatedFiles.map((f) => ({ label: '', command: `${cli} rerun ${relativeToCwd(f)}${cliSuffix}` }));
|
|
177
|
+
sections.push({
|
|
178
|
+
label: `Automated tests (${automatedFiles.length})`,
|
|
179
|
+
commands,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
printNextSteps(sections);
|
|
171
183
|
await explorBot.stop();
|
|
172
184
|
await showStatsAndExit(0);
|
|
173
185
|
}
|
|
@@ -336,7 +348,7 @@ program
|
|
|
336
348
|
});
|
|
337
349
|
program
|
|
338
350
|
.command('clean [target]')
|
|
339
|
-
.description('Clean files: states, research, plans, experiences, output (default: output + experiences)')
|
|
351
|
+
.description('Clean files: states, research, plans, tests, experiences, output (default: output + experiences)')
|
|
340
352
|
.option('-p, --path <path>', 'Custom path to clean')
|
|
341
353
|
.action(async (target, options) => {
|
|
342
354
|
const customPath = options.path;
|
|
@@ -346,6 +358,7 @@ program
|
|
|
346
358
|
states: { description: 'page states', dir: path.join(basePath, 'output', 'states') },
|
|
347
359
|
research: { description: 'research cache', dir: path.join(basePath, 'output', 'research') },
|
|
348
360
|
plans: { description: 'test plans', dir: path.join(basePath, 'output', 'plans') },
|
|
361
|
+
tests: { description: 'generated tests', dir: path.join(basePath, 'output', 'tests') },
|
|
349
362
|
experiences: { description: 'experience files', dir: path.join(basePath, 'experience') },
|
|
350
363
|
output: { description: 'all output files', dir: path.join(basePath, 'output') },
|
|
351
364
|
};
|
|
@@ -439,6 +452,64 @@ program
|
|
|
439
452
|
process.exit(1);
|
|
440
453
|
}
|
|
441
454
|
});
|
|
455
|
+
program
|
|
456
|
+
.command('compact [target]')
|
|
457
|
+
.description('Compact stored experience files; pass filename or URL substring to limit scope')
|
|
458
|
+
.option('--dry-run', 'Preview without running AI or writing files')
|
|
459
|
+
.option('--no-merge', 'Skip the cross-URL merge step when compacting all')
|
|
460
|
+
.option('-p, --path <path>', 'Working directory path')
|
|
461
|
+
.option('-c, --config <path>', 'Path to configuration file')
|
|
462
|
+
.option('-v, --verbose', 'Enable verbose logging')
|
|
463
|
+
.action(async (target, options) => {
|
|
464
|
+
try {
|
|
465
|
+
const explorBot = new ExplorBot({
|
|
466
|
+
path: options.path,
|
|
467
|
+
config: options.config,
|
|
468
|
+
verbose: options.verbose,
|
|
469
|
+
});
|
|
470
|
+
await explorBot.startProviderOnly();
|
|
471
|
+
const { CompactCommand } = await import('../src/commands/compact-command.js');
|
|
472
|
+
const argParts = [target, options.dryRun && '--dry-run', options.merge === false && '--no-merge'].filter(Boolean).join(' ');
|
|
473
|
+
const command = new CompactCommand(explorBot);
|
|
474
|
+
await command.execute(argParts);
|
|
475
|
+
command.printSuggestions();
|
|
476
|
+
await showStatsAndExit(0);
|
|
477
|
+
}
|
|
478
|
+
catch (error) {
|
|
479
|
+
console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
|
|
480
|
+
await showStatsAndExit(1);
|
|
481
|
+
}
|
|
482
|
+
});
|
|
483
|
+
program
|
|
484
|
+
.command('experience [filter] [index]')
|
|
485
|
+
.description('List stored experiences grouped by URL; pass URL substring to filter; pass tag (A.1) or index to expand a section')
|
|
486
|
+
.option('-p, --path <path>', 'Working directory path')
|
|
487
|
+
.option('-c, --config <path>', 'Path to configuration file')
|
|
488
|
+
.option('--recent', 'Only files modified within the last 30 days')
|
|
489
|
+
.option('--old', 'Only files modified more than 30 days ago')
|
|
490
|
+
.action(async (filter, index, options) => {
|
|
491
|
+
try {
|
|
492
|
+
await ConfigParser.getInstance().loadConfig({
|
|
493
|
+
config: options.config,
|
|
494
|
+
path: options.path || process.cwd(),
|
|
495
|
+
});
|
|
496
|
+
const { ExperienceCommand } = await import('../src/commands/experience-command.js');
|
|
497
|
+
const explorBot = new ExplorBot({ path: options.path });
|
|
498
|
+
const command = new ExperienceCommand(explorBot);
|
|
499
|
+
const flags = [];
|
|
500
|
+
if (options.recent)
|
|
501
|
+
flags.push('--recent');
|
|
502
|
+
if (options.old)
|
|
503
|
+
flags.push('--old');
|
|
504
|
+
const args = [...flags, filter, index].filter(Boolean).join(' ');
|
|
505
|
+
await command.execute(args);
|
|
506
|
+
command.printSuggestions();
|
|
507
|
+
}
|
|
508
|
+
catch (error) {
|
|
509
|
+
console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
|
|
510
|
+
process.exit(1);
|
|
511
|
+
}
|
|
512
|
+
});
|
|
442
513
|
addCommonOptions(program.command('research <url>').description('Research a page and print UI analysis').option('--data', 'Include data extraction in research').option('--deep', 'Enable deep analysis (expand hidden elements)').option('--no-fix', 'Skip locator fix cycle (for debugging)')).action(async (url, options) => {
|
|
443
514
|
try {
|
|
444
515
|
const explorBot = new ExplorBot(buildExplorBotOptions(url, options));
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { tool } from 'ai';
|
|
2
|
-
import { expect } from 'expect';
|
|
3
1
|
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { tool } from 'ai';
|
|
4
3
|
import dedent from 'dedent';
|
|
4
|
+
import { expect } from 'expect';
|
|
5
5
|
import { z } from 'zod';
|
|
6
6
|
import { TestResult } from "../../../../src/test-plan.js";
|
|
7
7
|
import { tag } from "../../../../src/utils/logger.js";
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { existsSync, mkdirSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { AIProvider } from "../../../src/ai/provider.js";
|
|
4
|
+
import { RequestStore } from "../../../src/api/request-store.js";
|
|
5
|
+
import { extractEndpointDefinition, loadSpec, searchEndpoints, validateSpecs } from "../../../src/api/spec-reader.js";
|
|
4
6
|
import { Reporter } from "../../../src/reporter.js";
|
|
5
7
|
import { Plan } from "../../../src/test-plan.js";
|
|
6
8
|
import { setVerboseMode, tag } from "../../../src/utils/logger.js";
|
|
@@ -8,8 +10,6 @@ import { Chief } from "./ai/chief.js";
|
|
|
8
10
|
import { Curler } from "./ai/curler.js";
|
|
9
11
|
import { ApiClient } from "./api-client.js";
|
|
10
12
|
import { ApibotConfigParser } from "./config.js";
|
|
11
|
-
import { RequestStore } from "../../../src/api/request-store.js";
|
|
12
|
-
import { extractEndpointDefinition, loadSpec, searchEndpoints, validateSpecs } from "../../../src/api/spec-reader.js";
|
|
13
13
|
export class ApiBot {
|
|
14
14
|
configParser;
|
|
15
15
|
provider;
|
package/dist/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "explorbot",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.11",
|
|
4
4
|
"description": "CLI app built with React Ink, CodeceptJS, and Playwright",
|
|
5
5
|
"license": "Elastic-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
"axe-core": "^4.11.1",
|
|
84
84
|
"bash-tool": "^1.3.15",
|
|
85
85
|
"cli-highlight": "^2.1.11",
|
|
86
|
-
"codeceptjs": "4.0.0-rc.
|
|
86
|
+
"codeceptjs": "4.0.0-rc.16",
|
|
87
87
|
"commander": "^14.0.1",
|
|
88
88
|
"debug": "^4.4.3",
|
|
89
89
|
"dedent": "^1.6.0",
|
|
@@ -13,6 +13,10 @@ In <explanation> write only one line without heading or bullet list or any other
|
|
|
13
13
|
Check previous solutions, if there is already successful solution, use it!
|
|
14
14
|
CodeceptJS code must start with "I."
|
|
15
15
|
All lines of code must be CodeceptJS code and start with "I."
|
|
16
|
+
|
|
17
|
+
Do not mix form filling with navigation in the same code block.
|
|
18
|
+
If the code block fills a form and clicks a submit/confirm control, stop there — do not append I.amOnPage afterwards. Submitting the form already triggers navigation on the server side, and a follow-up I.amOnPage cancels that in-flight navigation and discards the just-submitted state (session, cookies, redirect target).
|
|
19
|
+
If the action does not cause navigation on its own and a separate page visit is required to reach the target, put I.amOnPage in its own code block as a distinct step, not glued onto the form submission block.
|
|
16
20
|
</rules>
|
|
17
21
|
|
|
18
22
|
<output>
|
|
@@ -42,6 +46,11 @@ Use only locators from HTML PAGE that was passed in <page> context.
|
|
|
42
46
|
<example_output>
|
|
43
47
|
Trying to fill the form on the page
|
|
44
48
|
|
|
49
|
+
```js
|
|
50
|
+
I.fillField({ "role": "textbox", "text": "Name" }, 'Value');
|
|
51
|
+
I.click({ "role": "button", "text": "Submit" });
|
|
52
|
+
```
|
|
53
|
+
|
|
45
54
|
```js
|
|
46
55
|
I.fillField('Name', 'Value');
|
|
47
56
|
I.click('Submit');
|
|
@@ -113,6 +113,8 @@ For input field values, ALWAYS use I.seeInField() — never check value via CSS
|
|
|
113
113
|
Prefer text locators (label, name, placeholder) for form fields: I.seeInField('Search', 'value') over I.seeInField('input[name="search"]', 'value').
|
|
114
114
|
Only use locators that exist in the provided HTML or ARIA snapshot.
|
|
115
115
|
Verify exact conditions, not approximate matches.
|
|
116
|
+
NEVER use `:has-text(...)` inside a seeElement/dontSeeElement locator. Checking text inside an element is the job of I.see(text, context) — the `:has-text()` form duplicates that capability with a fragile selector.
|
|
117
|
+
NEVER emit two assertions that check the same fact with different shapes. `I.see(text, locator)` and `I.seeElement("<locator>:has-text('text')")` verify the same thing — pick one (prefer I.see). One claim, one assertion.
|
|
116
118
|
</verification_rules>
|
|
117
119
|
|
|
118
120
|
[DO NEVER USE OTHER CODECEPTJS COMMANDS THAN PROPOSED HERE]
|
|
@@ -502,7 +502,7 @@ export class ActionResult {
|
|
|
502
502
|
}
|
|
503
503
|
}
|
|
504
504
|
if (processedParts.length > 0) {
|
|
505
|
-
pageDiff.htmlParts = processedParts;
|
|
505
|
+
pageDiff.htmlParts = collapseHtmlParts(processedParts);
|
|
506
506
|
}
|
|
507
507
|
}
|
|
508
508
|
if (pageDiff.ariaChanges && this.iframeSnapshots.length > 0) {
|
|
@@ -517,6 +517,28 @@ export class ActionResult {
|
|
|
517
517
|
return result;
|
|
518
518
|
}
|
|
519
519
|
}
|
|
520
|
+
const HTML_PARTS_TOTAL_BUDGET = 8000;
|
|
521
|
+
const HTML_PARTS_COUNT_LIMIT = 8;
|
|
522
|
+
const HTML_PART_SUBTREE_BUDGET = 2000;
|
|
523
|
+
function collapseHtmlParts(parts) {
|
|
524
|
+
const total = parts.reduce((sum, p) => sum + p.subtree.length, 0);
|
|
525
|
+
const fullPageReRender = total > HTML_PARTS_TOTAL_BUDGET || parts.length > HTML_PARTS_COUNT_LIMIT;
|
|
526
|
+
if (fullPageReRender) {
|
|
527
|
+
return parts.map((part) => ({
|
|
528
|
+
...part,
|
|
529
|
+
subtree: `<html><head></head><body>...collapsed (${part.subtree.length} chars, ${part.added.length} added, ${part.removed.length} removed)...</body></html>`,
|
|
530
|
+
}));
|
|
531
|
+
}
|
|
532
|
+
return parts.map((part) => {
|
|
533
|
+
if (part.subtree.length <= HTML_PART_SUBTREE_BUDGET)
|
|
534
|
+
return part;
|
|
535
|
+
const head = part.subtree.slice(0, HTML_PART_SUBTREE_BUDGET);
|
|
536
|
+
return {
|
|
537
|
+
...part,
|
|
538
|
+
subtree: `${head}...<!-- truncated ${part.subtree.length - HTML_PART_SUBTREE_BUDGET} chars -->`,
|
|
539
|
+
};
|
|
540
|
+
});
|
|
541
|
+
}
|
|
520
542
|
export class Diff {
|
|
521
543
|
current;
|
|
522
544
|
previous;
|
package/dist/src/action.js
CHANGED
|
@@ -24,11 +24,15 @@ class Action {
|
|
|
24
24
|
expectation = null;
|
|
25
25
|
lastError = null;
|
|
26
26
|
playwrightHelper;
|
|
27
|
-
|
|
27
|
+
playwrightGroupId = null;
|
|
28
|
+
assertionSteps = [];
|
|
29
|
+
recorder;
|
|
30
|
+
constructor(actor, stateManager, recorder) {
|
|
28
31
|
this.actor = actor;
|
|
29
32
|
this.stateManager = stateManager;
|
|
30
33
|
this.config = ConfigParser.getInstance().getConfig();
|
|
31
34
|
this.playwrightHelper = container.helpers('Playwright');
|
|
35
|
+
this.recorder = recorder;
|
|
32
36
|
}
|
|
33
37
|
async caputrePageWithScreenshot() {
|
|
34
38
|
return this.capturePageState({ includeScreenshot: true });
|
|
@@ -57,7 +61,15 @@ class Action {
|
|
|
57
61
|
const timestamp = Date.now();
|
|
58
62
|
const page = this.playwrightHelper.page;
|
|
59
63
|
const frame = this.playwrightHelper.frame;
|
|
60
|
-
|
|
64
|
+
await page?.waitForLoadState('domcontentloaded', { timeout: 10000 })?.catch(() => { });
|
|
65
|
+
const grabAll = () => Promise.all([this.actor.grabSource(), this.actor.grabTitle(), this.captureBrowserLogs()]);
|
|
66
|
+
const [html, title, browserLogs] = await grabAll().catch(async (err) => {
|
|
67
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
68
|
+
if (!/navigating and changing the content/i.test(msg))
|
|
69
|
+
throw err;
|
|
70
|
+
await page?.waitForLoadState('domcontentloaded', { timeout: 10000 })?.catch(() => { });
|
|
71
|
+
return grabAll();
|
|
72
|
+
});
|
|
61
73
|
const url = page?.url() || (await this.actor.grabCurrentUrl?.());
|
|
62
74
|
let screenshotFile = undefined;
|
|
63
75
|
if (includeScreenshot) {
|
|
@@ -183,7 +195,10 @@ class Action {
|
|
|
183
195
|
setActivity('🔎 Browsing...', 'action');
|
|
184
196
|
let codeString = code.replace(/^\(I\) => /, '').trim();
|
|
185
197
|
const executedSteps = [];
|
|
186
|
-
|
|
198
|
+
const assertionSteps = [];
|
|
199
|
+
const stepListener = attachStepLogger(executedSteps, assertionSteps);
|
|
200
|
+
const groupId = this.recorder ? await this.recorder.beginAction(codeString) : null;
|
|
201
|
+
this.playwrightGroupId = groupId;
|
|
187
202
|
const activeSpan = Observability.getSpan();
|
|
188
203
|
const tracer = trace.getTracer('ai');
|
|
189
204
|
const stepSpan = activeSpan ? tracer.startSpan('codeceptjs.step', undefined, trace.setSpan(context.active(), activeSpan)) : null;
|
|
@@ -213,6 +228,7 @@ class Action {
|
|
|
213
228
|
}
|
|
214
229
|
this.stateManager.updateState(pageState, codeString);
|
|
215
230
|
this.actionResult = pageState;
|
|
231
|
+
this.assertionSteps = assertionSteps;
|
|
216
232
|
}
|
|
217
233
|
catch (err) {
|
|
218
234
|
debugLog('Action error', errorToString(err));
|
|
@@ -221,10 +237,13 @@ class Action {
|
|
|
221
237
|
await recorder.reset();
|
|
222
238
|
await recorder.start();
|
|
223
239
|
}
|
|
240
|
+
this.assertionSteps = [];
|
|
224
241
|
throw err;
|
|
225
242
|
}
|
|
226
243
|
finally {
|
|
227
|
-
|
|
244
|
+
if (groupId)
|
|
245
|
+
await this.recorder.endAction();
|
|
246
|
+
detachStepLogger(stepListener);
|
|
228
247
|
if (stepSpan) {
|
|
229
248
|
stepSpan.end();
|
|
230
249
|
}
|
|
@@ -350,39 +369,28 @@ function hasPlaywrightCommands(code) {
|
|
|
350
369
|
function sleep(ms) {
|
|
351
370
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
352
371
|
}
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
const
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
if (stepLoggerRegistered) {
|
|
374
|
-
return;
|
|
375
|
-
}
|
|
376
|
-
stepLoggerRegistered = true;
|
|
377
|
-
codeceptjs.event.dispatcher.on(codeceptjs.event.step.passed, stepLogger);
|
|
378
|
-
codeceptjs.event.dispatcher.on(codeceptjs.event.step.failed, stepLogger);
|
|
372
|
+
const ASSERTION_STEP_NAMES = new Set(['see', 'dontSee', 'seeElement', 'dontSeeElement', 'seeInField', 'dontSeeInField', 'seeInCurrentUrl', 'dontSeeInCurrentUrl']);
|
|
373
|
+
const attachStepLogger = (target, assertionsTarget) => {
|
|
374
|
+
const listener = (step, error) => {
|
|
375
|
+
if (!step?.toCode)
|
|
376
|
+
return;
|
|
377
|
+
if (step.name?.startsWith('grab'))
|
|
378
|
+
return;
|
|
379
|
+
target.push(step.toCode());
|
|
380
|
+
if (assertionsTarget && ASSERTION_STEP_NAMES.has(step.name)) {
|
|
381
|
+
assertionsTarget.push({ name: step.name, args: step.args || [] });
|
|
382
|
+
}
|
|
383
|
+
if (error) {
|
|
384
|
+
tag('step').log(step, error);
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
tag('step').log(step);
|
|
388
|
+
};
|
|
389
|
+
codeceptjs.event.dispatcher.on(codeceptjs.event.step.passed, listener);
|
|
390
|
+
codeceptjs.event.dispatcher.on(codeceptjs.event.step.failed, listener);
|
|
391
|
+
return listener;
|
|
379
392
|
};
|
|
380
|
-
const
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
return;
|
|
384
|
-
}
|
|
385
|
-
stepLoggerRegistered = false;
|
|
386
|
-
codeceptjs.event.dispatcher.off(codeceptjs.event.step.passed, stepLogger);
|
|
387
|
-
codeceptjs.event.dispatcher.off(codeceptjs.event.step.failed, stepLogger);
|
|
393
|
+
const detachStepLogger = (listener) => {
|
|
394
|
+
codeceptjs.event.dispatcher.off(codeceptjs.event.step.passed, listener);
|
|
395
|
+
codeceptjs.event.dispatcher.off(codeceptjs.event.step.failed, listener);
|
|
388
396
|
};
|