explorbot 0.5.10 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/bin/explorbot-cli.ts +494 -482
  2. package/boat/api-tester/src/ai/curler.ts +1 -1
  3. package/boat/api-tester/src/cli.ts +4 -4
  4. package/boat/doc-collector/src/screenshots.ts +1 -1
  5. package/dist/bin/explorbot-cli.js +455 -444
  6. package/dist/boat/api-tester/src/ai/curler.js +1 -1
  7. package/dist/boat/api-tester/src/cli.js +4 -4
  8. package/dist/models.json +1 -1
  9. package/dist/package.json +2 -1
  10. package/dist/rules/navigator/output.md +2 -8
  11. package/dist/rules/navigator/verification-actions.md +6 -2
  12. package/dist/rules/planner/ignored-pages.md +1 -0
  13. package/dist/src/action-result.js +1 -1
  14. package/dist/src/action.d.ts +0 -1
  15. package/dist/src/action.js +0 -4
  16. package/dist/src/ai/captain/idle-mode.js +42 -26
  17. package/dist/src/ai/captain/mixin.d.ts +2 -0
  18. package/dist/src/ai/captain/web-mode.js +11 -0
  19. package/dist/src/ai/captain.d.ts +6 -4
  20. package/dist/src/ai/captain.js +25 -1
  21. package/dist/src/ai/driller.js +1 -1
  22. package/dist/src/ai/experience-compactor.js +2 -2
  23. package/dist/src/ai/judge.d.ts +7 -0
  24. package/dist/src/ai/judge.js +8 -1
  25. package/dist/src/ai/navigator.d.ts +1 -0
  26. package/dist/src/ai/navigator.js +11 -7
  27. package/dist/src/ai/planner/subpages.js +1 -1
  28. package/dist/src/ai/planner.d.ts +12 -5
  29. package/dist/src/ai/planner.js +138 -86
  30. package/dist/src/ai/provider.d.ts +1 -1
  31. package/dist/src/ai/provider.js +16 -5
  32. package/dist/src/ai/rerunner.js +1 -1
  33. package/dist/src/ai/researcher/cache.js +14 -6
  34. package/dist/src/ai/researcher/deep-analysis.d.ts +2 -1
  35. package/dist/src/ai/researcher/deep-analysis.js +7 -2
  36. package/dist/src/ai/researcher/fingerprint-worker.js +4 -4
  37. package/dist/src/ai/researcher.js +44 -24
  38. package/dist/src/ai/rules.js +6 -7
  39. package/dist/src/ai/scout.d.ts +12 -2
  40. package/dist/src/ai/scout.js +103 -8
  41. package/dist/src/ai/tester.d.ts +9 -5
  42. package/dist/src/ai/tester.js +22 -42
  43. package/dist/src/ai/tools.js +10 -8
  44. package/dist/src/application-spec.d.ts +2 -0
  45. package/dist/src/application-spec.js +17 -12
  46. package/dist/src/commands/context-experience-command.js +0 -1
  47. package/dist/src/commands/context-knowledge-command.js +0 -1
  48. package/dist/src/commands/explore-command.d.ts +3 -2
  49. package/dist/src/commands/explore-command.js +35 -23
  50. package/dist/src/commands/help-json-command.js +3 -1
  51. package/dist/src/commands/init-command.js +4 -4
  52. package/dist/src/commands/learn-command.js +0 -2
  53. package/dist/src/commands/plan-command.d.ts +3 -0
  54. package/dist/src/commands/plan-command.js +43 -17
  55. package/dist/src/commands/plan-save-command.js +6 -17
  56. package/dist/src/commands/plans-command.js +1 -1
  57. package/dist/src/commands/test-command.js +12 -2
  58. package/dist/src/components/App.d.ts +1 -2
  59. package/dist/src/components/App.js +1 -3
  60. package/dist/src/components/InputPane.js +0 -5
  61. package/dist/src/config.d.ts +3 -2
  62. package/dist/src/config.js +2 -12
  63. package/dist/src/explorbot.d.ts +3 -2
  64. package/dist/src/explorbot.js +32 -10
  65. package/dist/src/explorer.js +6 -7
  66. package/dist/src/knowledge-tracker.d.ts +2 -2
  67. package/dist/src/knowledge-tracker.js +18 -6
  68. package/dist/src/playwright-recorder.d.ts +0 -1
  69. package/dist/src/playwright-recorder.js +0 -2
  70. package/dist/src/reporter.d.ts +1 -1
  71. package/dist/src/reporter.js +3 -3
  72. package/dist/src/test-plan.js +1 -1
  73. package/dist/src/utils/error-page.d.ts +17 -0
  74. package/dist/src/utils/error-page.js +53 -2
  75. package/dist/src/utils/html-diff.d.ts +1 -2
  76. package/dist/src/utils/html-diff.js +4 -44
  77. package/dist/src/utils/logger.js +27 -10
  78. package/dist/src/utils/markdown-terminal.js +1 -2
  79. package/dist/src/utils/next-steps.js +20 -16
  80. package/dist/src/utils/page-readiness.d.ts +2 -1
  81. package/dist/src/utils/page-readiness.js +24 -5
  82. package/dist/src/utils/test-plan-markdown.d.ts +3 -0
  83. package/dist/src/utils/test-plan-markdown.js +35 -4
  84. package/dist/src/utils/url-matcher.d.ts +1 -0
  85. package/dist/src/utils/url-matcher.js +5 -0
  86. package/docs/basics/providers.md +1 -1
  87. package/docs/contributing/regression-tests.md +19 -4
  88. package/docs/reference/commands.md +6 -2
  89. package/docs/reference/configuration.md +1 -1
  90. package/docs/web-testing/agents.md +8 -0
  91. package/docs/web-testing/customization.md +2 -0
  92. package/docs/web-testing/researcher.md +1 -1
  93. package/models.json +1 -1
  94. package/package.json +2 -1
  95. package/rules/navigator/output.md +2 -8
  96. package/rules/navigator/verification-actions.md +6 -2
  97. package/rules/planner/ignored-pages.md +1 -0
  98. package/src/action-result.ts +1 -2
  99. package/src/action.ts +0 -5
  100. package/src/ai/captain/idle-mode.ts +38 -26
  101. package/src/ai/captain/mixin.ts +2 -1
  102. package/src/ai/captain/web-mode.ts +12 -0
  103. package/src/ai/captain.ts +25 -7
  104. package/src/ai/driller.ts +1 -2
  105. package/src/ai/experience-compactor.ts +2 -2
  106. package/src/ai/judge.ts +10 -1
  107. package/src/ai/navigator.ts +12 -7
  108. package/src/ai/planner/subpages.ts +1 -2
  109. package/src/ai/planner.ts +140 -92
  110. package/src/ai/provider.ts +15 -6
  111. package/src/ai/rerunner.ts +1 -2
  112. package/src/ai/researcher/cache.ts +16 -7
  113. package/src/ai/researcher/deep-analysis.ts +7 -2
  114. package/src/ai/researcher/fingerprint-worker.ts +5 -5
  115. package/src/ai/researcher.ts +44 -28
  116. package/src/ai/rules.ts +6 -7
  117. package/src/ai/scout.ts +98 -10
  118. package/src/ai/tester.ts +29 -47
  119. package/src/ai/tools.ts +11 -9
  120. package/src/application-spec.ts +19 -13
  121. package/src/commands/clean-command.ts +1 -1
  122. package/src/commands/context-command.ts +0 -1
  123. package/src/commands/context-experience-command.ts +0 -1
  124. package/src/commands/context-knowledge-command.ts +0 -1
  125. package/src/commands/explore-command.ts +36 -23
  126. package/src/commands/help-json-command.ts +8 -6
  127. package/src/commands/init-command.ts +5 -3
  128. package/src/commands/learn-command.ts +0 -2
  129. package/src/commands/plan-command.ts +44 -17
  130. package/src/commands/plan-save-command.ts +6 -17
  131. package/src/commands/plans-command.ts +1 -1
  132. package/src/commands/test-command.ts +11 -2
  133. package/src/components/App.tsx +2 -6
  134. package/src/components/InputPane.tsx +0 -5
  135. package/src/config.ts +4 -16
  136. package/src/experience-tracker.ts +1 -1
  137. package/src/explorbot.ts +31 -11
  138. package/src/explorer.ts +6 -7
  139. package/src/knowledge-tracker.ts +18 -7
  140. package/src/playwright-recorder.ts +0 -2
  141. package/src/reporter.ts +3 -4
  142. package/src/test-plan.ts +1 -2
  143. package/src/utils/error-page.ts +64 -2
  144. package/src/utils/html-diff.ts +4 -56
  145. package/src/utils/html.ts +0 -1
  146. package/src/utils/logger.ts +30 -12
  147. package/src/utils/markdown-terminal.ts +1 -2
  148. package/src/utils/next-steps.ts +19 -17
  149. package/src/utils/page-readiness.ts +33 -7
  150. package/src/utils/test-plan-markdown.ts +36 -4
  151. package/src/utils/url-matcher.ts +6 -0
@@ -51,8 +51,8 @@ export function createCodeceptJSTools({ explorer, stateManager, judge }, task) {
51
51
  Never mix different elements — use separate click() calls instead.
52
52
  REQUIRED: include at least one command WITHOUT a container — a wrong or stale container always fails.
53
53
  Order by reliability:
54
- 1. I.click(text, container) - when the container is verified - e.g. I.click("Save", ".modal")
55
- 2. I.click(ARIA, container) - e.g. I.click({"role":"button","text":"Save"}, ".modal")
54
+ 1. I.click(ARIA, container) - e.g. I.click({"role":"button","text":"Save"}, ".modal")
55
+ 2. I.click(text, container) - when the container is verified - e.g. I.click("Save", ".modal")
56
56
  3. I.click(CSS, container) - e.g. I.click("#btn", ".modal")
57
57
  4. I.click(CSS) or I.click(XPath) - when locator already includes context (ID, XPath)
58
58
  After a result reporting multiple matches, reuse that locator with step.opts({ elementIndex: N }) as the last argument.
@@ -251,7 +251,7 @@ export function createCodeceptJSTools({ explorer, stateManager, judge }, task) {
251
251
  const normalizedKey = key.toLowerCase();
252
252
  const matchingStandardKey = Array.from(standardKeys).find((sk) => sk.toLowerCase() === normalizedKey);
253
253
  const isStandardKey = !!matchingStandardKey;
254
- const keyToUse = matchingStandardKey || key;
254
+ const keyToUse = KEY_ALIASES[matchingStandardKey || ''] || matchingStandardKey || key;
255
255
  if (!isSingleChar && !isStandardKey) {
256
256
  const previousState = ActionResult.fromState(stateManager.getCurrentState());
257
257
  const action = explorer.action();
@@ -405,7 +405,7 @@ export function createCodeceptJSTools({ explorer, stateManager, judge }, task) {
405
405
  commandsExecuted: lines.length,
406
406
  attempts: action.executedSteps,
407
407
  code: codeBlock,
408
- suggestion: 'Verify the form was filled in correctly using see() tool. If needed to submit: try click() tool or form() with I.pressKey("Enter").',
408
+ suggestion: 'Check the filled values with verify() before submitting, then submit with click().',
409
409
  }, action);
410
410
  }
411
411
  catch (error) {
@@ -587,7 +587,7 @@ export function createAgentTools({ explorer, stateManager, ai, judge, researcher
587
587
  inputSchema: z.object({
588
588
  reason: z.string().describe('Why do you need fresh context? Required to prevent overuse.'),
589
589
  }),
590
- execute: async ({ reason }) => {
590
+ execute: async () => {
591
591
  try {
592
592
  const currentState = stateManager.getCurrentState();
593
593
  if (!currentState) {
@@ -656,6 +656,7 @@ export function createAgentTools({ explorer, stateManager, ai, judge, researcher
656
656
  });
657
657
  }
658
658
  return failedToolResult('verify', `Verification failed: ${assertion}`, {
659
+ attempts: result.results.map(({ code, passed, error }) => ({ code, passed, error })),
659
660
  suggestion: 'The assertion could not be verified. Check if the condition is actually present on the page or try a different assertion.',
660
661
  });
661
662
  }
@@ -689,7 +690,7 @@ export function createAgentTools({ explorer, stateManager, ai, judge, researcher
689
690
  inputSchema: z.object({
690
691
  reason: z.string().describe('Why do you need research? What information is missing from existing context?'),
691
692
  }),
692
- execute: async ({ reason }) => {
693
+ execute: async () => {
693
694
  try {
694
695
  const currentState = stateManager.getCurrentState();
695
696
  if (!currentState) {
@@ -795,7 +796,7 @@ export function createAgentTools({ explorer, stateManager, ai, judge, researcher
795
796
  element: z.string().describe('Visual description of element to click (e.g., "blue Submit button in the modal footer", "Settings gear icon in top right")'),
796
797
  context: z.string().describe('What you already tried and why it failed - helps with accurate identification'),
797
798
  }),
798
- execute: async ({ element, context }) => {
799
+ execute: async ({ element }) => {
799
800
  if (Stats.visionDisabled) {
800
801
  return failedToolResult('visualClick', 'Vision tools are disabled for this session. Use xpathCheck() to find the element, then click() with the discovered locator.');
801
802
  }
@@ -929,7 +930,7 @@ export function createAgentTools({ explorer, stateManager, ai, judge, researcher
929
930
  xpath: z.string().describe('XPath expression — use broad patterns to avoid missing the element'),
930
931
  reason: z.string().describe('What element you are looking for and why'),
931
932
  }),
932
- execute: async ({ xpath, reason }) => {
933
+ execute: async ({ xpath }) => {
933
934
  const currentState = stateManager.getCurrentState();
934
935
  if (!currentState) {
935
936
  return failedToolResult('xpathCheck', 'No current page state available.');
@@ -1048,6 +1049,7 @@ const ARIA_OUTPUT_CAP = 4000;
1048
1049
  const HTML_OUTPUT_CAP = 6000;
1049
1050
  const ANALYSIS_OUTPUT_CAP = 2000;
1050
1051
  const JUDGE_RECENT_ACTIONS_LIMIT = 8;
1052
+ const KEY_ALIASES = { Esc: 'Escape', Del: 'Delete' };
1051
1053
  function cap(text, max) {
1052
1054
  if (!text)
1053
1055
  return '';
@@ -2,10 +2,12 @@ import { ActionResult } from './action-result.js';
2
2
  export declare class ApplicationSpec {
3
3
  pages: ApplicationSpecPage[];
4
4
  readonly sourcePath: string;
5
+ totalMarkdownFiles: number;
5
6
  constructor(sourcePath: string);
6
7
  renderFor(state: ActionResult): string;
7
8
  matchedUrls(state: ActionResult): string[];
8
9
  get pageCount(): number;
10
+ get markdownFileCount(): number;
9
11
  load(): void;
10
12
  resolveSourcePath(sourcePath: string): string;
11
13
  relevantPages(state: ActionResult): ApplicationSpecPage[];
@@ -8,6 +8,7 @@ import { loadMarkdownFiles } from "./utils/markdown-files.js";
8
8
  export class ApplicationSpec {
9
9
  pages = [];
10
10
  sourcePath;
11
+ totalMarkdownFiles = 0;
11
12
  constructor(sourcePath) {
12
13
  this.sourcePath = this.resolveSourcePath(sourcePath);
13
14
  this.load();
@@ -31,24 +32,23 @@ export class ApplicationSpec {
31
32
  get pageCount() {
32
33
  return this.pages.length;
33
34
  }
35
+ get markdownFileCount() {
36
+ return this.totalMarkdownFiles;
37
+ }
34
38
  load() {
35
39
  if (!existsSync(this.sourcePath)) {
36
40
  throw new Error(`Application spec not found: ${this.sourcePath}`);
37
41
  }
38
42
  const isDirectory = statSync(this.sourcePath).isDirectory();
39
- if (!isDirectory && path.basename(this.sourcePath).toLowerCase() !== 'index.md') {
40
- throw new Error(`Application spec file must be index.md: ${this.sourcePath}`);
41
- }
42
43
  const bundlePath = isDirectory ? this.sourcePath : path.dirname(this.sourcePath);
44
+ const allFiles = loadMarkdownFiles(bundlePath, { recursive: true });
43
45
  const indexPath = path.join(bundlePath, 'index.md');
44
- if (!existsSync(indexPath)) {
45
- throw new Error(`Application spec index not found: ${indexPath}`);
46
- }
47
- const pagesPath = path.join(bundlePath, 'pages');
48
- if (!existsSync(pagesPath)) {
49
- throw new Error(`Application spec pages directory not found: ${pagesPath}`);
50
- }
51
- for (const file of loadMarkdownFiles(pagesPath, { recursive: true })) {
46
+ this.totalMarkdownFiles = allFiles.filter((file) => path.resolve(file.filePath) !== path.resolve(indexPath)).length;
47
+ for (const file of allFiles) {
48
+ if (path.resolve(file.filePath) === path.resolve(indexPath))
49
+ continue;
50
+ if (!file.data.url && file.data.format === undefined && file.data.version === undefined)
51
+ continue;
52
52
  const parsed = APPLICATION_SPEC_PAGE_SCHEMA.safeParse(file.data);
53
53
  if (!parsed.success && parsed.error.issues.some((issue) => issue.path[0] === 'format')) {
54
54
  throw new Error(`Invalid application spec format in ${file.filePath}`);
@@ -61,8 +61,13 @@ export class ApplicationSpec {
61
61
  }
62
62
  this.pages.push({ url: parsed.data.url, content: file.content.trim() });
63
63
  }
64
+ if (this.totalMarkdownFiles === 0) {
65
+ tag('warning').log(`No markdown files found in spec: ${bundlePath}`);
66
+ return;
67
+ }
64
68
  if (this.pages.length === 0) {
65
- throw new Error(`Application spec contains no documented pages: ${pagesPath}`);
69
+ tag('debug').log(`${this.totalMarkdownFiles} spec files found in ${bundlePath}`);
70
+ return;
66
71
  }
67
72
  }
68
73
  resolveSourcePath(sourcePath) {
@@ -7,7 +7,6 @@ export class ContextExperienceCommand extends BaseCommand {
7
7
  name = 'context:experience';
8
8
  description = 'Print all matching experience for current page';
9
9
  async execute(_args) {
10
- const explorer = this.explorBot.getExplorer();
11
10
  const state = this.explorBot.stateManager().getCurrentState();
12
11
  if (!state) {
13
12
  throw new Error('No active page');
@@ -7,7 +7,6 @@ export class ContextKnowledgeCommand extends BaseCommand {
7
7
  name = 'context:knowledge';
8
8
  description = 'Print all matching knowledge for current page';
9
9
  async execute(_args) {
10
- const explorer = this.explorBot.getExplorer();
11
10
  const state = this.explorBot.stateManager().getCurrentState();
12
11
  if (!state) {
13
12
  throw new Error('No active page');
@@ -34,18 +34,19 @@ export declare class ExploreCommand extends BaseCommand {
34
34
  discoverNewSubPages(mainPlan: Plan, mainUrl: string | undefined, styles: string[] | undefined, knownUrls: Set<string>): Promise<void>;
35
35
  explorePage(url: string, feature: string | undefined, parentPlan: Plan | undefined, styles?: string[]): Promise<boolean>;
36
36
  replanAndRun(pageUrl: string | undefined, feature: string | undefined, existingPlan: Plan, styles?: string[]): Promise<void>;
37
- runAllStyles(pageUrl?: string, feature?: string, parentPlan?: Plan, completedPlans?: Plan[], styles?: string[]): Promise<void>;
37
+ runAllStyles(pageUrl?: string, feature?: string, parentPlan?: Plan, styles?: string[]): Promise<void>;
38
38
  rememberCurrentPlan(): void;
39
39
  planWithRetry(feature: string | undefined, opts: {
40
40
  fresh: boolean;
41
41
  style: string;
42
42
  extend?: Plan;
43
- completedPlans?: Plan[];
44
43
  noSave?: boolean;
45
44
  }, pageUrl?: string): Promise<void>;
45
+ printAddedTests(before: Set<Test>, style: string, pageUrl?: string): number;
46
46
  parseConfigure(raw: string | undefined): ConfigureSpec;
47
47
  printResults(): void;
48
48
  printNextSteps(savedPlanPath?: string | null): void;
49
+ skipBlockedTest(test: Test, error: unknown): void;
49
50
  isLimitReached(): boolean;
50
51
  isDeadlineReached(): boolean;
51
52
  runPendingTests(): Promise<void>;
@@ -6,7 +6,7 @@ import { Stats } from '../stats.js';
6
6
  import { TestResult } from '../test-plan.js';
7
7
  import { browserErrorMessage } from "../utils/browser-errors.js";
8
8
  import { getCliName } from "../utils/cli-name.js";
9
- import { ErrorPageError, getStateErrorPageError } from "../utils/error-page.js";
9
+ import { BlockedError, ErrorPageError, getStateErrorPageError } from "../utils/error-page.js";
10
10
  import { tag } from '../utils/logger.js';
11
11
  import { printNextSteps, relativeToCwd } from "../utils/next-steps.js";
12
12
  import { keepSiteUrls, readSitemap } from "../utils/sitemap.js";
@@ -14,6 +14,7 @@ import { parseSpecPairs } from "../utils/spec.js";
14
14
  import { safeFilename } from "../utils/strings.js";
15
15
  import { UrlFilter } from "../utils/url-filter.js";
16
16
  import { BaseCommand } from './base-command.js';
17
+ import { PlanCommand } from './plan-command.js';
17
18
  const MAX_SUB_PAGE_ATTEMPTS = 30;
18
19
  const PRIORITY_ORDER = { critical: 0, important: 1, high: 2, normal: 3, low: 4 };
19
20
  export const DEADLINE_RESERVE_MS = 3 * 60_000;
@@ -135,7 +136,7 @@ export class ExploreCommand extends BaseCommand {
135
136
  tag('multiline').log(lines.join('\n'), { maxLines: 24 });
136
137
  }
137
138
  async runFreshMode(mainUrl, feature, styles) {
138
- await this.runAllStyles(mainUrl, feature, undefined, undefined, styles);
139
+ await this.runAllStyles(mainUrl, feature, undefined, styles);
139
140
  this.rememberCurrentPlan();
140
141
  const mainPlan = this.explorBot.getCurrentPlan();
141
142
  if (!mainPlan)
@@ -319,7 +320,7 @@ export class ExploreCommand extends BaseCommand {
319
320
  tag('warning').log(`Skipping page: ${errorPage.message}`);
320
321
  return false;
321
322
  }
322
- await this.runAllStyles(url, feature, parentPlan, this.completedPlans, styles);
323
+ await this.runAllStyles(url, feature, parentPlan, styles);
323
324
  const plan = this.explorBot.getCurrentPlan();
324
325
  if (plan?.tests.length && !this.completedPlans.includes(plan)) {
325
326
  this.completedPlans.push(plan);
@@ -332,14 +333,14 @@ export class ExploreCommand extends BaseCommand {
332
333
  if (this.isLimitReached())
333
334
  break;
334
335
  this.explorBot.setCurrentPlan(existingPlan);
335
- const opts = { fresh: false, style, completedPlans: this.completedPlans };
336
+ const opts = { fresh: false, style };
336
337
  if (this.dryRun)
337
338
  opts.noSave = true;
338
339
  await this.planWithRetry(feature, opts, pageUrl);
339
340
  await this.runPendingTests();
340
341
  }
341
342
  }
342
- async runAllStyles(pageUrl, feature, parentPlan, completedPlans, styles) {
343
+ async runAllStyles(pageUrl, feature, parentPlan, styles) {
343
344
  const styleList = styles ?? Object.keys(getStyles());
344
345
  let fresh = true;
345
346
  for (const style of styleList) {
@@ -348,7 +349,7 @@ export class ExploreCommand extends BaseCommand {
348
349
  if (!fresh && pageUrl && !this.dryRun) {
349
350
  await this.explorBot.visit(pageUrl);
350
351
  }
351
- const opts = { fresh, style, completedPlans };
352
+ const opts = { fresh, style };
352
353
  if (fresh && parentPlan)
353
354
  opts.extend = parentPlan;
354
355
  if (this.dryRun)
@@ -377,6 +378,10 @@ export class ExploreCommand extends BaseCommand {
377
378
  async planWithRetry(feature, opts, pageUrl) {
378
379
  const before = new Set(this.explorBot.getCurrentPlan()?.tests ?? []);
379
380
  await this.explorBot.plan(feature, opts);
381
+ if (this.explorBot.lastPlanError instanceof BlockedError) {
382
+ tag('warning').log(`Skipping page: ${this.explorBot.lastPlanError.message}`);
383
+ return;
384
+ }
380
385
  if (this.explorBot.lastPlanError) {
381
386
  if (this.explorBot.lastPlanError instanceof ErrorPageError) {
382
387
  throw this.explorBot.lastPlanError;
@@ -390,14 +395,15 @@ export class ExploreCommand extends BaseCommand {
390
395
  return;
391
396
  }
392
397
  }
393
- const planAfter = this.explorBot.getCurrentPlan();
394
- if (!planAfter)
395
- return;
396
- const added = planAfter.tests.filter((t) => !before.has(t));
398
+ this.printAddedTests(before, opts.style, pageUrl);
399
+ }
400
+ printAddedTests(before, style, pageUrl) {
401
+ const added = this.explorBot.getCurrentPlan()?.tests.filter((t) => !before.has(t)) ?? [];
397
402
  if (added.length === 0)
398
- return;
403
+ return 0;
399
404
  const urlNote = pageUrl ? ` for ${pageUrl}` : '';
400
- this.printPreview(`Planner added ${added.length} new test(s) [style=${opts.style}]${urlNote}:`, added);
405
+ this.printPreview(`Planner added ${added.length} new test(s) [style=${style}]${urlNote}:`, added);
406
+ return added.length;
401
407
  }
402
408
  parseConfigure(raw) {
403
409
  const cfg = { enabled: false, newRatio: 1.0 };
@@ -542,16 +548,7 @@ export class ExploreCommand extends BaseCommand {
542
548
  const cli = getCliName();
543
549
  const sections = [];
544
550
  if (savedPlanPath) {
545
- const relPlan = relativeToCwd(savedPlanPath);
546
- sections.push({
547
- label: 'Plan',
548
- path: savedPlanPath,
549
- commands: [
550
- { label: 'Re-run', command: `${cli} test ${relPlan} 1` },
551
- { label: 'Run all', command: `${cli} test ${relPlan} *` },
552
- { label: 'Run range', command: `${cli} test ${relPlan} 1-3` },
553
- ],
554
- });
551
+ sections.push(PlanCommand.planSection(savedPlanPath));
555
552
  }
556
553
  const savedFiles = this.explorBot.agentHistorian().getSavedFiles();
557
554
  const screencasts = savedFiles.filter((f) => f.endsWith('.webm'));
@@ -585,6 +582,14 @@ export class ExploreCommand extends BaseCommand {
585
582
  });
586
583
  printNextSteps(sections);
587
584
  }
585
+ skipBlockedTest(test, error) {
586
+ if (!(error instanceof BlockedError))
587
+ throw error;
588
+ tag('warning').log(`Skipping test: ${error.message}`);
589
+ test.start();
590
+ test.addNote(error.message, TestResult.SKIPPED);
591
+ test.finish(TestResult.SKIPPED);
592
+ }
588
593
  isLimitReached() {
589
594
  if (this.maxTests != null && this.testsRun >= this.maxTests)
590
595
  return true;
@@ -623,7 +628,14 @@ export class ExploreCommand extends BaseCommand {
623
628
  test.finish(TestResult.SKIPPED);
624
629
  }
625
630
  else {
626
- await this.explorBot.agentTester().test(test, { deadline: this.hardDeadlineAt });
631
+ const run = () => this.explorBot.agentTester().test(test, { deadline: this.hardDeadlineAt });
632
+ await run()
633
+ .catch(async (err) => {
634
+ if (!(await this.explorBot.agentCaptain().resolveBlocker(err, () => this.explorBot.agentNavigator().isOn(test.startUrl))))
635
+ throw err;
636
+ return run();
637
+ })
638
+ .catch((err) => this.skipBlockedTest(test, err));
627
639
  }
628
640
  this.testsRun++;
629
641
  }
@@ -27,7 +27,9 @@ export class HelpJsonCommand {
27
27
  required: argument.required,
28
28
  variadic: argument.variadic,
29
29
  })),
30
- options: helper.visibleOptions(cmd).map((option) => ({
30
+ options: cmd.options
31
+ .filter((option) => !option.hidden)
32
+ .map((option) => ({
31
33
  flags: option.flags,
32
34
  description: option.description,
33
35
  default: option.defaultValue,
@@ -1,6 +1,5 @@
1
1
  import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
2
2
  import { dirname, extname, join, resolve } from 'node:path';
3
- import chalk from 'chalk';
4
3
  import figureSet from 'figures';
5
4
  import { ConfigParser, PROVIDERS, missingModelRoles } from "../config.js";
6
5
  import { findGlobalConfig, globalConfigPath, globalDir, globalEnvPath } from "../global-config.js";
@@ -299,10 +298,11 @@ function printInitNextSteps(options) {
299
298
  label: 'Docs',
300
299
  commands: [{ command: 'https://testomat.ai/docs/explorbot' }],
301
300
  });
301
+ sections.push({
302
+ label: 'Add knowledge (logins, credentials) — created on first use in ./knowledge',
303
+ commands: [{ command: `${cli} learn '*' 'to authorize use these credentials: admin@example.com / secret123'` }],
304
+ });
302
305
  printNextSteps(sections);
303
- log('');
304
- log('Add knowledge (logins, credentials) — created on first use in ./knowledge:');
305
- tag('substep').log(chalk.yellow(`${cli} learn '*' 'to authorize use these credentials: admin@example.com / secret123'`));
306
306
  tag('substep').log('You can use ${env.LOGIN} and ${env.PASSWORD} to reference environment variables.');
307
307
  }
308
308
  function writeEnvKey(key, value, envPath = globalEnvPath()) {
@@ -10,7 +10,6 @@ export class LearnCommand extends BaseCommand {
10
10
  const note = args.trim();
11
11
  if (!note) {
12
12
  const AddKnowledge = (await import('../components/AddKnowledge.js')).default;
13
- const explorer = this.explorBot.getExplorer();
14
13
  const state = this.explorBot.stateManager().getCurrentState();
15
14
  const initialUrl = state?.url || '';
16
15
  const { unmount } = render(React.createElement(AddKnowledge, {
@@ -24,7 +23,6 @@ export class LearnCommand extends BaseCommand {
24
23
  });
25
24
  return;
26
25
  }
27
- const explorer = this.explorBot.getExplorer();
28
26
  const state = this.explorBot.stateManager().getCurrentState();
29
27
  if (!state) {
30
28
  throw new Error('No active page to attach knowledge');
@@ -1,3 +1,4 @@
1
+ import { type NextStepSection } from '../utils/next-steps.js';
1
2
  import { BaseCommand, type Suggestion } from './base-command.js';
2
3
  export declare class PlanCommand extends BaseCommand {
3
4
  name: string;
@@ -8,7 +9,9 @@ export declare class PlanCommand extends BaseCommand {
8
9
  description: string;
9
10
  }[];
10
11
  execute(args: string): Promise<void>;
12
+ static planSection(planPath: string, cliSuffix?: string): NextStepSection;
11
13
  printNextSteps(): void;
14
+ cliSuffix(): string;
12
15
  printPlanSummary(): void;
13
16
  updateSuggestions(): void;
14
17
  }
@@ -1,3 +1,4 @@
1
+ import { existsSync } from 'node:fs';
1
2
  import path from 'node:path';
2
3
  import chalk from 'chalk';
3
4
  import figureSet from 'figures';
@@ -16,8 +17,12 @@ export class PlanCommand extends BaseCommand {
16
17
  options = [
17
18
  { flags: '--fresh', description: 'Regenerate plan from scratch' },
18
19
  { flags: '--clear', description: 'Clear plan before regenerating' },
20
+ { flags: '--append', description: 'Add tests to existing plan file' },
19
21
  { flags: '--style <name>', description: 'Planning style (normal, curious, psycho, performer)' },
20
22
  { flags: '--focus <feature>', description: 'Focus area for test planning' },
23
+ { flags: '--tests <count>', description: 'Exact number of tests to generate' },
24
+ { flags: '--min-tests <count>', description: 'Minimum number of tests to generate' },
25
+ { flags: '--max-tests <count>', description: 'Maximum number of tests to generate' },
21
26
  ];
22
27
  async execute(args) {
23
28
  const { opts, args: remaining } = this.parseArgs(args);
@@ -26,10 +31,18 @@ export class PlanCommand extends BaseCommand {
26
31
  this.explorBot.clearPlan();
27
32
  tag('success').log('Plan cleared');
28
33
  }
34
+ if (opts.append) {
35
+ const existingPlanPath = path.join(this.explorBot.getPlansDir(), this.explorBot.generatePlanFilename());
36
+ if (existsSync(existingPlanPath))
37
+ this.explorBot.loadPlan(existingPlanPath);
38
+ }
29
39
  if (focus) {
30
40
  tag('info').log(`Planning focus: ${focus}`);
31
41
  }
32
- await this.explorBot.plan(focus, { fresh: !!(opts.fresh || opts.clear), style: opts.style });
42
+ const testsCount = opts.tests ? Number.parseInt(opts.tests, 10) : undefined;
43
+ const minTests = testsCount ?? (opts.minTests ? Number.parseInt(opts.minTests, 10) : undefined);
44
+ const maxTests = testsCount ?? (opts.maxTests ? Number.parseInt(opts.maxTests, 10) : undefined);
45
+ await this.explorBot.plan(focus, { fresh: !!(opts.fresh || opts.clear), style: opts.style, minTests, maxTests });
33
46
  const plan = this.explorBot.getCurrentPlan();
34
47
  if (!plan?.tests.length) {
35
48
  throw new Error('No test scenarios in the current plan.');
@@ -38,28 +51,32 @@ export class PlanCommand extends BaseCommand {
38
51
  this.updateSuggestions();
39
52
  this.printNextSteps();
40
53
  }
54
+ static planSection(planPath, cliSuffix = '') {
55
+ const cli = getCliName();
56
+ const planName = path.basename(planPath, '.md');
57
+ return {
58
+ label: 'Plan',
59
+ path: planPath,
60
+ commands: [
61
+ { label: 'Run all tests', command: `${cli} test ${planName}${cliSuffix}` },
62
+ { label: 'Run one test', command: `${cli} test ${planName} 1${cliSuffix}` },
63
+ { label: 'Run a range', command: `${cli} test ${planName} 1-3${cliSuffix}` },
64
+ ],
65
+ };
66
+ }
41
67
  printNextSteps() {
42
68
  const savedPath = this.explorBot.lastSavedPlanPath;
43
69
  if (!savedPath)
44
70
  return;
45
71
  const cli = getCliName();
46
- const relPlan = relativeToCwd(savedPath);
47
- const sections = [
48
- {
49
- label: 'Plan',
50
- path: savedPath,
51
- commands: [
52
- { label: 'Re-run', command: `${cli} test ${relPlan} 1` },
53
- { label: 'Run all', command: `${cli} test ${relPlan} *` },
54
- { label: 'Run range', command: `${cli} test ${relPlan} 1-3` },
55
- { label: 'Reload', command: `/plan:load ${relPlan}` },
56
- ],
57
- },
58
- ];
72
+ const cliSuffix = this.cliSuffix();
73
+ const section = PlanCommand.planSection(savedPath, cliSuffix);
74
+ section.commands?.push({ label: 'Reload', command: `/plan:load ${path.basename(savedPath, '.md')}` });
75
+ const sections = [section];
59
76
  const suite = this.explorBot.getSuite();
60
77
  const files = suite && suite.automatedTestCount > 0 ? suite.getAutomatedTestFiles() : [];
61
78
  if (files.length > 0) {
62
- const commands = files.map((f) => ({ label: '', command: `${cli} rerun ${relativeToCwd(f)}` }));
79
+ const commands = files.map((f) => ({ label: '', command: `${cli} rerun ${relativeToCwd(f)}${cliSuffix}` }));
63
80
  sections.push({
64
81
  label: `Automated tests (${files.length})`,
65
82
  commands,
@@ -67,6 +84,15 @@ export class PlanCommand extends BaseCommand {
67
84
  }
68
85
  printNextSteps(sections);
69
86
  }
87
+ cliSuffix() {
88
+ const options = this.explorBot.getOptions();
89
+ let suffix = '';
90
+ if (options.path)
91
+ suffix += ` --path ${options.path}`;
92
+ if (options.session)
93
+ suffix += ' --session';
94
+ return suffix;
95
+ }
70
96
  printPlanSummary() {
71
97
  const suite = this.explorBot.getSuite();
72
98
  const plan = this.explorBot.getCurrentPlan();
@@ -78,10 +104,10 @@ export class PlanCommand extends BaseCommand {
78
104
  }
79
105
  }
80
106
  if (plan?.tests.length) {
81
- console.log(`\n${chalk.bold.cyan(`New test scenarios (${plan.tests.length})`)}`);
107
+ console.log(`\n${chalk.bold.cyan(`New test scenarios (${plan.tests.length})`)}\n`);
82
108
  for (let i = 0; i < plan.tests.length; i++) {
83
109
  const t = plan.tests[i];
84
- console.log(` ${chalk.dim(`${i + 1}.`)} ${chalk.green(figureSet.pointer)} ${t.scenario} ${chalk.dim(`[${t.priority}]`)}`);
110
+ console.log(` ${chalk.cyan(`${i + 1}.`)} ${t.scenario}`);
85
111
  }
86
112
  }
87
113
  }
@@ -1,5 +1,6 @@
1
- import { getCliName } from "../utils/cli-name.js";
2
- import { printNextSteps, relativeToCwd } from "../utils/next-steps.js";
1
+ import path from 'node:path';
2
+ import { printNextSteps } from "../utils/next-steps.js";
3
+ import { PlanCommand } from './plan-command.js';
3
4
  import { BaseCommand } from './base-command.js';
4
5
  export class PlanSaveCommand extends BaseCommand {
5
6
  name = 'plan:save';
@@ -14,20 +15,8 @@ export class PlanSaveCommand extends BaseCommand {
14
15
  const savedPath = this.explorBot.savePlan(filename);
15
16
  if (!savedPath)
16
17
  return;
17
- const cli = getCliName();
18
- const relPlan = relativeToCwd(savedPath);
19
- const sections = [
20
- {
21
- label: 'Plan',
22
- path: savedPath,
23
- commands: [
24
- { label: 'Re-run', command: `${cli} test ${relPlan} 1` },
25
- { label: 'Run all', command: `${cli} test ${relPlan} *` },
26
- { label: 'Run range', command: `${cli} test ${relPlan} 1-3` },
27
- { label: 'Reload', command: `/plan:load ${relPlan}` },
28
- ],
29
- },
30
- ];
31
- printNextSteps(sections);
18
+ const section = PlanCommand.planSection(savedPath);
19
+ section.commands?.push({ label: 'Reload', command: `/plan:load ${path.basename(savedPath, '.md')}` });
20
+ printNextSteps([section]);
32
21
  }
33
22
  }
@@ -44,7 +44,7 @@ export class PlansCommand extends BaseCommand {
44
44
  }
45
45
  tag('info').log('');
46
46
  tag('info').log('Run test from this plan as:');
47
- tag('info').log(`${getCliName()} test 1 --from-plan ${file.name}`);
47
+ tag('info').log(`${getCliName()} test ${file.name} 1`);
48
48
  }
49
49
  resolvePlanFile(target, files) {
50
50
  const index = Number.parseInt(target, 10);
@@ -1,5 +1,6 @@
1
1
  import { Stats } from '../stats.js';
2
2
  import { Plan, Test } from '../test-plan.js';
3
+ import { BlockedError } from "../utils/error-page.js";
3
4
  import { tag } from '../utils/logger.js';
4
5
  import { BaseCommand } from './base-command.js';
5
6
  export class TestCommand extends BaseCommand {
@@ -15,6 +16,11 @@ export class TestCommand extends BaseCommand {
15
16
  if (opts.fromPlan) {
16
17
  this.explorBot.loadPlan(String(opts.fromPlan));
17
18
  }
19
+ if (!opts.fromPlan && remaining[0] && Plan.resolveFile(remaining[0], this.explorBot.getPlansDir())) {
20
+ this.explorBot.loadPlan(remaining.shift());
21
+ if (remaining.length === 0)
22
+ remaining.push('*');
23
+ }
18
24
  const plan = this.explorBot.getCurrentPlan();
19
25
  const selector = remaining.join(' ');
20
26
  Stats.mode = 'test';
@@ -70,12 +76,16 @@ export class TestCommand extends BaseCommand {
70
76
  const tester = this.explorBot.agentTester();
71
77
  for (const [index, test] of toExecute.entries()) {
72
78
  tag('info').log(`Starting test ${index + 1}/${toExecute.length}: ${test.scenario}`);
73
- await tester.test(test);
79
+ await tester.test(test).catch((err) => {
80
+ if (!(err instanceof BlockedError))
81
+ throw err;
82
+ tag('warning').log(`Skipping test: ${err.message}`);
83
+ });
74
84
  }
75
85
  tag('success').log('Test execution finished');
76
86
  }
77
87
  completeArguments() {
78
- return Plan.listFiles(this.explorBot.getPlansDir()).map((file) => ({ value: `--from-plan ${file.name}`, display: file.label }));
88
+ return Plan.listFiles(this.explorBot.getPlansDir()).map((file) => ({ value: file.name, display: file.label }));
79
89
  }
80
90
  }
81
91
  function parseTestIndices(input, total) {
@@ -3,7 +3,6 @@ import type { ExplorBot } from '../explorbot.js';
3
3
  interface AppProps {
4
4
  explorBot: ExplorBot;
5
5
  initialShowInput?: boolean;
6
- exitOnEmptyInput?: boolean;
7
6
  }
8
- export declare function App({ explorBot, initialShowInput, exitOnEmptyInput }: AppProps): React.JSX.Element;
7
+ export declare function App({ explorBot, initialShowInput }: AppProps): React.JSX.Element;
9
8
  export {};
@@ -13,14 +13,13 @@ import StateTransitionPane from './StateTransitionPane.js';
13
13
  import TaskPane, { WINDOW_SIZE } from './TaskPane.js';
14
14
  import WelcomeChecklist from './WelcomeChecklist.js';
15
15
  import WelcomeCommands from './WelcomeCommands.js';
16
- export function App({ explorBot, initialShowInput = false, exitOnEmptyInput = false }) {
16
+ export function App({ explorBot, initialShowInput = false }) {
17
17
  const sessionStartedAtRef = useRef(Date.now());
18
18
  const InputComponent = InputReadline;
19
19
  const [showSessionTimer, setShowSessionTimer] = useState(false);
20
20
  const [showInput, setShowInput] = useState(initialShowInput);
21
21
  const [interruptPrompt, setInterruptPrompt] = useState(null);
22
22
  const [currentState, setCurrentState] = useState(null);
23
- const [lastTransition, setLastTransition] = useState(null);
24
23
  const [tasks, setTasks] = useState([]);
25
24
  const tasksRef = useRef([]);
26
25
  const [showPlanEditor, setShowPlanEditor] = useState(false);
@@ -50,7 +49,6 @@ export function App({ explorBot, initialShowInput = false, exitOnEmptyInput = fa
50
49
  const manager = explorBot.stateManager();
51
50
  unsubscribe = manager.onStateChange((transition) => {
52
51
  if (mounted) {
53
- setLastTransition(transition);
54
52
  setCurrentState(transition.toState);
55
53
  }
56
54
  });