explorbot 0.5.9 → 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/README.md +1 -1
- package/bin/explorbot-cli.ts +495 -482
- package/boat/api-tester/src/ai/curler.ts +1 -1
- package/boat/api-tester/src/cli.ts +4 -4
- package/boat/api-tester/src/commands/init-command.ts +2 -0
- package/boat/doc-collector/bin/doc-collector-cli.ts +2 -1
- package/boat/doc-collector/src/screenshots.ts +1 -1
- package/boat/prima/bin/prima-cli.ts +2 -1
- package/dist/bin/explorbot-cli.js +456 -444
- package/dist/boat/api-tester/bin/apibot-cli.js +2 -1
- package/dist/boat/api-tester/src/ai/curler.js +1 -1
- package/dist/boat/api-tester/src/cli.js +4 -4
- package/dist/boat/api-tester/src/commands/init-command.js +2 -0
- package/dist/boat/doc-collector/bin/doc-collector-cli.js +2 -1
- package/dist/boat/prima/bin/prima-cli.js +2 -1
- 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 +8 -4
- package/dist/src/commands/learn-command.js +0 -2
- package/dist/src/commands/options/auth-option.d.ts +6 -0
- package/dist/src/commands/options/auth-option.js +14 -0
- package/dist/src/commands/options/index.d.ts +2 -0
- package/dist/src/commands/options/index.js +2 -0
- 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 +3 -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 +18 -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/docs/workflow/agentic-usage.md +7 -0
- package/docs/workflow/knowledge.md +6 -0
- 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 +10 -3
- package/src/commands/learn-command.ts +0 -2
- package/src/commands/options/auth-option.ts +15 -0
- package/src/commands/options/index.ts +2 -0
- 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 +5 -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
package/src/ai/tools.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { tool } from 'ai';
|
|
2
2
|
import dedent from 'dedent';
|
|
3
3
|
import { z } from 'zod';
|
|
4
|
-
import { ActionResult, type PageDiff
|
|
4
|
+
import { ActionResult, type PageDiff } from '../action-result.ts';
|
|
5
5
|
import type { ExecutedStep } from '../action.ts';
|
|
6
6
|
import { type ExperienceTracker, renderExperienceRecipes } from '../experience-tracker.ts';
|
|
7
7
|
import { Stats } from '../stats.ts';
|
|
@@ -65,8 +65,8 @@ export function createCodeceptJSTools({ explorer, stateManager, judge }: ToolDep
|
|
|
65
65
|
Never mix different elements — use separate click() calls instead.
|
|
66
66
|
REQUIRED: include at least one command WITHOUT a container — a wrong or stale container always fails.
|
|
67
67
|
Order by reliability:
|
|
68
|
-
1. I.click(
|
|
69
|
-
2. I.click(
|
|
68
|
+
1. I.click(ARIA, container) - e.g. I.click({"role":"button","text":"Save"}, ".modal")
|
|
69
|
+
2. I.click(text, container) - when the container is verified - e.g. I.click("Save", ".modal")
|
|
70
70
|
3. I.click(CSS, container) - e.g. I.click("#btn", ".modal")
|
|
71
71
|
4. I.click(CSS) or I.click(XPath) - when locator already includes context (ID, XPath)
|
|
72
72
|
After a result reporting multiple matches, reuse that locator with step.opts({ elementIndex: N }) as the last argument.
|
|
@@ -292,7 +292,7 @@ export function createCodeceptJSTools({ explorer, stateManager, judge }: ToolDep
|
|
|
292
292
|
const normalizedKey = key.toLowerCase();
|
|
293
293
|
const matchingStandardKey = Array.from(standardKeys).find((sk) => sk.toLowerCase() === normalizedKey);
|
|
294
294
|
const isStandardKey = !!matchingStandardKey;
|
|
295
|
-
const keyToUse = matchingStandardKey || key;
|
|
295
|
+
const keyToUse = KEY_ALIASES[matchingStandardKey || ''] || matchingStandardKey || key;
|
|
296
296
|
|
|
297
297
|
if (!isSingleChar && !isStandardKey) {
|
|
298
298
|
const previousState = ActionResult.fromState(stateManager.getCurrentState()!);
|
|
@@ -480,7 +480,7 @@ export function createCodeceptJSTools({ explorer, stateManager, judge }: ToolDep
|
|
|
480
480
|
commandsExecuted: lines.length,
|
|
481
481
|
attempts: action.executedSteps,
|
|
482
482
|
code: codeBlock,
|
|
483
|
-
suggestion: '
|
|
483
|
+
suggestion: 'Check the filled values with verify() before submitting, then submit with click().',
|
|
484
484
|
},
|
|
485
485
|
action
|
|
486
486
|
);
|
|
@@ -678,7 +678,7 @@ export function createAgentTools({ explorer, stateManager, ai, judge, researcher
|
|
|
678
678
|
inputSchema: z.object({
|
|
679
679
|
reason: z.string().describe('Why do you need fresh context? Required to prevent overuse.'),
|
|
680
680
|
}),
|
|
681
|
-
execute: async (
|
|
681
|
+
execute: async () => {
|
|
682
682
|
try {
|
|
683
683
|
const currentState = stateManager.getCurrentState();
|
|
684
684
|
|
|
@@ -760,6 +760,7 @@ export function createAgentTools({ explorer, stateManager, ai, judge, researcher
|
|
|
760
760
|
}
|
|
761
761
|
|
|
762
762
|
return failedToolResult('verify', `Verification failed: ${assertion}`, {
|
|
763
|
+
attempts: result.results.map(({ code, passed, error }) => ({ code, passed, error })),
|
|
763
764
|
suggestion: 'The assertion could not be verified. Check if the condition is actually present on the page or try a different assertion.',
|
|
764
765
|
});
|
|
765
766
|
} catch (error) {
|
|
@@ -793,7 +794,7 @@ export function createAgentTools({ explorer, stateManager, ai, judge, researcher
|
|
|
793
794
|
inputSchema: z.object({
|
|
794
795
|
reason: z.string().describe('Why do you need research? What information is missing from existing context?'),
|
|
795
796
|
}),
|
|
796
|
-
execute: async (
|
|
797
|
+
execute: async () => {
|
|
797
798
|
try {
|
|
798
799
|
const currentState = stateManager.getCurrentState();
|
|
799
800
|
|
|
@@ -907,7 +908,7 @@ export function createAgentTools({ explorer, stateManager, ai, judge, researcher
|
|
|
907
908
|
element: z.string().describe('Visual description of element to click (e.g., "blue Submit button in the modal footer", "Settings gear icon in top right")'),
|
|
908
909
|
context: z.string().describe('What you already tried and why it failed - helps with accurate identification'),
|
|
909
910
|
}),
|
|
910
|
-
execute: async ({ element
|
|
911
|
+
execute: async ({ element }) => {
|
|
911
912
|
if (Stats.visionDisabled) {
|
|
912
913
|
return failedToolResult('visualClick', 'Vision tools are disabled for this session. Use xpathCheck() to find the element, then click() with the discovered locator.');
|
|
913
914
|
}
|
|
@@ -1058,7 +1059,7 @@ export function createAgentTools({ explorer, stateManager, ai, judge, researcher
|
|
|
1058
1059
|
xpath: z.string().describe('XPath expression — use broad patterns to avoid missing the element'),
|
|
1059
1060
|
reason: z.string().describe('What element you are looking for and why'),
|
|
1060
1061
|
}),
|
|
1061
|
-
execute: async ({ xpath
|
|
1062
|
+
execute: async ({ xpath }) => {
|
|
1062
1063
|
const currentState = stateManager.getCurrentState();
|
|
1063
1064
|
|
|
1064
1065
|
if (!currentState) {
|
|
@@ -1202,6 +1203,7 @@ const ARIA_OUTPUT_CAP = 4000;
|
|
|
1202
1203
|
const HTML_OUTPUT_CAP = 6000;
|
|
1203
1204
|
const ANALYSIS_OUTPUT_CAP = 2000;
|
|
1204
1205
|
const JUDGE_RECENT_ACTIONS_LIMIT = 8;
|
|
1206
|
+
const KEY_ALIASES: Record<string, string> = { Esc: 'Escape', Del: 'Delete' };
|
|
1205
1207
|
|
|
1206
1208
|
function cap(text: string | undefined | null, max: number): string {
|
|
1207
1209
|
if (!text) return '';
|
package/src/application-spec.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { loadMarkdownFiles } from './utils/markdown-files.ts';
|
|
|
10
10
|
export class ApplicationSpec {
|
|
11
11
|
private pages: ApplicationSpecPage[] = [];
|
|
12
12
|
readonly sourcePath: string;
|
|
13
|
+
private totalMarkdownFiles = 0;
|
|
13
14
|
|
|
14
15
|
constructor(sourcePath: string) {
|
|
15
16
|
this.sourcePath = this.resolveSourcePath(sourcePath);
|
|
@@ -38,28 +39,27 @@ export class ApplicationSpec {
|
|
|
38
39
|
return this.pages.length;
|
|
39
40
|
}
|
|
40
41
|
|
|
42
|
+
get markdownFileCount(): number {
|
|
43
|
+
return this.totalMarkdownFiles;
|
|
44
|
+
}
|
|
45
|
+
|
|
41
46
|
private load(): void {
|
|
42
47
|
if (!existsSync(this.sourcePath)) {
|
|
43
48
|
throw new Error(`Application spec not found: ${this.sourcePath}`);
|
|
44
49
|
}
|
|
45
50
|
|
|
46
51
|
const isDirectory = statSync(this.sourcePath).isDirectory();
|
|
47
|
-
if (!isDirectory && path.basename(this.sourcePath).toLowerCase() !== 'index.md') {
|
|
48
|
-
throw new Error(`Application spec file must be index.md: ${this.sourcePath}`);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
52
|
const bundlePath = isDirectory ? this.sourcePath : path.dirname(this.sourcePath);
|
|
53
|
+
|
|
54
|
+
const allFiles = loadMarkdownFiles(bundlePath, { recursive: true });
|
|
52
55
|
const indexPath = path.join(bundlePath, 'index.md');
|
|
53
|
-
|
|
54
|
-
throw new Error(`Application spec index not found: ${indexPath}`);
|
|
55
|
-
}
|
|
56
|
+
this.totalMarkdownFiles = allFiles.filter((file) => path.resolve(file.filePath) !== path.resolve(indexPath)).length;
|
|
56
57
|
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
for (const file of allFiles) {
|
|
59
|
+
if (path.resolve(file.filePath) === path.resolve(indexPath)) continue;
|
|
60
|
+
|
|
61
|
+
if (!file.data.url && file.data.format === undefined && file.data.version === undefined) continue;
|
|
61
62
|
|
|
62
|
-
for (const file of loadMarkdownFiles(pagesPath, { recursive: true })) {
|
|
63
63
|
const parsed = APPLICATION_SPEC_PAGE_SCHEMA.safeParse(file.data);
|
|
64
64
|
if (!parsed.success && parsed.error.issues.some((issue) => issue.path[0] === 'format')) {
|
|
65
65
|
throw new Error(`Invalid application spec format in ${file.filePath}`);
|
|
@@ -73,8 +73,14 @@ export class ApplicationSpec {
|
|
|
73
73
|
this.pages.push({ url: parsed.data.url, content: file.content.trim() });
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
if (this.totalMarkdownFiles === 0) {
|
|
77
|
+
tag('warning').log(`No markdown files found in spec: ${bundlePath}`);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
76
81
|
if (this.pages.length === 0) {
|
|
77
|
-
|
|
82
|
+
tag('debug').log(`${this.totalMarkdownFiles} spec files found in ${bundlePath}`);
|
|
83
|
+
return;
|
|
78
84
|
}
|
|
79
85
|
}
|
|
80
86
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync, readdirSync, rmSync, statSync, unlinkSync } from 'node:fs';
|
|
2
|
-
import {
|
|
2
|
+
import { join } from 'node:path';
|
|
3
3
|
import { ConfigParser, outputPath } from '../config.js';
|
|
4
4
|
import { tag } from '../utils/logger.js';
|
|
5
5
|
import { BaseCommand, type Suggestion } from './base-command.js';
|
|
@@ -9,7 +9,6 @@ export class ContextExperienceCommand extends BaseCommand {
|
|
|
9
9
|
description = 'Print all matching experience for current page';
|
|
10
10
|
|
|
11
11
|
async execute(_args: string): Promise<void> {
|
|
12
|
-
const explorer = this.explorBot.getExplorer();
|
|
13
12
|
const state = this.explorBot.stateManager().getCurrentState();
|
|
14
13
|
|
|
15
14
|
if (!state) {
|
|
@@ -9,7 +9,6 @@ export class ContextKnowledgeCommand extends BaseCommand {
|
|
|
9
9
|
description = 'Print all matching knowledge for current page';
|
|
10
10
|
|
|
11
11
|
async execute(_args: string): Promise<void> {
|
|
12
|
-
const explorer = this.explorBot.getExplorer();
|
|
13
12
|
const state = this.explorBot.stateManager().getCurrentState();
|
|
14
13
|
|
|
15
14
|
if (!state) {
|
|
@@ -6,7 +6,7 @@ import { Stats } from '../stats.js';
|
|
|
6
6
|
import { type Plan, type Test, TestResult } from '../test-plan.js';
|
|
7
7
|
import { browserErrorMessage } from '../utils/browser-errors.ts';
|
|
8
8
|
import { getCliName } from '../utils/cli-name.ts';
|
|
9
|
-
import { ErrorPageError, getStateErrorPageError } from '../utils/error-page.ts';
|
|
9
|
+
import { BlockedError, ErrorPageError, getStateErrorPageError } from '../utils/error-page.ts';
|
|
10
10
|
import { tag } from '../utils/logger.js';
|
|
11
11
|
import { type NextStepSection, printNextSteps, relativeToCwd } from '../utils/next-steps.ts';
|
|
12
12
|
import { keepSiteUrls, readSitemap } from '../utils/sitemap.ts';
|
|
@@ -14,6 +14,7 @@ import { parseSpecPairs } from '../utils/spec.ts';
|
|
|
14
14
|
import { safeFilename } from '../utils/strings.ts';
|
|
15
15
|
import { UrlFilter } from '../utils/url-filter.ts';
|
|
16
16
|
import { type ArgumentCompletion, BaseCommand, type Suggestion } from './base-command.js';
|
|
17
|
+
import { PlanCommand } from './plan-command.js';
|
|
17
18
|
|
|
18
19
|
const MAX_SUB_PAGE_ATTEMPTS = 30;
|
|
19
20
|
const PRIORITY_ORDER: Record<string, number> = { critical: 0, important: 1, high: 2, normal: 3, low: 4 };
|
|
@@ -136,7 +137,7 @@ export class ExploreCommand extends BaseCommand {
|
|
|
136
137
|
}
|
|
137
138
|
|
|
138
139
|
private async runFreshMode(mainUrl: string | undefined, feature: string | undefined, styles?: string[]): Promise<void> {
|
|
139
|
-
await this.runAllStyles(mainUrl, feature, undefined,
|
|
140
|
+
await this.runAllStyles(mainUrl, feature, undefined, styles);
|
|
140
141
|
this.rememberCurrentPlan();
|
|
141
142
|
const mainPlan = this.explorBot.getCurrentPlan();
|
|
142
143
|
if (!mainPlan) return;
|
|
@@ -320,7 +321,7 @@ export class ExploreCommand extends BaseCommand {
|
|
|
320
321
|
tag('warning').log(`Skipping page: ${errorPage.message}`);
|
|
321
322
|
return false;
|
|
322
323
|
}
|
|
323
|
-
await this.runAllStyles(url, feature, parentPlan,
|
|
324
|
+
await this.runAllStyles(url, feature, parentPlan, styles);
|
|
324
325
|
const plan = this.explorBot.getCurrentPlan();
|
|
325
326
|
if (plan?.tests.length && !this.completedPlans.includes(plan)) {
|
|
326
327
|
this.completedPlans.push(plan);
|
|
@@ -333,14 +334,14 @@ export class ExploreCommand extends BaseCommand {
|
|
|
333
334
|
for (const style of styleList) {
|
|
334
335
|
if (this.isLimitReached()) break;
|
|
335
336
|
this.explorBot.setCurrentPlan(existingPlan);
|
|
336
|
-
const opts: { fresh: boolean; style: string;
|
|
337
|
+
const opts: { fresh: boolean; style: string; noSave?: boolean } = { fresh: false, style };
|
|
337
338
|
if (this.dryRun) opts.noSave = true;
|
|
338
339
|
await this.planWithRetry(feature, opts, pageUrl);
|
|
339
340
|
await this.runPendingTests();
|
|
340
341
|
}
|
|
341
342
|
}
|
|
342
343
|
|
|
343
|
-
private async runAllStyles(pageUrl?: string, feature?: string, parentPlan?: Plan,
|
|
344
|
+
private async runAllStyles(pageUrl?: string, feature?: string, parentPlan?: Plan, styles?: string[]): Promise<void> {
|
|
344
345
|
const styleList = styles ?? Object.keys(getStyles());
|
|
345
346
|
let fresh = true;
|
|
346
347
|
for (const style of styleList) {
|
|
@@ -348,7 +349,7 @@ export class ExploreCommand extends BaseCommand {
|
|
|
348
349
|
if (!fresh && pageUrl && !this.dryRun) {
|
|
349
350
|
await this.explorBot.visit(pageUrl);
|
|
350
351
|
}
|
|
351
|
-
const opts: { fresh: boolean; style: string; extend?: Plan;
|
|
352
|
+
const opts: { fresh: boolean; style: string; extend?: Plan; noSave?: boolean } = { fresh, style };
|
|
352
353
|
if (fresh && parentPlan) opts.extend = parentPlan;
|
|
353
354
|
if (this.dryRun) opts.noSave = true;
|
|
354
355
|
await this.planWithRetry(feature, opts, pageUrl);
|
|
@@ -371,10 +372,14 @@ export class ExploreCommand extends BaseCommand {
|
|
|
371
372
|
this.completedPlans.push(plan);
|
|
372
373
|
}
|
|
373
374
|
|
|
374
|
-
private async planWithRetry(feature: string | undefined, opts: { fresh: boolean; style: string; extend?: Plan;
|
|
375
|
+
private async planWithRetry(feature: string | undefined, opts: { fresh: boolean; style: string; extend?: Plan; noSave?: boolean }, pageUrl?: string): Promise<void> {
|
|
375
376
|
const before = new Set(this.explorBot.getCurrentPlan()?.tests ?? []);
|
|
376
377
|
|
|
377
378
|
await this.explorBot.plan(feature, opts);
|
|
379
|
+
if (this.explorBot.lastPlanError instanceof BlockedError) {
|
|
380
|
+
tag('warning').log(`Skipping page: ${this.explorBot.lastPlanError.message}`);
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
378
383
|
if (this.explorBot.lastPlanError) {
|
|
379
384
|
if (this.explorBot.lastPlanError instanceof ErrorPageError) {
|
|
380
385
|
throw this.explorBot.lastPlanError;
|
|
@@ -388,12 +393,15 @@ export class ExploreCommand extends BaseCommand {
|
|
|
388
393
|
}
|
|
389
394
|
}
|
|
390
395
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
396
|
+
this.printAddedTests(before, opts.style, pageUrl);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
private printAddedTests(before: Set<Test>, style: string, pageUrl?: string): number {
|
|
400
|
+
const added = this.explorBot.getCurrentPlan()?.tests.filter((t) => !before.has(t)) ?? [];
|
|
401
|
+
if (added.length === 0) return 0;
|
|
395
402
|
const urlNote = pageUrl ? ` for ${pageUrl}` : '';
|
|
396
|
-
this.printPreview(`Planner added ${added.length} new test(s) [style=${
|
|
403
|
+
this.printPreview(`Planner added ${added.length} new test(s) [style=${style}]${urlNote}:`, added);
|
|
404
|
+
return added.length;
|
|
397
405
|
}
|
|
398
406
|
|
|
399
407
|
private parseConfigure(raw: string | undefined): ConfigureSpec {
|
|
@@ -545,16 +553,7 @@ export class ExploreCommand extends BaseCommand {
|
|
|
545
553
|
const sections: NextStepSection[] = [];
|
|
546
554
|
|
|
547
555
|
if (savedPlanPath) {
|
|
548
|
-
|
|
549
|
-
sections.push({
|
|
550
|
-
label: 'Plan',
|
|
551
|
-
path: savedPlanPath,
|
|
552
|
-
commands: [
|
|
553
|
-
{ label: 'Re-run', command: `${cli} test ${relPlan} 1` },
|
|
554
|
-
{ label: 'Run all', command: `${cli} test ${relPlan} *` },
|
|
555
|
-
{ label: 'Run range', command: `${cli} test ${relPlan} 1-3` },
|
|
556
|
-
],
|
|
557
|
-
});
|
|
556
|
+
sections.push(PlanCommand.planSection(savedPlanPath));
|
|
558
557
|
}
|
|
559
558
|
|
|
560
559
|
const savedFiles = this.explorBot.agentHistorian().getSavedFiles();
|
|
@@ -594,6 +593,14 @@ export class ExploreCommand extends BaseCommand {
|
|
|
594
593
|
printNextSteps(sections);
|
|
595
594
|
}
|
|
596
595
|
|
|
596
|
+
private skipBlockedTest(test: Test, error: unknown): void {
|
|
597
|
+
if (!(error instanceof BlockedError)) throw error;
|
|
598
|
+
tag('warning').log(`Skipping test: ${error.message}`);
|
|
599
|
+
test.start();
|
|
600
|
+
test.addNote(error.message, TestResult.SKIPPED);
|
|
601
|
+
test.finish(TestResult.SKIPPED);
|
|
602
|
+
}
|
|
603
|
+
|
|
597
604
|
private isLimitReached(): boolean {
|
|
598
605
|
if (this.maxTests != null && this.testsRun >= this.maxTests) return true;
|
|
599
606
|
return this.isDeadlineReached();
|
|
@@ -628,7 +635,13 @@ export class ExploreCommand extends BaseCommand {
|
|
|
628
635
|
test.start();
|
|
629
636
|
test.finish(TestResult.SKIPPED);
|
|
630
637
|
} else {
|
|
631
|
-
|
|
638
|
+
const run = () => this.explorBot.agentTester().test(test, { deadline: this.hardDeadlineAt });
|
|
639
|
+
await run()
|
|
640
|
+
.catch(async (err) => {
|
|
641
|
+
if (!(await this.explorBot.agentCaptain().resolveBlocker(err, () => this.explorBot.agentNavigator().isOn(test.startUrl)))) throw err;
|
|
642
|
+
return run();
|
|
643
|
+
})
|
|
644
|
+
.catch((err) => this.skipBlockedTest(test, err));
|
|
632
645
|
}
|
|
633
646
|
this.testsRun++;
|
|
634
647
|
}
|
|
@@ -31,12 +31,14 @@ export class HelpJsonCommand {
|
|
|
31
31
|
required: argument.required,
|
|
32
32
|
variadic: argument.variadic,
|
|
33
33
|
})),
|
|
34
|
-
options:
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
34
|
+
options: cmd.options
|
|
35
|
+
.filter((option) => !option.hidden)
|
|
36
|
+
.map((option) => ({
|
|
37
|
+
flags: option.flags,
|
|
38
|
+
description: option.description,
|
|
39
|
+
default: option.defaultValue,
|
|
40
|
+
choices: option.argChoices,
|
|
41
|
+
})),
|
|
40
42
|
commands: helper.visibleCommands(cmd).map((sub) => HelpJsonCommand.data(sub)),
|
|
41
43
|
};
|
|
42
44
|
|
|
@@ -327,6 +327,11 @@ function printInitNextSteps(options: { url?: string; siteCommands?: NextStepComm
|
|
|
327
327
|
|
|
328
328
|
if (options.siteCommands) sections.push({ label: 'Explore any site', commands: options.siteCommands });
|
|
329
329
|
|
|
330
|
+
sections.push({
|
|
331
|
+
label: 'Log in for one run (kept in memory, never saved)',
|
|
332
|
+
commands: [{ command: `${cli} explore /dashboard --auth 'login: admin@example.com password: secret123'` }],
|
|
333
|
+
});
|
|
334
|
+
|
|
330
335
|
sections.push(
|
|
331
336
|
{
|
|
332
337
|
label: 'Run it from VS Code',
|
|
@@ -338,11 +343,13 @@ function printInitNextSteps(options: { url?: string; siteCommands?: NextStepComm
|
|
|
338
343
|
}
|
|
339
344
|
);
|
|
340
345
|
|
|
346
|
+
sections.push({
|
|
347
|
+
label: 'Add knowledge (logins, credentials) — created on first use in ./knowledge',
|
|
348
|
+
commands: [{ command: `${cli} learn '*' 'to authorize use these credentials: admin@example.com / secret123'` }],
|
|
349
|
+
});
|
|
350
|
+
|
|
341
351
|
printNextSteps(sections);
|
|
342
352
|
|
|
343
|
-
log('');
|
|
344
|
-
log('Add knowledge (logins, credentials) — created on first use in ./knowledge:');
|
|
345
|
-
tag('substep').log(chalk.yellow(`${cli} learn '*' 'to authorize use these credentials: admin@example.com / secret123'`));
|
|
346
353
|
tag('substep').log('You can use ${env.LOGIN} and ${env.PASSWORD} to reference environment variables.');
|
|
347
354
|
}
|
|
348
355
|
|
|
@@ -13,7 +13,6 @@ export class LearnCommand extends BaseCommand {
|
|
|
13
13
|
|
|
14
14
|
if (!note) {
|
|
15
15
|
const AddKnowledge = (await import('../components/AddKnowledge.js')).default;
|
|
16
|
-
const explorer = this.explorBot.getExplorer();
|
|
17
16
|
const state = this.explorBot.stateManager().getCurrentState();
|
|
18
17
|
const initialUrl = state?.url || '';
|
|
19
18
|
|
|
@@ -32,7 +31,6 @@ export class LearnCommand extends BaseCommand {
|
|
|
32
31
|
return;
|
|
33
32
|
}
|
|
34
33
|
|
|
35
|
-
const explorer = this.explorBot.getExplorer();
|
|
36
34
|
const state = this.explorBot.stateManager().getCurrentState();
|
|
37
35
|
|
|
38
36
|
if (!state) {
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { KnowledgeTracker } from '../../knowledge-tracker.js';
|
|
2
|
+
import { registerSecret } from '../../utils/secrets.js';
|
|
3
|
+
import { BaseOption } from './base-option.js';
|
|
4
|
+
|
|
5
|
+
export class AuthOption extends BaseOption {
|
|
6
|
+
flags = '--auth <text>';
|
|
7
|
+
description = 'Auth details for this run only, not saved to disk, in free form (e.g. "login: admin password: secret" or "Bearer: <token>"). Falls back to EXPLORBOT_AUTH';
|
|
8
|
+
|
|
9
|
+
protected apply(options: Record<string, any>): void {
|
|
10
|
+
const auth = options.auth || process.env.EXPLORBOT_AUTH;
|
|
11
|
+
if (!auth) return;
|
|
12
|
+
registerSecret(auth);
|
|
13
|
+
KnowledgeTracker.appendSessionKnowledge(auth);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import { AuthOption } from './auth-option.js';
|
|
1
2
|
import { DecisionModelOption } from './decision-model-option.js';
|
|
2
3
|
import { KnowledgeOption } from './knowledge-option.js';
|
|
3
4
|
import { WsOption } from './ws-option.js';
|
|
4
5
|
|
|
5
6
|
export { BaseOption } from './base-option.js';
|
|
6
7
|
|
|
8
|
+
export const authOption = new AuthOption();
|
|
7
9
|
export const decisionModelOption = new DecisionModelOption();
|
|
8
10
|
export const knowledgeOption = new KnowledgeOption();
|
|
9
11
|
export const wsOption = new WsOption();
|
|
@@ -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
|
|