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.
- package/boat/api-tester/src/ai/curler.ts +70 -66
- package/boat/api-tester/src/apibot.ts +19 -2
- package/boat/api-tester/src/cli.ts +87 -274
- package/boat/api-tester/src/commands/api-command.ts +10 -0
- package/boat/api-tester/src/commands/explore-command.ts +52 -0
- package/boat/api-tester/src/commands/init-command.ts +119 -0
- package/boat/api-tester/src/commands/know-command.ts +44 -0
- package/boat/api-tester/src/commands/plan-command.ts +42 -0
- package/boat/api-tester/src/commands/test-command.ts +54 -0
- package/boat/api-tester/src/config.ts +18 -1
- package/dist/boat/api-tester/src/ai/curler.js +55 -56
- package/dist/boat/api-tester/src/apibot.js +15 -1
- package/dist/boat/api-tester/src/cli.js +89 -243
- package/dist/boat/api-tester/src/commands/api-command.js +7 -0
- package/dist/boat/api-tester/src/commands/explore-command.js +41 -0
- package/dist/boat/api-tester/src/commands/init-command.js +88 -0
- package/dist/boat/api-tester/src/commands/know-command.js +39 -0
- package/dist/boat/api-tester/src/commands/plan-command.js +37 -0
- package/dist/boat/api-tester/src/commands/test-command.js +45 -0
- package/dist/boat/api-tester/src/config.js +3 -1
- package/dist/package.json +4 -4
- package/dist/rules/researcher/pagination.md +6 -0
- package/dist/src/action-result.d.ts +6 -0
- package/dist/src/action-result.js +12 -0
- package/dist/src/ai/planner.js +4 -0
- package/dist/src/ai/researcher/deep-analysis.d.ts +1 -1
- package/dist/src/ai/researcher/deep-analysis.js +14 -6
- package/dist/src/ai/researcher/locators.js +1 -1
- package/dist/src/ai/researcher/pagination.d.ts +16 -0
- package/dist/src/ai/researcher/pagination.js +62 -0
- package/dist/src/ai/researcher/parser.d.ts +3 -0
- package/dist/src/ai/researcher/parser.js +22 -6
- package/dist/src/ai/researcher/sections.js +1 -1
- package/dist/src/ai/researcher.js +7 -2
- package/dist/src/ai/rules.js +16 -0
- package/dist/src/ai/scout.js +8 -2
- package/dist/src/ai/tools.d.ts +1 -1
- package/dist/src/ai/tools.js +25 -13
- package/dist/src/api/spec-reader.d.ts +1 -0
- package/dist/src/api/spec-reader.js +93 -1
- package/dist/src/commands/base-command.d.ts +3 -3
- package/dist/src/commands/init-command.d.ts +3 -0
- package/dist/src/commands/init-command.js +6 -3
- package/dist/src/commands/options/ws-option.d.ts +7 -0
- package/dist/src/commands/options/ws-option.js +14 -0
- package/dist/src/config.d.ts +1 -0
- package/dist/src/config.js +14 -11
- package/dist/src/explorer.d.ts +1 -1
- package/dist/src/explorer.js +1 -1
- package/dist/src/remote.d.ts +2 -0
- package/dist/src/remote.js +23 -16
- package/dist/src/utils/aria.d.ts +2 -0
- package/dist/src/utils/aria.js +6 -1
- package/dist/src/utils/html-diff.js +4 -1
- package/dist/src/utils/markdown-query.d.ts +2 -0
- package/dist/src/utils/markdown-query.js +39 -0
- package/dist/src/utils/pagination.d.ts +16 -0
- package/dist/src/utils/pagination.js +20 -0
- package/docs/api-testing/basics.md +26 -2
- package/docs/superpowers/plans/2026-09-10-pagination.md +1420 -0
- package/docs/superpowers/specs/2026-09-09-pagination-rule-design.md +345 -0
- package/package.json +4 -4
- package/rules/researcher/pagination.md +6 -0
- package/src/action-result.ts +16 -0
- package/src/ai/planner.ts +4 -0
- package/src/ai/researcher/deep-analysis.ts +13 -6
- package/src/ai/researcher/locators.ts +1 -1
- package/src/ai/researcher/pagination.ts +68 -0
- package/src/ai/researcher/parser.ts +23 -5
- package/src/ai/researcher/sections.ts +1 -1
- package/src/ai/researcher.ts +9 -3
- package/src/ai/rules.ts +16 -0
- package/src/ai/scout.ts +9 -2
- package/src/ai/tools.ts +22 -14
- package/src/api/spec-reader.ts +106 -1
- package/src/commands/base-command.ts +3 -3
- package/src/commands/init-command.ts +6 -3
- package/src/commands/options/ws-option.ts +14 -0
- package/src/config.ts +15 -11
- package/src/explorer.ts +1 -1
- package/src/remote.ts +22 -15
- package/src/utils/aria.ts +8 -1
- package/src/utils/html-diff.ts +3 -1
- package/src/utils/markdown-query.ts +39 -0
- 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 {
|
|
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
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
54
|
+
<task>
|
|
55
|
+
Continue testing. Review the request log above and proceed with the next step.
|
|
56
|
+
</task>
|
|
47
57
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
60
|
-
|
|
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
|
-
|
|
68
|
-
tag('
|
|
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
|
}
|