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/dist/src/ai/planner.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { ActionResult } from '../action-result.js';
|
|
2
2
|
import type { ExperienceTracker } from '../experience-tracker.js';
|
|
3
|
-
import type Explorer from '../explorer.js';
|
|
4
3
|
import type { StateManager } from '../state-manager.js';
|
|
5
4
|
import { Suite } from '../suite.js';
|
|
6
5
|
import { Plan, Test } from '../test-plan.js';
|
|
@@ -9,7 +8,7 @@ import { Conversation } from './conversation.js';
|
|
|
9
8
|
import type { Fisherman } from './fisherman.js';
|
|
10
9
|
import type { Provider } from './provider.js';
|
|
11
10
|
import { type Researcher } from './researcher.js';
|
|
12
|
-
import type
|
|
11
|
+
import { type Scout } from './scout.js';
|
|
13
12
|
declare const PlannerBase: {
|
|
14
13
|
new (...args: any[]): {
|
|
15
14
|
currentPlan: Plan | null;
|
|
@@ -47,13 +46,14 @@ declare const PlannerBase: {
|
|
|
47
46
|
} & (new (...args: any[]) => object);
|
|
48
47
|
export declare class Planner extends PlannerBase implements Agent {
|
|
49
48
|
emoji: string;
|
|
50
|
-
explorer: Explorer;
|
|
51
49
|
provider: Provider;
|
|
52
50
|
stateManager: StateManager;
|
|
53
51
|
experienceTracker: ExperienceTracker;
|
|
54
52
|
knowledgeTracker: AgentDeps['knowledgeTracker'];
|
|
55
53
|
MIN_TASKS: number;
|
|
56
54
|
MAX_TASKS: number;
|
|
55
|
+
minTasksOverride?: number;
|
|
56
|
+
maxTasksOverride?: number;
|
|
57
57
|
currentPlan: Plan | null;
|
|
58
58
|
freshStart: boolean;
|
|
59
59
|
lastStyleName: string;
|
|
@@ -66,14 +66,19 @@ export declare class Planner extends PlannerBase implements Agent {
|
|
|
66
66
|
setScout(scout: Scout): void;
|
|
67
67
|
get sectionOrder(): string[];
|
|
68
68
|
get docsWeight(): number;
|
|
69
|
+
get effectiveMinTasks(): number;
|
|
70
|
+
get effectiveMaxTasks(): number;
|
|
71
|
+
setTestCount(opts: {
|
|
72
|
+
minTests?: number;
|
|
73
|
+
maxTests?: number;
|
|
74
|
+
}): void;
|
|
69
75
|
getDefaultStartUrl(state: {
|
|
70
76
|
url: string;
|
|
71
|
-
fullUrl?: string;
|
|
72
77
|
}): string;
|
|
73
78
|
getSystemMessage(feature?: string): string;
|
|
74
79
|
setPlan(plan: Plan): void;
|
|
75
80
|
static getCachedPlan(url: string): Plan | null;
|
|
76
|
-
plan(feature?: string, style?: string, parentPlan?: Plan
|
|
81
|
+
plan(feature?: string, style?: string, parentPlan?: Plan): Promise<Plan>;
|
|
77
82
|
getSuite(): Suite | null;
|
|
78
83
|
addNewTests(tests: Test[], defaultStartUrl: string): Test[];
|
|
79
84
|
getExistingTestFileScenarios(currentUrl?: string): Set<string>;
|
|
@@ -81,5 +86,7 @@ export declare class Planner extends PlannerBase implements Agent {
|
|
|
81
86
|
buildApproach(style?: string): string;
|
|
82
87
|
buildConversation(state: ActionResult, style?: string, parentPlan?: Plan, feature?: string): Promise<Conversation>;
|
|
83
88
|
getTasksMessage(): string;
|
|
89
|
+
collectResearch(state: ActionResult): Promise<string>;
|
|
90
|
+
collectDocs(state: ActionResult, feature?: string): Promise<string>;
|
|
84
91
|
}
|
|
85
92
|
export {};
|
package/dist/src/ai/planner.js
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import dedent from 'dedent';
|
|
2
|
-
import { z } from 'zod';
|
|
3
2
|
import { ActionResult } from "../action-result.js";
|
|
4
3
|
import { setActivity } from "../activity.js";
|
|
5
4
|
import { ConfigParser } from "../config.js";
|
|
6
5
|
import { Observability } from "../observability.js";
|
|
7
6
|
import { Stats } from "../stats.js";
|
|
8
7
|
import { Suite } from "../suite.js";
|
|
9
|
-
import { Plan
|
|
8
|
+
import { Plan } from "../test-plan.js";
|
|
9
|
+
import { PlanningBlockedError } from "../utils/error-page.js";
|
|
10
10
|
import { createDebug, tag } from '../utils/logger.js';
|
|
11
|
+
import { loop } from "../utils/loop.js";
|
|
11
12
|
import { jsonToTable } from "../utils/markdown-parser.js";
|
|
12
13
|
import { mdq } from '../utils/markdown-query.js';
|
|
13
|
-
import { planToCompactAiContext } from "../utils/test-plan-markdown.js";
|
|
14
|
+
import { findPlanMarkdownProblems, parsePlansFromMarkdownText, planToCompactAiContext, readPlanMarkdownComment } from "../utils/test-plan-markdown.js";
|
|
14
15
|
import { Conversation } from "./conversation.js";
|
|
15
16
|
import { WithSessionDedup } from "./planner/session-dedup.js";
|
|
16
17
|
import { getActiveStyle } from "./planner/styles.js";
|
|
@@ -20,32 +21,20 @@ import { findSimilarStateHash } from "./researcher/cache.js";
|
|
|
20
21
|
import { hasFocusedSection } from "./researcher/focus.js";
|
|
21
22
|
import { RulesLoader } from "../utils/rules-loader.js";
|
|
22
23
|
import { capabilityGroundingRule, dataProtectionRules, fileUploadRule } from "./rules.js";
|
|
24
|
+
import { ScoutQuery } from "./scout.js";
|
|
23
25
|
const debugLog = createDebug('explorbot:planner');
|
|
24
|
-
const
|
|
25
|
-
planName: z.string().describe('Short descriptive name for the test plan (e.g., "User Authentication Testing", "Product Catalog Navigation", "Form Validation Tests")'),
|
|
26
|
-
scenarios: z
|
|
27
|
-
.array(z.object({
|
|
28
|
-
scenario: z.string().describe('A single sentence describing the behavior to test.'),
|
|
29
|
-
priority: z.enum(['critical', 'important', 'high', 'normal', 'low']).describe('Priority of the task based on business importance'),
|
|
30
|
-
startUrl: z.string().nullable().describe('Start URL for the test if different from plan URL. Use only stable feature/list/detail pages, not transient create/edit/modal URLs unless the scenario specifically starts inside that form.'),
|
|
31
|
-
steps: z.array(z.string()).describe('List of steps to perform for this scenario. Each step should be a specific action (e.g., "Open the form", "Enter required data", "Submit the form"). Keep steps atomic and actionable.'),
|
|
32
|
-
expectedOutcomes: z
|
|
33
|
-
.array(z.string())
|
|
34
|
-
.describe('List of expected outcomes that can be verified. Each outcome should be simple, specific, and easy to check (e.g., "Success message appears", "URL changes to /dashboard", "Form field shows error"). Keep outcomes atomic - do not combine multiple checks into one.'),
|
|
35
|
-
}))
|
|
36
|
-
.describe('List of testing scenarios'),
|
|
37
|
-
notPlannableReason: z.string().nullish().describe('When scenarios is empty, one sentence saying why this page has nothing to test; otherwise null.'),
|
|
38
|
-
});
|
|
26
|
+
const MAX_FORMAT_ATTEMPTS = 3;
|
|
39
27
|
const PlannerBase = WithSessionDedup(WithSubPages(Object));
|
|
40
28
|
export class Planner extends PlannerBase {
|
|
41
29
|
emoji = '📋';
|
|
42
|
-
explorer;
|
|
43
30
|
provider;
|
|
44
31
|
stateManager;
|
|
45
32
|
experienceTracker;
|
|
46
33
|
knowledgeTracker;
|
|
47
34
|
MIN_TASKS = 3;
|
|
48
35
|
MAX_TASKS = 12;
|
|
36
|
+
minTasksOverride;
|
|
37
|
+
maxTasksOverride;
|
|
49
38
|
currentPlan = null;
|
|
50
39
|
freshStart = false;
|
|
51
40
|
lastStyleName = '';
|
|
@@ -55,7 +44,6 @@ export class Planner extends PlannerBase {
|
|
|
55
44
|
scout = null;
|
|
56
45
|
constructor(deps, researcher) {
|
|
57
46
|
super();
|
|
58
|
-
this.explorer = deps.explorer;
|
|
59
47
|
this.provider = deps.ai;
|
|
60
48
|
this.researcher = researcher;
|
|
61
49
|
this.stateManager = deps.stateManager;
|
|
@@ -75,8 +63,24 @@ export class Planner extends PlannerBase {
|
|
|
75
63
|
const value = ConfigParser.getInstance().getConfig().ai?.agents?.planner?.docsWeight ?? 70;
|
|
76
64
|
return Math.max(0, Math.min(100, value));
|
|
77
65
|
}
|
|
66
|
+
get effectiveMinTasks() {
|
|
67
|
+
if (this.minTasksOverride !== undefined)
|
|
68
|
+
return this.minTasksOverride;
|
|
69
|
+
return ConfigParser.getInstance().getConfig().ai?.agents?.planner?.minTests ?? this.MIN_TASKS;
|
|
70
|
+
}
|
|
71
|
+
get effectiveMaxTasks() {
|
|
72
|
+
if (this.maxTasksOverride !== undefined)
|
|
73
|
+
return this.maxTasksOverride;
|
|
74
|
+
return ConfigParser.getInstance().getConfig().ai?.agents?.planner?.maxTests ?? this.MAX_TASKS;
|
|
75
|
+
}
|
|
76
|
+
setTestCount(opts) {
|
|
77
|
+
if (opts.minTests !== undefined)
|
|
78
|
+
this.minTasksOverride = opts.minTests;
|
|
79
|
+
if (opts.maxTests !== undefined)
|
|
80
|
+
this.maxTasksOverride = opts.maxTests;
|
|
81
|
+
}
|
|
78
82
|
getDefaultStartUrl(state) {
|
|
79
|
-
return state.
|
|
83
|
+
return state.url;
|
|
80
84
|
}
|
|
81
85
|
getSystemMessage(feature) {
|
|
82
86
|
const currentUrl = this.stateManager.getCurrentState()?.url;
|
|
@@ -126,7 +130,7 @@ export class Planner extends PlannerBase {
|
|
|
126
130
|
static getCachedPlan(url) {
|
|
127
131
|
return getRegisteredPlan(url)?.plan || null;
|
|
128
132
|
}
|
|
129
|
-
async plan(feature, style, parentPlan
|
|
133
|
+
async plan(feature, style, parentPlan) {
|
|
130
134
|
Stats.plans++;
|
|
131
135
|
const state = this.stateManager.getCurrentState();
|
|
132
136
|
debugLog('Planning:', state?.url);
|
|
@@ -175,17 +179,37 @@ export class Planner extends PlannerBase {
|
|
|
175
179
|
else {
|
|
176
180
|
tag('step').log('Focusing on main content of this page');
|
|
177
181
|
}
|
|
178
|
-
debugLog('Sending planning prompt to AI provider
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
+
debugLog('Sending planning prompt to AI provider');
|
|
183
|
+
let markdown = '';
|
|
184
|
+
await loop(async ({ stop, iteration }) => {
|
|
185
|
+
const response = await this.provider.chat(conversation.messages, conversation.model, { agentName: 'planner' });
|
|
186
|
+
markdown = response.text;
|
|
187
|
+
const problems = findPlanMarkdownProblems(markdown);
|
|
188
|
+
if (problems.length === 0)
|
|
189
|
+
stop();
|
|
190
|
+
if (iteration >= MAX_FORMAT_ATTEMPTS)
|
|
191
|
+
throw new Error(`Planner output does not match the plan format: ${problems.join('; ')}`);
|
|
192
|
+
debugLog('Plan format problems: %o', problems);
|
|
193
|
+
conversation.addAssistantText(markdown);
|
|
194
|
+
conversation.addUserText(dedent `
|
|
195
|
+
Your plan does not match the required format:
|
|
196
|
+
${problems.map((problem) => `- ${problem}`).join('\n')}
|
|
197
|
+
Rewrite the whole plan in the format from <output_format>. Keep the same scenarios; fix only the structure.
|
|
198
|
+
`);
|
|
199
|
+
}, { maxAttempts: MAX_FORMAT_ATTEMPTS, interruptible: false });
|
|
200
|
+
const defaultStartUrl = this.getDefaultStartUrl(state);
|
|
201
|
+
markdown = mdq(markdown).query('h1').first().insertAfter(`### Prerequisite\n\n* URL: ${defaultStartUrl}`).toString();
|
|
202
|
+
const parsedPlan = parsePlansFromMarkdownText(markdown)[0];
|
|
203
|
+
const fromPlanning = parsedPlan?.tests || [];
|
|
204
|
+
const blockedBy = readPlanMarkdownComment(markdown, 'blocked');
|
|
205
|
+
const prerequisite = readPlanMarkdownComment(markdown, 'prerequisite');
|
|
206
|
+
if (!this.currentPlan && fromPlanning.length === 0 && blockedBy) {
|
|
207
|
+
tag('warning').log(`Page is not plannable: ${blockedBy}`);
|
|
182
208
|
}
|
|
183
|
-
if (!this.currentPlan &&
|
|
184
|
-
|
|
209
|
+
if (!this.currentPlan && fromPlanning.length === 0 && prerequisite) {
|
|
210
|
+
throw new PlanningBlockedError(state.url, prerequisite, feature, style);
|
|
185
211
|
}
|
|
186
|
-
|
|
187
|
-
const fromPlanning = aiResult.object.scenarios.map((s) => new Test(s.scenario, s.priority, s.expectedOutcomes, s.startUrl || defaultStartUrl, s.steps || []));
|
|
188
|
-
return { tests: fromPlanning, planName: aiResult.object.planName };
|
|
212
|
+
return { tests: fromPlanning, planName: parsedPlan?.title };
|
|
189
213
|
});
|
|
190
214
|
const tests = result.tests;
|
|
191
215
|
debugLog('Created tests:', tests);
|
|
@@ -304,9 +328,6 @@ export class Planner extends PlannerBase {
|
|
|
304
328
|
async buildConversation(state, style, parentPlan, feature) {
|
|
305
329
|
const model = this.provider.getAgenticModel('planner');
|
|
306
330
|
const conversation = new Conversation([], model);
|
|
307
|
-
conversation.autoTrimTag('page_research', 20000);
|
|
308
|
-
conversation.autoTrimTag('tested_scenarios', 10000);
|
|
309
|
-
conversation.autoTrimTag('docs_context', 8000);
|
|
310
331
|
conversation.addUserText(this.getSystemMessage(feature));
|
|
311
332
|
let ignoredPagesRule = '';
|
|
312
333
|
if (!feature)
|
|
@@ -337,23 +358,25 @@ export class Planner extends PlannerBase {
|
|
|
337
358
|
If exact wording is unknown, describe the expected result generically, for example "an authentication error is shown" or "the user stays on the login page" instead of guessing the literal text.
|
|
338
359
|
If exact redirect destination is unknown, describe the destination by visible page identity, for example "the dashboard page opens" or "the current workspace home page opens" instead of inventing a URL slug.
|
|
339
360
|
Only propose scenarios whose prerequisites are evident from page research, visited pages, or API data preparation context.
|
|
340
|
-
If a scenario needs existing records
|
|
341
|
-
If the page appears read-only, degraded, demo-limited, maintenance-like, or lacks
|
|
361
|
+
If a scenario needs existing records or other target data, propose using api if <api_data_preparation> section is set or reuired record is visible on page or can be created via UI.
|
|
362
|
+
If the page appears read-only, degraded, demo-limited, maintenance-like, or lacks UI controls skip that page.
|
|
342
363
|
Do not assume hidden data exists just because a control is present.
|
|
343
364
|
Do not put record IDs or unique record names in test plans. Describe which record is needed and let Pilot choose it during execution; name a specific record only when research shows a small, complete list of available records.
|
|
344
365
|
If the list is empty or no concrete item names are visible, do not invent "known" or "existing" items. Prefer empty-state, no-match search, clear-search, or read-only list behavior scenarios.
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
Detail-view scenarios must target visible data entities from list rows, cards, tree nodes, or detail links; do not use filter tabs, counters, status tabs, breadcrumbs, or navigation controls as detail targets.
|
|
348
|
-
DO NOT propose "verification-only" tests that merely open a UI element (modal, dropdown, panel) and check it exists.
|
|
366
|
+
|
|
367
|
+
<meaningful_tests>
|
|
349
368
|
Every test must complete a meaningful action that changes application state or produces a business outcome.
|
|
369
|
+
DO NOT propose "verification-only" tests that have only assertions and no real state change
|
|
350
370
|
Opening a modal is NOT a test — performing an action INSIDE the modal IS a test.
|
|
351
371
|
Clicking a dropdown is NOT a test — selecting an option and verifying the result IS a test.
|
|
372
|
+
</meaningful_tests>
|
|
373
|
+
|
|
352
374
|
<priority_order>
|
|
353
375
|
Tests that change application data are MORE valuable than tests that only change the UI display.
|
|
354
376
|
Prioritize interactions that create, update, or delete data — these test real application behavior.
|
|
355
377
|
Tests that only switch views, toggle filters, or paginate are LESS valuable — propose them only after data-changing tests are covered.
|
|
356
378
|
If multiple ways to create or modify data exist (different types, different forms), propose a separate test for each.
|
|
379
|
+
If <docs_content> is provided make sure you incorporate app knowledge into planning
|
|
357
380
|
</priority_order>
|
|
358
381
|
${capabilityGroundingRule}
|
|
359
382
|
${dataProtectionRules}
|
|
@@ -362,28 +385,7 @@ export class Planner extends PlannerBase {
|
|
|
362
385
|
`;
|
|
363
386
|
conversation.addUserText(planningPrompt);
|
|
364
387
|
conversation.addUserText(this.getTasksMessage());
|
|
365
|
-
const
|
|
366
|
-
const research = await this.researcher.research(currentState || state, {
|
|
367
|
-
deep: true,
|
|
368
|
-
});
|
|
369
|
-
let docsPromise = null;
|
|
370
|
-
if (this.scout && this.docsWeight > 0) {
|
|
371
|
-
docsPromise = this.scout.collectDocs({ url: state.url, title: state.title, feature, excludeUrls: this.knowledgeTracker.applicationSpecUrls(state) });
|
|
372
|
-
}
|
|
373
|
-
let plannerResearch = mdq(research).query('code').replace('').toString();
|
|
374
|
-
plannerResearch = mdq(plannerResearch)
|
|
375
|
-
.query('table')
|
|
376
|
-
.replaceEach((table) => {
|
|
377
|
-
const rows = table.toJson();
|
|
378
|
-
if (rows.length === 0 || !rows[0].Element)
|
|
379
|
-
return table.text();
|
|
380
|
-
const elementWithType = rows.map((r) => ({
|
|
381
|
-
Element: r.Element,
|
|
382
|
-
Type: r.Type || '',
|
|
383
|
-
}));
|
|
384
|
-
return jsonToTable(elementWithType, ['Element', 'Type']);
|
|
385
|
-
})
|
|
386
|
-
.toString();
|
|
388
|
+
const [plannerResearch, docs] = await Promise.all([this.collectResearch(state), this.collectDocs(state, feature)]);
|
|
387
389
|
const hasFocusedOverlay = hasFocusedSection(plannerResearch);
|
|
388
390
|
const focusNote = hasFocusedOverlay ? "IMPORTANT: One section is marked as **Focused** — this is the user's current focus area. Concentrate testing on the Focused section FIRST — test all interactions inside it before planning tests for the rest of the page." : '';
|
|
389
391
|
const featureFilter = feature ? `FOCUS FILTER: Only propose scenarios using elements relevant to "${feature}". Ignore all other elements.` : '';
|
|
@@ -401,21 +403,22 @@ export class Planner extends PlannerBase {
|
|
|
401
403
|
const applicationContext = this.knowledgeTracker.renderApplicationSpec(state);
|
|
402
404
|
if (applicationContext) {
|
|
403
405
|
conversation.addUserText(applicationContext);
|
|
406
|
+
const words = applicationContext.split(/\s+/).filter(Boolean).length;
|
|
407
|
+
tag('info').log(`Added application spec to planning: ${words} words for ${state.url}`);
|
|
404
408
|
}
|
|
405
|
-
if (
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
Documentation retrieved from the collected corpus by the Scout agent.
|
|
411
|
-
Ground scenarios in these documented capabilities where they apply; treat them as supporting context, not a script.
|
|
409
|
+
if (docs) {
|
|
410
|
+
conversation.addUserText(dedent `
|
|
411
|
+
<docs_context>
|
|
412
|
+
Documentation retrieved from the collected corpus by the Scout agent.
|
|
413
|
+
Ground scenarios in these documented capabilities where they apply; treat them as supporting context, not a script.
|
|
412
414
|
|
|
413
|
-
|
|
415
|
+
Aim for roughly ${this.docsWeight}% of the scenarios to exercise behavior documented above; the remainder may explore beyond the documentation.
|
|
414
416
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
417
|
+
${docs}
|
|
418
|
+
</docs_context>
|
|
419
|
+
`);
|
|
420
|
+
const words = docs.split(/\s+/).filter(Boolean).length;
|
|
421
|
+
tag('info').log(`Added spec to planning (${words} words)`);
|
|
419
422
|
}
|
|
420
423
|
let activeRegion = '';
|
|
421
424
|
if (state.overlay.isOpen)
|
|
@@ -579,20 +582,14 @@ export class Planner extends PlannerBase {
|
|
|
579
582
|
getTasksMessage() {
|
|
580
583
|
return dedent `
|
|
581
584
|
<task>
|
|
582
|
-
Provide testing scenarios
|
|
585
|
+
Provide testing scenarios with the following requirements:
|
|
583
586
|
1. Create a short, descriptive plan name that summarizes what will be tested (e.g., "User Authentication Testing", "Product Catalog Navigation", "Form Validation Tests")
|
|
584
|
-
2.
|
|
585
|
-
- CRITICAL: Core business functionality that defines the page purpose
|
|
586
|
-
- IMPORTANT: Key user flows, primary features, CRUD operations
|
|
587
|
-
- HIGH: Secondary features, edge cases for critical flows
|
|
588
|
-
- NORMAL: Supporting actions, settings, configuration
|
|
589
|
-
- LOW: Cosmetic checks, boundary testing, minor interactions
|
|
590
|
-
3. Propose tests following research section order: ${this.sectionOrder.join(', ')}
|
|
587
|
+
2. Propose tests following research section order: ${this.sectionOrder.join(', ')}
|
|
591
588
|
Cover sections in this order — first propose tests for elements from earlier sections, then later ones.
|
|
592
589
|
Extended Research sections come after the main sections.
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
590
|
+
3. Focus on interactive elements of the page. Skip navigation links that lead away from current page.
|
|
591
|
+
4. Focus on tests you are 100% sure relevant to this page and can be achived from UI.
|
|
592
|
+
5. For each task, provide BOTH steps and expected outcomes as separate lists:
|
|
596
593
|
|
|
597
594
|
STEPS - actionable commands:
|
|
598
595
|
- Each step should be a specific action to perform
|
|
@@ -623,8 +620,63 @@ export class Planner extends PlannerBase {
|
|
|
623
620
|
- Do not wrap text in ** or * quotes, ( or ) brackets.
|
|
624
621
|
- Avoid using emojis or special characters.
|
|
625
622
|
7. Only tests that can be tested from web UI should be proposed.
|
|
626
|
-
8. For a fresh plan propose at least ${this.
|
|
623
|
+
8. For a fresh plan propose at least ${this.effectiveMinTasks} and at most ${this.effectiveMaxTasks} tests; when expanding an existing plan return ONLY new scenarios not in the tested list, and no scenarios if none remain.
|
|
627
624
|
</task>
|
|
625
|
+
|
|
626
|
+
<output_format>
|
|
627
|
+
Reply with the plan as markdown in exactly this format and nothing else:
|
|
628
|
+
|
|
629
|
+
<!-- suite -->
|
|
630
|
+
# {short plan name summarizing what is tested}
|
|
631
|
+
|
|
632
|
+
<!-- test -->
|
|
633
|
+
# {one sentence describing the behavior to test}
|
|
634
|
+
|
|
635
|
+
## Steps
|
|
636
|
+
* {first action}
|
|
637
|
+
* {next action}
|
|
638
|
+
* {one bullet per action, as many as the workflow needs}
|
|
639
|
+
|
|
640
|
+
## Expected
|
|
641
|
+
* {first verifiable outcome}
|
|
642
|
+
* {one bullet per outcome, as many as the scenario verifies}
|
|
643
|
+
|
|
644
|
+
Repeat the block from <!-- test to ## Expected for every scenario.
|
|
645
|
+
Every scenario needs its own <!-- test --> comment, its steps and its expected outcomes.
|
|
646
|
+
Steps list every action of the workflow in order, one action per bullet, from the starting page to the final action.
|
|
647
|
+
Expected lists every outcome to verify, one outcome per bullet.
|
|
648
|
+
|
|
649
|
+
When there is nothing to test, write no scenarios; after the plan name add one comment instead:
|
|
650
|
+
<!-- prerequisite: {step that must be done first} --> when the page shows a sign-in form or another required step instead of the content its URL is for
|
|
651
|
+
<!-- blocked: {one sentence why the page has nothing to test} --> otherwise
|
|
652
|
+
</output_format>
|
|
628
653
|
`;
|
|
629
654
|
}
|
|
655
|
+
async collectResearch(state) {
|
|
656
|
+
const currentState = this.stateManager.getCurrentState();
|
|
657
|
+
const research = await this.researcher.research(currentState || state, { deep: true });
|
|
658
|
+
let plannerResearch = mdq(research).query('code').replace('').toString();
|
|
659
|
+
plannerResearch = mdq(plannerResearch)
|
|
660
|
+
.query('table')
|
|
661
|
+
.replaceEach((table) => {
|
|
662
|
+
const rows = table.toJson();
|
|
663
|
+
if (rows.length === 0 || !rows[0].Element)
|
|
664
|
+
return table.text();
|
|
665
|
+
const elementWithType = rows.map((r) => ({
|
|
666
|
+
Element: r.Element,
|
|
667
|
+
Type: r.Type || '',
|
|
668
|
+
}));
|
|
669
|
+
return jsonToTable(elementWithType, ['Element', 'Type']);
|
|
670
|
+
})
|
|
671
|
+
.toString();
|
|
672
|
+
return plannerResearch;
|
|
673
|
+
}
|
|
674
|
+
async collectDocs(state, feature) {
|
|
675
|
+
if (!this.scout || this.docsWeight <= 0)
|
|
676
|
+
return '';
|
|
677
|
+
const query = ScoutQuery.fromState(state);
|
|
678
|
+
query.feature = feature;
|
|
679
|
+
query.exclude = this.knowledgeTracker.applicationSpecUrls(state);
|
|
680
|
+
return this.scout.collectDocs(query);
|
|
681
|
+
}
|
|
630
682
|
}
|
|
@@ -23,7 +23,7 @@ export declare class Provider {
|
|
|
23
23
|
stop(): Promise<void>;
|
|
24
24
|
getModelForAgent(agentName?: string): any;
|
|
25
25
|
setJudge(judge: Judge | null): void;
|
|
26
|
-
oracle(statement: string, state: unknown, options?: any): Promise<boolean>;
|
|
26
|
+
oracle(statement: string, state: unknown, options?: any): Promise<boolean | null>;
|
|
27
27
|
getAgenticModel(agentName?: string): any;
|
|
28
28
|
getVisionModel(): any;
|
|
29
29
|
getConfiguredModels(): Record<string, string>;
|
package/dist/src/ai/provider.js
CHANGED
|
@@ -138,13 +138,24 @@ export class Provider {
|
|
|
138
138
|
this.judge = judge;
|
|
139
139
|
}
|
|
140
140
|
async oracle(statement, state, options = {}) {
|
|
141
|
-
const
|
|
141
|
+
const { screenshot, ...generateOptions } = options;
|
|
142
|
+
let judgeState = state;
|
|
143
|
+
if (screenshot)
|
|
144
|
+
judgeState = { ...state, screenshot: `data:image/png;base64,${screenshot.toString('base64')}` };
|
|
145
|
+
const judged = await this.judge?.assert(statement, judgeState);
|
|
142
146
|
if (typeof judged === 'boolean')
|
|
143
147
|
return judged;
|
|
144
148
|
const schema = z.object({ holds: z.boolean().describe('whether the statement is true') });
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
149
|
+
let content = `${statement}\n\n${JSON.stringify(state, null, 2)}`;
|
|
150
|
+
if (screenshot)
|
|
151
|
+
content = [
|
|
152
|
+
{ type: 'text', text: content },
|
|
153
|
+
{ type: 'file', mediaType: 'image/png', data: screenshot },
|
|
154
|
+
];
|
|
155
|
+
const response = await this.generateObject([{ role: 'user', content }], schema, this.getAgenticModel(generateOptions.agentName), generateOptions).catch(() => null);
|
|
156
|
+
if (!response)
|
|
157
|
+
return null;
|
|
158
|
+
return response.object?.holds === true;
|
|
148
159
|
}
|
|
149
160
|
getAgenticModel(agentName) {
|
|
150
161
|
if (agentName) {
|
|
@@ -471,7 +482,7 @@ export class Provider {
|
|
|
471
482
|
// Log tool usage summary
|
|
472
483
|
if (response.toolCalls && response.toolCalls.length > 0) {
|
|
473
484
|
responseLog(response.toolCalls);
|
|
474
|
-
response.toolCalls.forEach((call
|
|
485
|
+
response.toolCalls.forEach((call) => {
|
|
475
486
|
tag('debug').log(`${call.toolName} > ${Object.values(call?.input || []).join(', ')}`);
|
|
476
487
|
});
|
|
477
488
|
}
|
package/dist/src/ai/rerunner.js
CHANGED
|
@@ -267,7 +267,7 @@ export class Rerunner extends TaskAgent {
|
|
|
267
267
|
async healStep(step, error) {
|
|
268
268
|
const failedCode = step.toCode?.() || '';
|
|
269
269
|
console.log(chalk.yellow(` ${figureSet.arrowRight} Healing: ${failedCode}`));
|
|
270
|
-
return async (
|
|
270
|
+
return async () => {
|
|
271
271
|
const bashTool = await createBashTool({
|
|
272
272
|
destination: this.traceDir,
|
|
273
273
|
onBeforeBashCall: ({ command }) => {
|
|
@@ -12,6 +12,7 @@ const FINGERPRINT_WORKER_TIMEOUT_MS = 10_000;
|
|
|
12
12
|
const SIMILARITY_THRESHOLD = 90;
|
|
13
13
|
const memoryCache = new TTLCache(CACHE_TTL_MS);
|
|
14
14
|
let fingerprintWorker = null;
|
|
15
|
+
let fingerprintRequestId = 0;
|
|
15
16
|
export function researchPath(hash) {
|
|
16
17
|
return outputPath('research', `${hash}.md`);
|
|
17
18
|
}
|
|
@@ -84,13 +85,13 @@ function findSimilarMatch(combinedHtml, url) {
|
|
|
84
85
|
if (!existsSync(statesDir))
|
|
85
86
|
return Promise.resolve(null);
|
|
86
87
|
const worker = getFingerprintWorker();
|
|
88
|
+
const id = ++fingerprintRequestId;
|
|
87
89
|
return new Promise((resolve) => {
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}, FINGERPRINT_WORKER_TIMEOUT_MS);
|
|
92
|
-
worker.on('message', (data) => {
|
|
90
|
+
const onMessage = (data) => {
|
|
91
|
+
if (data.id !== id)
|
|
92
|
+
return;
|
|
93
93
|
clearTimeout(timeout);
|
|
94
|
+
worker.off('message', onMessage);
|
|
94
95
|
const { matchHash, similarity } = data;
|
|
95
96
|
if (!matchHash) {
|
|
96
97
|
resolve(null);
|
|
@@ -98,8 +99,15 @@ function findSimilarMatch(combinedHtml, url) {
|
|
|
98
99
|
}
|
|
99
100
|
debugLog(`Similar fingerprint found: ${matchHash} (${similarity}% similar)`);
|
|
100
101
|
resolve({ hash: matchHash, similarity, url: data.url });
|
|
101
|
-
}
|
|
102
|
+
};
|
|
103
|
+
const timeout = setTimeout(() => {
|
|
104
|
+
worker.off('message', onMessage);
|
|
105
|
+
debugLog('Fingerprint worker timed out');
|
|
106
|
+
resolve(null);
|
|
107
|
+
}, FINGERPRINT_WORKER_TIMEOUT_MS);
|
|
108
|
+
worker.on('message', onMessage);
|
|
102
109
|
worker.postMessage({
|
|
110
|
+
id,
|
|
103
111
|
html: combinedHtml,
|
|
104
112
|
statesDir,
|
|
105
113
|
maxAgeMs: FINGERPRINT_MAX_AGE_MS,
|
|
@@ -14,6 +14,7 @@ export declare function WithDeepAnalysis<T extends Constructor>(Base: T): {
|
|
|
14
14
|
stateManager: StateManager;
|
|
15
15
|
config: ExplorbotConfig;
|
|
16
16
|
actionResult: ActionResult | undefined;
|
|
17
|
+
researchedOverlays: Set<string>;
|
|
17
18
|
performDeepAnalysis(state: WebPageState, result: ResearchResult): Promise<void>;
|
|
18
19
|
researchOverlay(current: ActionResult, previous: ActionResult, pageStateHash: string): Promise<string | null>;
|
|
19
20
|
_loadPreviousExtendedSections(hash: string): PreviousSection[];
|
|
@@ -26,7 +27,7 @@ export declare function WithDeepAnalysis<T extends Constructor>(Base: T): {
|
|
|
26
27
|
code: string;
|
|
27
28
|
url: string;
|
|
28
29
|
}>): void;
|
|
29
|
-
_discoverExpandables(researchText: string, missing?: PreviousSection[]
|
|
30
|
+
_discoverExpandables(researchText: string, missing?: PreviousSection[]): Promise<ExpandableElement[]>;
|
|
30
31
|
_buildClickCommands(el: ExpandableElement): string[];
|
|
31
32
|
_selectExpandables(expandables: ExpandableElement[], url: string, maxClicks: number): Promise<ExpandableElement[]>;
|
|
32
33
|
_clickExpandableElements(elements: Array<{
|
|
@@ -14,6 +14,7 @@ const DEFAULT_MAX_EXPANDABLE_CLICKS = 10;
|
|
|
14
14
|
const MAX_HTML_DIFF_CHARS = 20_000;
|
|
15
15
|
export function WithDeepAnalysis(Base) {
|
|
16
16
|
return class extends Base {
|
|
17
|
+
researchedOverlays = new Set();
|
|
17
18
|
async performDeepAnalysis(state, result) {
|
|
18
19
|
tag('info').log('Starting deep analysis of expandable elements');
|
|
19
20
|
await this.navigateTo(state.fullUrl || state.url);
|
|
@@ -36,7 +37,7 @@ export function WithDeepAnalysis(Base) {
|
|
|
36
37
|
return;
|
|
37
38
|
}
|
|
38
39
|
}
|
|
39
|
-
let expandables = await this._discoverExpandables(result.text, missing
|
|
40
|
+
let expandables = await this._discoverExpandables(result.text, missing);
|
|
40
41
|
if (expandables.length > maxClicks) {
|
|
41
42
|
expandables = await this._selectExpandables(expandables, state.fullUrl || state.url, maxClicks);
|
|
42
43
|
tag('substep').log(`Selected ${expandables.length} expandables to click (max: ${maxClicks})`);
|
|
@@ -68,6 +69,10 @@ export function WithDeepAnalysis(Base) {
|
|
|
68
69
|
const region = current.overlay;
|
|
69
70
|
if (!region.isOpen || !region.name)
|
|
70
71
|
return null;
|
|
72
|
+
const overlayKey = `${pageStateHash}:${region.name}`;
|
|
73
|
+
if (this.researchedOverlays.has(overlayKey))
|
|
74
|
+
return null;
|
|
75
|
+
this.researchedOverlays.add(overlayKey);
|
|
71
76
|
const cached = getCachedResearch(pageStateHash);
|
|
72
77
|
if (!cached)
|
|
73
78
|
return null;
|
|
@@ -162,7 +167,7 @@ export function WithDeepAnalysis(Base) {
|
|
|
162
167
|
result.text += `\n\n## Navigation Links\n\n${links}`;
|
|
163
168
|
}
|
|
164
169
|
}
|
|
165
|
-
async _discoverExpandables(researchText, missing = []
|
|
170
|
+
async _discoverExpandables(researchText, missing = []) {
|
|
166
171
|
const allElements = new Map();
|
|
167
172
|
for (const section of parseResearchSections(researchText)) {
|
|
168
173
|
for (const el of section.elements) {
|
|
@@ -5,14 +5,14 @@ import { computeHtmlFingerprint } from "../../utils/html-diff.js";
|
|
|
5
5
|
import { isSamePageFamily } from "../../utils/url-matcher.js";
|
|
6
6
|
import { diceSimilarity } from "../../utils/similarity.js";
|
|
7
7
|
parentPort.on('message', (data) => {
|
|
8
|
-
const { html, statesDir, maxAgeMs, threshold, url } = data;
|
|
8
|
+
const { id, html, statesDir, maxAgeMs, threshold, url } = data;
|
|
9
9
|
if (!existsSync(statesDir)) {
|
|
10
|
-
parentPort.postMessage({ matchHash: null, similarity: 0 });
|
|
10
|
+
parentPort.postMessage({ id, matchHash: null, similarity: 0 });
|
|
11
11
|
return;
|
|
12
12
|
}
|
|
13
13
|
const currentFingerprint = new Set(computeHtmlFingerprint(html));
|
|
14
14
|
if (currentFingerprint.size === 0) {
|
|
15
|
-
parentPort.postMessage({ matchHash: null, similarity: 0 });
|
|
15
|
+
parentPort.postMessage({ id, matchHash: null, similarity: 0 });
|
|
16
16
|
return;
|
|
17
17
|
}
|
|
18
18
|
const now = Date.now();
|
|
@@ -37,7 +37,7 @@ parentPort.on('message', (data) => {
|
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
const matched = bestSimilarity >= threshold;
|
|
40
|
-
parentPort.postMessage({ matchHash: matched ? bestHash : null, similarity: bestSimilarity, url: matched ? bestUrl : undefined });
|
|
40
|
+
parentPort.postMessage({ id, matchHash: matched ? bestHash : null, similarity: bestSimilarity, url: matched ? bestUrl : undefined });
|
|
41
41
|
});
|
|
42
42
|
function readFingerprint(filePath) {
|
|
43
43
|
const content = readFileSync(filePath, 'utf8');
|