explorbot 0.5.10 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/explorbot-cli.ts +494 -482
- package/boat/api-tester/src/ai/curler.ts +1 -1
- package/boat/api-tester/src/cli.ts +4 -4
- package/boat/doc-collector/src/screenshots.ts +1 -1
- package/dist/bin/explorbot-cli.js +455 -444
- package/dist/boat/api-tester/src/ai/curler.js +1 -1
- package/dist/boat/api-tester/src/cli.js +4 -4
- package/dist/models.json +1 -1
- package/dist/package.json +2 -1
- package/dist/rules/navigator/output.md +2 -8
- package/dist/rules/navigator/verification-actions.md +6 -2
- package/dist/rules/planner/ignored-pages.md +1 -0
- package/dist/src/action-result.js +1 -1
- package/dist/src/action.d.ts +0 -1
- package/dist/src/action.js +0 -4
- package/dist/src/ai/captain/idle-mode.js +42 -26
- package/dist/src/ai/captain/mixin.d.ts +2 -0
- package/dist/src/ai/captain/web-mode.js +11 -0
- package/dist/src/ai/captain.d.ts +6 -4
- package/dist/src/ai/captain.js +25 -1
- package/dist/src/ai/driller.js +1 -1
- package/dist/src/ai/experience-compactor.js +2 -2
- package/dist/src/ai/judge.d.ts +7 -0
- package/dist/src/ai/judge.js +8 -1
- package/dist/src/ai/navigator.d.ts +1 -0
- package/dist/src/ai/navigator.js +11 -7
- package/dist/src/ai/planner/subpages.js +1 -1
- package/dist/src/ai/planner.d.ts +12 -5
- package/dist/src/ai/planner.js +138 -86
- package/dist/src/ai/provider.d.ts +1 -1
- package/dist/src/ai/provider.js +16 -5
- package/dist/src/ai/rerunner.js +1 -1
- package/dist/src/ai/researcher/cache.js +14 -6
- package/dist/src/ai/researcher/deep-analysis.d.ts +2 -1
- package/dist/src/ai/researcher/deep-analysis.js +7 -2
- package/dist/src/ai/researcher/fingerprint-worker.js +4 -4
- package/dist/src/ai/researcher.js +44 -24
- package/dist/src/ai/rules.js +6 -7
- package/dist/src/ai/scout.d.ts +12 -2
- package/dist/src/ai/scout.js +103 -8
- package/dist/src/ai/tester.d.ts +9 -5
- package/dist/src/ai/tester.js +22 -42
- package/dist/src/ai/tools.js +10 -8
- package/dist/src/application-spec.d.ts +2 -0
- package/dist/src/application-spec.js +17 -12
- package/dist/src/commands/context-experience-command.js +0 -1
- package/dist/src/commands/context-knowledge-command.js +0 -1
- package/dist/src/commands/explore-command.d.ts +3 -2
- package/dist/src/commands/explore-command.js +35 -23
- package/dist/src/commands/help-json-command.js +3 -1
- package/dist/src/commands/init-command.js +4 -4
- package/dist/src/commands/learn-command.js +0 -2
- package/dist/src/commands/plan-command.d.ts +3 -0
- package/dist/src/commands/plan-command.js +43 -17
- package/dist/src/commands/plan-save-command.js +6 -17
- package/dist/src/commands/plans-command.js +1 -1
- package/dist/src/commands/test-command.js +12 -2
- package/dist/src/components/App.d.ts +1 -2
- package/dist/src/components/App.js +1 -3
- package/dist/src/components/InputPane.js +0 -5
- package/dist/src/config.d.ts +3 -2
- package/dist/src/config.js +2 -12
- package/dist/src/explorbot.d.ts +3 -2
- package/dist/src/explorbot.js +32 -10
- package/dist/src/explorer.js +6 -7
- package/dist/src/knowledge-tracker.d.ts +2 -2
- package/dist/src/knowledge-tracker.js +18 -6
- package/dist/src/playwright-recorder.d.ts +0 -1
- package/dist/src/playwright-recorder.js +0 -2
- package/dist/src/reporter.d.ts +1 -1
- package/dist/src/reporter.js +3 -3
- package/dist/src/test-plan.js +1 -1
- package/dist/src/utils/error-page.d.ts +17 -0
- package/dist/src/utils/error-page.js +53 -2
- package/dist/src/utils/html-diff.d.ts +1 -2
- package/dist/src/utils/html-diff.js +4 -44
- package/dist/src/utils/logger.js +27 -10
- package/dist/src/utils/markdown-terminal.js +1 -2
- package/dist/src/utils/next-steps.js +20 -16
- package/dist/src/utils/page-readiness.d.ts +2 -1
- package/dist/src/utils/page-readiness.js +24 -5
- package/dist/src/utils/test-plan-markdown.d.ts +3 -0
- package/dist/src/utils/test-plan-markdown.js +35 -4
- package/dist/src/utils/url-matcher.d.ts +1 -0
- package/dist/src/utils/url-matcher.js +5 -0
- package/docs/basics/providers.md +1 -1
- package/docs/contributing/regression-tests.md +19 -4
- package/docs/reference/commands.md +6 -2
- package/docs/reference/configuration.md +1 -1
- package/docs/web-testing/agents.md +8 -0
- package/docs/web-testing/customization.md +2 -0
- package/docs/web-testing/researcher.md +1 -1
- package/models.json +1 -1
- package/package.json +2 -1
- package/rules/navigator/output.md +2 -8
- package/rules/navigator/verification-actions.md +6 -2
- package/rules/planner/ignored-pages.md +1 -0
- package/src/action-result.ts +1 -2
- package/src/action.ts +0 -5
- package/src/ai/captain/idle-mode.ts +38 -26
- package/src/ai/captain/mixin.ts +2 -1
- package/src/ai/captain/web-mode.ts +12 -0
- package/src/ai/captain.ts +25 -7
- package/src/ai/driller.ts +1 -2
- package/src/ai/experience-compactor.ts +2 -2
- package/src/ai/judge.ts +10 -1
- package/src/ai/navigator.ts +12 -7
- package/src/ai/planner/subpages.ts +1 -2
- package/src/ai/planner.ts +140 -92
- package/src/ai/provider.ts +15 -6
- package/src/ai/rerunner.ts +1 -2
- package/src/ai/researcher/cache.ts +16 -7
- package/src/ai/researcher/deep-analysis.ts +7 -2
- package/src/ai/researcher/fingerprint-worker.ts +5 -5
- package/src/ai/researcher.ts +44 -28
- package/src/ai/rules.ts +6 -7
- package/src/ai/scout.ts +98 -10
- package/src/ai/tester.ts +29 -47
- package/src/ai/tools.ts +11 -9
- package/src/application-spec.ts +19 -13
- package/src/commands/clean-command.ts +1 -1
- package/src/commands/context-command.ts +0 -1
- package/src/commands/context-experience-command.ts +0 -1
- package/src/commands/context-knowledge-command.ts +0 -1
- package/src/commands/explore-command.ts +36 -23
- package/src/commands/help-json-command.ts +8 -6
- package/src/commands/init-command.ts +5 -3
- package/src/commands/learn-command.ts +0 -2
- package/src/commands/plan-command.ts +44 -17
- package/src/commands/plan-save-command.ts +6 -17
- package/src/commands/plans-command.ts +1 -1
- package/src/commands/test-command.ts +11 -2
- package/src/components/App.tsx +2 -6
- package/src/components/InputPane.tsx +0 -5
- package/src/config.ts +4 -16
- package/src/experience-tracker.ts +1 -1
- package/src/explorbot.ts +31 -11
- package/src/explorer.ts +6 -7
- package/src/knowledge-tracker.ts +18 -7
- package/src/playwright-recorder.ts +0 -2
- package/src/reporter.ts +3 -4
- package/src/test-plan.ts +1 -2
- package/src/utils/error-page.ts +64 -2
- package/src/utils/html-diff.ts +4 -56
- package/src/utils/html.ts +0 -1
- package/src/utils/logger.ts +30 -12
- package/src/utils/markdown-terminal.ts +1 -2
- package/src/utils/next-steps.ts +19 -17
- package/src/utils/page-readiness.ts +33 -7
- package/src/utils/test-plan-markdown.ts +36 -4
- package/src/utils/url-matcher.ts +6 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { tool } from 'ai';
|
|
2
2
|
import dedent from 'dedent';
|
|
3
3
|
import { z } from 'zod';
|
|
4
|
+
import { pageEvidence } from '../judge.ts';
|
|
4
5
|
import { actionRule, locatorRule, sectionContextRule } from '../rules.ts';
|
|
5
6
|
import { createAgentTools, createCodeceptJSTools } from '../tools.ts';
|
|
6
7
|
import { type Constructor, type ModeContext, debugLog } from './mixin.ts';
|
|
@@ -37,6 +38,16 @@ export function WithWebMode<T extends Constructor>(Base: T) {
|
|
|
37
38
|
},
|
|
38
39
|
}),
|
|
39
40
|
|
|
41
|
+
oracle: tool({
|
|
42
|
+
description: 'Ask a yes/no question about the current page. Much cheaper than see or context.',
|
|
43
|
+
inputSchema: z.object({ question: z.string() }),
|
|
44
|
+
execute: async ({ question }) => {
|
|
45
|
+
const captured = await ctx.explorBot.getExplorer().capture({ screenshot: true });
|
|
46
|
+
const holds = await ctx.explorBot.getProvider().oracle(question, pageEvidence(captured), { agentName: 'captain', screenshot: captured.screenshot, telemetry: { functionId: 'captain.oracle' } });
|
|
47
|
+
return { success: holds !== null, question, answer: holds };
|
|
48
|
+
},
|
|
49
|
+
}),
|
|
50
|
+
|
|
40
51
|
browser: tool({
|
|
41
52
|
description: dedent`
|
|
42
53
|
Direct browser access via Playwright. Use for diagnostics and browser management.
|
|
@@ -157,6 +168,7 @@ export function WithWebMode<T extends Constructor>(Base: T) {
|
|
|
157
168
|
- click() accepts array of commands to try in order — include ARIA, CSS, XPath variants
|
|
158
169
|
- If click() fails with all provided commands, use visualClick() tool as fallback
|
|
159
170
|
- When an action fails or result is unexpected, investigate or inform the user instead of retrying silently
|
|
171
|
+
- For a quick yes/no question about the page, use oracle instead of see or context
|
|
160
172
|
`;
|
|
161
173
|
}
|
|
162
174
|
};
|
package/src/ai/captain.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type { ExplorBot } from '../explorbot.ts';
|
|
|
7
7
|
import type { WebPageState } from '../state-manager.ts';
|
|
8
8
|
import { Task, Test } from '../test-plan.ts';
|
|
9
9
|
import { formatHeadings } from '../utils/context-formatter.ts';
|
|
10
|
+
import { BlockedError } from '../utils/error-page.ts';
|
|
10
11
|
import { HooksRunner } from '../utils/hooks-runner.ts';
|
|
11
12
|
import { startLogCapture, stopLogCapture, tag } from '../utils/logger.js';
|
|
12
13
|
import { loop } from '../utils/loop.js';
|
|
@@ -21,6 +22,7 @@ import type { Navigator } from './navigator.ts';
|
|
|
21
22
|
import type { Provider } from './provider.ts';
|
|
22
23
|
import { Researcher } from './researcher.ts';
|
|
23
24
|
import { TaskAgent } from './task-agent.ts';
|
|
25
|
+
import type { SupervisorAction } from './tester.ts';
|
|
24
26
|
import { createLearnExperienceTool, withdrawVisionTools } from './tools.ts';
|
|
25
27
|
|
|
26
28
|
const MAX_STEPS = 15;
|
|
@@ -35,6 +37,9 @@ export class Captain extends CaptainBase implements Agent {
|
|
|
35
37
|
private hooksRunner: HooksRunner | null = null;
|
|
36
38
|
private commandExecutor: ((cmd: string) => Promise<void>) | null = null;
|
|
37
39
|
private commandDescriptions: { name: string; description: string; options: string }[] = [];
|
|
40
|
+
private unresolvedBlockers = new Set<string>();
|
|
41
|
+
private handling = false;
|
|
42
|
+
private blocker: BlockedError | null = null;
|
|
38
43
|
|
|
39
44
|
constructor(explorBot: ExplorBot) {
|
|
40
45
|
super();
|
|
@@ -123,6 +128,7 @@ export class Captain extends CaptainBase implements Agent {
|
|
|
123
128
|
- After a successful action, if the pageDiff confirms the goal, call done() immediately — do not verify with see() or context() unless the user explicitly asked for verification
|
|
124
129
|
- Prefer completing in fewer tool calls over thoroughness
|
|
125
130
|
- NEVER run tests unless the user explicitly asks
|
|
131
|
+
- When an agent reports it is blocked, decide which agent can resolve it and delegate to that agent
|
|
126
132
|
- If you are answering with information rather than completing a browser action, include the actual user-facing answer in done({ details }). Do not only say that it was shown or explained.
|
|
127
133
|
${mode === 'web' || mode === 'heal' ? this.webModeRules() : ''}
|
|
128
134
|
${mode === 'test' || mode === 'heal' ? this.testModeRules() : ''}
|
|
@@ -304,7 +310,7 @@ export class Captain extends CaptainBase implements Agent {
|
|
|
304
310
|
|
|
305
311
|
private async tools(task: Task, onDone: (summary: string) => void) {
|
|
306
312
|
const mode = this.getMode();
|
|
307
|
-
const ctx: ModeContext = { explorBot: this.explorBot, task };
|
|
313
|
+
const ctx: ModeContext = { explorBot: this.explorBot, task, blocker: this.blocker };
|
|
308
314
|
const core = this.coreTools(task, onDone);
|
|
309
315
|
const idle = await this.idleModeTools(ctx);
|
|
310
316
|
|
|
@@ -437,6 +443,7 @@ export class Captain extends CaptainBase implements Agent {
|
|
|
437
443
|
|
|
438
444
|
conversation.addUserText(initialPrompt);
|
|
439
445
|
|
|
446
|
+
this.handling = true;
|
|
440
447
|
await loop(
|
|
441
448
|
async ({ stop, iteration, userInput }) => {
|
|
442
449
|
debugLog(`Captain iteration ${iteration}`);
|
|
@@ -510,7 +517,9 @@ export class Captain extends CaptainBase implements Agent {
|
|
|
510
517
|
stop();
|
|
511
518
|
},
|
|
512
519
|
}
|
|
513
|
-
)
|
|
520
|
+
).finally(() => {
|
|
521
|
+
this.handling = false;
|
|
522
|
+
});
|
|
514
523
|
|
|
515
524
|
const finalUrl = stateManager.getCurrentState()?.url || startUrl;
|
|
516
525
|
await this.getHooksRunner().runAfterHook('captain', finalUrl);
|
|
@@ -523,15 +532,24 @@ export class Captain extends CaptainBase implements Agent {
|
|
|
523
532
|
|
|
524
533
|
return null;
|
|
525
534
|
}
|
|
535
|
+
|
|
536
|
+
async resolveBlocker(error: unknown, resolved: () => boolean | Promise<boolean>): Promise<boolean> {
|
|
537
|
+
if (!(error instanceof BlockedError) || this.handling) return false;
|
|
538
|
+
const key = `${error.name} ${error.url} ${error.blocker}`;
|
|
539
|
+
if (this.unresolvedBlockers.has(key)) return false;
|
|
540
|
+
tag('info').log(`Captain takes over: ${error.message}`);
|
|
541
|
+
this.blocker = error;
|
|
542
|
+
await this.handle(error.request, { reset: true }).finally(() => {
|
|
543
|
+
this.blocker = null;
|
|
544
|
+
});
|
|
545
|
+
if (await resolved()) return true;
|
|
546
|
+
this.unresolvedBlockers.add(key);
|
|
547
|
+
return false;
|
|
548
|
+
}
|
|
526
549
|
}
|
|
527
550
|
|
|
528
551
|
export default Captain;
|
|
529
552
|
|
|
530
|
-
interface SupervisorAction {
|
|
531
|
-
action: 'inject' | 'stop' | 'pass' | 'skip';
|
|
532
|
-
message: string;
|
|
533
|
-
}
|
|
534
|
-
|
|
535
553
|
interface ExecutionRecoveryAction {
|
|
536
554
|
action: 'continue' | 'stop';
|
|
537
555
|
message: string;
|
package/src/ai/driller.ts
CHANGED
|
@@ -15,7 +15,6 @@ import { eidxInContainer } from '../utils/web-eidx.ts';
|
|
|
15
15
|
import { WebElement } from '../utils/web-element.ts';
|
|
16
16
|
import type { Agent, AgentDeps } from './agent.ts';
|
|
17
17
|
import type { Navigator } from './navigator.ts';
|
|
18
|
-
import type { Provider } from './provider.ts';
|
|
19
18
|
import { drillLocatorRule } from './rules.ts';
|
|
20
19
|
import { TaskAgent, isInteractive } from './task-agent.ts';
|
|
21
20
|
import { createCodeceptJSTools, createLearnExperienceTool } from './tools.ts';
|
|
@@ -177,7 +176,7 @@ export class Driller extends TaskAgent implements Agent {
|
|
|
177
176
|
setActivity(`${this.emoji} Capturing annotated page state...`, 'action');
|
|
178
177
|
const action = this.explorer.action();
|
|
179
178
|
try {
|
|
180
|
-
|
|
179
|
+
await Promise.race([
|
|
181
180
|
this.explorer.withPage(annotatePageElements),
|
|
182
181
|
new Promise<never>((_, reject) => {
|
|
183
182
|
setTimeout(() => reject(new Error('annotateElements timeout')), 15000);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { unlinkSync } from 'node:fs';
|
|
2
|
-
import { basename } from 'node:path';
|
|
2
|
+
import { basename, relative } from 'node:path';
|
|
3
3
|
import dedent from 'dedent';
|
|
4
4
|
import { z } from 'zod';
|
|
5
5
|
import { type ExperienceFile, type ExperienceTracker, RECENT_WINDOW_DAYS } from '../experience-tracker.js';
|
|
@@ -322,7 +322,7 @@ export class ExperienceCompactor implements Agent {
|
|
|
322
322
|
};
|
|
323
323
|
|
|
324
324
|
this.experienceTracker.writeExperienceFile(targetStateHash, combinedContent, newFrontmatter);
|
|
325
|
-
log(`Merged ${group.files.length} experience files into ${targetFile.filePath}`);
|
|
325
|
+
log(`Merged ${group.files.length} experience files into ${relative(process.cwd(), targetFile.filePath)}`);
|
|
326
326
|
debugLog('New URL pattern:', group.pattern);
|
|
327
327
|
|
|
328
328
|
for (const sourceFile of sourceFiles) {
|
package/src/ai/judge.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { clearActivity, setActivity } from '../activity.ts';
|
|
2
|
+
import type { ActionResult } from '../action-result.ts';
|
|
2
3
|
import type { AIConfig } from '../config.ts';
|
|
3
4
|
import { Observability } from '../observability.ts';
|
|
4
|
-
import { createDebug } from '../utils/logger.ts';
|
|
5
|
+
import { createDebug, tag } from '../utils/logger.ts';
|
|
5
6
|
import { JudgeProvider } from './judge-provider.ts';
|
|
6
7
|
|
|
7
8
|
const debugLog = createDebug('explorbot:judge');
|
|
@@ -29,6 +30,8 @@ export class Decision {
|
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
export class Judge {
|
|
33
|
+
private failureReported = false;
|
|
34
|
+
|
|
32
35
|
constructor(
|
|
33
36
|
private provider: JudgeProvider,
|
|
34
37
|
private enabled: { tool: boolean; direct: boolean },
|
|
@@ -93,6 +96,8 @@ export class Judge {
|
|
|
93
96
|
|
|
94
97
|
private recordFailure(error: unknown): null {
|
|
95
98
|
debugLog('judge declined: %s', error);
|
|
99
|
+
if (!this.failureReported) tag('warning').log(`Decision model ${this.provider.model} failed (${error}); continuing without it`);
|
|
100
|
+
this.failureReported = true;
|
|
96
101
|
const span = Observability.getSpan();
|
|
97
102
|
span?.setAttribute('ai.telemetry.metadata.judgeError', String(error));
|
|
98
103
|
span?.setAttribute('langfuse.observation.level', 'ERROR');
|
|
@@ -100,3 +105,7 @@ export class Judge {
|
|
|
100
105
|
return null;
|
|
101
106
|
}
|
|
102
107
|
}
|
|
108
|
+
|
|
109
|
+
export function pageEvidence(actionResult: ActionResult) {
|
|
110
|
+
return { url: actionResult.url, title: actionResult.title, page: actionResult.getCompactARIA().slice(0, JUDGE_PAGE_CAP) };
|
|
111
|
+
}
|
package/src/ai/navigator.ts
CHANGED
|
@@ -13,7 +13,7 @@ import { type StateManager, normalizeUrl } from '../state-manager.js';
|
|
|
13
13
|
import { isFatalBrowserError } from '../utils/browser-errors.ts';
|
|
14
14
|
import { getCliName } from '../utils/cli-name.ts';
|
|
15
15
|
import { extractCodeBlocks } from '../utils/code-extractor.js';
|
|
16
|
-
import { isLoginPage } from '../utils/error-page.ts';
|
|
16
|
+
import { NavigationBlockedError, isLoginPage } from '../utils/error-page.ts';
|
|
17
17
|
import { HooksRunner } from '../utils/hooks-runner.ts';
|
|
18
18
|
import { createDebug, pluralize, tag } from '../utils/logger.js';
|
|
19
19
|
import { loop, pause } from '../utils/loop.js';
|
|
@@ -22,7 +22,6 @@ import { normalizeInlineText } from '../utils/strings.ts';
|
|
|
22
22
|
import { extractStatePath, isSameHostFamily, matchesNavigationUrl } from '../utils/url-matcher.js';
|
|
23
23
|
import type { UrlFilter } from '../utils/url-filter.ts';
|
|
24
24
|
import type { Agent, AgentDeps } from './agent.js';
|
|
25
|
-
import type { Conversation } from './conversation.js';
|
|
26
25
|
import { type Decision, JUDGE_PAGE_CAP, type Judge, UNDECIDED } from './judge.ts';
|
|
27
26
|
import type { Provider } from './provider.js';
|
|
28
27
|
import { Researcher } from './researcher.ts';
|
|
@@ -31,6 +30,7 @@ import { isInteractive } from './task-agent.js';
|
|
|
31
30
|
import { createLearnExperienceTool } from './tools.ts';
|
|
32
31
|
|
|
33
32
|
const debugLog = createDebug('explorbot:navigator');
|
|
33
|
+
const MAX_TRIED_STEPS = 5;
|
|
34
34
|
|
|
35
35
|
class Navigator implements Agent {
|
|
36
36
|
emoji = '🧭';
|
|
@@ -138,6 +138,10 @@ class Navigator implements Agent {
|
|
|
138
138
|
return matchesNavigationUrl(expectedUrl, currentUrl);
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
+
isOn(url: string): boolean {
|
|
142
|
+
return this.isOnExpectedPage(url, this.stateManager);
|
|
143
|
+
}
|
|
144
|
+
|
|
141
145
|
async visit(url: string, opts: { recover?: boolean } = {}): Promise<boolean> {
|
|
142
146
|
return this.visitOnce(url, opts.recover !== false);
|
|
143
147
|
}
|
|
@@ -195,8 +199,9 @@ class Navigator implements Agent {
|
|
|
195
199
|
}
|
|
196
200
|
|
|
197
201
|
private navigationError(url: string, fallback: string): Error {
|
|
198
|
-
|
|
199
|
-
|
|
202
|
+
const currentUrl = this.stateManager.getCurrentState()?.url || '';
|
|
203
|
+
const tried = this.executedSteps.slice(-MAX_TRIED_STEPS).map((step) => step.command);
|
|
204
|
+
return new NavigationBlockedError(url, currentUrl, this.lastFailureReason || fallback, tried);
|
|
200
205
|
}
|
|
201
206
|
|
|
202
207
|
private failureReason(stopReason: string | null, knowledge: string, url: string): string | null {
|
|
@@ -402,7 +407,7 @@ class Navigator implements Agent {
|
|
|
402
407
|
Propose different solutions to achieve the result.
|
|
403
408
|
Solution should be valid CodeceptJS code.
|
|
404
409
|
Use only data from the <page> context to plan the solution.
|
|
405
|
-
The page is given as an ARIA snapshot only: use ARIA and text locators from it.
|
|
410
|
+
The page is given as an ARIA snapshot only: use ARIA and text locators from it; propose CSS/XPath only once <page_html> is provided.
|
|
406
411
|
Full HTML is provided if these solutions fail.
|
|
407
412
|
Try various ways to achieve the result
|
|
408
413
|
</task>
|
|
@@ -772,7 +777,7 @@ class Navigator implements Agent {
|
|
|
772
777
|
Do not generate assertions that would pass even if the specific claim is false.
|
|
773
778
|
</task>
|
|
774
779
|
|
|
775
|
-
${RulesLoader.loadRules('navigator', ['verification-actions'], actionResult.url || '')}
|
|
780
|
+
${RulesLoader.loadRules('navigator', ['verification-actions', 'verification-output'], actionResult.url || '').replace('{{maxAttempts}}', String(this.verifyAttempts))}
|
|
776
781
|
|
|
777
782
|
${experience}
|
|
778
783
|
|
|
@@ -858,7 +863,7 @@ class Navigator implements Agent {
|
|
|
858
863
|
|
|
859
864
|
const totalAttempted = Math.min(codeBlocks.length, this.verifyAttempts);
|
|
860
865
|
let verified = successfulCodes.length > 0 && !failures;
|
|
861
|
-
if (successfulCodes.length && failures) verified = await this.provider.oracle(message, { assertions: results.map(({ code, passed, error }) => ({ code, passed, error })) }, { agentName: 'navigator', telemetry: { functionId: 'navigator.verifyState' } });
|
|
866
|
+
if (successfulCodes.length && failures) verified = (await this.provider.oracle(message, { assertions: results.map(({ code, passed, error }) => ({ code, passed, error })) }, { agentName: 'navigator', telemetry: { functionId: 'navigator.verifyState' } })) === true;
|
|
862
867
|
if (alreadyVerified) verified = true;
|
|
863
868
|
|
|
864
869
|
const inexpressible = !alreadyVerified && totalAttempted === 0;
|
|
@@ -3,7 +3,6 @@ import { z } from 'zod';
|
|
|
3
3
|
import { normalizeUrl } from '../../state-manager.ts';
|
|
4
4
|
import type { StateManager } from '../../state-manager.ts';
|
|
5
5
|
import type { Plan } from '../../test-plan.ts';
|
|
6
|
-
import { tag } from '../../utils/logger.ts';
|
|
7
6
|
import { isSamePageFamily } from '../../utils/url-matcher.ts';
|
|
8
7
|
import type { Provider } from '../provider.ts';
|
|
9
8
|
import type { Constructor } from '../researcher/mixin.ts';
|
|
@@ -85,7 +84,7 @@ export function WithSubPages<T extends Constructor>(Base: T) {
|
|
|
85
84
|
async pickNextSubPage(candidates: SubPageCandidate[]): Promise<{ url: string; reason: string } | null> {
|
|
86
85
|
if (candidates.length === 0) return null;
|
|
87
86
|
|
|
88
|
-
const plannedEntries = [...planRegistry.entries()].map(([
|
|
87
|
+
const plannedEntries = [...planRegistry.entries()].map(([, record]) => `- ${record.url} (${record.plan.tests.length} tests${record.feature ? `, feature: ${record.feature}` : ''})`).join('\n');
|
|
89
88
|
|
|
90
89
|
const candidateList = candidates.map((c) => `- ${c.url} (visits: ${c.visitCount}${c.title ? `, title: "${c.title}"` : ''}${c.h1 ? `, h1: "${c.h1}"` : ''})`).join('\n');
|
|
91
90
|
|
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
|
}
|