explorbot 0.4.5 → 0.4.7

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/boat/api-tester/src/ai/curler.ts +70 -66
  2. package/boat/api-tester/src/apibot.ts +19 -2
  3. package/boat/api-tester/src/cli.ts +87 -274
  4. package/boat/api-tester/src/commands/api-command.ts +10 -0
  5. package/boat/api-tester/src/commands/explore-command.ts +52 -0
  6. package/boat/api-tester/src/commands/init-command.ts +119 -0
  7. package/boat/api-tester/src/commands/know-command.ts +44 -0
  8. package/boat/api-tester/src/commands/plan-command.ts +42 -0
  9. package/boat/api-tester/src/commands/test-command.ts +54 -0
  10. package/boat/api-tester/src/config.ts +18 -1
  11. package/dist/boat/api-tester/src/ai/curler.js +55 -56
  12. package/dist/boat/api-tester/src/apibot.js +15 -1
  13. package/dist/boat/api-tester/src/cli.js +89 -243
  14. package/dist/boat/api-tester/src/commands/api-command.js +7 -0
  15. package/dist/boat/api-tester/src/commands/explore-command.js +41 -0
  16. package/dist/boat/api-tester/src/commands/init-command.js +88 -0
  17. package/dist/boat/api-tester/src/commands/know-command.js +39 -0
  18. package/dist/boat/api-tester/src/commands/plan-command.js +37 -0
  19. package/dist/boat/api-tester/src/commands/test-command.js +45 -0
  20. package/dist/boat/api-tester/src/config.js +3 -1
  21. package/dist/package.json +4 -4
  22. package/dist/rules/researcher/pagination.md +6 -0
  23. package/dist/src/action-result.d.ts +6 -0
  24. package/dist/src/action-result.js +12 -0
  25. package/dist/src/ai/planner.js +4 -0
  26. package/dist/src/ai/researcher/deep-analysis.d.ts +1 -1
  27. package/dist/src/ai/researcher/deep-analysis.js +14 -6
  28. package/dist/src/ai/researcher/locators.js +1 -1
  29. package/dist/src/ai/researcher/pagination.d.ts +16 -0
  30. package/dist/src/ai/researcher/pagination.js +62 -0
  31. package/dist/src/ai/researcher/parser.d.ts +3 -0
  32. package/dist/src/ai/researcher/parser.js +22 -6
  33. package/dist/src/ai/researcher/sections.js +1 -1
  34. package/dist/src/ai/researcher.js +7 -2
  35. package/dist/src/ai/rules.js +16 -0
  36. package/dist/src/ai/scout.js +8 -2
  37. package/dist/src/ai/tools.d.ts +1 -1
  38. package/dist/src/ai/tools.js +25 -13
  39. package/dist/src/api/spec-reader.d.ts +1 -0
  40. package/dist/src/api/spec-reader.js +93 -1
  41. package/dist/src/commands/base-command.d.ts +3 -3
  42. package/dist/src/commands/init-command.d.ts +3 -0
  43. package/dist/src/commands/init-command.js +6 -3
  44. package/dist/src/commands/options/ws-option.d.ts +7 -0
  45. package/dist/src/commands/options/ws-option.js +14 -0
  46. package/dist/src/config.d.ts +1 -0
  47. package/dist/src/config.js +14 -11
  48. package/dist/src/explorer.d.ts +1 -1
  49. package/dist/src/explorer.js +1 -1
  50. package/dist/src/remote.d.ts +2 -0
  51. package/dist/src/remote.js +23 -16
  52. package/dist/src/utils/aria.d.ts +2 -0
  53. package/dist/src/utils/aria.js +6 -1
  54. package/dist/src/utils/html-diff.js +4 -1
  55. package/dist/src/utils/markdown-query.d.ts +2 -0
  56. package/dist/src/utils/markdown-query.js +39 -0
  57. package/dist/src/utils/pagination.d.ts +16 -0
  58. package/dist/src/utils/pagination.js +20 -0
  59. package/docs/api-testing/basics.md +26 -2
  60. package/docs/superpowers/plans/2026-09-10-pagination.md +1420 -0
  61. package/docs/superpowers/specs/2026-09-09-pagination-rule-design.md +345 -0
  62. package/package.json +4 -4
  63. package/rules/researcher/pagination.md +6 -0
  64. package/src/action-result.ts +16 -0
  65. package/src/ai/planner.ts +4 -0
  66. package/src/ai/researcher/deep-analysis.ts +13 -6
  67. package/src/ai/researcher/locators.ts +1 -1
  68. package/src/ai/researcher/pagination.ts +68 -0
  69. package/src/ai/researcher/parser.ts +23 -5
  70. package/src/ai/researcher/sections.ts +1 -1
  71. package/src/ai/researcher.ts +9 -3
  72. package/src/ai/rules.ts +16 -0
  73. package/src/ai/scout.ts +9 -2
  74. package/src/ai/tools.ts +22 -14
  75. package/src/api/spec-reader.ts +106 -1
  76. package/src/commands/base-command.ts +3 -3
  77. package/src/commands/init-command.ts +6 -3
  78. package/src/commands/options/ws-option.ts +14 -0
  79. package/src/config.ts +15 -11
  80. package/src/explorer.ts +1 -1
  81. package/src/remote.ts +22 -15
  82. package/src/utils/aria.ts +8 -1
  83. package/src/utils/html-diff.ts +3 -1
  84. package/src/utils/markdown-query.ts +39 -0
  85. package/src/utils/pagination.ts +36 -0
@@ -0,0 +1,119 @@
1
+ import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import chalk from 'chalk';
4
+ import { envTemplate, modelLines } from '../../../../src/commands/init-command.ts';
5
+ import { missingModelRoles } from '../../../../src/config.ts';
6
+ import { log, tag } from '../../../../src/utils/logger.ts';
7
+
8
+ export async function runInit(options: InitOptions): Promise<void> {
9
+ const provider = options.provider || 'openrouter';
10
+ const originalCwd = process.cwd();
11
+
12
+ if (options.path) {
13
+ const dir = path.resolve(options.path);
14
+ mkdirSync(dir, { recursive: true });
15
+ process.chdir(dir);
16
+ log(`Working in directory: ${dir}`);
17
+ }
18
+
19
+ const configPath = path.resolve('apibot.config.js');
20
+ if (existsSync(configPath) && !options.force) {
21
+ log(`Config file already exists: ${configPath}`);
22
+ log('Use --force to overwrite existing file');
23
+ process.exit(1);
24
+ }
25
+
26
+ const answers = await ask(options);
27
+ if (!answers.baseEndpoint) {
28
+ tag('error').log('Base endpoint is required.');
29
+ process.exit(1);
30
+ }
31
+
32
+ if (!answers.spec) {
33
+ tag('error').log('OpenAPI spec is required. Chief plans from it and Curler looks up schemas in it.');
34
+ process.exit(1);
35
+ }
36
+
37
+ writeFileSync(configPath, configTemplate(provider, answers.baseEndpoint, answers.spec), 'utf8');
38
+ log(`Created config file: ${configPath}`);
39
+
40
+ const envPath = path.resolve('.env');
41
+ if (!existsSync(envPath)) {
42
+ writeFileSync(envPath, `${envTemplate(provider)}\n`, 'utf8');
43
+ log(`Created env file: ${envPath}`);
44
+ }
45
+
46
+ mkdirSync('output', { recursive: true });
47
+ mkdirSync('knowledge', { recursive: true });
48
+
49
+ if (answers.knowledge) {
50
+ const knowledgePath = path.resolve('knowledge', 'general.md');
51
+ writeFileSync(knowledgePath, `---\nendpoint: "*"\n---\n${answers.knowledge}\n`, 'utf8');
52
+ log(`Created knowledge file: ${knowledgePath}`);
53
+ }
54
+
55
+ const missing = missingModelRoles(provider);
56
+ if (missing.length) {
57
+ tag('warning').log(`No recommended ${missing.join(' and ')} for ${provider} — set the model ids in ${configPath}`);
58
+ }
59
+
60
+ log('');
61
+ log('Next steps:');
62
+ log('1. Add your provider API key to .env');
63
+ log('2. Describe the API so the plans match it');
64
+ tag('substep').log(chalk.yellow(`${options.prefix} know /users "CRUD endpoint for user management"`));
65
+ log('3. Plan and run tests for one endpoint');
66
+ tag('substep').log(chalk.yellow(`${options.prefix} explore /users`));
67
+
68
+ if (process.cwd() !== originalCwd) process.chdir(originalCwd);
69
+ }
70
+
71
+ function configTemplate(provider: string, baseEndpoint: string, spec: string): string {
72
+ return `// Models are written as 'provider/model-id' so they resolve without a local node_modules.
73
+ // https://github.com/testomatio/explorbot/blob/main/docs/basics/providers.md
74
+
75
+ export default {
76
+ ai: {
77
+ ${modelLines(provider, ['model', 'agenticModel'])}
78
+ },
79
+
80
+ api: {
81
+ baseEndpoint: '${baseEndpoint}',
82
+ spec: ['${spec}'],
83
+ },
84
+ };
85
+ `;
86
+ }
87
+
88
+ async function ask(options: InitOptions): Promise<Answers> {
89
+ if (options.baseEndpoint) {
90
+ return { baseEndpoint: options.baseEndpoint, spec: options.spec || '', knowledge: '' };
91
+ }
92
+
93
+ const rl = await import('node:readline');
94
+ const iface = rl.createInterface({ input: process.stdin, output: process.stdout });
95
+ const question = (text: string): Promise<string> => new Promise((resolve) => iface.question(text, (answer: string) => resolve(answer.trim())));
96
+
97
+ log('Apibot — API Testing Tool Setup\n');
98
+ const baseEndpoint = await question('Base API endpoint (e.g. https://api.example.com/v1): ');
99
+ const spec = await question('OpenAPI spec file or URL: ');
100
+ const knowledge = await question('Describe your API, its auth and its rules (Enter to skip): ');
101
+ iface.close();
102
+
103
+ return { baseEndpoint, spec, knowledge };
104
+ }
105
+
106
+ interface InitOptions {
107
+ force?: boolean;
108
+ path?: string;
109
+ provider?: string;
110
+ baseEndpoint?: string;
111
+ spec?: string;
112
+ prefix: string;
113
+ }
114
+
115
+ interface Answers {
116
+ baseEndpoint: string;
117
+ spec: string;
118
+ knowledge: string;
119
+ }
@@ -0,0 +1,44 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { tag } from '../../../../src/utils/logger.ts';
4
+ import { ApiCommand } from './api-command.ts';
5
+
6
+ export class KnowCommand extends ApiCommand {
7
+ name = 'know';
8
+ aliases = ['add-knowledge'];
9
+ description = 'Add API knowledge for an endpoint';
10
+ knowledge = '';
11
+
12
+ async execute(endpoint: string): Promise<void> {
13
+ if (!this.knowledge) {
14
+ throw new Error('Description is required.');
15
+ }
16
+
17
+ const knowledgeDir = await this.resolveKnowledgeDir();
18
+ fs.mkdirSync(knowledgeDir, { recursive: true });
19
+
20
+ const filename = endpoint.replace(/^\//, '').replace(/[^a-zA-Z0-9]/g, '_') || 'general';
21
+ const filePath = path.join(knowledgeDir, `${filename}.md`);
22
+
23
+ if (fs.existsSync(filePath)) {
24
+ fs.appendFileSync(filePath, `\n---\n${this.knowledge}\n`, 'utf8');
25
+ tag('success').log(`Updated: ${filePath}`);
26
+ return;
27
+ }
28
+
29
+ fs.writeFileSync(filePath, `---\nendpoint: "${endpoint}"\n---\n${this.knowledge}\n`, 'utf8');
30
+ tag('success').log(`Created: ${filePath}`);
31
+ }
32
+
33
+ private async resolveKnowledgeDir(): Promise<string> {
34
+ const parser = this.bot.getConfigParser();
35
+ const options = this.bot.getOptions();
36
+ try {
37
+ await parser.loadConfig(options);
38
+ return parser.getKnowledgeDir();
39
+ } catch {
40
+ if (options.path) return path.join(path.resolve(options.path), 'knowledge');
41
+ return 'knowledge';
42
+ }
43
+ }
44
+ }
@@ -0,0 +1,42 @@
1
+ import { tag } from '../../../../src/utils/logger.ts';
2
+ import { type NextStepSection, printNextSteps, relativeToCwd } from '../../../../src/utils/next-steps.ts';
3
+ import { ApiCommand } from './api-command.ts';
4
+
5
+ export class PlanCommand extends ApiCommand {
6
+ name = 'plan';
7
+ description = 'Generate a test plan for an API endpoint';
8
+ style?: string;
9
+ fresh = false;
10
+
11
+ async execute(endpoint: string): Promise<void> {
12
+ await this.bot.plan(endpoint, { style: this.style, fresh: this.fresh });
13
+
14
+ const plan = this.bot.getCurrentPlan();
15
+ if (!plan?.tests.length) {
16
+ throw new Error('No test scenarios generated.');
17
+ }
18
+
19
+ const lines = [`Plan: ${plan.title} (${plan.tests.length} tests)`];
20
+ for (const [i, test] of plan.tests.entries()) {
21
+ lines.push(` ${String(i + 1).padStart(2)}. [${test.priority}] ${test.scenario}`);
22
+ }
23
+ tag('multiline').log(lines.join('\n'), { maxLines: 24 });
24
+
25
+ const savedPath = this.bot.savePlan();
26
+ if (!savedPath) return;
27
+
28
+ const relative = relativeToCwd(savedPath);
29
+ const sections: NextStepSection[] = [
30
+ {
31
+ label: 'Plan',
32
+ path: savedPath,
33
+ commands: [
34
+ { label: 'Run first', command: `${this.prefix} test ${relative} 1` },
35
+ { label: 'Run all', command: `${this.prefix} test ${relative} *` },
36
+ { label: 'Run range', command: `${this.prefix} test ${relative} 1-3` },
37
+ ],
38
+ },
39
+ ];
40
+ printNextSteps(sections);
41
+ }
42
+ }
@@ -0,0 +1,54 @@
1
+ import figureSet from 'figures';
2
+ import type { Test } from '../../../../src/test-plan.ts';
3
+ import { tag } from '../../../../src/utils/logger.ts';
4
+ import { ApiCommand } from './api-command.ts';
5
+
6
+ export class TestCommand extends ApiCommand {
7
+ name = 'test';
8
+ description = 'Execute tests from a plan file. Index: 1, 1-3, *';
9
+ index?: string;
10
+ failed = 0;
11
+
12
+ async execute(planfile: string): Promise<void> {
13
+ const plan = this.bot.loadPlan(planfile);
14
+ tag('info').log(`Plan loaded: "${plan.title}" (${plan.tests.length} tests)`);
15
+
16
+ const tests = selectTests(plan.tests, this.index);
17
+ tag('info').log(`Running ${tests.length} test(s)`);
18
+
19
+ let passed = 0;
20
+ for (const test of tests) {
21
+ const result = await this.bot.runTest(test);
22
+ if (result.success) passed++;
23
+ else this.failed++;
24
+ }
25
+
26
+ this.bot.savePlan();
27
+ tag('info').log(`${figureSet.tick} ${tests.length} tests completed: ${passed} passed, ${this.failed} failed`);
28
+ }
29
+ }
30
+
31
+ export function selectTests(tests: Test[], index?: string): Test[] {
32
+ if (!index || index === '*' || index === 'all') {
33
+ return tests.filter((t) => t.status === 'pending');
34
+ }
35
+
36
+ const rangeMatch = index.match(/^(\d+)-(\d+)$/);
37
+ if (rangeMatch) {
38
+ const start = Number.parseInt(rangeMatch[1]) - 1;
39
+ const end = Number.parseInt(rangeMatch[2]);
40
+ return tests.slice(start, end);
41
+ }
42
+
43
+ if (index.includes(',')) {
44
+ const indices = index.split(',').map((i) => Number.parseInt(i.trim()) - 1);
45
+ return indices.map((i) => tests[i]).filter(Boolean);
46
+ }
47
+
48
+ const num = Number.parseInt(index);
49
+ if (!Number.isNaN(num) && tests[num - 1]) {
50
+ return [tests[num - 1]];
51
+ }
52
+
53
+ return tests.filter((t) => t.status === 'pending');
54
+ }
@@ -2,7 +2,22 @@ import { existsSync, mkdirSync, readFileSync } from 'node:fs';
2
2
  import path, { resolve } from 'node:path';
3
3
  import { pathToFileURL } from 'node:url';
4
4
  import { parseEnv } from 'node:util';
5
- import { type AIConfig, type ApiHookFn, type ApiConfig as BaseApiConfig, ConfigMissingError, EXPLORBOT_CONFIG_PATHS, createModel, envConfigRequested, materializeKnowledge, missingConfigMessage, resolveConfigModels, resolveModel, resolveOutputRoot, setOutputDir } from '../../../src/config.ts';
5
+ import {
6
+ type AIConfig,
7
+ type ApiHookFn,
8
+ type ApiConfig as BaseApiConfig,
9
+ ConfigMissingError,
10
+ EXPLORBOT_CONFIG_PATHS,
11
+ createModel,
12
+ envConfigRequested,
13
+ materializeKnowledge,
14
+ missingConfigMessage,
15
+ resolveConfigModels,
16
+ resolveLangfuse,
17
+ resolveModel,
18
+ resolveOutputRoot,
19
+ setOutputDir,
20
+ } from '../../../src/config.ts';
6
21
  import { type SiteRecord, findGlobalConfig, globalEnvPath, isGlobalConfigPath, registerSite, resolveSiteTarget } from '../../../src/global-config.ts';
7
22
 
8
23
  export type { AIConfig };
@@ -101,6 +116,7 @@ export class ApibotConfigParser {
101
116
  this.applyEnvHeaders(this.config.api);
102
117
  if (options?.baseEndpoint) this.config.api.baseEndpoint = options.baseEndpoint.replace(/\/$/, '');
103
118
  await resolveConfigModels(this.config.ai);
119
+ resolveLangfuse(this.config.ai);
104
120
  this.configPath = resolvedPath;
105
121
  this.site = null;
106
122
 
@@ -236,6 +252,7 @@ export class ApibotConfigParser {
236
252
  api,
237
253
  dirs: { output: '.', knowledge: 'knowledge' },
238
254
  };
255
+ resolveLangfuse(this.config.ai);
239
256
  this.configPath = path.join(outputRoot, 'apibot.config.js');
240
257
  this.validateConfig(this.config);
241
258
  setOutputDir(this.getOutputDir());
@@ -1,5 +1,6 @@
1
1
  import dedent from 'dedent';
2
2
  import { z } from 'zod';
3
+ import { Observability } from "../../../../src/observability.js";
3
4
  import { TestResult } from "../../../../src/test-plan.js";
4
5
  import { createDebug, tag } from "../../../../src/utils/logger.js";
5
6
  import { loop } from "../../../../src/utils/loop.js";
@@ -32,67 +33,65 @@ export class Curler {
32
33
  conversation.addUserText(knowledge);
33
34
  const initialPrompt = this.buildTestPrompt(test, opts?.specDefinition, opts?.baseEndpoint);
34
35
  conversation.addUserText(initialPrompt);
35
- await loop(async ({ stop, iteration }) => {
36
- debugLog(`Iteration ${iteration}`);
37
- if (iteration > 1) {
38
- const requestLog = this.requestState.toLog();
39
- const nextStep = dedent `
40
- <request_log>
41
- ${requestLog || 'No requests made yet'}
42
- </request_log>
36
+ await Observability.run(`curler: ${test.scenario}`, {
37
+ sessionId: test.sessionName,
38
+ tags: ['curler'],
39
+ input: {
40
+ scenario: test.scenario,
41
+ startUrl: test.startUrl,
42
+ expected: test.expected,
43
+ },
44
+ }, async () => {
45
+ await loop(async ({ stop, iteration }) => {
46
+ debugLog(`Iteration ${iteration}`);
47
+ if (iteration > 1) {
48
+ const requestLog = this.requestState.toLog();
49
+ const nextStep = dedent `
50
+ <request_log>
51
+ ${requestLog || 'No requests made yet'}
52
+ </request_log>
43
53
 
44
- <task>
45
- Continue testing. Review the request log above and proceed with the next step.
46
- </task>
54
+ <task>
55
+ Continue testing. Review the request log above and proceed with the next step.
56
+ </task>
47
57
 
48
- <notes>
49
- ${test.notesToString() || 'No notes yet'}
50
- </notes>
51
- `;
52
- conversation.addUserText(nextStep);
53
- }
54
- const result = await this.provider.invokeConversation(conversation, tools, {
55
- maxToolRoundtrips: 5,
56
- toolChoice: 'required',
57
- agentName: 'curler',
58
+ <notes>
59
+ ${test.notesToString() || 'No notes yet'}
60
+ </notes>
61
+ `;
62
+ conversation.addUserText(nextStep);
63
+ }
64
+ const result = await this.provider.invokeConversation(conversation, tools, {
65
+ maxToolRoundtrips: 5,
66
+ toolChoice: 'required',
67
+ agentName: 'curler',
68
+ });
69
+ if (!result)
70
+ throw new Error('Failed to get response from provider');
71
+ const toolNames = result.toolExecutions?.map((e) => e.toolName) || [];
72
+ debugLog('Tool calls:', toolNames.join(', '));
73
+ if (test.hasFinished) {
74
+ stop();
75
+ return;
76
+ }
77
+ if (iteration >= MAX_ITERATIONS) {
78
+ tag('warning').log('Max iterations reached, running final review...');
79
+ stop();
80
+ }
81
+ }, {
82
+ maxAttempts: MAX_ITERATIONS,
83
+ catch: async ({ error, stop }) => {
84
+ tag('error').log(`Test execution error: ${error}`);
85
+ stop();
86
+ },
58
87
  });
59
- if (!result)
60
- throw new Error('Failed to get response from provider');
61
- const toolNames = result.toolExecutions?.map((e) => e.toolName) || [];
62
- debugLog('Tool calls:', toolNames.join(', '));
63
- if (test.hasFinished) {
64
- stop();
65
- return;
88
+ try {
89
+ await this.finalReview(test);
66
90
  }
67
- if (iteration >= MAX_ITERATIONS) {
68
- tag('warning').log('Max iterations reached, running final review...');
69
- stop();
91
+ catch (error) {
92
+ tag('error').log(`Final review failed: ${error}`);
70
93
  }
71
- }, {
72
- maxAttempts: MAX_ITERATIONS,
73
- observability: {
74
- name: `curler: ${test.scenario}`,
75
- agent: 'curler',
76
- sessionId: test.sessionName,
77
- metadata: {
78
- input: {
79
- scenario: test.scenario,
80
- startUrl: test.startUrl,
81
- expected: test.expected,
82
- },
83
- },
84
- },
85
- catch: async ({ error, stop }) => {
86
- tag('error').log(`Test execution error: ${error}`);
87
- stop();
88
- },
89
94
  });
90
- try {
91
- await this.finalReview(test);
92
- }
93
- catch (error) {
94
- tag('error').log(`Final review failed: ${error}`);
95
- }
96
95
  this.finishTest(test);
97
96
  const meta = {
98
97
  endpoint: test.startUrl,
@@ -160,7 +159,7 @@ export class Curler {
160
159
  3. Should the test pass or fail overall?
161
160
  `,
162
161
  },
163
- ], schema, model);
162
+ ], schema, model, { agentName: 'curler', telemetryFunctionId: 'curler.finalReview' });
164
163
  const result = response?.object;
165
164
  if (!result)
166
165
  return;
@@ -2,7 +2,7 @@ import { existsSync, mkdirSync } from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { AIProvider } from "../../../src/ai/provider.js";
4
4
  import { RequestStore } from "../../../src/api/request-store.js";
5
- import { extractEndpointDefinition, loadSpec, searchEndpoints, validateSpecs } from "../../../src/api/spec-reader.js";
5
+ import { extractEndpointDefinition, loadSpec, resolveEndpoints, searchEndpoints, validateSpecs } from "../../../src/api/spec-reader.js";
6
6
  import { KnowledgeTracker } from "../../../src/knowledge-tracker.js";
7
7
  import { Reporter } from "../../../src/reporter.js";
8
8
  import { Plan } from "../../../src/test-plan.js";
@@ -74,6 +74,7 @@ export class ApiBot {
74
74
  async stop() {
75
75
  await this.reporter?.finishRun();
76
76
  await this.apiClient?.teardown();
77
+ await this.provider?.stop();
77
78
  }
78
79
  createAgent(factory) {
79
80
  return factory({
@@ -143,12 +144,25 @@ export class ApiBot {
143
144
  getConfigParser() {
144
145
  return this.configParser;
145
146
  }
147
+ getOptions() {
148
+ return this.options;
149
+ }
150
+ async runTest(test) {
151
+ return this.agentCurler().test(test, {
152
+ specDefinition: this.tryGetEndpointDefinition(test.startUrl),
153
+ baseEndpoint: this.config.api.baseEndpoint,
154
+ searchSpec: (query) => this.searchSpec(query),
155
+ });
156
+ }
146
157
  getRequestState() {
147
158
  return this.requestState;
148
159
  }
149
160
  getEndpointDefinition(endpoint) {
150
161
  return extractEndpointDefinition(this.apiSpec, endpoint, this.config.api.baseEndpoint);
151
162
  }
163
+ expandEndpoints(pattern) {
164
+ return resolveEndpoints(this.apiSpec, this.configParser.resolveEndpointPath(pattern), this.config.api.baseEndpoint);
165
+ }
152
166
  searchSpec(query) {
153
167
  return searchEndpoints(this.apiSpec, query, this.config.api.baseEndpoint);
154
168
  }