explorbot 0.0.5 → 0.1.1
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 +97 -39
- package/dist/bin/explorbot-cli.js +75 -19
- package/dist/rules/rerunner/healing-approach.md +19 -0
- package/dist/src/action.js +8 -7
- package/dist/src/ai/historian.js +34 -3
- package/dist/src/ai/navigator.js +35 -28
- package/dist/src/ai/pilot.js +33 -9
- package/dist/src/ai/planner/subpages.js +42 -6
- package/dist/src/ai/planner.js +44 -13
- package/dist/src/ai/rerunner.js +472 -0
- package/dist/src/ai/researcher/cache.js +13 -8
- package/dist/src/ai/researcher/coordinates.js +4 -2
- package/dist/src/ai/researcher/deep-analysis.js +16 -19
- package/dist/src/ai/researcher/locators.js +1 -1
- package/dist/src/ai/researcher/parser.js +4 -3
- package/dist/src/ai/researcher/research-result.js +2 -0
- package/dist/src/ai/researcher.js +3 -3
- package/dist/src/ai/rules.js +2 -2
- package/dist/src/ai/tools.js +6 -2
- package/dist/src/commands/add-rule-command.js +1 -2
- package/dist/src/commands/base-command.js +12 -0
- package/dist/src/commands/context-command.js +10 -3
- package/dist/src/commands/drill-command.js +0 -1
- package/dist/src/commands/explore-command.js +21 -6
- package/dist/src/commands/freesail-command.js +8 -22
- package/dist/src/commands/index.js +4 -0
- package/dist/src/commands/init-command.js +7 -5
- package/dist/src/commands/path-command.js +2 -1
- package/dist/src/commands/plan-command.js +38 -11
- package/dist/src/commands/rerun-command.js +42 -0
- package/dist/src/commands/research-command.js +10 -4
- package/dist/src/commands/runs-command.js +22 -0
- package/dist/src/commands/start-command.js +0 -1
- package/dist/src/commands/test-command.js +3 -3
- package/dist/src/components/App.js +8 -0
- package/dist/src/config.js +3 -0
- package/dist/src/explorbot.js +20 -1
- package/dist/src/explorer.js +59 -16
- package/dist/src/suite.js +115 -0
- package/dist/src/utils/html.js +2 -5
- package/dist/src/utils/rules-loader.js +33 -17
- package/dist/src/utils/test-files.js +103 -0
- package/dist/src/utils/web-element.js +6 -4
- package/package.json +3 -2
- package/rules/rerunner/healing-approach.md +19 -0
- package/src/action.ts +8 -6
- package/src/ai/historian.ts +37 -3
- package/src/ai/navigator.ts +35 -28
- package/src/ai/pilot.ts +33 -9
- package/src/ai/planner/subpages.ts +37 -7
- package/src/ai/planner.ts +44 -12
- package/src/ai/rerunner.ts +532 -0
- package/src/ai/researcher/cache.ts +14 -8
- package/src/ai/researcher/coordinates.ts +8 -7
- package/src/ai/researcher/deep-analysis.ts +18 -21
- package/src/ai/researcher/locators.ts +3 -3
- package/src/ai/researcher/parser.ts +4 -4
- package/src/ai/researcher/research-result.ts +1 -0
- package/src/ai/researcher.ts +3 -3
- package/src/ai/rules.ts +2 -2
- package/src/ai/tools.ts +7 -2
- package/src/commands/add-rule-command.ts +1 -2
- package/src/commands/base-command.ts +13 -0
- package/src/commands/context-command.ts +10 -3
- package/src/commands/drill-command.ts +0 -1
- package/src/commands/explore-command.ts +22 -6
- package/src/commands/freesail-command.ts +6 -23
- package/src/commands/index.ts +4 -0
- package/src/commands/init-command.ts +8 -5
- package/src/commands/path-command.ts +2 -1
- package/src/commands/plan-command.ts +46 -12
- package/src/commands/rerun-command.ts +46 -0
- package/src/commands/research-command.ts +10 -4
- package/src/commands/runs-command.ts +27 -0
- package/src/commands/start-command.ts +0 -1
- package/src/commands/test-command.ts +3 -3
- package/src/components/App.tsx +8 -0
- package/src/config.ts +24 -0
- package/src/explorbot.ts +22 -1
- package/src/explorer.ts +68 -20
- package/src/suite.ts +135 -0
- package/src/utils/html.ts +1 -5
- package/src/utils/rules-loader.ts +35 -17
- package/src/utils/test-files.ts +122 -0
- package/src/utils/web-element.ts +12 -10
package/dist/src/ai/pilot.js
CHANGED
|
@@ -222,9 +222,18 @@ export class Pilot {
|
|
|
222
222
|
|
|
223
223
|
Plan the test execution for this scenario.
|
|
224
224
|
|
|
225
|
-
FIRST:
|
|
226
|
-
|
|
227
|
-
|
|
225
|
+
FIRST: Decide if precondition() is needed.
|
|
226
|
+
|
|
227
|
+
Call precondition() WHEN:
|
|
228
|
+
- The scenario edits/deletes/modifies an item, and you want a DISPOSABLE item to act on safely
|
|
229
|
+
- The scenario needs specific data clearly NOT on the current page (e.g., items with specific statuses for filtering)
|
|
230
|
+
|
|
231
|
+
SKIP precondition() WHEN:
|
|
232
|
+
- The scenario is "Create X" — the test itself creates the item
|
|
233
|
+
- The current page already shows the item the test will act on (check <state> and <page_summary>)
|
|
234
|
+
- The scenario tests navigation, UI behavior, or viewing — no data mutation needed
|
|
235
|
+
|
|
236
|
+
If needed, call precondition() now. If not, proceed directly to planning.
|
|
228
237
|
|
|
229
238
|
THEN: Based on the page elements and current state, outline:
|
|
230
239
|
1. Which elements to interact with and in what order
|
|
@@ -608,6 +617,8 @@ export class Pilot {
|
|
|
608
617
|
- Click succeeded but ariaDiff shows elements unrelated to tester's intention (e.g., clicked "Edit" but dropdown appeared) → wrong button or unexpected behavior. Instruct Tester to Escape and try a different approach.
|
|
609
618
|
- form(I.type()) succeeded → I.type() sends keys to whatever is focused, no guarantee it's the right field. Instruct Tester to verify with see() that text appeared in the correct field. If targetedHtml shows a button/link, text went to wrong element — click the correct field first and retry.
|
|
610
619
|
- ariaDiff shows 5+ elements removed/added after clicking content → page entered a different mode (editor, panel, modal). Instruct Tester to call context() to see current state before guessing selectors.
|
|
620
|
+
- Dropdown/select opened but contains NO options, or a list/table is empty when items were expected → data doesn't exist yet. Call precondition() to create the missing items (labels, categories, etc.), then instruct Tester to retry.
|
|
621
|
+
- Tester tries to select/filter/assign something but the option list is empty or expected value is not present → missing auxiliary data. Call precondition() to create it.
|
|
611
622
|
|
|
612
623
|
Detecting logically wrong successes — review "executed", "element", and "skipped" fields:
|
|
613
624
|
- Click SUCCESS but "executed" command differs from "explanation" intent → wrong element was clicked. The intended element wasn't found and a different one was clicked instead.
|
|
@@ -657,23 +668,36 @@ export class Pilot {
|
|
|
657
668
|
YOUR tools (Pilot-only):
|
|
658
669
|
- precondition(description) — create FRESH test data via API that the test will act on. Do NOT request users.
|
|
659
670
|
|
|
660
|
-
PRECONDITIONS — what to create:
|
|
671
|
+
PRECONDITIONS — when and what to create:
|
|
661
672
|
Preconditions create NEW disposable items that the test will modify, delete, or interact with.
|
|
662
|
-
Do NOT describe what already exists on the page — describe what NEW data the test needs to act on.
|
|
663
673
|
|
|
664
674
|
Ask yourself: "What object will this test change/delete/use? Create THAT."
|
|
665
675
|
|
|
666
|
-
|
|
676
|
+
When to call precondition():
|
|
677
|
+
- Scenario edits/deletes/modifies an item → create a disposable target
|
|
678
|
+
- Scenario needs auxiliary data (labels, categories, statuses to filter by)
|
|
679
|
+
- Tester failed because required data is missing (empty dropdown, no items to select)
|
|
680
|
+
|
|
681
|
+
When to SKIP precondition():
|
|
682
|
+
- Scenario is "Create X" — the test itself creates the item, no precondition needed
|
|
683
|
+
- Current page already shows the exact data needed (check <state> h1/title and <page_summary>)
|
|
684
|
+
- Scenario tests navigation, search UI, or viewing — no data mutation involved
|
|
685
|
+
|
|
686
|
+
Examples — when to create:
|
|
667
687
|
- "Edit test description" → precondition("1 test") — the test will edit this item
|
|
668
688
|
- "Delete a comment" → precondition("1 comment") — the test will delete this item
|
|
669
689
|
- "Assign a label to item" → precondition("1 item and 1 label named Bug") — test assigns the label
|
|
670
690
|
- "Filter by status" → precondition("3 items: 2 with status Open, 1 with status Closed")
|
|
671
|
-
- "Move item between lists" → precondition("1 item in list A")
|
|
672
691
|
|
|
673
|
-
|
|
692
|
+
Examples — when to skip:
|
|
693
|
+
- "Create a new blog post" → SKIP, the test creates it
|
|
694
|
+
- "Edit blog post" while on a blog post page → SKIP, data already exists
|
|
695
|
+
- "View dashboard" → SKIP, no data mutation
|
|
696
|
+
|
|
697
|
+
WRONG: precondition("1 test suite named Updated Suite with existing tests") — describes the page, not what to create
|
|
674
698
|
RIGHT: precondition("1 test") — create a fresh test that the scenario will edit
|
|
675
699
|
|
|
676
|
-
|
|
700
|
+
Keep descriptions short and specific.
|
|
677
701
|
|
|
678
702
|
Response format:
|
|
679
703
|
PROGRESS: <1 sentence assessment>
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import dedent from 'dedent';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
+
import { ConfigParser } from "../../config.js";
|
|
3
4
|
import { normalizeUrl } from "../../state-manager.js";
|
|
4
5
|
const planRegistry = new Map();
|
|
5
|
-
export function registerPlan(url, plan, feature) {
|
|
6
|
+
export function registerPlan(url, plan, feature, stateHash) {
|
|
6
7
|
const key = buildKey(url, feature);
|
|
7
|
-
planRegistry.set(key, { plan, feature, url });
|
|
8
|
+
planRegistry.set(key, { plan, feature, url, stateHash });
|
|
8
9
|
}
|
|
9
10
|
export function getRegisteredPlan(url, feature) {
|
|
10
11
|
return planRegistry.get(buildKey(url, feature));
|
|
@@ -26,7 +27,36 @@ function buildKey(url, feature) {
|
|
|
26
27
|
return `${normalized}::${feature}`;
|
|
27
28
|
return normalized;
|
|
28
29
|
}
|
|
29
|
-
function
|
|
30
|
+
export function isDynamicSegment(segment) {
|
|
31
|
+
try {
|
|
32
|
+
const configRegex = ConfigParser.getInstance().getConfig().dynamicPageRegex;
|
|
33
|
+
if (configRegex)
|
|
34
|
+
return new RegExp(configRegex, 'i').test(segment);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
/* config not loaded yet */
|
|
38
|
+
}
|
|
39
|
+
// numeric: /users/123
|
|
40
|
+
if (/^\d+$/.test(segment))
|
|
41
|
+
return true;
|
|
42
|
+
// UUID: /items/550e8400-e29b-41d4-a716-446655440000
|
|
43
|
+
if (/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i.test(segment))
|
|
44
|
+
return true;
|
|
45
|
+
// ULID: /items/01ARZ3NDEKTSV4RRFFQ69G5FAV
|
|
46
|
+
if (/^[0-9A-HJKMNP-TV-Z]{26}$/.test(segment))
|
|
47
|
+
return true;
|
|
48
|
+
// hex ID (4+ chars): /suite/70dae98a
|
|
49
|
+
if (/^[a-f0-9]{4,}$/i.test(segment))
|
|
50
|
+
return true;
|
|
51
|
+
// hex-prefixed slug (8+ hex before dash): /suite/95ef0c94-mobile
|
|
52
|
+
if (/^[a-f0-9]{8,}-/i.test(segment))
|
|
53
|
+
return true;
|
|
54
|
+
// short mixed alphanumeric (digits + letters, ≤8 chars, no dash): /item/x7f2
|
|
55
|
+
if (segment.length <= 8 && !segment.includes('-') && /\d/.test(segment) && /[a-z]/i.test(segment))
|
|
56
|
+
return true;
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
export function isTemplateMatch(urlA, urlB) {
|
|
30
60
|
const partsA = normalizeUrl(urlA).split('/');
|
|
31
61
|
const partsB = normalizeUrl(urlB).split('/');
|
|
32
62
|
if (partsA.length !== partsB.length)
|
|
@@ -38,12 +68,18 @@ function isTemplateMatch(urlA, urlB) {
|
|
|
38
68
|
diffCount++;
|
|
39
69
|
if (diffCount > 1)
|
|
40
70
|
return false;
|
|
41
|
-
|
|
42
|
-
if (!isNumericOrShortId.test(partsA[i]) && !isNumericOrShortId.test(partsB[i]))
|
|
71
|
+
if (!isDynamicSegment(partsA[i]) && !isDynamicSegment(partsB[i]))
|
|
43
72
|
return false;
|
|
44
73
|
}
|
|
45
74
|
return diffCount === 1;
|
|
46
75
|
}
|
|
76
|
+
export function getPlannedByStateHash(hash) {
|
|
77
|
+
for (const record of planRegistry.values()) {
|
|
78
|
+
if (record.stateHash === hash)
|
|
79
|
+
return record;
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
47
83
|
const SubPagePickSchema = z.object({
|
|
48
84
|
url: z.string().nullable(),
|
|
49
85
|
reason: z.string(),
|
|
@@ -58,7 +94,7 @@ export function WithSubPages(Base) {
|
|
|
58
94
|
const pagePath = normalizeUrl(page.url);
|
|
59
95
|
if (!pagePath.startsWith(currentPath) || pagePath === currentPath)
|
|
60
96
|
continue;
|
|
61
|
-
if (
|
|
97
|
+
if (this.findSimilarPlan(page.url))
|
|
62
98
|
continue;
|
|
63
99
|
if (candidates.some((c) => normalizeUrl(c.url) === pagePath))
|
|
64
100
|
continue;
|
package/dist/src/ai/planner.js
CHANGED
|
@@ -14,9 +14,11 @@ import { mdq } from '../utils/markdown-query.js';
|
|
|
14
14
|
import { Conversation } from "./conversation.js";
|
|
15
15
|
import { getActiveStyle, getStyles } from "./planner/styles.js";
|
|
16
16
|
import { WithSessionDedup } from "./planner/session-dedup.js";
|
|
17
|
-
import { WithSubPages, getRegisteredPlan, registerPlan } from "./planner/subpages.js";
|
|
17
|
+
import { WithSubPages, getPlannedByStateHash, getRegisteredPlan, registerPlan } from "./planner/subpages.js";
|
|
18
|
+
import { findSimilarStateHash } from "./researcher/cache.js";
|
|
18
19
|
import { hasFocusedSection } from "./researcher/focus.js";
|
|
19
20
|
import { POSSIBLE_SECTIONS, Researcher } from "./researcher.js";
|
|
21
|
+
import { Suite } from "../suite.js";
|
|
20
22
|
import { fileUploadRule, protectionRule } from "./rules.js";
|
|
21
23
|
const debugLog = createDebug('explorbot:planner');
|
|
22
24
|
const TasksSchema = z.object({
|
|
@@ -45,6 +47,7 @@ export class Planner extends PlannerBase {
|
|
|
45
47
|
currentPlan = null;
|
|
46
48
|
freshStart = false;
|
|
47
49
|
lastStyleName = '';
|
|
50
|
+
lastSuite = null;
|
|
48
51
|
researcher;
|
|
49
52
|
fisherman = null;
|
|
50
53
|
constructor(explorer, provider) {
|
|
@@ -108,13 +111,24 @@ export class Planner extends PlannerBase {
|
|
|
108
111
|
debugLog('Planning:', state?.url);
|
|
109
112
|
if (!state)
|
|
110
113
|
throw new Error('No state found');
|
|
111
|
-
if (!
|
|
114
|
+
if (!feature && !this.currentPlan && state.url) {
|
|
112
115
|
const similar = this.findSimilarPlan(state.url);
|
|
113
116
|
if (similar) {
|
|
114
117
|
tag('info').log(`Similar page already planned: ${similar.url} (${similar.plan.tests.length} tests)`);
|
|
115
118
|
this.registerPlanInSession(similar.plan);
|
|
116
119
|
return similar.plan;
|
|
117
120
|
}
|
|
121
|
+
const actionResult = ActionResult.fromState(state);
|
|
122
|
+
const combinedHtml = await actionResult.combinedHtml();
|
|
123
|
+
const similarHash = await findSimilarStateHash(combinedHtml);
|
|
124
|
+
if (similarHash) {
|
|
125
|
+
const planned = getPlannedByStateHash(similarHash);
|
|
126
|
+
if (planned) {
|
|
127
|
+
tag('info').log(`Page content similar to already-planned: ${planned.url} — skipping`);
|
|
128
|
+
this.registerPlanInSession(planned.plan);
|
|
129
|
+
return planned.plan;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
118
132
|
}
|
|
119
133
|
if (!this.freshStart && !this.currentPlan && state.url) {
|
|
120
134
|
this.currentPlan = Planner.getCachedPlan(state.url);
|
|
@@ -161,6 +175,9 @@ export class Planner extends PlannerBase {
|
|
|
161
175
|
if (parentPlan)
|
|
162
176
|
this.currentPlan.parentPlan = parentPlan;
|
|
163
177
|
const allPreviousScenarios = this.getPreviousSessionScenarios();
|
|
178
|
+
const existingTestScenarios = this.getExistingTestFileScenarios(state.url);
|
|
179
|
+
for (const s of existingTestScenarios)
|
|
180
|
+
allPreviousScenarios.add(s);
|
|
164
181
|
for (const t of tests) {
|
|
165
182
|
if (allPreviousScenarios.has(t.scenario.toLowerCase()))
|
|
166
183
|
continue;
|
|
@@ -168,8 +185,6 @@ export class Planner extends PlannerBase {
|
|
|
168
185
|
t.startUrl = state.url;
|
|
169
186
|
this.currentPlan.addTest(t);
|
|
170
187
|
}
|
|
171
|
-
const summary = `Scenarios:\n${this.currentPlan.tests.map((t) => `- [${t.priority}] ${t.scenario}`).join('\n')}`;
|
|
172
|
-
tag('multiline').log(summary);
|
|
173
188
|
}
|
|
174
189
|
else {
|
|
175
190
|
tag('step').log(`Expanding plan: "${this.currentPlan.title}"`);
|
|
@@ -180,22 +195,16 @@ export class Planner extends PlannerBase {
|
|
|
180
195
|
tag('multiline').log(summary);
|
|
181
196
|
}
|
|
182
197
|
}
|
|
183
|
-
this.moveExecutedTestsToEnd();
|
|
184
198
|
const availableStyles = Object.keys(getStyles()).join(', ');
|
|
185
199
|
tag('success').log(`Planning complete! ${this.currentPlan.tests.length} tests in plan: ${this.currentPlan.title}`);
|
|
186
200
|
tag('info').log(`Planning style: ${this.lastStyleName} (available: ${availableStyles})`);
|
|
187
201
|
if (state.url)
|
|
188
|
-
registerPlan(state.url, this.currentPlan, feature);
|
|
202
|
+
registerPlan(state.url, this.currentPlan, feature, state.hash);
|
|
189
203
|
this.registerPlanInSession(this.currentPlan);
|
|
190
204
|
return this.currentPlan;
|
|
191
205
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
return;
|
|
195
|
-
const pending = this.currentPlan.tests.filter((t) => t.result === null);
|
|
196
|
-
const executed = this.currentPlan.tests.filter((t) => t.result !== null);
|
|
197
|
-
this.currentPlan.tests = [...pending, ...executed];
|
|
198
|
-
this.currentPlan.notifyChange();
|
|
206
|
+
getSuite() {
|
|
207
|
+
return this.lastSuite;
|
|
199
208
|
}
|
|
200
209
|
addNewTests(tests, defaultStartUrl) {
|
|
201
210
|
if (!this.currentPlan)
|
|
@@ -217,6 +226,18 @@ export class Planner extends PlannerBase {
|
|
|
217
226
|
}
|
|
218
227
|
return added;
|
|
219
228
|
}
|
|
229
|
+
getExistingTestFileScenarios(currentUrl) {
|
|
230
|
+
if (!currentUrl)
|
|
231
|
+
return new Set();
|
|
232
|
+
try {
|
|
233
|
+
this.lastSuite = new Suite(currentUrl);
|
|
234
|
+
return this.lastSuite.getActiveScenarioTitles();
|
|
235
|
+
}
|
|
236
|
+
catch (err) {
|
|
237
|
+
debugLog('Failed to load existing test files: %s', err.message);
|
|
238
|
+
return new Set();
|
|
239
|
+
}
|
|
240
|
+
}
|
|
220
241
|
cleanExperienceFlows(text) {
|
|
221
242
|
const seenTitles = new Set();
|
|
222
243
|
let result = text;
|
|
@@ -364,6 +385,16 @@ export class Planner extends PlannerBase {
|
|
|
364
385
|
`);
|
|
365
386
|
}
|
|
366
387
|
}
|
|
388
|
+
if (this.lastSuite && this.lastSuite.automatedTestCount > 0) {
|
|
389
|
+
const automatedNames = this.lastSuite.getAutomatedTestNames();
|
|
390
|
+
conversation.addUserText(dedent `
|
|
391
|
+
<existing_automated_tests>
|
|
392
|
+
The following ${automatedNames.length} tests are already implemented and automated for this URL.
|
|
393
|
+
Do not propose tests that duplicate these:
|
|
394
|
+
${automatedNames.map((n) => `- ${n}`).join('\n')}
|
|
395
|
+
</existing_automated_tests>
|
|
396
|
+
`);
|
|
397
|
+
}
|
|
367
398
|
if (this.currentPlan) {
|
|
368
399
|
tag('step').log('Analyzing current plan to expand testing');
|
|
369
400
|
const allTests = this.currentPlan.getAllTests();
|