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/src/ai/pilot.ts
CHANGED
|
@@ -256,9 +256,18 @@ export class Pilot implements Agent {
|
|
|
256
256
|
|
|
257
257
|
Plan the test execution for this scenario.
|
|
258
258
|
|
|
259
|
-
FIRST:
|
|
260
|
-
|
|
261
|
-
|
|
259
|
+
FIRST: Decide if precondition() is needed.
|
|
260
|
+
|
|
261
|
+
Call precondition() WHEN:
|
|
262
|
+
- The scenario edits/deletes/modifies an item, and you want a DISPOSABLE item to act on safely
|
|
263
|
+
- The scenario needs specific data clearly NOT on the current page (e.g., items with specific statuses for filtering)
|
|
264
|
+
|
|
265
|
+
SKIP precondition() WHEN:
|
|
266
|
+
- The scenario is "Create X" — the test itself creates the item
|
|
267
|
+
- The current page already shows the item the test will act on (check <state> and <page_summary>)
|
|
268
|
+
- The scenario tests navigation, UI behavior, or viewing — no data mutation needed
|
|
269
|
+
|
|
270
|
+
If needed, call precondition() now. If not, proceed directly to planning.
|
|
262
271
|
|
|
263
272
|
THEN: Based on the page elements and current state, outline:
|
|
264
273
|
1. Which elements to interact with and in what order
|
|
@@ -701,6 +710,8 @@ export class Pilot implements Agent {
|
|
|
701
710
|
- 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.
|
|
702
711
|
- 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.
|
|
703
712
|
- 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.
|
|
713
|
+
- 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.
|
|
714
|
+
- 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.
|
|
704
715
|
|
|
705
716
|
Detecting logically wrong successes — review "executed", "element", and "skipped" fields:
|
|
706
717
|
- 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.
|
|
@@ -750,23 +761,36 @@ export class Pilot implements Agent {
|
|
|
750
761
|
YOUR tools (Pilot-only):
|
|
751
762
|
- precondition(description) — create FRESH test data via API that the test will act on. Do NOT request users.
|
|
752
763
|
|
|
753
|
-
PRECONDITIONS — what to create:
|
|
764
|
+
PRECONDITIONS — when and what to create:
|
|
754
765
|
Preconditions create NEW disposable items that the test will modify, delete, or interact with.
|
|
755
|
-
Do NOT describe what already exists on the page — describe what NEW data the test needs to act on.
|
|
756
766
|
|
|
757
767
|
Ask yourself: "What object will this test change/delete/use? Create THAT."
|
|
758
768
|
|
|
759
|
-
|
|
769
|
+
When to call precondition():
|
|
770
|
+
- Scenario edits/deletes/modifies an item → create a disposable target
|
|
771
|
+
- Scenario needs auxiliary data (labels, categories, statuses to filter by)
|
|
772
|
+
- Tester failed because required data is missing (empty dropdown, no items to select)
|
|
773
|
+
|
|
774
|
+
When to SKIP precondition():
|
|
775
|
+
- Scenario is "Create X" — the test itself creates the item, no precondition needed
|
|
776
|
+
- Current page already shows the exact data needed (check <state> h1/title and <page_summary>)
|
|
777
|
+
- Scenario tests navigation, search UI, or viewing — no data mutation involved
|
|
778
|
+
|
|
779
|
+
Examples — when to create:
|
|
760
780
|
- "Edit test description" → precondition("1 test") — the test will edit this item
|
|
761
781
|
- "Delete a comment" → precondition("1 comment") — the test will delete this item
|
|
762
782
|
- "Assign a label to item" → precondition("1 item and 1 label named Bug") — test assigns the label
|
|
763
783
|
- "Filter by status" → precondition("3 items: 2 with status Open, 1 with status Closed")
|
|
764
|
-
- "Move item between lists" → precondition("1 item in list A")
|
|
765
784
|
|
|
766
|
-
|
|
785
|
+
Examples — when to skip:
|
|
786
|
+
- "Create a new blog post" → SKIP, the test creates it
|
|
787
|
+
- "Edit blog post" while on a blog post page → SKIP, data already exists
|
|
788
|
+
- "View dashboard" → SKIP, no data mutation
|
|
789
|
+
|
|
790
|
+
WRONG: precondition("1 test suite named Updated Suite with existing tests") — describes the page, not what to create
|
|
767
791
|
RIGHT: precondition("1 test") — create a fresh test that the scenario will edit
|
|
768
792
|
|
|
769
|
-
|
|
793
|
+
Keep descriptions short and specific.
|
|
770
794
|
|
|
771
795
|
Response format:
|
|
772
796
|
PROGRESS: <1 sentence assessment>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import dedent from 'dedent';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
+
import { ConfigParser } from '../../config.ts';
|
|
3
4
|
import { normalizeUrl } from '../../state-manager.ts';
|
|
4
5
|
import type { StateManager } from '../../state-manager.ts';
|
|
5
6
|
import type { Plan } from '../../test-plan.ts';
|
|
@@ -9,9 +10,9 @@ import type { Constructor } from '../researcher/mixin.ts';
|
|
|
9
10
|
|
|
10
11
|
const planRegistry: Map<string, PlanRecord> = new Map();
|
|
11
12
|
|
|
12
|
-
export function registerPlan(url: string, plan: Plan, feature?: string): void {
|
|
13
|
+
export function registerPlan(url: string, plan: Plan, feature?: string, stateHash?: string): void {
|
|
13
14
|
const key = buildKey(url, feature);
|
|
14
|
-
planRegistry.set(key, { plan, feature, url });
|
|
15
|
+
planRegistry.set(key, { plan, feature, url, stateHash });
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
export function getRegisteredPlan(url: string, feature?: string): PlanRecord | undefined {
|
|
@@ -37,7 +38,30 @@ function buildKey(url: string, feature?: string): string {
|
|
|
37
38
|
return normalized;
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
function
|
|
41
|
+
export function isDynamicSegment(segment: string): boolean {
|
|
42
|
+
try {
|
|
43
|
+
const configRegex = ConfigParser.getInstance().getConfig().dynamicPageRegex;
|
|
44
|
+
if (configRegex) return new RegExp(configRegex, 'i').test(segment);
|
|
45
|
+
} catch {
|
|
46
|
+
/* config not loaded yet */
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// numeric: /users/123
|
|
50
|
+
if (/^\d+$/.test(segment)) return true;
|
|
51
|
+
// UUID: /items/550e8400-e29b-41d4-a716-446655440000
|
|
52
|
+
if (/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i.test(segment)) return true;
|
|
53
|
+
// ULID: /items/01ARZ3NDEKTSV4RRFFQ69G5FAV
|
|
54
|
+
if (/^[0-9A-HJKMNP-TV-Z]{26}$/.test(segment)) return true;
|
|
55
|
+
// hex ID (4+ chars): /suite/70dae98a
|
|
56
|
+
if (/^[a-f0-9]{4,}$/i.test(segment)) return true;
|
|
57
|
+
// hex-prefixed slug (8+ hex before dash): /suite/95ef0c94-mobile
|
|
58
|
+
if (/^[a-f0-9]{8,}-/i.test(segment)) return true;
|
|
59
|
+
// short mixed alphanumeric (digits + letters, ≤8 chars, no dash): /item/x7f2
|
|
60
|
+
if (segment.length <= 8 && !segment.includes('-') && /\d/.test(segment) && /[a-z]/i.test(segment)) return true;
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function isTemplateMatch(urlA: string, urlB: string): boolean {
|
|
41
65
|
const partsA = normalizeUrl(urlA).split('/');
|
|
42
66
|
const partsB = normalizeUrl(urlB).split('/');
|
|
43
67
|
if (partsA.length !== partsB.length) return false;
|
|
@@ -47,12 +71,18 @@ function isTemplateMatch(urlA: string, urlB: string): boolean {
|
|
|
47
71
|
if (partsA[i] === partsB[i]) continue;
|
|
48
72
|
diffCount++;
|
|
49
73
|
if (diffCount > 1) return false;
|
|
50
|
-
|
|
51
|
-
if (!isNumericOrShortId.test(partsA[i]) && !isNumericOrShortId.test(partsB[i])) return false;
|
|
74
|
+
if (!isDynamicSegment(partsA[i]) && !isDynamicSegment(partsB[i])) return false;
|
|
52
75
|
}
|
|
53
76
|
return diffCount === 1;
|
|
54
77
|
}
|
|
55
78
|
|
|
79
|
+
export function getPlannedByStateHash(hash: string): PlanRecord | null {
|
|
80
|
+
for (const record of planRegistry.values()) {
|
|
81
|
+
if (record.stateHash === hash) return record;
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
56
86
|
const SubPagePickSchema = z.object({
|
|
57
87
|
url: z.string().nullable(),
|
|
58
88
|
reason: z.string(),
|
|
@@ -71,7 +101,7 @@ export function WithSubPages<T extends Constructor>(Base: T) {
|
|
|
71
101
|
for (const page of visited) {
|
|
72
102
|
const pagePath = normalizeUrl(page.url);
|
|
73
103
|
if (!pagePath.startsWith(currentPath) || pagePath === currentPath) continue;
|
|
74
|
-
if (
|
|
104
|
+
if (this.findSimilarPlan(page.url)) continue;
|
|
75
105
|
if (candidates.some((c) => normalizeUrl(c.url) === pagePath)) continue;
|
|
76
106
|
|
|
77
107
|
candidates.push({
|
|
@@ -136,6 +166,6 @@ export function WithSubPages<T extends Constructor>(Base: T) {
|
|
|
136
166
|
};
|
|
137
167
|
}
|
|
138
168
|
|
|
139
|
-
type PlanRecord = { plan: Plan; feature?: string; url: string };
|
|
169
|
+
type PlanRecord = { plan: Plan; feature?: string; url: string; stateHash?: string };
|
|
140
170
|
|
|
141
171
|
type SubPageCandidate = { url: string; title?: string; h1?: string; visitCount: number };
|
package/src/ai/planner.ts
CHANGED
|
@@ -18,10 +18,12 @@ import { Conversation } from './conversation.ts';
|
|
|
18
18
|
import type { Fisherman } from './fisherman.ts';
|
|
19
19
|
import { getActiveStyle, getStyles } from './planner/styles.ts';
|
|
20
20
|
import { WithSessionDedup } from './planner/session-dedup.ts';
|
|
21
|
-
import { WithSubPages, getRegisteredPlan, registerPlan } from './planner/subpages.ts';
|
|
21
|
+
import { WithSubPages, getPlannedByStateHash, getRegisteredPlan, registerPlan } from './planner/subpages.ts';
|
|
22
|
+
import { findSimilarStateHash } from './researcher/cache.ts';
|
|
22
23
|
import type { Provider } from './provider.js';
|
|
23
24
|
import { hasFocusedSection } from './researcher/focus.ts';
|
|
24
25
|
import { POSSIBLE_SECTIONS, Researcher } from './researcher.ts';
|
|
26
|
+
import { Suite } from '../suite.ts';
|
|
25
27
|
import { fileUploadRule, protectionRule } from './rules.ts';
|
|
26
28
|
|
|
27
29
|
const debugLog = createDebug('explorbot:planner');
|
|
@@ -57,6 +59,7 @@ export class Planner extends PlannerBase implements Agent {
|
|
|
57
59
|
currentPlan: Plan | null = null;
|
|
58
60
|
freshStart = false;
|
|
59
61
|
private lastStyleName = '';
|
|
62
|
+
private lastSuite: Suite | null = null;
|
|
60
63
|
researcher: Researcher;
|
|
61
64
|
private fisherman: Fisherman | null = null;
|
|
62
65
|
|
|
@@ -127,13 +130,25 @@ export class Planner extends PlannerBase implements Agent {
|
|
|
127
130
|
debugLog('Planning:', state?.url);
|
|
128
131
|
if (!state) throw new Error('No state found');
|
|
129
132
|
|
|
130
|
-
if (!
|
|
133
|
+
if (!feature && !this.currentPlan && state.url) {
|
|
131
134
|
const similar = this.findSimilarPlan(state.url);
|
|
132
135
|
if (similar) {
|
|
133
136
|
tag('info').log(`Similar page already planned: ${similar.url} (${similar.plan.tests.length} tests)`);
|
|
134
137
|
this.registerPlanInSession(similar.plan);
|
|
135
138
|
return similar.plan;
|
|
136
139
|
}
|
|
140
|
+
|
|
141
|
+
const actionResult = ActionResult.fromState(state);
|
|
142
|
+
const combinedHtml = await actionResult.combinedHtml();
|
|
143
|
+
const similarHash = await findSimilarStateHash(combinedHtml);
|
|
144
|
+
if (similarHash) {
|
|
145
|
+
const planned = getPlannedByStateHash(similarHash);
|
|
146
|
+
if (planned) {
|
|
147
|
+
tag('info').log(`Page content similar to already-planned: ${planned.url} — skipping`);
|
|
148
|
+
this.registerPlanInSession(planned.plan);
|
|
149
|
+
return planned.plan;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
137
152
|
}
|
|
138
153
|
|
|
139
154
|
if (!this.freshStart && !this.currentPlan && state.url) {
|
|
@@ -188,14 +203,14 @@ export class Planner extends PlannerBase implements Agent {
|
|
|
188
203
|
this.currentPlan.url = state.url;
|
|
189
204
|
if (parentPlan) this.currentPlan.parentPlan = parentPlan;
|
|
190
205
|
const allPreviousScenarios = this.getPreviousSessionScenarios();
|
|
206
|
+
const existingTestScenarios = this.getExistingTestFileScenarios(state.url);
|
|
207
|
+
for (const s of existingTestScenarios) allPreviousScenarios.add(s);
|
|
191
208
|
for (const t of tests) {
|
|
192
209
|
if (allPreviousScenarios.has(t.scenario.toLowerCase())) continue;
|
|
193
210
|
t.style = this.lastStyleName;
|
|
194
211
|
t.startUrl = state.url;
|
|
195
212
|
this.currentPlan.addTest(t);
|
|
196
213
|
}
|
|
197
|
-
const summary = `Scenarios:\n${this.currentPlan.tests.map((t) => `- [${t.priority}] ${t.scenario}`).join('\n')}`;
|
|
198
|
-
tag('multiline').log(summary);
|
|
199
214
|
} else {
|
|
200
215
|
tag('step').log(`Expanding plan: "${this.currentPlan.title}"`);
|
|
201
216
|
this.currentPlan.nextIteration();
|
|
@@ -206,24 +221,19 @@ export class Planner extends PlannerBase implements Agent {
|
|
|
206
221
|
}
|
|
207
222
|
}
|
|
208
223
|
|
|
209
|
-
this.moveExecutedTestsToEnd();
|
|
210
224
|
const availableStyles = Object.keys(getStyles()).join(', ');
|
|
211
225
|
tag('success').log(`Planning complete! ${this.currentPlan.tests.length} tests in plan: ${this.currentPlan.title}`);
|
|
212
226
|
tag('info').log(`Planning style: ${this.lastStyleName} (available: ${availableStyles})`);
|
|
213
227
|
|
|
214
|
-
if (state.url) registerPlan(state.url, this.currentPlan, feature);
|
|
228
|
+
if (state.url) registerPlan(state.url, this.currentPlan, feature, state.hash);
|
|
215
229
|
|
|
216
230
|
this.registerPlanInSession(this.currentPlan);
|
|
217
231
|
|
|
218
232
|
return this.currentPlan;
|
|
219
233
|
}
|
|
220
234
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
const pending = this.currentPlan.tests.filter((t) => t.result === null);
|
|
224
|
-
const executed = this.currentPlan.tests.filter((t) => t.result !== null);
|
|
225
|
-
this.currentPlan.tests = [...pending, ...executed];
|
|
226
|
-
this.currentPlan.notifyChange();
|
|
235
|
+
getSuite(): Suite | null {
|
|
236
|
+
return this.lastSuite;
|
|
227
237
|
}
|
|
228
238
|
|
|
229
239
|
private addNewTests(tests: Test[], defaultStartUrl: string): Test[] {
|
|
@@ -249,6 +259,17 @@ export class Planner extends PlannerBase implements Agent {
|
|
|
249
259
|
return added;
|
|
250
260
|
}
|
|
251
261
|
|
|
262
|
+
private getExistingTestFileScenarios(currentUrl?: string): Set<string> {
|
|
263
|
+
if (!currentUrl) return new Set<string>();
|
|
264
|
+
try {
|
|
265
|
+
this.lastSuite = new Suite(currentUrl);
|
|
266
|
+
return this.lastSuite.getActiveScenarioTitles();
|
|
267
|
+
} catch (err: any) {
|
|
268
|
+
debugLog('Failed to load existing test files: %s', err.message);
|
|
269
|
+
return new Set<string>();
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
252
273
|
private cleanExperienceFlows(text: string): string | null {
|
|
253
274
|
const seenTitles = new Set<string>();
|
|
254
275
|
let result = text;
|
|
@@ -408,6 +429,17 @@ export class Planner extends PlannerBase implements Agent {
|
|
|
408
429
|
}
|
|
409
430
|
}
|
|
410
431
|
|
|
432
|
+
if (this.lastSuite && this.lastSuite.automatedTestCount > 0) {
|
|
433
|
+
const automatedNames = this.lastSuite.getAutomatedTestNames();
|
|
434
|
+
conversation.addUserText(dedent`
|
|
435
|
+
<existing_automated_tests>
|
|
436
|
+
The following ${automatedNames.length} tests are already implemented and automated for this URL.
|
|
437
|
+
Do not propose tests that duplicate these:
|
|
438
|
+
${automatedNames.map((n) => `- ${n}`).join('\n')}
|
|
439
|
+
</existing_automated_tests>
|
|
440
|
+
`);
|
|
441
|
+
}
|
|
442
|
+
|
|
411
443
|
if (this.currentPlan) {
|
|
412
444
|
tag('step').log('Analyzing current plan to expand testing');
|
|
413
445
|
|