explorbot 0.5.10 → 0.6.0
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 +494 -482
- package/boat/api-tester/src/ai/curler.ts +1 -1
- package/boat/api-tester/src/cli.ts +4 -4
- package/boat/doc-collector/src/screenshots.ts +1 -1
- package/dist/bin/explorbot-cli.js +455 -444
- package/dist/boat/api-tester/src/ai/curler.js +1 -1
- package/dist/boat/api-tester/src/cli.js +4 -4
- package/dist/models.json +1 -1
- package/dist/package.json +2 -1
- package/dist/rules/navigator/output.md +2 -8
- package/dist/rules/navigator/verification-actions.md +6 -2
- package/dist/rules/planner/ignored-pages.md +1 -0
- package/dist/src/action-result.js +1 -1
- package/dist/src/action.d.ts +0 -1
- package/dist/src/action.js +0 -4
- package/dist/src/ai/captain/idle-mode.js +42 -26
- package/dist/src/ai/captain/mixin.d.ts +2 -0
- package/dist/src/ai/captain/web-mode.js +11 -0
- package/dist/src/ai/captain.d.ts +6 -4
- package/dist/src/ai/captain.js +25 -1
- package/dist/src/ai/driller.js +1 -1
- package/dist/src/ai/experience-compactor.js +2 -2
- package/dist/src/ai/judge.d.ts +7 -0
- package/dist/src/ai/judge.js +8 -1
- package/dist/src/ai/navigator.d.ts +1 -0
- package/dist/src/ai/navigator.js +11 -7
- package/dist/src/ai/planner/subpages.js +1 -1
- package/dist/src/ai/planner.d.ts +12 -5
- package/dist/src/ai/planner.js +138 -86
- package/dist/src/ai/provider.d.ts +1 -1
- package/dist/src/ai/provider.js +16 -5
- package/dist/src/ai/rerunner.js +1 -1
- package/dist/src/ai/researcher/cache.js +14 -6
- package/dist/src/ai/researcher/deep-analysis.d.ts +2 -1
- package/dist/src/ai/researcher/deep-analysis.js +7 -2
- package/dist/src/ai/researcher/fingerprint-worker.js +4 -4
- package/dist/src/ai/researcher.js +44 -24
- package/dist/src/ai/rules.js +6 -7
- package/dist/src/ai/scout.d.ts +12 -2
- package/dist/src/ai/scout.js +103 -8
- package/dist/src/ai/tester.d.ts +9 -5
- package/dist/src/ai/tester.js +22 -42
- package/dist/src/ai/tools.js +10 -8
- package/dist/src/application-spec.d.ts +2 -0
- package/dist/src/application-spec.js +17 -12
- package/dist/src/commands/context-experience-command.js +0 -1
- package/dist/src/commands/context-knowledge-command.js +0 -1
- package/dist/src/commands/explore-command.d.ts +3 -2
- package/dist/src/commands/explore-command.js +35 -23
- package/dist/src/commands/help-json-command.js +3 -1
- package/dist/src/commands/init-command.js +4 -4
- package/dist/src/commands/learn-command.js +0 -2
- package/dist/src/commands/plan-command.d.ts +3 -0
- package/dist/src/commands/plan-command.js +43 -17
- package/dist/src/commands/plan-save-command.js +6 -17
- package/dist/src/commands/plans-command.js +1 -1
- package/dist/src/commands/test-command.js +12 -2
- package/dist/src/components/App.d.ts +1 -2
- package/dist/src/components/App.js +1 -3
- package/dist/src/components/InputPane.js +0 -5
- package/dist/src/config.d.ts +3 -2
- package/dist/src/config.js +2 -12
- package/dist/src/explorbot.d.ts +3 -2
- package/dist/src/explorbot.js +32 -10
- package/dist/src/explorer.js +6 -7
- package/dist/src/knowledge-tracker.d.ts +2 -2
- package/dist/src/knowledge-tracker.js +18 -6
- package/dist/src/playwright-recorder.d.ts +0 -1
- package/dist/src/playwright-recorder.js +0 -2
- package/dist/src/reporter.d.ts +1 -1
- package/dist/src/reporter.js +3 -3
- package/dist/src/test-plan.js +1 -1
- package/dist/src/utils/error-page.d.ts +17 -0
- package/dist/src/utils/error-page.js +53 -2
- package/dist/src/utils/html-diff.d.ts +1 -2
- package/dist/src/utils/html-diff.js +4 -44
- package/dist/src/utils/logger.js +27 -10
- package/dist/src/utils/markdown-terminal.js +1 -2
- package/dist/src/utils/next-steps.js +20 -16
- package/dist/src/utils/page-readiness.d.ts +2 -1
- package/dist/src/utils/page-readiness.js +24 -5
- package/dist/src/utils/test-plan-markdown.d.ts +3 -0
- package/dist/src/utils/test-plan-markdown.js +35 -4
- package/dist/src/utils/url-matcher.d.ts +1 -0
- package/dist/src/utils/url-matcher.js +5 -0
- package/docs/basics/providers.md +1 -1
- package/docs/contributing/regression-tests.md +19 -4
- package/docs/reference/commands.md +6 -2
- package/docs/reference/configuration.md +1 -1
- package/docs/web-testing/agents.md +8 -0
- package/docs/web-testing/customization.md +2 -0
- package/docs/web-testing/researcher.md +1 -1
- package/models.json +1 -1
- package/package.json +2 -1
- package/rules/navigator/output.md +2 -8
- package/rules/navigator/verification-actions.md +6 -2
- package/rules/planner/ignored-pages.md +1 -0
- package/src/action-result.ts +1 -2
- package/src/action.ts +0 -5
- package/src/ai/captain/idle-mode.ts +38 -26
- package/src/ai/captain/mixin.ts +2 -1
- package/src/ai/captain/web-mode.ts +12 -0
- package/src/ai/captain.ts +25 -7
- package/src/ai/driller.ts +1 -2
- package/src/ai/experience-compactor.ts +2 -2
- package/src/ai/judge.ts +10 -1
- package/src/ai/navigator.ts +12 -7
- package/src/ai/planner/subpages.ts +1 -2
- package/src/ai/planner.ts +140 -92
- package/src/ai/provider.ts +15 -6
- package/src/ai/rerunner.ts +1 -2
- package/src/ai/researcher/cache.ts +16 -7
- package/src/ai/researcher/deep-analysis.ts +7 -2
- package/src/ai/researcher/fingerprint-worker.ts +5 -5
- package/src/ai/researcher.ts +44 -28
- package/src/ai/rules.ts +6 -7
- package/src/ai/scout.ts +98 -10
- package/src/ai/tester.ts +29 -47
- package/src/ai/tools.ts +11 -9
- package/src/application-spec.ts +19 -13
- package/src/commands/clean-command.ts +1 -1
- package/src/commands/context-command.ts +0 -1
- package/src/commands/context-experience-command.ts +0 -1
- package/src/commands/context-knowledge-command.ts +0 -1
- package/src/commands/explore-command.ts +36 -23
- package/src/commands/help-json-command.ts +8 -6
- package/src/commands/init-command.ts +5 -3
- package/src/commands/learn-command.ts +0 -2
- package/src/commands/plan-command.ts +44 -17
- package/src/commands/plan-save-command.ts +6 -17
- package/src/commands/plans-command.ts +1 -1
- package/src/commands/test-command.ts +11 -2
- package/src/components/App.tsx +2 -6
- package/src/components/InputPane.tsx +0 -5
- package/src/config.ts +4 -16
- package/src/experience-tracker.ts +1 -1
- package/src/explorbot.ts +31 -11
- package/src/explorer.ts +6 -7
- package/src/knowledge-tracker.ts +18 -7
- package/src/playwright-recorder.ts +0 -2
- package/src/reporter.ts +3 -4
- package/src/test-plan.ts +1 -2
- package/src/utils/error-page.ts +64 -2
- package/src/utils/html-diff.ts +4 -56
- package/src/utils/html.ts +0 -1
- package/src/utils/logger.ts +30 -12
- package/src/utils/markdown-terminal.ts +1 -2
- package/src/utils/next-steps.ts +19 -17
- package/src/utils/page-readiness.ts +33 -7
- package/src/utils/test-plan-markdown.ts +36 -4
- package/src/utils/url-matcher.ts +6 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
1
2
|
import path from 'node:path';
|
|
2
3
|
import chalk from 'chalk';
|
|
3
4
|
import figureSet from 'figures';
|
|
@@ -17,8 +18,12 @@ export class PlanCommand extends BaseCommand {
|
|
|
17
18
|
options = [
|
|
18
19
|
{ flags: '--fresh', description: 'Regenerate plan from scratch' },
|
|
19
20
|
{ flags: '--clear', description: 'Clear plan before regenerating' },
|
|
21
|
+
{ flags: '--append', description: 'Add tests to existing plan file' },
|
|
20
22
|
{ flags: '--style <name>', description: 'Planning style (normal, curious, psycho, performer)' },
|
|
21
23
|
{ flags: '--focus <feature>', description: 'Focus area for test planning' },
|
|
24
|
+
{ flags: '--tests <count>', description: 'Exact number of tests to generate' },
|
|
25
|
+
{ flags: '--min-tests <count>', description: 'Minimum number of tests to generate' },
|
|
26
|
+
{ flags: '--max-tests <count>', description: 'Maximum number of tests to generate' },
|
|
22
27
|
];
|
|
23
28
|
|
|
24
29
|
async execute(args: string): Promise<void> {
|
|
@@ -30,11 +35,20 @@ export class PlanCommand extends BaseCommand {
|
|
|
30
35
|
tag('success').log('Plan cleared');
|
|
31
36
|
}
|
|
32
37
|
|
|
38
|
+
if (opts.append) {
|
|
39
|
+
const existingPlanPath = path.join(this.explorBot.getPlansDir(), this.explorBot.generatePlanFilename());
|
|
40
|
+
if (existsSync(existingPlanPath)) this.explorBot.loadPlan(existingPlanPath);
|
|
41
|
+
}
|
|
42
|
+
|
|
33
43
|
if (focus) {
|
|
34
44
|
tag('info').log(`Planning focus: ${focus}`);
|
|
35
45
|
}
|
|
36
46
|
|
|
37
|
-
|
|
47
|
+
const testsCount = opts.tests ? Number.parseInt(opts.tests as string, 10) : undefined;
|
|
48
|
+
const minTests = testsCount ?? (opts.minTests ? Number.parseInt(opts.minTests as string, 10) : undefined);
|
|
49
|
+
const maxTests = testsCount ?? (opts.maxTests ? Number.parseInt(opts.maxTests as string, 10) : undefined);
|
|
50
|
+
|
|
51
|
+
await this.explorBot.plan(focus, { fresh: !!(opts.fresh || opts.clear), style: opts.style as string, minTests, maxTests });
|
|
38
52
|
|
|
39
53
|
const plan = this.explorBot.getCurrentPlan();
|
|
40
54
|
if (!plan?.tests.length) {
|
|
@@ -46,29 +60,34 @@ export class PlanCommand extends BaseCommand {
|
|
|
46
60
|
this.printNextSteps();
|
|
47
61
|
}
|
|
48
62
|
|
|
63
|
+
static planSection(planPath: string, cliSuffix = ''): NextStepSection {
|
|
64
|
+
const cli = getCliName();
|
|
65
|
+
const planName = path.basename(planPath, '.md');
|
|
66
|
+
return {
|
|
67
|
+
label: 'Plan',
|
|
68
|
+
path: planPath,
|
|
69
|
+
commands: [
|
|
70
|
+
{ label: 'Run all tests', command: `${cli} test ${planName}${cliSuffix}` },
|
|
71
|
+
{ label: 'Run one test', command: `${cli} test ${planName} 1${cliSuffix}` },
|
|
72
|
+
{ label: 'Run a range', command: `${cli} test ${planName} 1-3${cliSuffix}` },
|
|
73
|
+
],
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
49
77
|
private printNextSteps(): void {
|
|
50
78
|
const savedPath = this.explorBot.lastSavedPlanPath;
|
|
51
79
|
if (!savedPath) return;
|
|
52
80
|
|
|
53
81
|
const cli = getCliName();
|
|
54
|
-
const
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
path: savedPath,
|
|
59
|
-
commands: [
|
|
60
|
-
{ label: 'Re-run', command: `${cli} test ${relPlan} 1` },
|
|
61
|
-
{ label: 'Run all', command: `${cli} test ${relPlan} *` },
|
|
62
|
-
{ label: 'Run range', command: `${cli} test ${relPlan} 1-3` },
|
|
63
|
-
{ label: 'Reload', command: `/plan:load ${relPlan}` },
|
|
64
|
-
],
|
|
65
|
-
},
|
|
66
|
-
];
|
|
82
|
+
const cliSuffix = this.cliSuffix();
|
|
83
|
+
const section = PlanCommand.planSection(savedPath, cliSuffix);
|
|
84
|
+
section.commands?.push({ label: 'Reload', command: `/plan:load ${path.basename(savedPath, '.md')}` });
|
|
85
|
+
const sections: NextStepSection[] = [section];
|
|
67
86
|
|
|
68
87
|
const suite = this.explorBot.getSuite();
|
|
69
88
|
const files = suite && suite.automatedTestCount > 0 ? suite.getAutomatedTestFiles() : [];
|
|
70
89
|
if (files.length > 0) {
|
|
71
|
-
const commands = files.map((f) => ({ label: '', command: `${cli} rerun ${relativeToCwd(f)}` }));
|
|
90
|
+
const commands = files.map((f) => ({ label: '', command: `${cli} rerun ${relativeToCwd(f)}${cliSuffix}` }));
|
|
72
91
|
sections.push({
|
|
73
92
|
label: `Automated tests (${files.length})`,
|
|
74
93
|
commands,
|
|
@@ -78,6 +97,14 @@ export class PlanCommand extends BaseCommand {
|
|
|
78
97
|
printNextSteps(sections);
|
|
79
98
|
}
|
|
80
99
|
|
|
100
|
+
private cliSuffix(): string {
|
|
101
|
+
const options = this.explorBot.getOptions();
|
|
102
|
+
let suffix = '';
|
|
103
|
+
if (options.path) suffix += ` --path ${options.path}`;
|
|
104
|
+
if (options.session) suffix += ' --session';
|
|
105
|
+
return suffix;
|
|
106
|
+
}
|
|
107
|
+
|
|
81
108
|
private printPlanSummary(): void {
|
|
82
109
|
const suite = this.explorBot.getSuite();
|
|
83
110
|
const plan = this.explorBot.getCurrentPlan();
|
|
@@ -91,10 +118,10 @@ export class PlanCommand extends BaseCommand {
|
|
|
91
118
|
}
|
|
92
119
|
|
|
93
120
|
if (plan?.tests.length) {
|
|
94
|
-
console.log(`\n${chalk.bold.cyan(`New test scenarios (${plan.tests.length})`)}`);
|
|
121
|
+
console.log(`\n${chalk.bold.cyan(`New test scenarios (${plan.tests.length})`)}\n`);
|
|
95
122
|
for (let i = 0; i < plan.tests.length; i++) {
|
|
96
123
|
const t = plan.tests[i];
|
|
97
|
-
console.log(` ${chalk.
|
|
124
|
+
console.log(` ${chalk.cyan(`${i + 1}.`)} ${t.scenario}`);
|
|
98
125
|
}
|
|
99
126
|
}
|
|
100
127
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { printNextSteps } from '../utils/next-steps.ts';
|
|
3
|
+
import { PlanCommand } from './plan-command.js';
|
|
3
4
|
import { BaseCommand, type Suggestion } from './base-command.js';
|
|
4
5
|
|
|
5
6
|
export class PlanSaveCommand extends BaseCommand {
|
|
@@ -17,20 +18,8 @@ export class PlanSaveCommand extends BaseCommand {
|
|
|
17
18
|
const savedPath = this.explorBot.savePlan(filename);
|
|
18
19
|
if (!savedPath) return;
|
|
19
20
|
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
{
|
|
24
|
-
label: 'Plan',
|
|
25
|
-
path: savedPath,
|
|
26
|
-
commands: [
|
|
27
|
-
{ label: 'Re-run', command: `${cli} test ${relPlan} 1` },
|
|
28
|
-
{ label: 'Run all', command: `${cli} test ${relPlan} *` },
|
|
29
|
-
{ label: 'Run range', command: `${cli} test ${relPlan} 1-3` },
|
|
30
|
-
{ label: 'Reload', command: `/plan:load ${relPlan}` },
|
|
31
|
-
],
|
|
32
|
-
},
|
|
33
|
-
];
|
|
34
|
-
printNextSteps(sections);
|
|
21
|
+
const section = PlanCommand.planSection(savedPath);
|
|
22
|
+
section.commands?.push({ label: 'Reload', command: `/plan:load ${path.basename(savedPath, '.md')}` });
|
|
23
|
+
printNextSteps([section]);
|
|
35
24
|
}
|
|
36
25
|
}
|
|
@@ -52,7 +52,7 @@ export class PlansCommand extends BaseCommand {
|
|
|
52
52
|
}
|
|
53
53
|
tag('info').log('');
|
|
54
54
|
tag('info').log('Run test from this plan as:');
|
|
55
|
-
tag('info').log(`${getCliName()} test
|
|
55
|
+
tag('info').log(`${getCliName()} test ${file.name} 1`);
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
private resolvePlanFile(target: string, files: PlanFile[]): { plan: Plan; file: PlanFile } {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Stats } from '../stats.js';
|
|
2
2
|
import { Plan, Test } from '../test-plan.js';
|
|
3
|
+
import { BlockedError } from '../utils/error-page.ts';
|
|
3
4
|
import { tag } from '../utils/logger.js';
|
|
4
5
|
import { type ArgumentCompletion, BaseCommand, type Suggestion } from './base-command.js';
|
|
5
6
|
|
|
@@ -18,6 +19,11 @@ export class TestCommand extends BaseCommand {
|
|
|
18
19
|
this.explorBot.loadPlan(String(opts.fromPlan));
|
|
19
20
|
}
|
|
20
21
|
|
|
22
|
+
if (!opts.fromPlan && remaining[0] && Plan.resolveFile(remaining[0], this.explorBot.getPlansDir())) {
|
|
23
|
+
this.explorBot.loadPlan(remaining.shift()!);
|
|
24
|
+
if (remaining.length === 0) remaining.push('*');
|
|
25
|
+
}
|
|
26
|
+
|
|
21
27
|
const plan = this.explorBot.getCurrentPlan();
|
|
22
28
|
const selector = remaining.join(' ');
|
|
23
29
|
Stats.mode = 'test';
|
|
@@ -71,13 +77,16 @@ export class TestCommand extends BaseCommand {
|
|
|
71
77
|
const tester = this.explorBot.agentTester();
|
|
72
78
|
for (const [index, test] of toExecute.entries()) {
|
|
73
79
|
tag('info').log(`Starting test ${index + 1}/${toExecute.length}: ${test.scenario}`);
|
|
74
|
-
await tester.test(test)
|
|
80
|
+
await tester.test(test).catch((err) => {
|
|
81
|
+
if (!(err instanceof BlockedError)) throw err;
|
|
82
|
+
tag('warning').log(`Skipping test: ${err.message}`);
|
|
83
|
+
});
|
|
75
84
|
}
|
|
76
85
|
tag('success').log('Test execution finished');
|
|
77
86
|
}
|
|
78
87
|
|
|
79
88
|
completeArguments(): ArgumentCompletion[] {
|
|
80
|
-
return Plan.listFiles(this.explorBot.getPlansDir()).map((file) => ({ value:
|
|
89
|
+
return Plan.listFiles(this.explorBot.getPlansDir()).map((file) => ({ value: file.name, display: file.label }));
|
|
81
90
|
}
|
|
82
91
|
}
|
|
83
92
|
|
package/src/components/App.tsx
CHANGED
|
@@ -3,14 +3,13 @@ import React, { useCallback, useEffect, useRef, useState } from 'react';
|
|
|
3
3
|
import { CommandHandler } from '../command-handler.js';
|
|
4
4
|
import type { ExplorbotConfig } from '../config.js';
|
|
5
5
|
import { executionController } from '../execution-controller.ts';
|
|
6
|
-
import type { ExplorBot
|
|
6
|
+
import type { ExplorBot } from '../explorbot.ts';
|
|
7
7
|
import type { KnowledgeTracker } from '../knowledge-tracker.js';
|
|
8
8
|
import type { StateTransition, WebPageState } from '../state-manager.js';
|
|
9
9
|
import { Test } from '../test-plan.ts';
|
|
10
10
|
import { tag } from '../utils/logger.js';
|
|
11
11
|
import ActivityPane from './ActivityPane.js';
|
|
12
12
|
import Autocomplete from './Autocomplete.js';
|
|
13
|
-
import InputPane from './InputPane.js';
|
|
14
13
|
import InputReadline from './InputReadline.js';
|
|
15
14
|
import LogPane from './LogPane.js';
|
|
16
15
|
import PlanEditor from './PlanEditor.js';
|
|
@@ -23,17 +22,15 @@ import WelcomeCommands from './WelcomeCommands.js';
|
|
|
23
22
|
interface AppProps {
|
|
24
23
|
explorBot: ExplorBot;
|
|
25
24
|
initialShowInput?: boolean;
|
|
26
|
-
exitOnEmptyInput?: boolean;
|
|
27
25
|
}
|
|
28
26
|
|
|
29
|
-
export function App({ explorBot, initialShowInput = false
|
|
27
|
+
export function App({ explorBot, initialShowInput = false }: AppProps) {
|
|
30
28
|
const sessionStartedAtRef = useRef<number>(Date.now());
|
|
31
29
|
const InputComponent = InputReadline;
|
|
32
30
|
const [showSessionTimer, setShowSessionTimer] = useState(false);
|
|
33
31
|
const [showInput, setShowInput] = useState(initialShowInput);
|
|
34
32
|
const [interruptPrompt, setInterruptPrompt] = useState<string | null>(null);
|
|
35
33
|
const [currentState, setCurrentState] = useState<WebPageState | null>(null);
|
|
36
|
-
const [lastTransition, setLastTransition] = useState<StateTransition | null>(null);
|
|
37
34
|
const [tasks, setTasks] = useState<Test[]>([]);
|
|
38
35
|
const tasksRef = useRef<Test[]>([]);
|
|
39
36
|
const [showPlanEditor, setShowPlanEditor] = useState(false);
|
|
@@ -72,7 +69,6 @@ export function App({ explorBot, initialShowInput = false, exitOnEmptyInput = fa
|
|
|
72
69
|
|
|
73
70
|
unsubscribe = manager.onStateChange((transition: StateTransition) => {
|
|
74
71
|
if (mounted) {
|
|
75
|
-
setLastTransition(transition);
|
|
76
72
|
setCurrentState(transition.toState);
|
|
77
73
|
}
|
|
78
74
|
});
|
|
@@ -19,7 +19,6 @@ const InputPane: React.FC<InputPaneProps> = ({ commandHandler, exitOnEmptyInput
|
|
|
19
19
|
const [cursorPosition, setCursorPosition] = useState(0);
|
|
20
20
|
const [showAutocomplete, setShowAutocomplete] = useState(false);
|
|
21
21
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
22
|
-
const [autoCompleteTriggered, setAutoCompleteTriggered] = useState(false);
|
|
23
22
|
const inputRef = useRef(inputValue);
|
|
24
23
|
const cursorRef = useRef(cursorPosition);
|
|
25
24
|
|
|
@@ -166,7 +165,6 @@ const InputPane: React.FC<InputPaneProps> = ({ commandHandler, exitOnEmptyInput
|
|
|
166
165
|
setShowAutocomplete(false);
|
|
167
166
|
setSelectedIndex(0);
|
|
168
167
|
setCursorPosition(selectedCommand.length);
|
|
169
|
-
setAutoCompleteTriggered(true);
|
|
170
168
|
}
|
|
171
169
|
return;
|
|
172
170
|
}
|
|
@@ -182,7 +180,6 @@ const InputPane: React.FC<InputPaneProps> = ({ commandHandler, exitOnEmptyInput
|
|
|
182
180
|
setInputValue(newValue);
|
|
183
181
|
setCursorPosition(nextCursor);
|
|
184
182
|
setSelectedIndex(0);
|
|
185
|
-
setAutoCompleteTriggered(false);
|
|
186
183
|
setShowAutocomplete(shouldShowAutocomplete(newValue));
|
|
187
184
|
}
|
|
188
185
|
return;
|
|
@@ -198,7 +195,6 @@ const InputPane: React.FC<InputPaneProps> = ({ commandHandler, exitOnEmptyInput
|
|
|
198
195
|
setInputValue(newValue);
|
|
199
196
|
setCursorPosition(nextCursor);
|
|
200
197
|
setSelectedIndex(0);
|
|
201
|
-
setAutoCompleteTriggered(false);
|
|
202
198
|
setShowAutocomplete(shouldShowAutocomplete(newValue));
|
|
203
199
|
}
|
|
204
200
|
},
|
|
@@ -247,7 +243,6 @@ const InputPane: React.FC<InputPaneProps> = ({ commandHandler, exitOnEmptyInput
|
|
|
247
243
|
setShowAutocomplete(false);
|
|
248
244
|
setSelectedIndex(0);
|
|
249
245
|
setCursorPosition(selectedCommand.length);
|
|
250
|
-
setAutoCompleteTriggered(true);
|
|
251
246
|
}
|
|
252
247
|
}}
|
|
253
248
|
visible={showAutocomplete}
|
package/src/config.ts
CHANGED
|
@@ -132,6 +132,8 @@ interface PlannerAgentConfig extends AgentConfig {
|
|
|
132
132
|
styles?: string[];
|
|
133
133
|
stylesDir?: string;
|
|
134
134
|
docsWeight?: number;
|
|
135
|
+
minTests?: number;
|
|
136
|
+
maxTests?: number;
|
|
135
137
|
}
|
|
136
138
|
|
|
137
139
|
interface ScoutAgentConfig extends AgentConfig {
|
|
@@ -245,7 +247,7 @@ interface ExplorbotConfig {
|
|
|
245
247
|
knowledge: string;
|
|
246
248
|
experience: string;
|
|
247
249
|
output: string;
|
|
248
|
-
spec?: string;
|
|
250
|
+
spec?: string | string[];
|
|
249
251
|
};
|
|
250
252
|
experience?: {
|
|
251
253
|
maxReadLines?: number;
|
|
@@ -258,17 +260,6 @@ interface ExplorbotConfig {
|
|
|
258
260
|
dynamicPageRegex?: string;
|
|
259
261
|
}
|
|
260
262
|
|
|
261
|
-
const config: ExplorbotConfig = {
|
|
262
|
-
playwright: {
|
|
263
|
-
browser: 'chromium',
|
|
264
|
-
url: 'http://localhost:3000',
|
|
265
|
-
},
|
|
266
|
-
|
|
267
|
-
ai: {
|
|
268
|
-
model: null as any,
|
|
269
|
-
},
|
|
270
|
-
};
|
|
271
|
-
|
|
272
263
|
type RuleEntry = string | Record<string, string>;
|
|
273
264
|
|
|
274
265
|
export { EXPLORBOT_CONFIG_PATHS };
|
|
@@ -763,10 +754,6 @@ export class ConfigParser {
|
|
|
763
754
|
}
|
|
764
755
|
}
|
|
765
756
|
|
|
766
|
-
private getNestedValue(obj: any, path: string): any {
|
|
767
|
-
return path.split('.').reduce((current, key) => current?.[key], obj);
|
|
768
|
-
}
|
|
769
|
-
|
|
770
757
|
public mergeWithDefaults(config: Partial<ExplorbotConfig>): ExplorbotConfig {
|
|
771
758
|
const defaults = {
|
|
772
759
|
playwright: {
|
|
@@ -879,6 +866,7 @@ export function configuredModels(ai?: AIConfig): Record<string, ConfiguredModel>
|
|
|
879
866
|
const models: Record<string, ConfiguredModel> = { model: describe(ai.model) };
|
|
880
867
|
if (ai.agenticModel) models.agenticModel = describe(ai.agenticModel);
|
|
881
868
|
if (ai.visionModel) models.visionModel = describe(ai.visionModel);
|
|
869
|
+
if (ai.decisionModel) models.decisionModel = { name: ai.decisionModel.model, provider: ai.decisionModel.provider };
|
|
882
870
|
for (const [agent, agentConfig] of Object.entries(ai.agents || {})) {
|
|
883
871
|
if (agentConfig?.model) models[agent] = describe(agentConfig.model);
|
|
884
872
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync,
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { basename, join } from 'node:path';
|
|
3
3
|
import matter from 'gray-matter';
|
|
4
4
|
import type { ActionResult } from './action-result.js';
|
package/src/explorbot.ts
CHANGED
|
@@ -55,7 +55,7 @@ export interface ExplorBotOptions {
|
|
|
55
55
|
optionalAi?: boolean;
|
|
56
56
|
reporter?: ReporterConfig;
|
|
57
57
|
attachedBrowser?: Browser;
|
|
58
|
-
applicationSpec?: string;
|
|
58
|
+
applicationSpec?: string | string[];
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
export type UserResolveFunction = (error?: Error, showWelcome?: boolean) => Promise<string | null>;
|
|
@@ -156,7 +156,13 @@ export class ExplorBot {
|
|
|
156
156
|
}
|
|
157
157
|
|
|
158
158
|
async visit(url: string): Promise<void> {
|
|
159
|
-
|
|
159
|
+
const target = this.configParser.resolveTargetPath(url);
|
|
160
|
+
await this.agentNavigator()
|
|
161
|
+
.visit(target)
|
|
162
|
+
.catch(async (err) => {
|
|
163
|
+
if (await this.agentCaptain().resolveBlocker(err, () => this.agentNavigator().isOn(target))) return;
|
|
164
|
+
throw err;
|
|
165
|
+
});
|
|
160
166
|
}
|
|
161
167
|
|
|
162
168
|
async openTab(): Promise<void> {
|
|
@@ -274,7 +280,7 @@ export class ExplorBot {
|
|
|
274
280
|
}
|
|
275
281
|
if (this.isHistorianEnabled()) this.agents.tester.setHistorian(this.agentHistorian());
|
|
276
282
|
this.agents.tester.setPilot(this.agentPilot());
|
|
277
|
-
this.agents.tester.
|
|
283
|
+
this.agents.tester.setSupervisor((input: string, test: Test) => this.agentCaptain().processSupervisorInterrupt(input, test));
|
|
278
284
|
|
|
279
285
|
const fisherman = this.agentFisherman();
|
|
280
286
|
if (fisherman) this.agentPilot().setFisherman(fisherman);
|
|
@@ -377,9 +383,14 @@ export class ExplorBot {
|
|
|
377
383
|
|
|
378
384
|
agentScout(): Scout | null {
|
|
379
385
|
const scoutConfig = this.config.ai?.agents?.scout;
|
|
380
|
-
if (scoutConfig?.enabled !== true) return null;
|
|
381
|
-
|
|
382
386
|
const dirs = this.scoutCorpusDirs();
|
|
387
|
+
const hasSpec = dirs.length > 0;
|
|
388
|
+
const explicitlyDisabled = scoutConfig?.enabled === false;
|
|
389
|
+
const explicitlyEnabled = scoutConfig?.enabled === true;
|
|
390
|
+
|
|
391
|
+
if (explicitlyDisabled) return null;
|
|
392
|
+
if (!explicitlyEnabled && !hasSpec) return null;
|
|
393
|
+
|
|
383
394
|
if (dirs.length === 0) {
|
|
384
395
|
tag('warning').log('Scout enabled but no documentation found — set --spec or ai.agents.scout.dirs');
|
|
385
396
|
return null;
|
|
@@ -391,11 +402,10 @@ export class ExplorBot {
|
|
|
391
402
|
private scoutCorpusDirs(): string[] {
|
|
392
403
|
const dirs: string[] = [];
|
|
393
404
|
|
|
394
|
-
const
|
|
395
|
-
|
|
405
|
+
const specPaths = normalizeSpecPaths(this.options.applicationSpec || this.config.dirs?.spec);
|
|
406
|
+
for (const specPath of specPaths) {
|
|
396
407
|
const bundle = resolveSpecBundlePath(specPath);
|
|
397
|
-
|
|
398
|
-
if (pagesDir && existsSync(pagesDir)) dirs.push(pagesDir);
|
|
408
|
+
if (bundle && existsSync(bundle)) dirs.push(bundle);
|
|
399
409
|
}
|
|
400
410
|
|
|
401
411
|
for (const dir of this.config.ai?.agents?.scout?.dirs || []) {
|
|
@@ -423,7 +433,7 @@ export class ExplorBot {
|
|
|
423
433
|
this.agents.planner = undefined;
|
|
424
434
|
}
|
|
425
435
|
|
|
426
|
-
async plan(feature?: string, opts: { fresh?: boolean; style?: string; extend?: Plan;
|
|
436
|
+
async plan(feature?: string, opts: { fresh?: boolean; style?: string; extend?: Plan; noSave?: boolean; minTests?: number; maxTests?: number } = {}) {
|
|
427
437
|
this.planFeature = feature;
|
|
428
438
|
|
|
429
439
|
if (opts.fresh) {
|
|
@@ -440,13 +450,18 @@ export class ExplorBot {
|
|
|
440
450
|
|
|
441
451
|
const planner = this.agentPlanner();
|
|
442
452
|
planner.freshStart = !!opts.fresh;
|
|
453
|
+
if (opts.minTests !== undefined || opts.maxTests !== undefined) {
|
|
454
|
+
planner.setTestCount({ minTests: opts.minTests, maxTests: opts.maxTests });
|
|
455
|
+
}
|
|
443
456
|
if (this.currentPlan) {
|
|
444
457
|
planner.setPlan(this.currentPlan);
|
|
445
458
|
}
|
|
446
459
|
this.lastPlanError = null;
|
|
460
|
+
const before = new Set(this.currentPlan?.tests ?? []);
|
|
447
461
|
try {
|
|
448
|
-
this.setCurrentPlan(await planner.plan(feature, opts.style, opts.extend
|
|
462
|
+
this.setCurrentPlan(await planner.plan(feature, opts.style, opts.extend));
|
|
449
463
|
} catch (err) {
|
|
464
|
+
if (await this.agentCaptain().resolveBlocker(err, () => !!this.currentPlan?.tests.some((t) => !before.has(t)))) return this.currentPlan;
|
|
450
465
|
this.lastPlanError = err instanceof Error ? err : new Error(String(err));
|
|
451
466
|
tag('warning').log(`Planning failed: ${this.lastPlanError.message}`);
|
|
452
467
|
if (!this.currentPlan) return undefined;
|
|
@@ -575,3 +590,8 @@ export class ExplorBot {
|
|
|
575
590
|
}
|
|
576
591
|
}
|
|
577
592
|
}
|
|
593
|
+
|
|
594
|
+
function normalizeSpecPaths(spec: string | string[] | undefined): string[] {
|
|
595
|
+
if (!spec) return [];
|
|
596
|
+
return Array.isArray(spec) ? spec : [spec];
|
|
597
|
+
}
|
package/src/explorer.ts
CHANGED
|
@@ -17,7 +17,7 @@ import type { KnowledgeTracker } from './knowledge-tracker.js';
|
|
|
17
17
|
import type { PlaywrightRecorder } from './playwright-recorder.ts';
|
|
18
18
|
import type { Reporter } from './reporter.ts';
|
|
19
19
|
import type { StateManager } from './state-manager.js';
|
|
20
|
-
import { Test
|
|
20
|
+
import { Test } from './test-plan.ts';
|
|
21
21
|
import { BrowserRecoveryError, browserErrorMessage, isFatalBrowserError, isNavigationTransitionError } from './utils/browser-errors.ts';
|
|
22
22
|
import { createDebug, log, tag } from './utils/logger.js';
|
|
23
23
|
import { sleep, waitForPageReadiness } from './utils/page-readiness.ts';
|
|
@@ -190,7 +190,6 @@ class Explorer {
|
|
|
190
190
|
if (!(await this.recoverOrRestart())) {
|
|
191
191
|
return { ok: false, action: 'stop', message: 'Browser page could not be recovered' };
|
|
192
192
|
}
|
|
193
|
-
this.watchActiveTestPage();
|
|
194
193
|
return { ok: true, action: 'continue', recovered: true, message: 'Browser page was recovered' };
|
|
195
194
|
}
|
|
196
195
|
|
|
@@ -201,7 +200,7 @@ class Explorer {
|
|
|
201
200
|
|
|
202
201
|
this._activeTest = test;
|
|
203
202
|
test.start();
|
|
204
|
-
await this.reporter.reportTestStart(
|
|
203
|
+
await this.reporter.reportTestStart();
|
|
205
204
|
await this.closeOtherTabs();
|
|
206
205
|
this.stateManager.otherTabs = [];
|
|
207
206
|
|
|
@@ -414,7 +413,7 @@ class Explorer {
|
|
|
414
413
|
|
|
415
414
|
private async runWithRecovery<T>(label: string, operation: () => Promise<T>): Promise<T> {
|
|
416
415
|
if (!this.page && !(await this.recoverOrRestart())) {
|
|
417
|
-
throw new Error(
|
|
416
|
+
throw new BrowserRecoveryError(label, new Error('Browser page is unavailable'), false);
|
|
418
417
|
}
|
|
419
418
|
|
|
420
419
|
try {
|
|
@@ -516,7 +515,6 @@ class Explorer {
|
|
|
516
515
|
return { ok: false, action: 'stop', recovered: false, message: `Browser could not be recovered after fatal error: ${message}` };
|
|
517
516
|
}
|
|
518
517
|
|
|
519
|
-
this.watchActiveTestPage();
|
|
520
518
|
return {
|
|
521
519
|
ok: true,
|
|
522
520
|
action: 'continue',
|
|
@@ -531,8 +529,9 @@ class Explorer {
|
|
|
531
529
|
}
|
|
532
530
|
|
|
533
531
|
private async recoverOrRestart(): Promise<boolean> {
|
|
534
|
-
if (await this.recoverPage()) return
|
|
535
|
-
|
|
532
|
+
if (!(await this.recoverPage()) && !(await this.restartBrowser())) return false;
|
|
533
|
+
this.watchActiveTestPage();
|
|
534
|
+
return true;
|
|
536
535
|
}
|
|
537
536
|
|
|
538
537
|
private async recoverPage(): Promise<boolean> {
|
package/src/knowledge-tracker.ts
CHANGED
|
@@ -22,7 +22,7 @@ export class KnowledgeTracker {
|
|
|
22
22
|
private knowledgeFiles: Knowledge[] = [];
|
|
23
23
|
private sessionKnowledge: Knowledge[] = [];
|
|
24
24
|
private isLoaded = false;
|
|
25
|
-
private
|
|
25
|
+
private applicationSpecs: ApplicationSpec[] = [];
|
|
26
26
|
|
|
27
27
|
static appendSessionKnowledge(text: string): void {
|
|
28
28
|
sessionEntries.push(text);
|
|
@@ -49,9 +49,14 @@ export class KnowledgeTracker {
|
|
|
49
49
|
mkdirSync(this.knowledgeDir, { recursive: true });
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
for (const p of normalizeSpecPaths(specPath)) {
|
|
53
|
+
const spec = new ApplicationSpec(p);
|
|
54
|
+
this.applicationSpecs.push(spec);
|
|
55
|
+
if (spec.pageCount > 0) {
|
|
56
|
+
tag('info').log(`Loaded application spec with ${spec.pageCount} documented pages`);
|
|
57
|
+
} else if (spec.markdownFileCount > 0) {
|
|
58
|
+
tag('info').log(`Loaded spec corpus with ${spec.markdownFileCount} markdown files`);
|
|
59
|
+
}
|
|
55
60
|
}
|
|
56
61
|
|
|
57
62
|
this.sessionKnowledge = sessionEntries.map((entry, index) => {
|
|
@@ -99,11 +104,12 @@ export class KnowledgeTracker {
|
|
|
99
104
|
}
|
|
100
105
|
|
|
101
106
|
renderApplicationSpec(state: ActionResult): string {
|
|
102
|
-
|
|
107
|
+
const parts = this.applicationSpecs.map((spec) => spec.renderFor(state)).filter(Boolean);
|
|
108
|
+
return parts.join('\n\n');
|
|
103
109
|
}
|
|
104
110
|
|
|
105
111
|
applicationSpecUrls(state: ActionResult): string[] {
|
|
106
|
-
return this.
|
|
112
|
+
return this.applicationSpecs.flatMap((spec) => spec.matchedUrls(state));
|
|
107
113
|
}
|
|
108
114
|
|
|
109
115
|
addKnowledge(urlPattern: string, description: string, opts?: { replace?: boolean }): { filename: string; filePath: string; isNewFile: boolean } {
|
|
@@ -293,6 +299,11 @@ export class KnowledgeTracker {
|
|
|
293
299
|
}
|
|
294
300
|
}
|
|
295
301
|
|
|
302
|
+
function normalizeSpecPaths(spec: string | string[] | undefined): string[] {
|
|
303
|
+
if (!spec) return [];
|
|
304
|
+
return Array.isArray(spec) ? spec : [spec];
|
|
305
|
+
}
|
|
306
|
+
|
|
296
307
|
export interface Knowledge {
|
|
297
308
|
filePath: string;
|
|
298
309
|
url?: string;
|
|
@@ -302,6 +313,6 @@ export interface Knowledge {
|
|
|
302
313
|
}
|
|
303
314
|
|
|
304
315
|
export interface KnowledgeTrackerOptions {
|
|
305
|
-
applicationSpec?: string;
|
|
316
|
+
applicationSpec?: string | string[];
|
|
306
317
|
knowledgeDir?: string;
|
|
307
318
|
}
|
|
@@ -39,7 +39,6 @@ export interface VerificationStep {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
export class PlaywrightRecorder {
|
|
42
|
-
private context: any = null;
|
|
43
42
|
private tracing: any = null;
|
|
44
43
|
private active = false;
|
|
45
44
|
private nextGroupId = 0;
|
|
@@ -68,7 +67,6 @@ export class PlaywrightRecorder {
|
|
|
68
67
|
debugLog('start: no tracing on browserContext, recorder inactive');
|
|
69
68
|
return;
|
|
70
69
|
}
|
|
71
|
-
this.context = browserContext;
|
|
72
70
|
this.tracing = browserContext.tracing;
|
|
73
71
|
try {
|
|
74
72
|
await this.tracing.start({});
|
package/src/reporter.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { join } from 'node:path';
|
|
2
1
|
import { Client } from '@testomatio/reporter';
|
|
3
2
|
import type { Step } from '@testomatio/reporter/types/types.js';
|
|
4
3
|
import { outputPath } from './config.js';
|
|
@@ -132,14 +131,14 @@ export class Reporter {
|
|
|
132
131
|
process.env.TESTOMATIO_STEPS_PASSED = 'true';
|
|
133
132
|
}
|
|
134
133
|
|
|
135
|
-
async reportTestStart(
|
|
134
|
+
async reportTestStart(): Promise<void> {
|
|
136
135
|
await this.startRun();
|
|
137
136
|
}
|
|
138
137
|
|
|
139
138
|
protected combineStepsAndNotes(test: Test, lastScreenshotFile?: string): Step[] {
|
|
140
139
|
const noteEntries = Object.entries(test.notes)
|
|
141
140
|
.filter(([, note]) => !note.observation)
|
|
142
|
-
.map(([
|
|
141
|
+
.map(([, note]) => ({
|
|
143
142
|
startTime: note.startTime,
|
|
144
143
|
endTime: note.endTime,
|
|
145
144
|
message: note.message,
|
|
@@ -150,7 +149,7 @@ export class Reporter {
|
|
|
150
149
|
.sort((a, b) => a.startTime - b.startTime);
|
|
151
150
|
|
|
152
151
|
const stepEntries = Object.entries(test.steps)
|
|
153
|
-
.map(([
|
|
152
|
+
.map(([, stepData]) => ({
|
|
154
153
|
noteStartTime: stepData.noteStartTime,
|
|
155
154
|
text: stepData.text,
|
|
156
155
|
duration: stepData.duration,
|
package/src/test-plan.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import figures from 'figures';
|
|
5
4
|
import type { ActionResult } from './action-result.ts';
|
|
6
5
|
import { listSitePlanDirs } from './global-config.ts';
|
|
7
6
|
import { WebPageState } from './state-manager.ts';
|
|
@@ -545,7 +544,7 @@ export class Plan {
|
|
|
545
544
|
if (!plansDir) dirs.push(...listSitePlanDirs());
|
|
546
545
|
|
|
547
546
|
for (const dir of dirs) {
|
|
548
|
-
const filePath = names.map((name) => path.resolve(dir, name)).find(existsSync);
|
|
547
|
+
const filePath = names.map((name) => path.resolve(dir, name)).find((candidate) => existsSync(candidate) && statSync(candidate).isFile());
|
|
549
548
|
if (filePath) return filePath;
|
|
550
549
|
}
|
|
551
550
|
return null;
|