explorbot 0.4.0 → 0.4.2
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 +13 -6
- package/boat/api-tester/src/ai/chief.ts +7 -1
- package/boat/api-tester/src/ai/curler-tools.ts +1 -1
- package/boat/api-tester/src/ai/curler.ts +7 -1
- package/boat/api-tester/src/apibot.ts +10 -4
- package/boat/api-tester/src/cli.ts +15 -2
- package/boat/api-tester/src/config.ts +28 -8
- package/boat/doc-collector/bin/doc-collector-cli.ts +3 -2
- package/boat/doc-collector/src/ai/documentarian.ts +57 -5
- package/boat/doc-collector/src/ai/tools.ts +1 -1
- package/boat/doc-collector/src/cli.ts +21 -3
- package/boat/doc-collector/src/config.ts +3 -0
- package/boat/doc-collector/src/docbot.ts +57 -12
- package/boat/doc-collector/src/docs-renderer.ts +79 -55
- package/boat/doc-collector/src/interaction-screenshots.ts +19 -10
- package/boat/doc-collector/src/screenshots.ts +106 -3
- package/boat/doc-collector/src/template-dedup.ts +31 -0
- package/boat/prima/bin/prima-cli.ts +2 -0
- package/boat/prima/src/cli.ts +5 -0
- package/boat/prima/src/prima.ts +3 -4
- package/dist/bin/explorbot-cli.js +14 -6
- package/dist/boat/api-tester/bin/apibot-cli.js +3 -2
- package/dist/boat/api-tester/src/ai/chief.js +6 -1
- package/dist/boat/api-tester/src/ai/curler-tools.js +1 -1
- package/dist/boat/api-tester/src/ai/curler.js +6 -1
- package/dist/boat/api-tester/src/apibot.js +7 -3
- package/dist/boat/api-tester/src/cli.js +14 -2
- package/dist/boat/api-tester/src/config.js +31 -8
- package/dist/boat/doc-collector/bin/doc-collector-cli.js +3 -2
- package/dist/boat/doc-collector/src/ai/documentarian.js +62 -4
- package/dist/boat/doc-collector/src/ai/tools.js +1 -1
- package/dist/boat/doc-collector/src/cli.js +18 -3
- package/dist/boat/doc-collector/src/config.js +1 -0
- package/dist/boat/doc-collector/src/docbot.js +53 -11
- package/dist/boat/doc-collector/src/docs-renderer.js +69 -46
- package/dist/boat/doc-collector/src/interaction-screenshots.js +18 -11
- package/dist/boat/doc-collector/src/screenshots.js +104 -1
- package/dist/boat/doc-collector/src/template-dedup.js +27 -0
- package/dist/boat/prima/bin/prima-cli.js +2 -0
- package/dist/boat/prima/src/cli.js +5 -0
- package/dist/boat/prima/src/prima.js +3 -4
- package/dist/package.json +1 -1
- package/dist/rules/planner/styles/normal.md +1 -1
- package/dist/src/action-result.d.ts +2 -2
- package/dist/src/action-result.js +15 -17
- package/dist/src/action.d.ts +1 -1
- package/dist/src/action.js +15 -15
- package/dist/src/ai/captain.js +1 -1
- package/dist/src/ai/fisherman/request-haul.d.ts +11 -0
- package/dist/src/ai/fisherman/request-haul.js +28 -0
- package/dist/src/ai/fisherman-tools.d.ts +10 -1
- package/dist/src/ai/fisherman-tools.js +35 -31
- package/dist/src/ai/fisherman.d.ts +3 -2
- package/dist/src/ai/fisherman.js +8 -6
- package/dist/src/ai/navigator.js +1 -1
- package/dist/src/ai/pilot.js +21 -16
- package/dist/src/ai/planner.js +9 -7
- package/dist/src/ai/provider.js +8 -14
- package/dist/src/ai/researcher/coordinates.d.ts +2 -0
- package/dist/src/ai/researcher/coordinates.js +13 -1
- package/dist/src/ai/researcher/deep-analysis.js +9 -9
- package/dist/src/ai/researcher/fingerprint-worker.js +1 -11
- package/dist/src/ai/researcher.js +3 -1
- package/dist/src/ai/rules.js +11 -3
- package/dist/src/ai/tester.js +20 -20
- package/dist/src/ai/tools.d.ts +1 -1
- package/dist/src/ai/tools.js +18 -74
- package/dist/src/api/request-store.d.ts +1 -5
- package/dist/src/api/request-store.js +12 -22
- package/dist/src/api/spec-reader.js +1 -1
- package/dist/src/commands/config-command.js +3 -10
- package/dist/src/commands/drill-command.js +1 -1
- package/dist/src/commands/explore-command.js +12 -1
- package/dist/src/commands/index.js +2 -0
- package/dist/src/commands/init-command.js +3 -7
- package/dist/src/commands/options/base-option.d.ts +8 -0
- package/dist/src/commands/options/base-option.js +12 -0
- package/dist/src/commands/options/index.d.ts +5 -0
- package/dist/src/commands/options/index.js +5 -0
- package/dist/src/commands/options/knowledge-option.d.ts +7 -0
- package/dist/src/commands/options/knowledge-option.js +12 -0
- package/dist/src/commands/options/ws-option.d.ts +7 -0
- package/dist/src/commands/options/ws-option.js +21 -0
- package/dist/src/commands/plans-command.d.ts +4 -1
- package/dist/src/commands/plans-command.js +4 -4
- package/dist/src/commands/recommended-models-command.d.ts +9 -0
- package/dist/src/commands/recommended-models-command.js +85 -0
- package/dist/src/config.d.ts +3 -0
- package/dist/src/config.js +18 -2
- package/dist/src/experience-tracker.js +1 -1
- package/dist/src/explorbot.js +4 -4
- package/dist/src/global-config.d.ts +2 -0
- package/dist/src/global-config.js +6 -0
- package/dist/src/knowledge-tracker.d.ts +20 -7
- package/dist/src/knowledge-tracker.js +69 -31
- package/dist/src/remote.d.ts +0 -3
- package/dist/src/remote.js +0 -18
- package/dist/src/state-manager.d.ts +3 -3
- package/dist/src/state-manager.js +8 -8
- package/dist/src/test-plan.d.ts +1 -0
- package/dist/src/test-plan.js +13 -9
- package/dist/src/utils/aria.d.ts +1 -1
- package/dist/src/utils/aria.js +23 -2
- package/dist/src/utils/cli-section.d.ts +1 -0
- package/dist/src/utils/cli-section.js +12 -0
- package/dist/src/utils/html-diff.d.ts +1 -0
- package/dist/src/utils/html-diff.js +5 -5
- package/dist/src/utils/html.d.ts +2 -0
- package/dist/src/utils/html.js +5 -4
- package/dist/src/utils/overlay.d.ts +19 -55
- package/dist/src/utils/overlay.js +94 -148
- package/dist/src/utils/region.d.ts +41 -0
- package/dist/src/utils/region.js +99 -0
- package/dist/src/utils/similarity.d.ts +1 -0
- package/dist/src/utils/similarity.js +11 -0
- package/dist/src/utils/url-matcher.d.ts +1 -1
- package/dist/src/utils/url-matcher.js +4 -4
- package/docs/api-testing/basics.md +15 -0
- package/docs/api-testing/planning.md +10 -1
- package/docs/doc-collection/basics.md +11 -5
- package/docs/doc-collection/crawling.md +21 -1
- package/docs/reference/commands.md +27 -4
- package/docs/superpowers/plans/2026-09-03-model-resolves-click-ambiguity.md +509 -0
- package/docs/workflow/agentic-usage.md +11 -2
- package/docs/workflow/application-spec.md +4 -0
- package/docs/workflow/knowledge.md +46 -2
- package/package.json +1 -1
- package/rules/planner/styles/normal.md +1 -1
- package/src/action-result.ts +15 -16
- package/src/action.ts +16 -15
- package/src/ai/captain.ts +1 -1
- package/src/ai/fisherman/request-haul.ts +32 -0
- package/src/ai/fisherman-tools.ts +38 -32
- package/src/ai/fisherman.ts +8 -7
- package/src/ai/navigator.ts +1 -1
- package/src/ai/pilot.ts +19 -14
- package/src/ai/planner.ts +9 -8
- package/src/ai/provider.ts +8 -12
- package/src/ai/researcher/coordinates.ts +13 -1
- package/src/ai/researcher/deep-analysis.ts +9 -9
- package/src/ai/researcher/fingerprint-worker.ts +1 -10
- package/src/ai/researcher.ts +2 -1
- package/src/ai/rules.ts +11 -3
- package/src/ai/tester.ts +18 -18
- package/src/ai/tools.ts +18 -86
- package/src/api/request-store.ts +13 -27
- package/src/api/spec-reader.ts +1 -1
- package/src/commands/config-command.ts +3 -8
- package/src/commands/drill-command.ts +1 -1
- package/src/commands/explore-command.ts +12 -1
- package/src/commands/index.ts +2 -0
- package/src/commands/init-command.ts +4 -11
- package/src/commands/options/base-option.ts +18 -0
- package/src/commands/options/index.ts +7 -0
- package/src/commands/options/knowledge-option.ts +14 -0
- package/src/commands/options/ws-option.ts +24 -0
- package/src/commands/plans-command.ts +5 -5
- package/src/commands/recommended-models-command.ts +92 -0
- package/src/config.ts +20 -2
- package/src/experience-tracker.ts +1 -1
- package/src/explorbot.ts +4 -4
- package/src/global-config.ts +8 -0
- package/src/knowledge-tracker.ts +94 -36
- package/src/remote.ts +0 -20
- package/src/state-manager.ts +10 -9
- package/src/test-plan.ts +12 -10
- package/src/utils/aria.ts +26 -3
- package/src/utils/cli-section.ts +12 -0
- package/src/utils/html-diff.ts +6 -6
- package/src/utils/html.ts +6 -4
- package/src/utils/overlay.ts +105 -192
- package/src/utils/region.ts +132 -0
- package/src/utils/similarity.ts +9 -0
- package/src/utils/url-matcher.ts +3 -3
- package/dist/src/utils/request-map.d.ts +0 -7
- package/dist/src/utils/request-map.js +0 -16
- package/src/utils/request-map.ts +0 -19
|
@@ -45,7 +45,48 @@ While exploring, use the `/learn` command.
|
|
|
45
45
|
|
|
46
46
|
### API Testing
|
|
47
47
|
|
|
48
|
-
[API testing](../api-testing/basics.md) shares the same `knowledge/` directory. `npx explorbot api know <endpoint> "<description>"` adds endpoint-scoped notes, stored with an `endpoint:` frontmatter field instead of `url:`.
|
|
48
|
+
[API testing](../api-testing/basics.md) shares the same `knowledge/` directory. `npx explorbot api know <endpoint> "<description>"` adds endpoint-scoped notes, stored with an `endpoint:` frontmatter field instead of `url:`. Chief reads them when planning an endpoint and Curler reads them when running its tests, so auth headers and payload rules reach both.
|
|
49
|
+
|
|
50
|
+
## Per-Session Knowledge
|
|
51
|
+
|
|
52
|
+
`--knowledge` passes facts to a single run. Nothing is written to `knowledge/`, so credentials and one-off test data stay out of the repository.
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
npx explorbot explore /pay --knowledge 'My credit card is 4111 1111 1111 1111'
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Plain text applies to every page. Add frontmatter to scope it, with the same URL patterns knowledge files use:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
npx explorbot explore / --knowledge '---
|
|
62
|
+
url: /pay
|
|
63
|
+
---
|
|
64
|
+
Use the sandbox card 4111 1111 1111 1111 with any future expiry'
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Repeat the flag for several facts:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
npx explorbot explore / \
|
|
71
|
+
--knowledge '---
|
|
72
|
+
url: /login
|
|
73
|
+
---
|
|
74
|
+
Log in as admin@example.com / secret123' \
|
|
75
|
+
--knowledge 'Dismiss the cookie banner before anything else'
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Everything a knowledge file supports works here: `${env.VAR}` interpolation, and page automation fields such as `wait` and `waitForElement`.
|
|
79
|
+
|
|
80
|
+
The flag works on every command of `explorbot`, `explorbot api`, `explorbot docs` and `prima`, and can go anywhere on the line. Scope API knowledge with `endpoint:` instead of `url:`:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
npx explorbot api explore /orders --knowledge '---
|
|
84
|
+
endpoint: /orders/*
|
|
85
|
+
---
|
|
86
|
+
Send X-Api-Key: ${env.API_KEY} on every request'
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
For runs driven from the environment — config-free, or on the global configuration — `EXPLORBOT_KNOWLEDGE` does the same job for the length of one run. See [Agentic usage](./agentic-usage.md).
|
|
49
90
|
|
|
50
91
|
## URL Patterns
|
|
51
92
|
|
|
@@ -81,10 +122,13 @@ Notes:
|
|
|
81
122
|
|
|
82
123
|
| Field | Purpose |
|
|
83
124
|
|-------|---------|
|
|
84
|
-
| `url` | URL pattern to match
|
|
125
|
+
| `url` | Page URL pattern to match |
|
|
126
|
+
| `endpoint` | API endpoint pattern to match |
|
|
85
127
|
| `title` | Human-readable title (optional) |
|
|
86
128
|
| Custom fields | Any additional metadata for agents |
|
|
87
129
|
|
|
130
|
+
`url` scopes a file to browser pages and `endpoint` scopes it to API endpoints. A file with neither applies to both.
|
|
131
|
+
|
|
88
132
|
## Variables
|
|
89
133
|
|
|
90
134
|
Knowledge files support variable interpolation with `${namespace.key}` syntax. Explorbot resolves variables when it loads the knowledge.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Study the page and figure out its business purpose. What is this page FOR? What would a user come here to do?
|
|
2
2
|
|
|
3
3
|
Based on the page type, propose tests for COMPLETE user workflows:
|
|
4
|
-
- If this is a data page (lists, tables): test
|
|
4
|
+
- If this is a data page (lists, tables): test create, edit and delete as separate tests, each preparing the item it acts on
|
|
5
5
|
- If the page has inputs to fill in: test the full commit flow, not just that the controls render
|
|
6
6
|
- If this has filters and search: test filtering AND verify results change, not just "filter tab clicked"
|
|
7
7
|
- If this has modals/dropdowns: test the ACTION inside them, not just opening/closing them
|
package/src/action-result.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { type HtmlDiffPart, type HtmlDiffResult, htmlDiff, liveRegionMessages }
|
|
|
8
8
|
import { extractHeadings, extractLinks, extractTargetedHtml, htmlCombinedSnapshot, htmlMinimalUISnapshot, htmlTextSnapshot, minifyHtml } from './utils/html.ts';
|
|
9
9
|
import { createDebug } from './utils/logger.ts';
|
|
10
10
|
import { Overlay } from './utils/overlay.ts';
|
|
11
|
+
import { Region } from './utils/region.ts';
|
|
11
12
|
import { slugify } from './utils/strings.ts';
|
|
12
13
|
import { extractStatePath, matchesUrl } from './utils/url-matcher.ts';
|
|
13
14
|
|
|
@@ -88,7 +89,7 @@ export class ActionResult implements ActionResultData {
|
|
|
88
89
|
notes: string[] = [];
|
|
89
90
|
public links: Link[] = [];
|
|
90
91
|
public verifications?: Record<string, boolean>;
|
|
91
|
-
public overlay:
|
|
92
|
+
public overlay: Region = new Region();
|
|
92
93
|
private _diffCache: { previousId: number | undefined; diff: Diff } | null = null;
|
|
93
94
|
|
|
94
95
|
constructor(data: ActionResultData) {
|
|
@@ -264,7 +265,7 @@ export class ActionResult implements ActionResultData {
|
|
|
264
265
|
if (!record.url || !this.url) return false;
|
|
265
266
|
if (record.region && this.overlay.name !== record.region) return false;
|
|
266
267
|
if (record.root) {
|
|
267
|
-
if (!this.overlay.
|
|
268
|
+
if (!this.overlay.isOpen) return false;
|
|
268
269
|
if (this.overlay.root && this.overlay.root !== record.root) return false;
|
|
269
270
|
}
|
|
270
271
|
if (this.isMatchedBy(record)) return true;
|
|
@@ -545,16 +546,13 @@ export class ActionResult implements ActionResultData {
|
|
|
545
546
|
pageDiff.ariaChangeCount = diff.ariaChangeCount;
|
|
546
547
|
}
|
|
547
548
|
|
|
548
|
-
if (this.overlay.
|
|
549
|
+
if (this.overlay.isOpen && (!previousState.overlay.isOpen || previousState.overlay.name !== this.overlay.name)) {
|
|
549
550
|
pageDiff.areaOfInterest = this.overlay.describe();
|
|
550
551
|
}
|
|
551
552
|
|
|
552
553
|
if (pageDiff.areaOfInterest && this.overlay.html && this.overlay.root) {
|
|
553
554
|
const htmlConfig = ConfigParser.getInstance().getConfig().html;
|
|
554
|
-
|
|
555
|
-
if (subtree.length > HTML_PART_SUBTREE_BUDGET) {
|
|
556
|
-
subtree = `${subtree.slice(0, HTML_PART_SUBTREE_BUDGET)}...<!-- truncated -->`;
|
|
557
|
-
}
|
|
555
|
+
const subtree = trimSubtree(await minifyHtml(htmlCombinedSnapshot(this.overlay.html, htmlConfig?.combined)));
|
|
558
556
|
pageDiff.htmlParts = [{ container: this.overlay.root, subtree, rawSize: subtree.length, added: [], removed: [] }];
|
|
559
557
|
} else if (diff.isSameUrl() && diff.htmlParts.length > 0) {
|
|
560
558
|
const collapsed = collapseHtmlParts(await diff.cleanedHtmlParts());
|
|
@@ -581,7 +579,7 @@ export class ActionResult implements ActionResultData {
|
|
|
581
579
|
|
|
582
580
|
if (this.h1) parts.push(`h1_${this.h1}`);
|
|
583
581
|
if (this.h2) parts.push(`h2_${this.h2}`);
|
|
584
|
-
if (includeRegion && this.overlay.
|
|
582
|
+
if (includeRegion && this.overlay.isOpen && this.overlay.name) parts.push(`region_${this.overlay.name}`);
|
|
585
583
|
|
|
586
584
|
let stateString = slugify(parts.map((part) => part.substring(0, 100)).join('_'));
|
|
587
585
|
|
|
@@ -617,6 +615,7 @@ const CONSOLE_ERROR_LIMIT = 3;
|
|
|
617
615
|
const HTML_PARTS_TOTAL_BUDGET = 8000;
|
|
618
616
|
const HTML_PARTS_COUNT_LIMIT = 8;
|
|
619
617
|
const HTML_PART_SUBTREE_BUDGET = 2000;
|
|
618
|
+
const SUBTREE_TAIL_BUDGET = 600;
|
|
620
619
|
|
|
621
620
|
function collapseHtmlParts(parts: HtmlDiffPart[]): HtmlDiffPart[] {
|
|
622
621
|
const total = parts.reduce((sum, p) => sum + p.subtree.length, 0);
|
|
@@ -631,14 +630,14 @@ function collapseHtmlParts(parts: HtmlDiffPart[]): HtmlDiffPart[] {
|
|
|
631
630
|
}));
|
|
632
631
|
}
|
|
633
632
|
|
|
634
|
-
return parts.map((part) => {
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
}
|
|
633
|
+
return parts.map((part) => ({ ...part, subtree: trimSubtree(part.subtree) }));
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function trimSubtree(subtree: string): string {
|
|
637
|
+
if (subtree.length <= HTML_PART_SUBTREE_BUDGET) return subtree;
|
|
638
|
+
const head = subtree.slice(0, HTML_PART_SUBTREE_BUDGET - SUBTREE_TAIL_BUDGET);
|
|
639
|
+
const tail = subtree.slice(-SUBTREE_TAIL_BUDGET);
|
|
640
|
+
return `${head}...<!-- truncated ${subtree.length - HTML_PART_SUBTREE_BUDGET} chars -->${tail}`;
|
|
642
641
|
}
|
|
643
642
|
|
|
644
643
|
export class Diff {
|
package/src/action.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { browserErrorMessage, isFatalBrowserError, isNavigationTransitionError }
|
|
|
14
14
|
import { captureHtmlForSnapshot, htmlCombinedSnapshot, minifyHtml } from './utils/html.js';
|
|
15
15
|
import { createDebug, setStepSpanParent, tag } from './utils/logger.js';
|
|
16
16
|
import { Overlay, OverlayPage } from './utils/overlay.js';
|
|
17
|
+
import type { Region } from './utils/region.js';
|
|
17
18
|
import { sleep, waitForPageReadiness } from './utils/page-readiness.ts';
|
|
18
19
|
import { safeFilename } from './utils/strings.ts';
|
|
19
20
|
import { codeceptJSSandbox, hasPlaywrightCommands, playwrightSandbox, sanitizeCodeBlock } from './utils/web-sandbox.ts';
|
|
@@ -181,7 +182,7 @@ class Action {
|
|
|
181
182
|
focusedElement,
|
|
182
183
|
iframeURL: frame ? frame.url?.() || 'iframe' : undefined,
|
|
183
184
|
});
|
|
184
|
-
if (!frame) await this.
|
|
185
|
+
if (!frame) await this.detectRegion(result).catch((err: Error) => debugLog('Region detection failed:', err.message));
|
|
185
186
|
this.stateManager.updateState(result, codeBlock);
|
|
186
187
|
return result;
|
|
187
188
|
} catch (err) {
|
|
@@ -193,36 +194,36 @@ class Action {
|
|
|
193
194
|
}
|
|
194
195
|
}
|
|
195
196
|
|
|
196
|
-
private async
|
|
197
|
+
private async detectRegion(result: ActionResult): Promise<void> {
|
|
197
198
|
const previousState = this.stateManager.getCurrentState();
|
|
198
199
|
if (!previousState) return;
|
|
199
200
|
const previous = ActionResult.fromState(previousState);
|
|
200
|
-
const
|
|
201
|
+
const previousRegion = previous.overlay;
|
|
201
202
|
const sameUrl = !!previous.url && result.isSameUrl({ url: previous.url });
|
|
202
203
|
const overlayPage = new OverlayPage(this.playwrightHelper.page);
|
|
203
204
|
|
|
204
|
-
if (result.overlay.
|
|
205
|
-
result.overlay =
|
|
205
|
+
if (result.overlay.isModal && previousRegion.isModal && previousRegion.root && previousRegion.type === result.overlay.type && previousRegion.name === result.overlay.name) {
|
|
206
|
+
result.overlay = previousRegion;
|
|
206
207
|
return;
|
|
207
208
|
}
|
|
208
209
|
|
|
209
210
|
if (!previous.html) return;
|
|
210
211
|
|
|
211
212
|
if (previous.html === result.html) {
|
|
212
|
-
if (sameUrl &&
|
|
213
|
+
if (sameUrl && previousRegion.isOpen && previousRegion.xpath && !result.overlay.isModal) result.overlay = previousRegion;
|
|
213
214
|
return;
|
|
214
215
|
}
|
|
215
216
|
|
|
216
|
-
let carried:
|
|
217
|
-
if (sameUrl &&
|
|
218
|
-
if (await overlayPage.isStillOpen(
|
|
219
|
-
carried =
|
|
217
|
+
let carried: Region | null = null;
|
|
218
|
+
if (sameUrl && previousRegion.isOpen && previousRegion.xpath) {
|
|
219
|
+
if (await overlayPage.isStillOpen(previousRegion)) {
|
|
220
|
+
carried = previousRegion;
|
|
220
221
|
} else {
|
|
221
|
-
debugLog(`Region closed: ${
|
|
222
|
-
if (!result.overlay.
|
|
223
|
-
const parent =
|
|
222
|
+
debugLog(`Region closed: ${previousRegion.name || previousRegion.type}`);
|
|
223
|
+
if (!result.overlay.isModal) {
|
|
224
|
+
const parent = previousRegion.parent;
|
|
224
225
|
if (parent?.xpath) {
|
|
225
|
-
const restored =
|
|
226
|
+
const restored = Overlay.resolve({ overlay: parent });
|
|
226
227
|
if (await overlayPage.isStillOpen(restored)) result.overlay = restored;
|
|
227
228
|
}
|
|
228
229
|
return;
|
|
@@ -239,7 +240,7 @@ class Action {
|
|
|
239
240
|
previousHtml: previous.html,
|
|
240
241
|
});
|
|
241
242
|
|
|
242
|
-
if (result.overlay
|
|
243
|
+
if (result.overlay instanceof Overlay) {
|
|
243
244
|
if (detected) result.overlay = result.overlay.withGeometry(detected);
|
|
244
245
|
return;
|
|
245
246
|
}
|
package/src/ai/captain.ts
CHANGED
|
@@ -160,7 +160,7 @@ export class Captain extends CaptainBase implements Agent {
|
|
|
160
160
|
const headingsBlock = headingLines.join('\n');
|
|
161
161
|
|
|
162
162
|
let pageSummary = '';
|
|
163
|
-
const cachedResearch = Researcher.getCachedResearch(
|
|
163
|
+
const cachedResearch = Researcher.getCachedResearch(actionResult);
|
|
164
164
|
if (cachedResearch) {
|
|
165
165
|
pageSummary = `<page_summary>\n${this.explorBot.agentResearcher().extractBrief(cachedResearch)}\n</page_summary>`;
|
|
166
166
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { RequestResult } from '../../api/request-result.ts';
|
|
2
|
+
import { type RequestStore, isFailedRequest } from '../../api/request-store.ts';
|
|
3
|
+
|
|
4
|
+
export class RequestHaul {
|
|
5
|
+
private start: number;
|
|
6
|
+
|
|
7
|
+
constructor(private store: RequestStore) {
|
|
8
|
+
this.start = store.getMadeRequests().length;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
requests(): RequestResult[] {
|
|
12
|
+
return this.store.getMadeRequests().slice(this.start);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
failed(): RequestResult[] {
|
|
16
|
+
return this.requests().filter(isFailedRequest);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
successfulWrites(): RequestResult[] {
|
|
20
|
+
return this.requests().filter((r) => r.isWrite && !r.error && r.status >= 200 && r.status < 400);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
byId(): Map<string, RequestResult> {
|
|
24
|
+
const map = new Map<string, RequestResult>();
|
|
25
|
+
for (const request of this.successfulWrites()) {
|
|
26
|
+
const { id } = request.extractIdAndTitle();
|
|
27
|
+
if (id === undefined) continue;
|
|
28
|
+
map.set(String(id), request);
|
|
29
|
+
}
|
|
30
|
+
return map;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -6,21 +6,18 @@ import type { RequestResult } from '../api/request-result.ts';
|
|
|
6
6
|
import type { RequestStore } from '../api/request-store.ts';
|
|
7
7
|
import { extractEndpointDefinition } from '../api/spec-reader.ts';
|
|
8
8
|
import { tag } from '../utils/logger.ts';
|
|
9
|
-
import { RequestMap } from '../utils/request-map.ts';
|
|
10
9
|
import { isDynamicSegment } from '../utils/url-matcher.ts';
|
|
10
|
+
import type { RequestHaul } from './fisherman/request-haul.ts';
|
|
11
11
|
|
|
12
|
-
export function createFishermanTools(apiClient: ApiClient, requestStore: RequestStore, opts: { spec?: any; baseEndpoint?: string }) {
|
|
12
|
+
export function createFishermanTools(apiClient: ApiClient, requestStore: RequestStore, haul: RequestHaul, opts: { spec?: any; baseEndpoint?: string }) {
|
|
13
13
|
let finished = false;
|
|
14
14
|
let result: FishermanResult | null = null;
|
|
15
|
-
const ledgerStart = requestStore.getMadeRequests().length;
|
|
16
15
|
|
|
17
|
-
const
|
|
18
|
-
const successfulWrites = () => runRequests().filter((r) => r.isWrite && !r.error && r.status >= 200 && r.status < 400);
|
|
19
|
-
const getResult = () => result ?? synthesizeResult(runRequests(), successfulWrites(), false);
|
|
16
|
+
const getResult = () => result ?? synthesizeResult(haul, false);
|
|
20
17
|
const isFinished = () => finished;
|
|
21
18
|
const finishFromText = (text?: string) => {
|
|
22
19
|
finished = true;
|
|
23
|
-
const synthesized = synthesizeResult(
|
|
20
|
+
const synthesized = synthesizeResult(haul, true);
|
|
24
21
|
if (text && synthesized.success) synthesized.summary = text;
|
|
25
22
|
result = synthesized;
|
|
26
23
|
};
|
|
@@ -160,32 +157,12 @@ export function createFishermanTools(apiClient: ApiClient, requestStore: Request
|
|
|
160
157
|
.describe('List of items that could not be created'),
|
|
161
158
|
}),
|
|
162
159
|
execute: async ({ summary, created, failed }) => {
|
|
163
|
-
const
|
|
164
|
-
if (
|
|
165
|
-
tag('warning').log('Fisherman: finish rejected — no successful write request in this run');
|
|
166
|
-
return { finished: false, error: 'No successful write request was made in this run, so nothing was created. Keep working, or call stop if the data cannot be prepared.' };
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
const createdRequests = new RequestMap(writes);
|
|
170
|
-
|
|
171
|
-
const verified: FishermanResult['created'] = [];
|
|
172
|
-
for (const item of created) {
|
|
173
|
-
if (item.id === undefined) {
|
|
174
|
-
verified.push(item);
|
|
175
|
-
continue;
|
|
176
|
-
}
|
|
177
|
-
const request = createdRequests.get(item.id);
|
|
178
|
-
if (!request) {
|
|
179
|
-
tag('warning').log(`Fisherman: dropped unverified created item ${item.type} (id: ${item.id})`);
|
|
180
|
-
continue;
|
|
181
|
-
}
|
|
182
|
-
verified.push({ ...item, request: request.toEndpoint() });
|
|
183
|
-
}
|
|
184
|
-
if (verified.length === 0) verified.push(...writes.map(toCreatedItem));
|
|
160
|
+
const { result: verified, error } = verifyFinish(haul, { summary, created, failed });
|
|
161
|
+
if (!verified) return { finished: false, error };
|
|
185
162
|
|
|
186
163
|
tag('success').log(`Fisherman done: ${summary}`);
|
|
187
164
|
finished = true;
|
|
188
|
-
result =
|
|
165
|
+
result = verified;
|
|
189
166
|
return { finished: true };
|
|
190
167
|
},
|
|
191
168
|
}),
|
|
@@ -207,8 +184,37 @@ export function createFishermanTools(apiClient: ApiClient, requestStore: Request
|
|
|
207
184
|
return { tools, getResult, isFinished, finishFromText };
|
|
208
185
|
}
|
|
209
186
|
|
|
210
|
-
function
|
|
211
|
-
const
|
|
187
|
+
export function verifyFinish(haul: RequestHaul, input: { summary: string; created: FishermanResult['created']; failed?: FishermanResult['failed'] }): { result: FishermanResult | null; error?: string } {
|
|
188
|
+
const writes = haul.successfulWrites();
|
|
189
|
+
if (writes.length === 0) {
|
|
190
|
+
tag('warning').log('Fisherman: finish rejected — no successful write request in this run');
|
|
191
|
+
return { result: null, error: 'No successful write request was made in this run, so nothing was created. Keep working, or call stop if the data cannot be prepared.' };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const createdRequests = haul.byId();
|
|
195
|
+
|
|
196
|
+
const verified: FishermanResult['created'] = [];
|
|
197
|
+
for (const item of input.created) {
|
|
198
|
+
if (item.id === undefined) {
|
|
199
|
+
verified.push(item);
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
const request = createdRequests.get(String(item.id));
|
|
203
|
+
if (!request) {
|
|
204
|
+
tag('warning').log(`Fisherman: dropped unverified created item ${item.type} (id: ${item.id})`);
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
verified.push({ ...item, request: request.toEndpoint() });
|
|
208
|
+
}
|
|
209
|
+
if (verified.length === 0) verified.push(...writes.map(toCreatedItem));
|
|
210
|
+
|
|
211
|
+
return { result: { success: true, summary: input.summary, created: verified, failed: input.failed || [] } };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function synthesizeResult(haul: RequestHaul, declaredDone: boolean): FishermanResult {
|
|
215
|
+
const made = haul.requests();
|
|
216
|
+
const writes = haul.successfulWrites();
|
|
217
|
+
const failures = haul.failed();
|
|
212
218
|
let summary = `Stopped before finishing: ${made.length} requests, ${writes.length} successful writes, ${failures.length} failed`;
|
|
213
219
|
const lastFailure = failures[failures.length - 1];
|
|
214
220
|
if (lastFailure) summary += `; last failure: ${lastFailure.toSummary()}`;
|
package/src/ai/fisherman.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import dedent from 'dedent';
|
|
2
2
|
import type { ApiClient } from '../api/api-client.ts';
|
|
3
|
-
import type
|
|
3
|
+
import { type RequestStore, isFailedRequest } from '../api/request-store.ts';
|
|
4
4
|
import { listAllEndpoints } from '../api/spec-reader.ts';
|
|
5
5
|
import { createDebug, tag } from '../utils/logger.ts';
|
|
6
6
|
|
|
@@ -8,6 +8,7 @@ const debugLog = createDebug('explorbot:fisherman');
|
|
|
8
8
|
import { loop } from '../utils/loop.ts';
|
|
9
9
|
import type { Agent } from './agent.ts';
|
|
10
10
|
import { type FishermanResult, createFishermanTools } from './fisherman-tools.ts';
|
|
11
|
+
import { RequestHaul } from './fisherman/request-haul.ts';
|
|
11
12
|
import type { Provider } from './provider.ts';
|
|
12
13
|
import { dataProtectionRules } from './rules.ts';
|
|
13
14
|
|
|
@@ -79,11 +80,11 @@ export class Fisherman implements Agent {
|
|
|
79
80
|
await this.refreshAuth();
|
|
80
81
|
debugLog(`auth headers: ${Object.keys(this.apiClient.getHeaders()).join(', ')}`);
|
|
81
82
|
|
|
82
|
-
const
|
|
83
|
+
const haul = new RequestHaul(this.requestStore);
|
|
84
|
+
const { tools, getResult, isFinished, finishFromText } = createFishermanTools(this.apiClient, this.requestStore, haul, {
|
|
83
85
|
spec: this.spec,
|
|
84
86
|
baseEndpoint: this.baseEndpoint,
|
|
85
87
|
});
|
|
86
|
-
const ledgerStart = this.requestStore.getMadeRequests().length;
|
|
87
88
|
|
|
88
89
|
const conversation = this.provider.startConversation(this.buildSystemPrompt(endpointList, Object.keys(tools), scopeUrl), 'fisherman');
|
|
89
90
|
conversation.addUserText(this.buildTaskPrompt(instructions));
|
|
@@ -109,7 +110,7 @@ export class Fisherman implements Agent {
|
|
|
109
110
|
return;
|
|
110
111
|
}
|
|
111
112
|
|
|
112
|
-
if (this.isStuckOnEndpoint(
|
|
113
|
+
if (this.isStuckOnEndpoint(haul)) {
|
|
113
114
|
tag('warning').log('Fisherman: repeated failures on the same endpoint — stopping');
|
|
114
115
|
stop();
|
|
115
116
|
return;
|
|
@@ -228,12 +229,12 @@ export class Fisherman implements Agent {
|
|
|
228
229
|
`;
|
|
229
230
|
}
|
|
230
231
|
|
|
231
|
-
private isStuckOnEndpoint(
|
|
232
|
-
const made =
|
|
232
|
+
private isStuckOnEndpoint(haul: RequestHaul): boolean {
|
|
233
|
+
const made = haul.requests();
|
|
233
234
|
if (made.length < REPEATED_FAILURE_LIMIT) return false;
|
|
234
235
|
const recent = made.slice(-REPEATED_FAILURE_LIMIT);
|
|
235
236
|
const first = recent[0];
|
|
236
|
-
return recent.every((r) => (r
|
|
237
|
+
return recent.every((r) => isFailedRequest(r) && r.method === first.method && r.path === first.path);
|
|
237
238
|
}
|
|
238
239
|
|
|
239
240
|
private buildTaskPrompt(instructions: string): string {
|
package/src/ai/navigator.ts
CHANGED
|
@@ -575,7 +575,7 @@ class Navigator implements Agent {
|
|
|
575
575
|
}
|
|
576
576
|
|
|
577
577
|
const currentActionResult = actionResult || ActionResult.fromState(state);
|
|
578
|
-
const research = Researcher.getCachedResearch(
|
|
578
|
+
const research = Researcher.getCachedResearch(currentActionResult) || '';
|
|
579
579
|
const combinedHtml = await currentActionResult.combinedHtml();
|
|
580
580
|
|
|
581
581
|
const history = stateManager.getStateHistory();
|
package/src/ai/pilot.ts
CHANGED
|
@@ -31,6 +31,7 @@ const META_TOOLS = ['record', 'reset', 'stop', 'finish'];
|
|
|
31
31
|
const PILOT_REASONING_LIMIT = 500;
|
|
32
32
|
const PILOT_MESSAGE_LIMIT = 2;
|
|
33
33
|
const PILOT_MESSAGE_MAX_LENGTH = 160;
|
|
34
|
+
const PILOT_REQUEST_LIMIT = 5;
|
|
34
35
|
|
|
35
36
|
export class Pilot implements Agent {
|
|
36
37
|
emoji = '🧭';
|
|
@@ -826,17 +827,17 @@ export class Pilot implements Agent {
|
|
|
826
827
|
lines.push(`h3: ${state.h3 || ''}`);
|
|
827
828
|
lines.push(`h4: ${state.h4 || ''}`);
|
|
828
829
|
|
|
829
|
-
const
|
|
830
|
-
if (
|
|
831
|
-
let line = `
|
|
832
|
-
if (
|
|
830
|
+
const region = state.overlay;
|
|
831
|
+
if (region.isModal) {
|
|
832
|
+
let line = `overlay: ${region.name || region.type}`;
|
|
833
|
+
if (region.root) line += ` (root: ${region.root})`;
|
|
833
834
|
lines.push(line);
|
|
834
|
-
} else if (
|
|
835
|
-
let line = `region: ${
|
|
836
|
-
if (
|
|
835
|
+
} else if (region.isOpen) {
|
|
836
|
+
let line = `region: ${region.name || 'unnamed'} (inline`;
|
|
837
|
+
if (region.root) line += `, root: ${region.root}`;
|
|
837
838
|
lines.push(`${line})`);
|
|
838
839
|
} else {
|
|
839
|
-
lines.push('
|
|
840
|
+
lines.push('overlay: none');
|
|
840
841
|
}
|
|
841
842
|
|
|
842
843
|
const tabs = this.stateManager.otherTabs;
|
|
@@ -1071,9 +1072,13 @@ export class Pilot implements Agent {
|
|
|
1071
1072
|
|
|
1072
1073
|
if (t.output?.pageDiff?.urlChanged) line += `\n moved: ${t.output.pageDiff.previousUrl} → ${t.output.pageDiff.currentUrl}`;
|
|
1073
1074
|
|
|
1074
|
-
const
|
|
1075
|
-
|
|
1076
|
-
|
|
1075
|
+
const pageRequests = t.output?.pageDiff?.requests ?? [];
|
|
1076
|
+
const requests = pageRequests
|
|
1077
|
+
.filter((r: any) => r.status >= 400)
|
|
1078
|
+
.concat(pageRequests.filter((r: any) => r.status < 400))
|
|
1079
|
+
.slice(0, PILOT_REQUEST_LIMIT);
|
|
1080
|
+
if (requests.length > 0) {
|
|
1081
|
+
line += `\n requests: ${requests.map((r: any) => `${r.method} ${r.path} → ${r.status}`).join(', ')}`;
|
|
1077
1082
|
}
|
|
1078
1083
|
|
|
1079
1084
|
const messages = (t.output?.pageDiff?.messages ?? []).slice(0, PILOT_MESSAGE_LIMIT);
|
|
@@ -1139,15 +1144,15 @@ export class Pilot implements Agent {
|
|
|
1139
1144
|
|
|
1140
1145
|
Diagnostic patterns (use <state>, executed/element/skipped fields, ariaDiff):
|
|
1141
1146
|
- Click failed + button in "disabled buttons" → required field missing. Instruct fill first.
|
|
1142
|
-
- "
|
|
1147
|
+
- "overlay: none" but Tester targets an overlay → overlay closed; re-trigger.
|
|
1143
1148
|
- "region:" in <state> → a large area appeared in place without navigation (subview, wizard step, panel). Direct Tester to act inside it; the rest of the page is still usable.
|
|
1144
1149
|
- Action SUCCESS but ariaDiff empty → may have worked without visible DOM change; check result message.
|
|
1145
|
-
- MultipleElementsFound →
|
|
1150
|
+
- MultipleElementsFound → nothing was clicked. Tell Tester to reuse the same locator with step.opts({ elementIndex: N }) from the numbered elements list.
|
|
1146
1151
|
- Wrong page (settings vs feature) → getVisitedStates() then back() or reset(). Don't try breadcrumbs (SPA back-nav is unreliable).
|
|
1147
1152
|
- Click SUCCESS but executed locator ≠ explanation intent, or "skipped" attempts present → wrong element clicked.
|
|
1148
1153
|
- form(I.type()) SUCCESS but "element" shows a button/link → keys went to wrong element; click the input first.
|
|
1149
1154
|
- ariaDiff shows 5+ added/removed → page entered new mode (editor/modal); call context() before guessing selectors.
|
|
1150
|
-
- Empty dropdown/list when items expected →
|
|
1155
|
+
- Empty dropdown/list when items expected → wait explicitly, then check the state changed: ariaDiff and any GET that loaded data. If still nothing loaded, confirm the empty state with verify().
|
|
1151
1156
|
- Search-and-select needs SEQUENCE: focus trigger → type to filter → click option. Tell Tester to split into separate tool calls.
|
|
1152
1157
|
- Multi-action explanation in one tool call → instruct Tester to split.
|
|
1153
1158
|
|
package/src/ai/planner.ts
CHANGED
|
@@ -114,11 +114,15 @@ export class Planner extends PlannerBase implements Agent {
|
|
|
114
114
|
Tests must be relevant to the page
|
|
115
115
|
Tests must be achievable from UI
|
|
116
116
|
Tests must be verifiable from UI
|
|
117
|
-
|
|
117
|
+
One test verifies ONE business operation: the steps that reach it, the action itself, and its verification.
|
|
118
|
+
Steps that only reach the action — opening a form, expanding a panel, creating or locating the item to act on — belong to that test. A second operation with its own verification does not.
|
|
118
119
|
Bad: "Open delete dropdown" + "Confirm deletion" — these are ONE test, not two.
|
|
119
120
|
Bad: "Search for X" + "Verify search results" — searching and verifying is ONE test.
|
|
120
121
|
Bad: "Leave field empty" + "Click submit" — that's one negative test, not two.
|
|
121
|
-
|
|
122
|
+
Bad: "Create a record, rename it, delete it" — three verified operations, so THREE tests, not one.
|
|
123
|
+
Good: "Rename existing record and verify the new title" — ONE test; creating is skipped, we assume record already exists, only the rename is verified.
|
|
124
|
+
You may rely on another test having run first in case we deal with empty state and no relevant data was created yet and we expect another our test creates it
|
|
125
|
+
When the page reports a record is missing or unavailable, it is not a testable surface — plan list-level or recovery behavior instead of operations on that record.${featureDirective}${focusExistingDataDirective}
|
|
122
126
|
</task>
|
|
123
127
|
|
|
124
128
|
${customPrompt || ''}
|
|
@@ -195,10 +199,6 @@ export class Planner extends PlannerBase implements Agent {
|
|
|
195
199
|
throw new Error('No tasks were created successfully');
|
|
196
200
|
}
|
|
197
201
|
|
|
198
|
-
if (aiResult.object.scenarios.length === 0 && !this.currentPlan) {
|
|
199
|
-
throw new Error('No tasks were created successfully');
|
|
200
|
-
}
|
|
201
|
-
|
|
202
202
|
const defaultStartUrl = this.getDefaultStartUrl(state);
|
|
203
203
|
const fromPlanning = aiResult.object.scenarios.map((s: any) => new Test(s.scenario, s.priority, s.expectedOutcomes, s.startUrl || defaultStartUrl, s.steps || []));
|
|
204
204
|
|
|
@@ -335,6 +335,7 @@ export class Planner extends PlannerBase implements Agent {
|
|
|
335
335
|
<task>
|
|
336
336
|
Based on the page research, create ${this.MIN_TASKS}-${this.MAX_TASKS} exploratory testing scenarios.
|
|
337
337
|
For each scenario provide specific steps and expected outcomes.
|
|
338
|
+
Exception: if the page reports the requested resource is missing, shows a failure state, or holds no content and no controls, return an empty scenarios list. Never invent tests for a page with nothing to exercise.
|
|
338
339
|
</task>
|
|
339
340
|
|
|
340
341
|
<rules>
|
|
@@ -348,13 +349,13 @@ export class Planner extends PlannerBase implements Agent {
|
|
|
348
349
|
Focus on error or success messages as outcome.
|
|
349
350
|
Focus on URL page change or data persistency after page reload.
|
|
350
351
|
If there are subpages (pages with same URL path) plan testing of those subpages as well
|
|
351
|
-
|
|
352
|
+
Plan CRUD operations in order: create, read, update, delete.
|
|
352
353
|
Do not invent specific route names, success messages, validation texts, badge counts, or welcome messages unless they are visible in research, visited pages, or prior observed flows.
|
|
353
354
|
When validation placement or wording was not observed, require feedback associated with the invalid input without inventing a specific location or message.
|
|
354
355
|
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.
|
|
355
356
|
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.
|
|
356
357
|
Only propose scenarios whose prerequisites are evident from page research, visited pages, or API data preparation context.
|
|
357
|
-
If a scenario needs existing records, recipients, results, notifications, or other target data, propose it only when that data is visible
|
|
358
|
+
If a scenario needs existing records, recipients, results, notifications, or other target data, propose it only when that data is visible, API preconditions can create it, or the scenario itself creates the record as its setup.
|
|
358
359
|
If the page appears read-only, degraded, demo-limited, maintenance-like, or lacks write controls, prefer read-only scenarios such as opening panels, inspecting visible lists, filtering, searching, or verifying current state.
|
|
359
360
|
Do not assume hidden data exists just because a control is present.
|
|
360
361
|
For scenarios that act on existing items or search/filter by existing values, use only item names or values visible in research, visited pages, or prior observed flows.
|