explorbot 0.4.6 → 0.4.8
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/chief.ts +3 -1
- package/boat/api-tester/src/ai/curler.ts +74 -66
- package/boat/api-tester/src/apibot.ts +1 -0
- package/boat/api-tester/src/cli.ts +2 -0
- package/boat/api-tester/src/config.ts +18 -1
- package/dist/boat/api-tester/src/ai/chief.js +3 -1
- package/dist/boat/api-tester/src/ai/curler.js +59 -56
- package/dist/boat/api-tester/src/apibot.js +1 -0
- package/dist/boat/api-tester/src/cli.js +2 -0
- package/dist/boat/api-tester/src/config.js +3 -1
- package/dist/package.json +2 -2
- package/dist/rules/chief/general.md +2 -0
- package/dist/rules/researcher/pagination.md +7 -0
- package/dist/src/action-result.d.ts +6 -0
- package/dist/src/action-result.js +12 -0
- package/dist/src/action.js +3 -2
- package/dist/src/ai/navigator.js +1 -4
- package/dist/src/ai/pilot.js +8 -12
- package/dist/src/ai/planner/session-dedup.d.ts +2 -1
- package/dist/src/ai/planner/session-dedup.js +18 -1
- package/dist/src/ai/planner.js +12 -4
- package/dist/src/ai/provider.js +18 -4
- 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 +17 -0
- package/dist/src/ai/scout.js +8 -2
- package/dist/src/ai/tester.js +1 -1
- package/dist/src/ai/tools.js +12 -4
- 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/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/code-extractor.js +6 -2
- 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/superpowers/plans/2026-09-10-pagination.md +1420 -0
- package/docs/superpowers/specs/2026-09-09-pagination-rule-design.md +125 -97
- package/package.json +2 -2
- package/rules/chief/general.md +2 -0
- package/rules/researcher/pagination.md +7 -0
- package/src/action-result.ts +16 -0
- package/src/action.ts +3 -2
- package/src/ai/navigator.ts +1 -4
- package/src/ai/pilot.ts +8 -12
- package/src/ai/planner/session-dedup.ts +16 -2
- package/src/ai/planner.ts +12 -4
- package/src/ai/provider.ts +18 -3
- 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 +17 -0
- package/src/ai/scout.ts +9 -2
- package/src/ai/tester.ts +1 -1
- package/src/ai/tools.ts +9 -4
- package/src/commands/options/ws-option.ts +14 -0
- package/src/config.ts +15 -11
- package/src/remote.ts +22 -15
- package/src/utils/aria.ts +8 -1
- package/src/utils/code-extractor.ts +6 -2
- package/src/utils/markdown-query.ts +39 -0
- package/src/utils/pagination.ts +36 -0
|
@@ -261,7 +261,9 @@ export class Chief extends ChiefBase {
|
|
|
261
261
|
- Use real enum values discovered in the data
|
|
262
262
|
- Each test MUST use DIFFERENT data — never reuse the same field values across tests
|
|
263
263
|
- For "create" tests: base payload on a real record but change field values to create new unique data
|
|
264
|
-
-
|
|
264
|
+
- Treat records and IDs from sample_data as read-only. Never update, patch, delete, archive, or otherwise mutate them
|
|
265
|
+
- For update/delete tests: the same scenario must first create its own target, then mutate only that target
|
|
266
|
+
- For negative or unsupported-method tests that could mutate data if accepted: create a scenario-owned target first; if that setup is impossible, do not send the destructive request
|
|
265
267
|
- For tests needing parent references: use real _id field values from sample_data
|
|
266
268
|
`);
|
|
267
269
|
}
|
|
@@ -3,6 +3,7 @@ import { z } from 'zod';
|
|
|
3
3
|
import type { AIProvider } from '../../../../src/ai/provider.ts';
|
|
4
4
|
import type { RequestStore } from '../../../../src/api/request-store.ts';
|
|
5
5
|
import type { KnowledgeTracker } from '../../../../src/knowledge-tracker.ts';
|
|
6
|
+
import { Observability } from '../../../../src/observability.ts';
|
|
6
7
|
import type { Reporter } from '../../../../src/reporter.ts';
|
|
7
8
|
import { type Test, TestResult } from '../../../../src/test-plan.ts';
|
|
8
9
|
import { createDebug, tag } from '../../../../src/utils/logger.ts';
|
|
@@ -46,75 +47,77 @@ export class Curler {
|
|
|
46
47
|
const initialPrompt = this.buildTestPrompt(test, opts?.specDefinition, opts?.baseEndpoint);
|
|
47
48
|
conversation.addUserText(initialPrompt);
|
|
48
49
|
|
|
49
|
-
await
|
|
50
|
-
|
|
51
|
-
debugLog(`Iteration ${iteration}`);
|
|
52
|
-
|
|
53
|
-
if (iteration > 1) {
|
|
54
|
-
const requestLog = this.requestState.toLog();
|
|
55
|
-
const nextStep = dedent`
|
|
56
|
-
<request_log>
|
|
57
|
-
${requestLog || 'No requests made yet'}
|
|
58
|
-
</request_log>
|
|
59
|
-
|
|
60
|
-
<task>
|
|
61
|
-
Continue testing. Review the request log above and proceed with the next step.
|
|
62
|
-
</task>
|
|
63
|
-
|
|
64
|
-
<notes>
|
|
65
|
-
${test.notesToString() || 'No notes yet'}
|
|
66
|
-
</notes>
|
|
67
|
-
`;
|
|
68
|
-
conversation.addUserText(nextStep);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const result = await this.provider.invokeConversation(conversation, tools, {
|
|
72
|
-
maxToolRoundtrips: 5,
|
|
73
|
-
toolChoice: 'required',
|
|
74
|
-
agentName: 'curler',
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
if (!result) throw new Error('Failed to get response from provider');
|
|
78
|
-
|
|
79
|
-
const toolNames = result.toolExecutions?.map((e: any) => e.toolName) || [];
|
|
80
|
-
debugLog('Tool calls:', toolNames.join(', '));
|
|
81
|
-
|
|
82
|
-
if (test.hasFinished) {
|
|
83
|
-
stop();
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
if (iteration >= MAX_ITERATIONS) {
|
|
88
|
-
tag('warning').log('Max iterations reached, running final review...');
|
|
89
|
-
stop();
|
|
90
|
-
}
|
|
91
|
-
},
|
|
50
|
+
await Observability.run(
|
|
51
|
+
`curler: ${test.scenario}`,
|
|
92
52
|
{
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
input: {
|
|
100
|
-
scenario: test.scenario,
|
|
101
|
-
startUrl: test.startUrl,
|
|
102
|
-
expected: test.expected,
|
|
103
|
-
},
|
|
104
|
-
},
|
|
105
|
-
},
|
|
106
|
-
catch: async ({ error, stop }) => {
|
|
107
|
-
tag('error').log(`Test execution error: ${error}`);
|
|
108
|
-
stop();
|
|
53
|
+
sessionId: test.sessionName,
|
|
54
|
+
tags: ['curler'],
|
|
55
|
+
input: {
|
|
56
|
+
scenario: test.scenario,
|
|
57
|
+
startUrl: test.startUrl,
|
|
58
|
+
expected: test.expected,
|
|
109
59
|
},
|
|
60
|
+
},
|
|
61
|
+
async () => {
|
|
62
|
+
await loop(
|
|
63
|
+
async ({ stop, iteration }) => {
|
|
64
|
+
debugLog(`Iteration ${iteration}`);
|
|
65
|
+
|
|
66
|
+
if (iteration > 1) {
|
|
67
|
+
const requestLog = this.requestState.toLog();
|
|
68
|
+
const nextStep = dedent`
|
|
69
|
+
<request_log>
|
|
70
|
+
${requestLog || 'No requests made yet'}
|
|
71
|
+
</request_log>
|
|
72
|
+
|
|
73
|
+
<task>
|
|
74
|
+
Continue testing. Review the request log above and proceed with the next step.
|
|
75
|
+
</task>
|
|
76
|
+
|
|
77
|
+
<notes>
|
|
78
|
+
${test.notesToString() || 'No notes yet'}
|
|
79
|
+
</notes>
|
|
80
|
+
`;
|
|
81
|
+
conversation.addUserText(nextStep);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const result = await this.provider.invokeConversation(conversation, tools, {
|
|
85
|
+
maxToolRoundtrips: 5,
|
|
86
|
+
toolChoice: 'required',
|
|
87
|
+
agentName: 'curler',
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
if (!result) throw new Error('Failed to get response from provider');
|
|
91
|
+
|
|
92
|
+
const toolNames = result.toolExecutions?.map((e: any) => e.toolName) || [];
|
|
93
|
+
debugLog('Tool calls:', toolNames.join(', '));
|
|
94
|
+
|
|
95
|
+
if (test.hasFinished) {
|
|
96
|
+
stop();
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (iteration >= MAX_ITERATIONS) {
|
|
101
|
+
tag('warning').log('Max iterations reached, running final review...');
|
|
102
|
+
stop();
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
maxAttempts: MAX_ITERATIONS,
|
|
107
|
+
catch: async ({ error, stop }) => {
|
|
108
|
+
tag('error').log(`Test execution error: ${error}`);
|
|
109
|
+
stop();
|
|
110
|
+
},
|
|
111
|
+
}
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
await this.finalReview(test);
|
|
116
|
+
} catch (error) {
|
|
117
|
+
tag('error').log(`Final review failed: ${error}`);
|
|
118
|
+
}
|
|
110
119
|
}
|
|
111
120
|
);
|
|
112
|
-
|
|
113
|
-
try {
|
|
114
|
-
await this.finalReview(test);
|
|
115
|
-
} catch (error) {
|
|
116
|
-
tag('error').log(`Final review failed: ${error}`);
|
|
117
|
-
}
|
|
118
121
|
this.finishTest(test);
|
|
119
122
|
const meta: Record<string, string | undefined> = {
|
|
120
123
|
endpoint: test.startUrl,
|
|
@@ -190,7 +193,8 @@ export class Curler {
|
|
|
190
193
|
},
|
|
191
194
|
],
|
|
192
195
|
schema,
|
|
193
|
-
model
|
|
196
|
+
model,
|
|
197
|
+
{ agentName: 'curler', telemetryFunctionId: 'curler.finalReview' }
|
|
194
198
|
);
|
|
195
199
|
|
|
196
200
|
const result = response?.object;
|
|
@@ -280,6 +284,10 @@ export class Curler {
|
|
|
280
284
|
- Record important findings as you go
|
|
281
285
|
- Be precise about what you expect vs what you observe
|
|
282
286
|
- If a test requires data from another endpoint, use schemaFor to look it up before guessing
|
|
287
|
+
- Treat existing records, sample data, and IDs supplied by the plan as read-only
|
|
288
|
+
- Before PUT, PATCH, DELETE, archive, or another destructive request, create the target inside the current scenario and mutate only that target
|
|
289
|
+
- This also applies when testing an unsupported method: the server may unexpectedly accept it, so never probe destructively against pre-existing data
|
|
290
|
+
- If a scenario-owned target cannot be created, use stop rather than risking existing data
|
|
283
291
|
</rules>
|
|
284
292
|
`;
|
|
285
293
|
}
|
|
@@ -85,6 +85,7 @@ export class ApiBot {
|
|
|
85
85
|
async stop(): Promise<void> {
|
|
86
86
|
await this.reporter?.finishRun();
|
|
87
87
|
await this.apiClient?.teardown();
|
|
88
|
+
await this.provider?.stop();
|
|
88
89
|
}
|
|
89
90
|
|
|
90
91
|
createAgent<T>(factory: (deps: { ai: AIProvider; config: ApibotConfig; apiClient: ApiClient; requestState: RequestStore; knowledge: KnowledgeTracker }) => T): T {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
+
import { flushTelemetry } from '../../../src/ai/provider.ts';
|
|
2
3
|
import { ConfigCommand } from '../../../src/commands/config-command.ts';
|
|
3
4
|
import { RecommendedModelsCommand } from '../../../src/commands/recommended-models-command.ts';
|
|
4
5
|
import { listSites } from '../../../src/global-config.ts';
|
|
@@ -111,6 +112,7 @@ async function run(name: string, options: any, endpoint: string | undefined, bod
|
|
|
111
112
|
process.exit(code);
|
|
112
113
|
} catch (error) {
|
|
113
114
|
console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
|
|
115
|
+
await flushTelemetry();
|
|
114
116
|
process.exit(1);
|
|
115
117
|
}
|
|
116
118
|
}
|
|
@@ -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());
|
|
@@ -230,7 +230,9 @@ export class Chief extends ChiefBase {
|
|
|
230
230
|
- Use real enum values discovered in the data
|
|
231
231
|
- Each test MUST use DIFFERENT data — never reuse the same field values across tests
|
|
232
232
|
- For "create" tests: base payload on a real record but change field values to create new unique data
|
|
233
|
-
-
|
|
233
|
+
- Treat records and IDs from sample_data as read-only. Never update, patch, delete, archive, or otherwise mutate them
|
|
234
|
+
- For update/delete tests: the same scenario must first create its own target, then mutate only that target
|
|
235
|
+
- For negative or unsupported-method tests that could mutate data if accepted: create a scenario-owned target first; if that setup is impossible, do not send the destructive request
|
|
234
236
|
- For tests needing parent references: use real _id field values from sample_data
|
|
235
237
|
`);
|
|
236
238
|
}
|
|
@@ -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;
|
|
@@ -244,6 +243,10 @@ export class Curler {
|
|
|
244
243
|
- Record important findings as you go
|
|
245
244
|
- Be precise about what you expect vs what you observe
|
|
246
245
|
- If a test requires data from another endpoint, use schemaFor to look it up before guessing
|
|
246
|
+
- Treat existing records, sample data, and IDs supplied by the plan as read-only
|
|
247
|
+
- Before PUT, PATCH, DELETE, archive, or another destructive request, create the target inside the current scenario and mutate only that target
|
|
248
|
+
- This also applies when testing an unsupported method: the server may unexpectedly accept it, so never probe destructively against pre-existing data
|
|
249
|
+
- If a scenario-owned target cannot be created, use stop rather than risking existing data
|
|
247
250
|
</rules>
|
|
248
251
|
`;
|
|
249
252
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
+
import { flushTelemetry } from "../../../src/ai/provider.js";
|
|
2
3
|
import { ConfigCommand } from "../../../src/commands/config-command.js";
|
|
3
4
|
import { RecommendedModelsCommand } from "../../../src/commands/recommended-models-command.js";
|
|
4
5
|
import { listSites } from "../../../src/global-config.js";
|
|
@@ -108,6 +109,7 @@ async function run(name, options, endpoint, body) {
|
|
|
108
109
|
}
|
|
109
110
|
catch (error) {
|
|
110
111
|
console.error('Failed:', error instanceof Error ? error.message : 'Unknown error');
|
|
112
|
+
await flushTelemetry();
|
|
111
113
|
process.exit(1);
|
|
112
114
|
}
|
|
113
115
|
}
|
|
@@ -10,7 +10,7 @@ import { existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
|
10
10
|
import path, { resolve } from 'node:path';
|
|
11
11
|
import { pathToFileURL } from 'node:url';
|
|
12
12
|
import { parseEnv } from 'node:util';
|
|
13
|
-
import { ConfigMissingError, EXPLORBOT_CONFIG_PATHS, createModel, envConfigRequested, materializeKnowledge, missingConfigMessage, resolveConfigModels, resolveModel, resolveOutputRoot, setOutputDir } from "../../../src/config.js";
|
|
13
|
+
import { ConfigMissingError, EXPLORBOT_CONFIG_PATHS, createModel, envConfigRequested, materializeKnowledge, missingConfigMessage, resolveConfigModels, resolveLangfuse, resolveModel, resolveOutputRoot, setOutputDir, } from "../../../src/config.js";
|
|
14
14
|
import { findGlobalConfig, globalEnvPath, isGlobalConfigPath, registerSite, resolveSiteTarget } from "../../../src/global-config.js";
|
|
15
15
|
function isAbsoluteEndpoint(value) {
|
|
16
16
|
return !!value && (value.startsWith('http://') || value.startsWith('https://'));
|
|
@@ -82,6 +82,7 @@ export class ApibotConfigParser {
|
|
|
82
82
|
if (options?.baseEndpoint)
|
|
83
83
|
this.config.api.baseEndpoint = options.baseEndpoint.replace(/\/$/, '');
|
|
84
84
|
await resolveConfigModels(this.config.ai);
|
|
85
|
+
resolveLangfuse(this.config.ai);
|
|
85
86
|
this.configPath = resolvedPath;
|
|
86
87
|
this.site = null;
|
|
87
88
|
if (isGlobalConfigPath(resolvedPath)) {
|
|
@@ -208,6 +209,7 @@ export class ApibotConfigParser {
|
|
|
208
209
|
api,
|
|
209
210
|
dirs: { output: '.', knowledge: 'knowledge' },
|
|
210
211
|
};
|
|
212
|
+
resolveLangfuse(this.config.ai);
|
|
211
213
|
this.configPath = path.join(outputRoot, 'apibot.config.js');
|
|
212
214
|
this.validateConfig(this.config);
|
|
213
215
|
setOutputDir(this.getOutputDir());
|
package/dist/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "explorbot",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.8",
|
|
4
4
|
"description": "CLI app built with React Ink, CodeceptJS, and Playwright",
|
|
5
5
|
"license": "Elastic-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -101,7 +101,7 @@
|
|
|
101
101
|
"bash-tool": "^1.3.15",
|
|
102
102
|
"chalk": "^5.6.2",
|
|
103
103
|
"cli-highlight": "^2.1.11",
|
|
104
|
-
"codeceptjs": "4.2.0-beta.
|
|
104
|
+
"codeceptjs": "^4.2.0-beta.3",
|
|
105
105
|
"commander": "^14.0.1",
|
|
106
106
|
"debug": "^4.4.3",
|
|
107
107
|
"dedent": "^1.6.0",
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
- Steps should specify exact HTTP methods, paths, and key payload details
|
|
3
3
|
- Expected outcomes should be specific and verifiable (status codes, response fields, error messages)
|
|
4
4
|
- For CRUD operations, each test should handle its own setup and teardown
|
|
5
|
+
- Treat existing records and IDs discovered from the API, knowledge, or sample data as read-only
|
|
6
|
+
- A scenario that updates, patches, deletes, archives, or otherwise mutates a record must create that target inside the same scenario first; omit the scenario if safe setup is impossible
|
|
5
7
|
- Expect standard REST conventions: 200 OK, 201 Created, 204 No Content, 400 Bad Request, 404 Not Found, 422 Unprocessable Entity
|
|
6
8
|
- NEVER propose scenarios that test the same thing. "Create a basic suite" and "Successful creation of a simple suite" are DUPLICATES. Each scenario must test a DISTINCT behavior or aspect.
|
|
7
9
|
- Before finalizing, review all scenarios and remove any that overlap in what they actually verify.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<pagination>
|
|
2
|
+
When a section is a list that continues beyond what is shown, add one line under its `> Container:` line:
|
|
3
|
+
`> Pagination: controls` — it has page numbers (1, 2, 3), prev/next arrows, or a "load more" button.
|
|
4
|
+
`> Pagination: infinite` — it has none of those and loads more as it is scrolled.
|
|
5
|
+
Omit the line when the items already shown are the whole collection.
|
|
6
|
+
Sorting, filtering and switching tabs are not pagination — omit the line then.
|
|
7
|
+
</pagination>
|
|
@@ -37,6 +37,8 @@ export interface PageDiff {
|
|
|
37
37
|
currentUrl: string;
|
|
38
38
|
ariaChanges?: string | null;
|
|
39
39
|
ariaChangeCount?: number;
|
|
40
|
+
ariaAdded?: number;
|
|
41
|
+
ariaRemoved?: number;
|
|
40
42
|
messages?: string[];
|
|
41
43
|
requests?: NetworkCall[];
|
|
42
44
|
consoleErrors?: string[];
|
|
@@ -137,6 +139,8 @@ export declare class Diff {
|
|
|
137
139
|
_messages: string[];
|
|
138
140
|
_ariaDiffResult: string | null;
|
|
139
141
|
_ariaChangeCount: number;
|
|
142
|
+
_ariaAdded: number;
|
|
143
|
+
_ariaRemoved: number;
|
|
140
144
|
_isSameUrl: boolean;
|
|
141
145
|
constructor(current: ActionResult, previous: ActionResult | null);
|
|
142
146
|
static create(current: ActionResult, previous: ActionResult | null): Promise<Diff>;
|
|
@@ -147,6 +151,8 @@ export declare class Diff {
|
|
|
147
151
|
cleanedHtmlParts(): Promise<HtmlDiffPart[]>;
|
|
148
152
|
get ariaChanged(): string | null;
|
|
149
153
|
get ariaChangeCount(): number;
|
|
154
|
+
get ariaAdded(): number;
|
|
155
|
+
get ariaRemoved(): number;
|
|
150
156
|
get htmlDiff(): HtmlDiffResult | null;
|
|
151
157
|
get messages(): string[];
|
|
152
158
|
get similarity(): number;
|
|
@@ -451,6 +451,8 @@ export class ActionResult {
|
|
|
451
451
|
if (diff.ariaChanged) {
|
|
452
452
|
pageDiff.ariaChanges = diff.ariaChanged;
|
|
453
453
|
pageDiff.ariaChangeCount = diff.ariaChangeCount;
|
|
454
|
+
pageDiff.ariaAdded = diff.ariaAdded;
|
|
455
|
+
pageDiff.ariaRemoved = diff.ariaRemoved;
|
|
454
456
|
}
|
|
455
457
|
if (this.overlay.isOpen && (!previousState.overlay.isOpen || previousState.overlay.name !== this.overlay.name)) {
|
|
456
458
|
pageDiff.areaOfInterest = this.overlay.describe();
|
|
@@ -542,6 +544,8 @@ export class Diff {
|
|
|
542
544
|
_messages = [];
|
|
543
545
|
_ariaDiffResult = null;
|
|
544
546
|
_ariaChangeCount = 0;
|
|
547
|
+
_ariaAdded = 0;
|
|
548
|
+
_ariaRemoved = 0;
|
|
545
549
|
_isSameUrl;
|
|
546
550
|
constructor(current, previous) {
|
|
547
551
|
this.current = current;
|
|
@@ -590,6 +594,12 @@ export class Diff {
|
|
|
590
594
|
get ariaChangeCount() {
|
|
591
595
|
return this._ariaChangeCount;
|
|
592
596
|
}
|
|
597
|
+
get ariaAdded() {
|
|
598
|
+
return this._ariaAdded;
|
|
599
|
+
}
|
|
600
|
+
get ariaRemoved() {
|
|
601
|
+
return this._ariaRemoved;
|
|
602
|
+
}
|
|
593
603
|
get htmlDiff() {
|
|
594
604
|
return this._htmlDiffResult;
|
|
595
605
|
}
|
|
@@ -614,5 +624,7 @@ export class Diff {
|
|
|
614
624
|
const ariaDiff = diffAriaSnapshots(this.previous.ariaSnapshot, this.current.ariaSnapshot);
|
|
615
625
|
this._ariaDiffResult = ariaDiff.text;
|
|
616
626
|
this._ariaChangeCount = ariaDiff.count;
|
|
627
|
+
this._ariaAdded = ariaDiff.added;
|
|
628
|
+
this._ariaRemoved = ariaDiff.removed;
|
|
617
629
|
}
|
|
618
630
|
}
|
package/dist/src/action.js
CHANGED
|
@@ -552,10 +552,11 @@ export const attachStepLogger = (target, assertionsTarget) => {
|
|
|
552
552
|
}
|
|
553
553
|
tag('step').log(step);
|
|
554
554
|
};
|
|
555
|
-
|
|
555
|
+
const onPassed = (step) => listener(step);
|
|
556
|
+
codeceptjs.event.dispatcher.on(codeceptjs.event.step.passed, onPassed);
|
|
556
557
|
codeceptjs.event.dispatcher.on(codeceptjs.event.step.failed, listener);
|
|
557
558
|
return () => {
|
|
558
|
-
codeceptjs.event.dispatcher.off(codeceptjs.event.step.passed,
|
|
559
|
+
codeceptjs.event.dispatcher.off(codeceptjs.event.step.passed, onPassed);
|
|
559
560
|
codeceptjs.event.dispatcher.off(codeceptjs.event.step.failed, listener);
|
|
560
561
|
};
|
|
561
562
|
};
|
package/dist/src/ai/navigator.js
CHANGED
|
@@ -630,7 +630,7 @@ class Navigator {
|
|
|
630
630
|
const cachedVerification = actionResult.getVerification(message);
|
|
631
631
|
if (cachedVerification !== null) {
|
|
632
632
|
tag('operation').log(`Reusing cached verification: ${cachedVerification ? 'PASS' : 'FAIL'}`);
|
|
633
|
-
return { verified: cachedVerification, successfulCodes: [], assertionSteps: [], totalAttempted: 0 };
|
|
633
|
+
return { verified: cachedVerification, inexpressible: false, results: [], successfulCodes: [], assertionSteps: [], totalAttempted: 0 };
|
|
634
634
|
}
|
|
635
635
|
const knowledge = this.knowledgeTracker.renderRelevantContext(actionResult);
|
|
636
636
|
let experience = '';
|
|
@@ -741,9 +741,6 @@ class Navigator {
|
|
|
741
741
|
observability: {
|
|
742
742
|
agent: 'navigator',
|
|
743
743
|
},
|
|
744
|
-
catch: async (error) => {
|
|
745
|
-
debugLog(error);
|
|
746
|
-
},
|
|
747
744
|
});
|
|
748
745
|
}
|
|
749
746
|
finally {
|
package/dist/src/ai/pilot.js
CHANGED
|
@@ -126,14 +126,6 @@ export class Pilot {
|
|
|
126
126
|
${sessionLog || 'No actions recorded'}
|
|
127
127
|
</session_log>
|
|
128
128
|
|
|
129
|
-
Decide and commit. "continue" extends the loop and burns iterations — choose it only when
|
|
130
|
-
evidence is genuinely insufficient to call pass/fail, not as a safety hedge.
|
|
131
|
-
- "pass" if final state proves the SCENARIO GOAL is accomplished. Set requestVerification.
|
|
132
|
-
- "fail" if scenario was attempted but goal not achieved.
|
|
133
|
-
- "skipped" if scenario is irrelevant/inapplicable, OR systematic infrastructure failures.
|
|
134
|
-
- "continue" only when a concrete missing piece of evidence (a verify/see) would change your verdict.
|
|
135
|
-
- Mixed evidence + final state shows success → pass. Mixed + final state unclear → continue with guidance.
|
|
136
|
-
|
|
137
129
|
When deciding "pass", you MUST also set requestVerification to a one-sentence natural-language
|
|
138
130
|
claim about the current page (e.g., "New item Foo is visible in the items list"). NOT
|
|
139
131
|
code — do not write I.*, expect(), .then(), or any JavaScript. Choose the strongest single
|
|
@@ -352,7 +344,7 @@ export class Pilot {
|
|
|
352
344
|
buildVerdictSystemPrompt(task) {
|
|
353
345
|
return dedent `
|
|
354
346
|
You are Pilot — final decision maker for test pass/fail. Review the evidence and commit to a
|
|
355
|
-
verdict
|
|
347
|
+
verdict.
|
|
356
348
|
|
|
357
349
|
${capabilityGroundingRule}
|
|
358
350
|
|
|
@@ -366,10 +358,11 @@ export class Pilot {
|
|
|
366
358
|
DOM assertion can't be made.
|
|
367
359
|
Do not pass when Tester achieved only a related navigation/filter/tab/status outcome instead of the
|
|
368
360
|
requested action, workflow, or entity detail goal.
|
|
369
|
-
- "fail":
|
|
361
|
+
- "fail": goal not achieved and no further step toward it is available on the current page.
|
|
370
362
|
- "skipped": scenario is irrelevant to the app, OR systematic infrastructure failures (LLM errors,
|
|
371
363
|
crashes) prevented testing. NOT for "test failed to interact" — that's "fail" or "continue".
|
|
372
|
-
- "continue":
|
|
364
|
+
- "continue": goal incomplete but the control for the NEXT step is present on the current page, or a
|
|
365
|
+
concrete missing check would change your verdict. Guidance must name that step.
|
|
373
366
|
If a verify() asserted a state that was ALREADY TRUE before the test, it proves nothing — reject.
|
|
374
367
|
|
|
375
368
|
reason field: one short sentence, maximum 120 characters. Do NOT restate the decision
|
|
@@ -1030,7 +1023,9 @@ export class Pilot {
|
|
|
1030
1023
|
${interactive ? '- Use askUser() only as last resort.' : ''}
|
|
1031
1024
|
|
|
1032
1025
|
Diagnostic patterns (use <state>, executed/element/skipped fields, ariaDiff):
|
|
1033
|
-
-
|
|
1026
|
+
- Scenario's target control in "disabled buttons" → a precondition is unmet; identify which before acting.
|
|
1027
|
+
Other disabled controls often name the unsatisfied constraint; "active form" marks [required] fields.
|
|
1028
|
+
Aim Tester at the constraint the page names, not the one the scenario assumed — note the difference in PROGRESS.
|
|
1034
1029
|
- "overlay: none" but Tester targets an overlay → overlay closed; re-trigger.
|
|
1035
1030
|
- "region:" in <state> → a large area appeared in place without navigation (subview, wizard step, panel). Direct Tester to act inside it; the rest of the page is still usable.
|
|
1036
1031
|
- Action SUCCESS but ariaDiff empty → may have worked without visible DOM change; check result message.
|
|
@@ -1052,6 +1047,7 @@ export class Pilot {
|
|
|
1052
1047
|
Tester tools: click, pressKey, form, see, verify, interact, context, research, xpathCheck,
|
|
1053
1048
|
visualClick, back, getVisitedStates, reset, stop, finish, record.
|
|
1054
1049
|
Use tool names exactly as listed. Do not invent combined names or aliases.
|
|
1050
|
+
Reloading is not a tool: to re-read a page from the server, instruct Tester to run I.reloadPage() through form.
|
|
1055
1051
|
|
|
1056
1052
|
${capabilityGroundingRule}
|
|
1057
1053
|
|