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.
Files changed (85) hide show
  1. package/bin/explorbot-cli.ts +97 -39
  2. package/dist/bin/explorbot-cli.js +75 -19
  3. package/dist/rules/rerunner/healing-approach.md +19 -0
  4. package/dist/src/action.js +8 -7
  5. package/dist/src/ai/historian.js +34 -3
  6. package/dist/src/ai/navigator.js +35 -28
  7. package/dist/src/ai/pilot.js +33 -9
  8. package/dist/src/ai/planner/subpages.js +42 -6
  9. package/dist/src/ai/planner.js +44 -13
  10. package/dist/src/ai/rerunner.js +472 -0
  11. package/dist/src/ai/researcher/cache.js +13 -8
  12. package/dist/src/ai/researcher/coordinates.js +4 -2
  13. package/dist/src/ai/researcher/deep-analysis.js +16 -19
  14. package/dist/src/ai/researcher/locators.js +1 -1
  15. package/dist/src/ai/researcher/parser.js +4 -3
  16. package/dist/src/ai/researcher/research-result.js +2 -0
  17. package/dist/src/ai/researcher.js +3 -3
  18. package/dist/src/ai/rules.js +2 -2
  19. package/dist/src/ai/tools.js +6 -2
  20. package/dist/src/commands/add-rule-command.js +1 -2
  21. package/dist/src/commands/base-command.js +12 -0
  22. package/dist/src/commands/context-command.js +10 -3
  23. package/dist/src/commands/drill-command.js +0 -1
  24. package/dist/src/commands/explore-command.js +21 -6
  25. package/dist/src/commands/freesail-command.js +8 -22
  26. package/dist/src/commands/index.js +4 -0
  27. package/dist/src/commands/init-command.js +7 -5
  28. package/dist/src/commands/path-command.js +2 -1
  29. package/dist/src/commands/plan-command.js +38 -11
  30. package/dist/src/commands/rerun-command.js +42 -0
  31. package/dist/src/commands/research-command.js +10 -4
  32. package/dist/src/commands/runs-command.js +22 -0
  33. package/dist/src/commands/start-command.js +0 -1
  34. package/dist/src/commands/test-command.js +3 -3
  35. package/dist/src/components/App.js +8 -0
  36. package/dist/src/config.js +3 -0
  37. package/dist/src/explorbot.js +20 -1
  38. package/dist/src/explorer.js +59 -16
  39. package/dist/src/suite.js +115 -0
  40. package/dist/src/utils/html.js +2 -5
  41. package/dist/src/utils/rules-loader.js +33 -17
  42. package/dist/src/utils/test-files.js +103 -0
  43. package/dist/src/utils/web-element.js +6 -4
  44. package/package.json +3 -2
  45. package/rules/rerunner/healing-approach.md +19 -0
  46. package/src/action.ts +8 -6
  47. package/src/ai/historian.ts +37 -3
  48. package/src/ai/navigator.ts +35 -28
  49. package/src/ai/pilot.ts +33 -9
  50. package/src/ai/planner/subpages.ts +37 -7
  51. package/src/ai/planner.ts +44 -12
  52. package/src/ai/rerunner.ts +532 -0
  53. package/src/ai/researcher/cache.ts +14 -8
  54. package/src/ai/researcher/coordinates.ts +8 -7
  55. package/src/ai/researcher/deep-analysis.ts +18 -21
  56. package/src/ai/researcher/locators.ts +3 -3
  57. package/src/ai/researcher/parser.ts +4 -4
  58. package/src/ai/researcher/research-result.ts +1 -0
  59. package/src/ai/researcher.ts +3 -3
  60. package/src/ai/rules.ts +2 -2
  61. package/src/ai/tools.ts +7 -2
  62. package/src/commands/add-rule-command.ts +1 -2
  63. package/src/commands/base-command.ts +13 -0
  64. package/src/commands/context-command.ts +10 -3
  65. package/src/commands/drill-command.ts +0 -1
  66. package/src/commands/explore-command.ts +22 -6
  67. package/src/commands/freesail-command.ts +6 -23
  68. package/src/commands/index.ts +4 -0
  69. package/src/commands/init-command.ts +8 -5
  70. package/src/commands/path-command.ts +2 -1
  71. package/src/commands/plan-command.ts +46 -12
  72. package/src/commands/rerun-command.ts +46 -0
  73. package/src/commands/research-command.ts +10 -4
  74. package/src/commands/runs-command.ts +27 -0
  75. package/src/commands/start-command.ts +0 -1
  76. package/src/commands/test-command.ts +3 -3
  77. package/src/components/App.tsx +8 -0
  78. package/src/config.ts +24 -0
  79. package/src/explorbot.ts +22 -1
  80. package/src/explorer.ts +68 -20
  81. package/src/suite.ts +135 -0
  82. package/src/utils/html.ts +1 -5
  83. package/src/utils/rules-loader.ts +35 -17
  84. package/src/utils/test-files.ts +122 -0
  85. package/src/utils/web-element.ts +12 -10
@@ -222,9 +222,18 @@ export class Pilot {
222
222
 
223
223
  Plan the test execution for this scenario.
224
224
 
225
- FIRST: Call precondition() to create fresh data that this test will act on.
226
- Ask: "What will this test edit/delete/use?" — create THAT item via precondition.
227
- Do not describe what's already on the page — create new disposable items for the test.
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
- Examples:
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
- WRONG: precondition("1 test suite named Updated Suite with existing tests") this describes the page, not what to create
692
+ Exampleswhen 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
- Call precondition() for EVERY item the scenario will act on. Keep descriptions short and specific.
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 isTemplateMatch(urlA, urlB) {
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
- const isNumericOrShortId = /^\d+$|^[a-f0-9]{4,}$/i;
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 (isPagePlanned(page.url))
97
+ if (this.findSimilarPlan(page.url))
62
98
  continue;
63
99
  if (candidates.some((c) => normalizeUrl(c.url) === pagePath))
64
100
  continue;
@@ -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 (!this.freshStart && !feature && !this.currentPlan && state.url) {
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
- moveExecutedTestsToEnd() {
193
- if (!this.currentPlan)
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();