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/planner.ts
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
import dedent from 'dedent';
|
|
2
|
-
import { z } from 'zod';
|
|
3
2
|
import { ActionResult } from '../action-result.ts';
|
|
4
3
|
import { setActivity } from '../activity.ts';
|
|
5
4
|
import { ConfigParser } from '../config.ts';
|
|
6
5
|
import type { ExperienceTracker } from '../experience-tracker.ts';
|
|
7
|
-
import type Explorer from '../explorer.ts';
|
|
8
6
|
import { Observability } from '../observability.ts';
|
|
9
7
|
import type { StateManager } from '../state-manager.js';
|
|
10
8
|
import { Stats } from '../stats.ts';
|
|
11
9
|
import { Suite } from '../suite.ts';
|
|
12
10
|
import { Plan, Test } from '../test-plan.ts';
|
|
11
|
+
import { PlanningBlockedError } from '../utils/error-page.ts';
|
|
13
12
|
import { createDebug, tag } from '../utils/logger.js';
|
|
13
|
+
import { loop } from '../utils/loop.ts';
|
|
14
14
|
import { jsonToTable } from '../utils/markdown-parser.ts';
|
|
15
15
|
import { mdq } from '../utils/markdown-query.js';
|
|
16
|
-
import { planToCompactAiContext } from '../utils/test-plan-markdown.ts';
|
|
16
|
+
import { findPlanMarkdownProblems, parsePlansFromMarkdownText, planToCompactAiContext, readPlanMarkdownComment } from '../utils/test-plan-markdown.ts';
|
|
17
17
|
import type { Agent, AgentDeps } from './agent.js';
|
|
18
18
|
import { Conversation } from './conversation.ts';
|
|
19
19
|
import type { Fisherman } from './fisherman.ts';
|
|
@@ -26,33 +26,16 @@ import { findSimilarStateHash } from './researcher/cache.ts';
|
|
|
26
26
|
import { hasFocusedSection } from './researcher/focus.ts';
|
|
27
27
|
import { RulesLoader } from '../utils/rules-loader.ts';
|
|
28
28
|
import { capabilityGroundingRule, dataProtectionRules, fileUploadRule } from './rules.ts';
|
|
29
|
-
import type
|
|
29
|
+
import { ScoutQuery, type Scout } from './scout.ts';
|
|
30
30
|
|
|
31
31
|
const debugLog = createDebug('explorbot:planner');
|
|
32
32
|
|
|
33
|
-
const
|
|
34
|
-
planName: z.string().describe('Short descriptive name for the test plan (e.g., "User Authentication Testing", "Product Catalog Navigation", "Form Validation Tests")'),
|
|
35
|
-
scenarios: z
|
|
36
|
-
.array(
|
|
37
|
-
z.object({
|
|
38
|
-
scenario: z.string().describe('A single sentence describing the behavior to test.'),
|
|
39
|
-
priority: z.enum(['critical', 'important', 'high', 'normal', 'low']).describe('Priority of the task based on business importance'),
|
|
40
|
-
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.'),
|
|
41
|
-
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.'),
|
|
42
|
-
expectedOutcomes: z
|
|
43
|
-
.array(z.string())
|
|
44
|
-
.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.'),
|
|
45
|
-
})
|
|
46
|
-
)
|
|
47
|
-
.describe('List of testing scenarios'),
|
|
48
|
-
notPlannableReason: z.string().nullish().describe('When scenarios is empty, one sentence saying why this page has nothing to test; otherwise null.'),
|
|
49
|
-
});
|
|
33
|
+
const MAX_FORMAT_ATTEMPTS = 3;
|
|
50
34
|
|
|
51
35
|
const PlannerBase = WithSessionDedup(WithSubPages(Object as unknown as new (...args: any[]) => object));
|
|
52
36
|
|
|
53
37
|
export class Planner extends PlannerBase implements Agent {
|
|
54
38
|
emoji = '📋';
|
|
55
|
-
private explorer: Explorer;
|
|
56
39
|
provider: Provider;
|
|
57
40
|
stateManager: StateManager;
|
|
58
41
|
private experienceTracker: ExperienceTracker;
|
|
@@ -60,6 +43,8 @@ export class Planner extends PlannerBase implements Agent {
|
|
|
60
43
|
|
|
61
44
|
MIN_TASKS = 3;
|
|
62
45
|
MAX_TASKS = 12;
|
|
46
|
+
private minTasksOverride?: number;
|
|
47
|
+
private maxTasksOverride?: number;
|
|
63
48
|
currentPlan: Plan | null = null;
|
|
64
49
|
freshStart = false;
|
|
65
50
|
private lastStyleName = '';
|
|
@@ -70,7 +55,6 @@ export class Planner extends PlannerBase implements Agent {
|
|
|
70
55
|
|
|
71
56
|
constructor(deps: AgentDeps, researcher: Researcher) {
|
|
72
57
|
super();
|
|
73
|
-
this.explorer = deps.explorer;
|
|
74
58
|
this.provider = deps.ai;
|
|
75
59
|
this.researcher = researcher;
|
|
76
60
|
this.stateManager = deps.stateManager;
|
|
@@ -95,8 +79,23 @@ export class Planner extends PlannerBase implements Agent {
|
|
|
95
79
|
return Math.max(0, Math.min(100, value));
|
|
96
80
|
}
|
|
97
81
|
|
|
98
|
-
private
|
|
99
|
-
|
|
82
|
+
private get effectiveMinTasks(): number {
|
|
83
|
+
if (this.minTasksOverride !== undefined) return this.minTasksOverride;
|
|
84
|
+
return ConfigParser.getInstance().getConfig().ai?.agents?.planner?.minTests ?? this.MIN_TASKS;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private get effectiveMaxTasks(): number {
|
|
88
|
+
if (this.maxTasksOverride !== undefined) return this.maxTasksOverride;
|
|
89
|
+
return ConfigParser.getInstance().getConfig().ai?.agents?.planner?.maxTests ?? this.MAX_TASKS;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
setTestCount(opts: { minTests?: number; maxTests?: number }): void {
|
|
93
|
+
if (opts.minTests !== undefined) this.minTasksOverride = opts.minTests;
|
|
94
|
+
if (opts.maxTests !== undefined) this.maxTasksOverride = opts.maxTests;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private getDefaultStartUrl(state: { url: string }): string {
|
|
98
|
+
return state.url;
|
|
100
99
|
}
|
|
101
100
|
|
|
102
101
|
getSystemMessage(feature?: string): string {
|
|
@@ -150,7 +149,7 @@ export class Planner extends PlannerBase implements Agent {
|
|
|
150
149
|
return getRegisteredPlan(url)?.plan || null;
|
|
151
150
|
}
|
|
152
151
|
|
|
153
|
-
async plan(feature?: string, style?: string, parentPlan?: Plan
|
|
152
|
+
async plan(feature?: string, style?: string, parentPlan?: Plan): Promise<Plan> {
|
|
154
153
|
Stats.plans++;
|
|
155
154
|
const state = this.stateManager.getCurrentState();
|
|
156
155
|
debugLog('Planning:', state?.url);
|
|
@@ -204,22 +203,42 @@ export class Planner extends PlannerBase implements Agent {
|
|
|
204
203
|
tag('step').log('Focusing on main content of this page');
|
|
205
204
|
}
|
|
206
205
|
|
|
207
|
-
debugLog('Sending planning prompt to AI provider
|
|
208
|
-
|
|
209
|
-
|
|
206
|
+
debugLog('Sending planning prompt to AI provider');
|
|
207
|
+
|
|
208
|
+
let markdown = '';
|
|
209
|
+
await loop(
|
|
210
|
+
async ({ stop, iteration }) => {
|
|
211
|
+
const response = await this.provider.chat(conversation.messages, conversation.model, { agentName: 'planner' });
|
|
212
|
+
markdown = response.text;
|
|
213
|
+
const problems = findPlanMarkdownProblems(markdown);
|
|
214
|
+
if (problems.length === 0) stop();
|
|
215
|
+
if (iteration >= MAX_FORMAT_ATTEMPTS) throw new Error(`Planner output does not match the plan format: ${problems.join('; ')}`);
|
|
216
|
+
debugLog('Plan format problems: %o', problems);
|
|
217
|
+
conversation.addAssistantText(markdown);
|
|
218
|
+
conversation.addUserText(dedent`
|
|
219
|
+
Your plan does not match the required format:
|
|
220
|
+
${problems.map((problem) => `- ${problem}`).join('\n')}
|
|
221
|
+
Rewrite the whole plan in the format from <output_format>. Keep the same scenarios; fix only the structure.
|
|
222
|
+
`);
|
|
223
|
+
},
|
|
224
|
+
{ maxAttempts: MAX_FORMAT_ATTEMPTS, interruptible: false }
|
|
225
|
+
);
|
|
210
226
|
|
|
211
|
-
|
|
212
|
-
|
|
227
|
+
const defaultStartUrl = this.getDefaultStartUrl(state);
|
|
228
|
+
markdown = mdq(markdown).query('h1').first().insertAfter(`### Prerequisite\n\n* URL: ${defaultStartUrl}`).toString();
|
|
229
|
+
const parsedPlan = parsePlansFromMarkdownText(markdown)[0];
|
|
230
|
+
const fromPlanning = parsedPlan?.tests || [];
|
|
231
|
+
const blockedBy = readPlanMarkdownComment(markdown, 'blocked');
|
|
232
|
+
const prerequisite = readPlanMarkdownComment(markdown, 'prerequisite');
|
|
233
|
+
|
|
234
|
+
if (!this.currentPlan && fromPlanning.length === 0 && blockedBy) {
|
|
235
|
+
tag('warning').log(`Page is not plannable: ${blockedBy}`);
|
|
213
236
|
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
tag('warning').log(`Page is not plannable: ${aiResult.object.notPlannableReason}`);
|
|
237
|
+
if (!this.currentPlan && fromPlanning.length === 0 && prerequisite) {
|
|
238
|
+
throw new PlanningBlockedError(state.url, prerequisite, feature, style);
|
|
217
239
|
}
|
|
218
240
|
|
|
219
|
-
|
|
220
|
-
const fromPlanning = aiResult.object.scenarios.map((s: any) => new Test(s.scenario, s.priority, s.expectedOutcomes, s.startUrl || defaultStartUrl, s.steps || []));
|
|
221
|
-
|
|
222
|
-
return { tests: fromPlanning, planName: aiResult.object.planName };
|
|
241
|
+
return { tests: fromPlanning, planName: parsedPlan?.title };
|
|
223
242
|
});
|
|
224
243
|
|
|
225
244
|
const tests = result.tests;
|
|
@@ -345,9 +364,6 @@ export class Planner extends PlannerBase implements Agent {
|
|
|
345
364
|
private async buildConversation(state: ActionResult, style?: string, parentPlan?: Plan, feature?: string): Promise<Conversation> {
|
|
346
365
|
const model = this.provider.getAgenticModel('planner');
|
|
347
366
|
const conversation = new Conversation([], model);
|
|
348
|
-
conversation.autoTrimTag('page_research', 20000);
|
|
349
|
-
conversation.autoTrimTag('tested_scenarios', 10000);
|
|
350
|
-
conversation.autoTrimTag('docs_context', 8000);
|
|
351
367
|
|
|
352
368
|
conversation.addUserText(this.getSystemMessage(feature));
|
|
353
369
|
|
|
@@ -380,23 +396,25 @@ export class Planner extends PlannerBase implements Agent {
|
|
|
380
396
|
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.
|
|
381
397
|
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.
|
|
382
398
|
Only propose scenarios whose prerequisites are evident from page research, visited pages, or API data preparation context.
|
|
383
|
-
If a scenario needs existing records
|
|
384
|
-
If the page appears read-only, degraded, demo-limited, maintenance-like, or lacks
|
|
399
|
+
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.
|
|
400
|
+
If the page appears read-only, degraded, demo-limited, maintenance-like, or lacks UI controls skip that page.
|
|
385
401
|
Do not assume hidden data exists just because a control is present.
|
|
386
402
|
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.
|
|
387
403
|
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.
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
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.
|
|
391
|
-
DO NOT propose "verification-only" tests that merely open a UI element (modal, dropdown, panel) and check it exists.
|
|
404
|
+
|
|
405
|
+
<meaningful_tests>
|
|
392
406
|
Every test must complete a meaningful action that changes application state or produces a business outcome.
|
|
407
|
+
DO NOT propose "verification-only" tests that have only assertions and no real state change
|
|
393
408
|
Opening a modal is NOT a test — performing an action INSIDE the modal IS a test.
|
|
394
409
|
Clicking a dropdown is NOT a test — selecting an option and verifying the result IS a test.
|
|
410
|
+
</meaningful_tests>
|
|
411
|
+
|
|
395
412
|
<priority_order>
|
|
396
413
|
Tests that change application data are MORE valuable than tests that only change the UI display.
|
|
397
414
|
Prioritize interactions that create, update, or delete data — these test real application behavior.
|
|
398
415
|
Tests that only switch views, toggle filters, or paginate are LESS valuable — propose them only after data-changing tests are covered.
|
|
399
416
|
If multiple ways to create or modify data exist (different types, different forms), propose a separate test for each.
|
|
417
|
+
If <docs_content> is provided make sure you incorporate app knowledge into planning
|
|
400
418
|
</priority_order>
|
|
401
419
|
${capabilityGroundingRule}
|
|
402
420
|
${dataProtectionRules}
|
|
@@ -406,28 +424,8 @@ export class Planner extends PlannerBase implements Agent {
|
|
|
406
424
|
|
|
407
425
|
conversation.addUserText(planningPrompt);
|
|
408
426
|
conversation.addUserText(this.getTasksMessage());
|
|
409
|
-
const currentState = this.stateManager.getCurrentState();
|
|
410
|
-
const research = await this.researcher.research(currentState || state, {
|
|
411
|
-
deep: true,
|
|
412
|
-
});
|
|
413
427
|
|
|
414
|
-
|
|
415
|
-
if (this.scout && this.docsWeight > 0) {
|
|
416
|
-
docsPromise = this.scout.collectDocs({ url: state.url, title: state.title, feature, excludeUrls: this.knowledgeTracker.applicationSpecUrls(state) });
|
|
417
|
-
}
|
|
418
|
-
let plannerResearch = mdq(research).query('code').replace('').toString();
|
|
419
|
-
plannerResearch = mdq(plannerResearch)
|
|
420
|
-
.query('table')
|
|
421
|
-
.replaceEach((table) => {
|
|
422
|
-
const rows = table.toJson();
|
|
423
|
-
if (rows.length === 0 || !rows[0].Element) return table.text();
|
|
424
|
-
const elementWithType = rows.map((r) => ({
|
|
425
|
-
Element: r.Element,
|
|
426
|
-
Type: r.Type || '',
|
|
427
|
-
}));
|
|
428
|
-
return jsonToTable(elementWithType, ['Element', 'Type']);
|
|
429
|
-
})
|
|
430
|
-
.toString();
|
|
428
|
+
const [plannerResearch, docs] = await Promise.all([this.collectResearch(state), this.collectDocs(state, feature)]);
|
|
431
429
|
|
|
432
430
|
const hasFocusedOverlay = hasFocusedSection(plannerResearch);
|
|
433
431
|
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." : '';
|
|
@@ -448,22 +446,23 @@ export class Planner extends PlannerBase implements Agent {
|
|
|
448
446
|
const applicationContext = this.knowledgeTracker.renderApplicationSpec(state);
|
|
449
447
|
if (applicationContext) {
|
|
450
448
|
conversation.addUserText(applicationContext);
|
|
449
|
+
const words = applicationContext.split(/\s+/).filter(Boolean).length;
|
|
450
|
+
tag('info').log(`Added application spec to planning: ${words} words for ${state.url}`);
|
|
451
451
|
}
|
|
452
452
|
|
|
453
|
-
if (
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
Documentation retrieved from the collected corpus by the Scout agent.
|
|
459
|
-
Ground scenarios in these documented capabilities where they apply; treat them as supporting context, not a script.
|
|
453
|
+
if (docs) {
|
|
454
|
+
conversation.addUserText(dedent`
|
|
455
|
+
<docs_context>
|
|
456
|
+
Documentation retrieved from the collected corpus by the Scout agent.
|
|
457
|
+
Ground scenarios in these documented capabilities where they apply; treat them as supporting context, not a script.
|
|
460
458
|
|
|
461
|
-
|
|
459
|
+
Aim for roughly ${this.docsWeight}% of the scenarios to exercise behavior documented above; the remainder may explore beyond the documentation.
|
|
462
460
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
461
|
+
${docs}
|
|
462
|
+
</docs_context>
|
|
463
|
+
`);
|
|
464
|
+
const words = docs.split(/\s+/).filter(Boolean).length;
|
|
465
|
+
tag('info').log(`Added spec to planning (${words} words)`);
|
|
467
466
|
}
|
|
468
467
|
|
|
469
468
|
let activeRegion = '';
|
|
@@ -638,20 +637,14 @@ export class Planner extends PlannerBase implements Agent {
|
|
|
638
637
|
private getTasksMessage(): string {
|
|
639
638
|
return dedent`
|
|
640
639
|
<task>
|
|
641
|
-
Provide testing scenarios
|
|
640
|
+
Provide testing scenarios with the following requirements:
|
|
642
641
|
1. Create a short, descriptive plan name that summarizes what will be tested (e.g., "User Authentication Testing", "Product Catalog Navigation", "Form Validation Tests")
|
|
643
|
-
2.
|
|
644
|
-
- CRITICAL: Core business functionality that defines the page purpose
|
|
645
|
-
- IMPORTANT: Key user flows, primary features, CRUD operations
|
|
646
|
-
- HIGH: Secondary features, edge cases for critical flows
|
|
647
|
-
- NORMAL: Supporting actions, settings, configuration
|
|
648
|
-
- LOW: Cosmetic checks, boundary testing, minor interactions
|
|
649
|
-
3. Propose tests following research section order: ${this.sectionOrder.join(', ')}
|
|
642
|
+
2. Propose tests following research section order: ${this.sectionOrder.join(', ')}
|
|
650
643
|
Cover sections in this order — first propose tests for elements from earlier sections, then later ones.
|
|
651
644
|
Extended Research sections come after the main sections.
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
645
|
+
3. Focus on interactive elements of the page. Skip navigation links that lead away from current page.
|
|
646
|
+
4. Focus on tests you are 100% sure relevant to this page and can be achived from UI.
|
|
647
|
+
5. For each task, provide BOTH steps and expected outcomes as separate lists:
|
|
655
648
|
|
|
656
649
|
STEPS - actionable commands:
|
|
657
650
|
- Each step should be a specific action to perform
|
|
@@ -682,8 +675,63 @@ export class Planner extends PlannerBase implements Agent {
|
|
|
682
675
|
- Do not wrap text in ** or * quotes, ( or ) brackets.
|
|
683
676
|
- Avoid using emojis or special characters.
|
|
684
677
|
7. Only tests that can be tested from web UI should be proposed.
|
|
685
|
-
8. For a fresh plan propose at least ${this.
|
|
678
|
+
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.
|
|
686
679
|
</task>
|
|
680
|
+
|
|
681
|
+
<output_format>
|
|
682
|
+
Reply with the plan as markdown in exactly this format and nothing else:
|
|
683
|
+
|
|
684
|
+
<!-- suite -->
|
|
685
|
+
# {short plan name summarizing what is tested}
|
|
686
|
+
|
|
687
|
+
<!-- test -->
|
|
688
|
+
# {one sentence describing the behavior to test}
|
|
689
|
+
|
|
690
|
+
## Steps
|
|
691
|
+
* {first action}
|
|
692
|
+
* {next action}
|
|
693
|
+
* {one bullet per action, as many as the workflow needs}
|
|
694
|
+
|
|
695
|
+
## Expected
|
|
696
|
+
* {first verifiable outcome}
|
|
697
|
+
* {one bullet per outcome, as many as the scenario verifies}
|
|
698
|
+
|
|
699
|
+
Repeat the block from <!-- test to ## Expected for every scenario.
|
|
700
|
+
Every scenario needs its own <!-- test --> comment, its steps and its expected outcomes.
|
|
701
|
+
Steps list every action of the workflow in order, one action per bullet, from the starting page to the final action.
|
|
702
|
+
Expected lists every outcome to verify, one outcome per bullet.
|
|
703
|
+
|
|
704
|
+
When there is nothing to test, write no scenarios; after the plan name add one comment instead:
|
|
705
|
+
<!-- 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
|
|
706
|
+
<!-- blocked: {one sentence why the page has nothing to test} --> otherwise
|
|
707
|
+
</output_format>
|
|
687
708
|
`;
|
|
688
709
|
}
|
|
710
|
+
|
|
711
|
+
private async collectResearch(state: ActionResult): Promise<string> {
|
|
712
|
+
const currentState = this.stateManager.getCurrentState();
|
|
713
|
+
const research = await this.researcher.research(currentState || state, { deep: true });
|
|
714
|
+
let plannerResearch = mdq(research).query('code').replace('').toString();
|
|
715
|
+
plannerResearch = mdq(plannerResearch)
|
|
716
|
+
.query('table')
|
|
717
|
+
.replaceEach((table) => {
|
|
718
|
+
const rows = table.toJson();
|
|
719
|
+
if (rows.length === 0 || !rows[0].Element) return table.text();
|
|
720
|
+
const elementWithType = rows.map((r) => ({
|
|
721
|
+
Element: r.Element,
|
|
722
|
+
Type: r.Type || '',
|
|
723
|
+
}));
|
|
724
|
+
return jsonToTable(elementWithType, ['Element', 'Type']);
|
|
725
|
+
})
|
|
726
|
+
.toString();
|
|
727
|
+
return plannerResearch;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
private async collectDocs(state: ActionResult, feature?: string): Promise<string> {
|
|
731
|
+
if (!this.scout || this.docsWeight <= 0) return '';
|
|
732
|
+
const query = ScoutQuery.fromState(state);
|
|
733
|
+
query.feature = feature;
|
|
734
|
+
query.exclude = this.knowledgeTracker.applicationSpecUrls(state);
|
|
735
|
+
return this.scout.collectDocs(query);
|
|
736
|
+
}
|
|
689
737
|
}
|
package/src/ai/provider.ts
CHANGED
|
@@ -155,14 +155,23 @@ export class Provider {
|
|
|
155
155
|
this.judge = judge;
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
-
async oracle(statement: string, state: unknown, options: any = {}): Promise<boolean> {
|
|
159
|
-
const
|
|
158
|
+
async oracle(statement: string, state: unknown, options: any = {}): Promise<boolean | null> {
|
|
159
|
+
const { screenshot, ...generateOptions } = options;
|
|
160
|
+
let judgeState = state;
|
|
161
|
+
if (screenshot) judgeState = { ...(state as object), screenshot: `data:image/png;base64,${screenshot.toString('base64')}` };
|
|
162
|
+
const judged = await this.judge?.assert(statement, judgeState);
|
|
160
163
|
if (typeof judged === 'boolean') return judged;
|
|
161
164
|
|
|
162
165
|
const schema = z.object({ holds: z.boolean().describe('whether the statement is true') });
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
+
let content: any = `${statement}\n\n${JSON.stringify(state, null, 2)}`;
|
|
167
|
+
if (screenshot)
|
|
168
|
+
content = [
|
|
169
|
+
{ type: 'text', text: content },
|
|
170
|
+
{ type: 'file', mediaType: 'image/png', data: screenshot },
|
|
171
|
+
];
|
|
172
|
+
const response = await this.generateObject([{ role: 'user' as const, content }], schema, this.getAgenticModel(generateOptions.agentName), generateOptions).catch(() => null);
|
|
173
|
+
if (!response) return null;
|
|
174
|
+
return response.object?.holds === true;
|
|
166
175
|
}
|
|
167
176
|
|
|
168
177
|
getAgenticModel(agentName?: string): any {
|
|
@@ -520,7 +529,7 @@ export class Provider {
|
|
|
520
529
|
// Log tool usage summary
|
|
521
530
|
if (response.toolCalls && response.toolCalls.length > 0) {
|
|
522
531
|
responseLog(response.toolCalls);
|
|
523
|
-
response.toolCalls.forEach((call: any
|
|
532
|
+
response.toolCalls.forEach((call: any) => {
|
|
524
533
|
tag('debug').log(`${call.toolName} > ${Object.values(call?.input || []).join(', ')}`);
|
|
525
534
|
});
|
|
526
535
|
}
|
package/src/ai/rerunner.ts
CHANGED
|
@@ -23,7 +23,6 @@ import { ensureCodeceptResolvable } from '../utils/test-files.ts';
|
|
|
23
23
|
import type { Agent, AgentDeps } from './agent.ts';
|
|
24
24
|
import { toolExecutionLabel } from './conversation.ts';
|
|
25
25
|
import type { Navigator } from './navigator.ts';
|
|
26
|
-
import { Provider } from './provider.ts';
|
|
27
26
|
import { actionRule, locatorRule, sectionContextRule } from './rules.ts';
|
|
28
27
|
import { TaskAgent } from './task-agent.ts';
|
|
29
28
|
import { createCodeceptJSTools, withdrawVisionTools } from './tools.ts';
|
|
@@ -300,7 +299,7 @@ export class Rerunner extends TaskAgent implements Agent {
|
|
|
300
299
|
const failedCode = step.toCode?.() || '';
|
|
301
300
|
console.log(chalk.yellow(` ${figureSet.arrowRight} Healing: ${failedCode}`));
|
|
302
301
|
|
|
303
|
-
return async (
|
|
302
|
+
return async () => {
|
|
304
303
|
const bashTool = await createBashTool({
|
|
305
304
|
destination: this.traceDir,
|
|
306
305
|
onBeforeBashCall: ({ command }) => {
|
|
@@ -15,6 +15,7 @@ const SIMILARITY_THRESHOLD = 90;
|
|
|
15
15
|
const memoryCache = new TTLCache<string>(CACHE_TTL_MS);
|
|
16
16
|
|
|
17
17
|
let fingerprintWorker: Worker | null = null;
|
|
18
|
+
let fingerprintRequestId = 0;
|
|
18
19
|
|
|
19
20
|
export function researchPath(hash: string): string {
|
|
20
21
|
return outputPath('research', `${hash}.md`);
|
|
@@ -90,14 +91,13 @@ function findSimilarMatch(combinedHtml: string, url?: string): Promise<Fingerpri
|
|
|
90
91
|
|
|
91
92
|
const worker = getFingerprintWorker();
|
|
92
93
|
|
|
93
|
-
|
|
94
|
-
const timeout = setTimeout(() => {
|
|
95
|
-
debugLog('Fingerprint worker timed out');
|
|
96
|
-
resolve(null);
|
|
97
|
-
}, FINGERPRINT_WORKER_TIMEOUT_MS);
|
|
94
|
+
const id = ++fingerprintRequestId;
|
|
98
95
|
|
|
99
|
-
|
|
96
|
+
return new Promise((resolve) => {
|
|
97
|
+
const onMessage = (data: { id: number; matchHash: string | null; similarity: number; url?: string }) => {
|
|
98
|
+
if (data.id !== id) return;
|
|
100
99
|
clearTimeout(timeout);
|
|
100
|
+
worker.off('message', onMessage);
|
|
101
101
|
const { matchHash, similarity } = data;
|
|
102
102
|
if (!matchHash) {
|
|
103
103
|
resolve(null);
|
|
@@ -106,9 +106,18 @@ function findSimilarMatch(combinedHtml: string, url?: string): Promise<Fingerpri
|
|
|
106
106
|
|
|
107
107
|
debugLog(`Similar fingerprint found: ${matchHash} (${similarity}% similar)`);
|
|
108
108
|
resolve({ hash: matchHash, similarity, url: data.url });
|
|
109
|
-
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const timeout = setTimeout(() => {
|
|
112
|
+
worker.off('message', onMessage);
|
|
113
|
+
debugLog('Fingerprint worker timed out');
|
|
114
|
+
resolve(null);
|
|
115
|
+
}, FINGERPRINT_WORKER_TIMEOUT_MS);
|
|
116
|
+
|
|
117
|
+
worker.on('message', onMessage);
|
|
110
118
|
|
|
111
119
|
worker.postMessage({
|
|
120
|
+
id,
|
|
112
121
|
html: combinedHtml,
|
|
113
122
|
statesDir,
|
|
114
123
|
maxAgeMs: FINGERPRINT_MAX_AGE_MS,
|
|
@@ -26,6 +26,7 @@ export function WithDeepAnalysis<T extends Constructor>(Base: T) {
|
|
|
26
26
|
declare stateManager: StateManager;
|
|
27
27
|
declare config: ExplorbotConfig;
|
|
28
28
|
declare actionResult: ActionResult | undefined;
|
|
29
|
+
researchedOverlays = new Set<string>();
|
|
29
30
|
|
|
30
31
|
async performDeepAnalysis(state: WebPageState, result: ResearchResult): Promise<void> {
|
|
31
32
|
tag('info').log('Starting deep analysis of expandable elements');
|
|
@@ -54,7 +55,7 @@ export function WithDeepAnalysis<T extends Constructor>(Base: T) {
|
|
|
54
55
|
}
|
|
55
56
|
}
|
|
56
57
|
|
|
57
|
-
let expandables = await this._discoverExpandables(result.text, missing
|
|
58
|
+
let expandables = await this._discoverExpandables(result.text, missing);
|
|
58
59
|
if (expandables.length > maxClicks) {
|
|
59
60
|
expandables = await this._selectExpandables(expandables, state.fullUrl || state.url, maxClicks);
|
|
60
61
|
tag('substep').log(`Selected ${expandables.length} expandables to click (max: ${maxClicks})`);
|
|
@@ -93,6 +94,10 @@ export function WithDeepAnalysis<T extends Constructor>(Base: T) {
|
|
|
93
94
|
const region = current.overlay;
|
|
94
95
|
if (!region.isOpen || !region.name) return null;
|
|
95
96
|
|
|
97
|
+
const overlayKey = `${pageStateHash}:${region.name}`;
|
|
98
|
+
if (this.researchedOverlays.has(overlayKey)) return null;
|
|
99
|
+
this.researchedOverlays.add(overlayKey);
|
|
100
|
+
|
|
96
101
|
const cached = getCachedResearch(pageStateHash);
|
|
97
102
|
if (!cached) return null;
|
|
98
103
|
|
|
@@ -199,7 +204,7 @@ export function WithDeepAnalysis<T extends Constructor>(Base: T) {
|
|
|
199
204
|
}
|
|
200
205
|
}
|
|
201
206
|
|
|
202
|
-
private async _discoverExpandables(researchText: string, missing: PreviousSection[] = []
|
|
207
|
+
private async _discoverExpandables(researchText: string, missing: PreviousSection[] = []): Promise<ExpandableElement[]> {
|
|
203
208
|
const allElements = new Map<string, ExpandableElement>();
|
|
204
209
|
for (const section of parseResearchSections(researchText)) {
|
|
205
210
|
for (const el of section.elements) {
|
|
@@ -6,16 +6,16 @@ import { isSamePageFamily } from '../../utils/url-matcher.ts';
|
|
|
6
6
|
import { diceSimilarity } from '../../utils/similarity.ts';
|
|
7
7
|
|
|
8
8
|
parentPort!.on('message', (data: FingerprintRequest) => {
|
|
9
|
-
const { html, statesDir, maxAgeMs, threshold, url } = data;
|
|
9
|
+
const { id, html, statesDir, maxAgeMs, threshold, url } = data;
|
|
10
10
|
|
|
11
11
|
if (!existsSync(statesDir)) {
|
|
12
|
-
parentPort!.postMessage({ matchHash: null, similarity: 0 });
|
|
12
|
+
parentPort!.postMessage({ id, matchHash: null, similarity: 0 });
|
|
13
13
|
return;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
const currentFingerprint = new Set(computeHtmlFingerprint(html));
|
|
17
17
|
if (currentFingerprint.size === 0) {
|
|
18
|
-
parentPort!.postMessage({ matchHash: null, similarity: 0 });
|
|
18
|
+
parentPort!.postMessage({ id, matchHash: null, similarity: 0 });
|
|
19
19
|
return;
|
|
20
20
|
}
|
|
21
21
|
|
|
@@ -44,7 +44,7 @@ parentPort!.on('message', (data: FingerprintRequest) => {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
const matched = bestSimilarity >= threshold;
|
|
47
|
-
parentPort!.postMessage({ matchHash: matched ? bestHash : null, similarity: bestSimilarity, url: matched ? bestUrl : undefined });
|
|
47
|
+
parentPort!.postMessage({ id, matchHash: matched ? bestHash : null, similarity: bestSimilarity, url: matched ? bestUrl : undefined });
|
|
48
48
|
});
|
|
49
49
|
|
|
50
50
|
function readFingerprint(filePath: string): FingerprintRecord {
|
|
@@ -59,4 +59,4 @@ function readFingerprint(filePath: string): FingerprintRecord {
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
type FingerprintRecord = { entries: string[]; url?: string };
|
|
62
|
-
type FingerprintRequest = { html: string; statesDir: string; maxAgeMs: number; threshold: number; url?: string };
|
|
62
|
+
type FingerprintRequest = { id: number; html: string; statesDir: string; maxAgeMs: number; threshold: number; url?: string };
|