explorbot 0.1.13 → 0.1.15
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/dist/package.json +3 -2
- package/dist/src/action.js +3 -2
- package/dist/src/ai/conversation.js +20 -4
- package/dist/src/ai/historian/utils.js +8 -1
- package/dist/src/ai/pilot.js +198 -260
- package/dist/src/ai/provider.js +25 -12
- package/dist/src/ai/quartermaster.js +2 -2
- package/dist/src/ai/rules.js +2 -0
- package/dist/src/ai/session-analyst.js +46 -41
- package/dist/src/ai/tester.js +56 -20
- package/dist/src/ai/tools.js +19 -4
- package/dist/src/commands/explore-command.js +8 -2
- package/dist/src/components/StatusPane.js +6 -1
- package/dist/src/experience-tracker.js +9 -0
- package/dist/src/explorer.js +2 -5
- package/dist/src/reporter.js +41 -1
- package/dist/src/stats.js +2 -1
- package/dist/src/test-plan.js +47 -3
- package/package.json +3 -2
- package/src/action.ts +3 -2
- package/src/ai/conversation.ts +21 -4
- package/src/ai/historian/utils.ts +8 -1
- package/src/ai/pilot.ts +199 -259
- package/src/ai/provider.ts +24 -12
- package/src/ai/quartermaster.ts +2 -2
- package/src/ai/rules.ts +2 -0
- package/src/ai/session-analyst.ts +47 -41
- package/src/ai/tester.ts +48 -18
- package/src/ai/tools.ts +18 -4
- package/src/commands/explore-command.ts +9 -2
- package/src/components/StatusPane.tsx +6 -3
- package/src/experience-tracker.ts +9 -0
- package/src/explorer.ts +1 -4
- package/src/reporter.ts +44 -1
- package/src/stats.ts +3 -1
- package/src/test-plan.ts +62 -3
package/dist/src/reporter.js
CHANGED
|
@@ -87,7 +87,7 @@ export class Reporter {
|
|
|
87
87
|
this.client = new Client({ apiKey: process.env.TESTOMATIO || '', title: this.buildTitle() });
|
|
88
88
|
const timeoutMs = Number(process.env.TESTOMATIO_TIMEOUT_MS || '15000');
|
|
89
89
|
const timeoutPromise = new Promise((resolve) => setTimeout(() => resolve('timeout'), timeoutMs));
|
|
90
|
-
const result = await Promise.race([this.client.createRun().then(() => 'success'), timeoutPromise]);
|
|
90
|
+
const result = await Promise.race([this.client.createRun({ configuration: { exploratory: true } }).then(() => 'success'), timeoutPromise]);
|
|
91
91
|
if (result === 'timeout') {
|
|
92
92
|
debugLog('Reporter run creation timed out');
|
|
93
93
|
return;
|
|
@@ -117,6 +117,7 @@ export class Reporter {
|
|
|
117
117
|
message: note.message,
|
|
118
118
|
status: note.status,
|
|
119
119
|
screenshot: note.screenshot,
|
|
120
|
+
log: note.log,
|
|
120
121
|
}))
|
|
121
122
|
.sort((a, b) => a.startTime - b.startTime);
|
|
122
123
|
const stepEntries = Object.entries(test.steps)
|
|
@@ -148,8 +149,16 @@ export class Reporter {
|
|
|
148
149
|
if (noteEntry.screenshot) {
|
|
149
150
|
step.artifacts = [outputPath('states', noteEntry.screenshot)];
|
|
150
151
|
}
|
|
152
|
+
if (noteEntry.log) {
|
|
153
|
+
step.log = noteEntry.log;
|
|
154
|
+
}
|
|
151
155
|
steps.push(step);
|
|
152
156
|
}
|
|
157
|
+
const verificationStep = this.buildVerificationStep(test, lastScreenshotFile);
|
|
158
|
+
if (verificationStep) {
|
|
159
|
+
steps.push(verificationStep);
|
|
160
|
+
return steps;
|
|
161
|
+
}
|
|
153
162
|
if (lastScreenshotFile && steps.length > 0) {
|
|
154
163
|
const lastStep = steps[steps.length - 1];
|
|
155
164
|
const screenshotPath = outputPath('states', lastScreenshotFile);
|
|
@@ -162,6 +171,37 @@ export class Reporter {
|
|
|
162
171
|
}
|
|
163
172
|
return steps;
|
|
164
173
|
}
|
|
174
|
+
buildVerificationStep(test, lastScreenshotFile) {
|
|
175
|
+
const v = test.verification;
|
|
176
|
+
if (!v)
|
|
177
|
+
return undefined;
|
|
178
|
+
const subSteps = [];
|
|
179
|
+
if (v.message)
|
|
180
|
+
subSteps.push({ category: 'framework', title: v.message, duration: 0 });
|
|
181
|
+
if (v.url) {
|
|
182
|
+
subSteps.push({
|
|
183
|
+
category: 'framework',
|
|
184
|
+
title: v.pageLabel ? `Navigated to ${v.pageLabel}` : 'Final page',
|
|
185
|
+
log: v.url,
|
|
186
|
+
duration: 0,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
for (const detail of v.details) {
|
|
190
|
+
subSteps.push({ category: 'framework', title: detail, duration: 0 });
|
|
191
|
+
}
|
|
192
|
+
const screenshotFile = v.screenshot || lastScreenshotFile;
|
|
193
|
+
const step = {
|
|
194
|
+
category: 'user',
|
|
195
|
+
title: 'Verification',
|
|
196
|
+
duration: 0,
|
|
197
|
+
status: v.status || 'none',
|
|
198
|
+
steps: subSteps.length > 0 ? subSteps : undefined,
|
|
199
|
+
};
|
|
200
|
+
if (screenshotFile) {
|
|
201
|
+
step.artifacts = [outputPath('states', screenshotFile)];
|
|
202
|
+
}
|
|
203
|
+
return step;
|
|
204
|
+
}
|
|
165
205
|
async reportTest(test, meta) {
|
|
166
206
|
await this.startRun();
|
|
167
207
|
if (!this.isRunStarted) {
|
package/dist/src/stats.js
CHANGED
|
@@ -10,11 +10,12 @@ export class Stats {
|
|
|
10
10
|
static models = {};
|
|
11
11
|
static recordTokens(_agent, model, usage) {
|
|
12
12
|
if (!Stats.models[model]) {
|
|
13
|
-
Stats.models[model] = { input: 0, output: 0, total: 0 };
|
|
13
|
+
Stats.models[model] = { input: 0, output: 0, total: 0, cached: 0 };
|
|
14
14
|
}
|
|
15
15
|
Stats.models[model].input += usage.input;
|
|
16
16
|
Stats.models[model].output += usage.output;
|
|
17
17
|
Stats.models[model].total += usage.total;
|
|
18
|
+
Stats.models[model].cached = (Stats.models[model].cached ?? 0) + (usage.cached ?? 0);
|
|
18
19
|
}
|
|
19
20
|
static getElapsedTime() {
|
|
20
21
|
const elapsed = Date.now() - Stats.startTime;
|
package/dist/src/test-plan.js
CHANGED
|
@@ -17,6 +17,7 @@ export class ActiveNote {
|
|
|
17
17
|
message;
|
|
18
18
|
status;
|
|
19
19
|
screenshot;
|
|
20
|
+
log;
|
|
20
21
|
constructor(task, message, status) {
|
|
21
22
|
this.task = task;
|
|
22
23
|
this.startTime = performance.now();
|
|
@@ -41,6 +42,7 @@ export class Task {
|
|
|
41
42
|
steps;
|
|
42
43
|
states;
|
|
43
44
|
startUrl;
|
|
45
|
+
verification;
|
|
44
46
|
timestampCounter = 0;
|
|
45
47
|
activeNote;
|
|
46
48
|
constructor(description, startUrl = '') {
|
|
@@ -67,6 +69,7 @@ export class Task {
|
|
|
67
69
|
startTime: activeNote.getStartTime(),
|
|
68
70
|
endTime,
|
|
69
71
|
screenshot: activeNote.screenshot,
|
|
72
|
+
log: activeNote.log,
|
|
70
73
|
};
|
|
71
74
|
this.activeNote = undefined;
|
|
72
75
|
}
|
|
@@ -80,13 +83,30 @@ export class Task {
|
|
|
80
83
|
.map((n) => `- ${n}`)
|
|
81
84
|
.join('\n');
|
|
82
85
|
}
|
|
83
|
-
addNote(message, status = null, screenshot) {
|
|
84
|
-
const isDuplicate = Object.values(this.notes).some((note) => note.message === message && note.status === status);
|
|
86
|
+
addNote(message, status = null, screenshot, log) {
|
|
87
|
+
const isDuplicate = Object.values(this.notes).some((note) => note.message === message && note.status === status && note.log === log);
|
|
85
88
|
if (isDuplicate)
|
|
86
89
|
return;
|
|
87
90
|
const now = performance.now();
|
|
88
91
|
const timestamp = `${now}_${this.timestampCounter++}`;
|
|
89
|
-
this.notes[timestamp] = { message, status, startTime: now, endTime: now, screenshot };
|
|
92
|
+
this.notes[timestamp] = { message, status, startTime: now, endTime: now, screenshot, log };
|
|
93
|
+
}
|
|
94
|
+
addUrlNote(state, prevState) {
|
|
95
|
+
const fullUrl = state.fullUrl || state.url;
|
|
96
|
+
if (!fullUrl)
|
|
97
|
+
return;
|
|
98
|
+
let label;
|
|
99
|
+
if (state.title && state.title !== prevState?.title)
|
|
100
|
+
label = state.title;
|
|
101
|
+
else if (state.h1 && state.h1 !== prevState?.h1)
|
|
102
|
+
label = state.h1;
|
|
103
|
+
else if (state.h2 && state.h2 !== prevState?.h2)
|
|
104
|
+
label = state.h2;
|
|
105
|
+
else
|
|
106
|
+
label = state.title || state.h1 || state.h2;
|
|
107
|
+
if (!label)
|
|
108
|
+
return;
|
|
109
|
+
this.addNote(`Navigated to ${label}`, TestResult.PASSED, state.screenshotFile, fullUrl);
|
|
90
110
|
}
|
|
91
111
|
addState(state) {
|
|
92
112
|
this.states.push(state);
|
|
@@ -95,6 +115,30 @@ export class Task {
|
|
|
95
115
|
const timestamp = `${performance.now()}_${this.timestampCounter++}`;
|
|
96
116
|
this.steps[timestamp] = { text, duration, status, error, log, artifacts, noteStartTime: this.activeNote?.getStartTime() };
|
|
97
117
|
}
|
|
118
|
+
setActiveNoteScreenshot(screenshotFile) {
|
|
119
|
+
if (!this.activeNote || !screenshotFile)
|
|
120
|
+
return;
|
|
121
|
+
this.activeNote.screenshot = screenshotFile;
|
|
122
|
+
}
|
|
123
|
+
setVerification(message, status, state) {
|
|
124
|
+
this.verification ||= { message: '', status: null, details: [] };
|
|
125
|
+
this.verification.message = message;
|
|
126
|
+
this.verification.status = status;
|
|
127
|
+
if (!state)
|
|
128
|
+
return;
|
|
129
|
+
if (state.screenshotFile)
|
|
130
|
+
this.verification.screenshot = state.screenshotFile;
|
|
131
|
+
const fullUrl = state.fullUrl || state.url;
|
|
132
|
+
if (fullUrl)
|
|
133
|
+
this.verification.url = fullUrl;
|
|
134
|
+
this.verification.pageLabel = state.title || state.h1 || state.h2 || undefined;
|
|
135
|
+
}
|
|
136
|
+
addVerificationDetail(detail) {
|
|
137
|
+
if (!detail)
|
|
138
|
+
return;
|
|
139
|
+
this.verification ||= { message: '', status: null, details: [] };
|
|
140
|
+
this.verification.details.push(detail);
|
|
141
|
+
}
|
|
98
142
|
getLog() {
|
|
99
143
|
const merged = {};
|
|
100
144
|
for (const [key, stepData] of Object.entries(this.steps)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "explorbot",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.15",
|
|
4
4
|
"description": "CLI app built with React Ink, CodeceptJS, and Playwright",
|
|
5
5
|
"license": "Elastic-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -67,6 +67,7 @@
|
|
|
67
67
|
"@ai-sdk/openai": "^3.0",
|
|
68
68
|
"@axe-core/playwright": "^4.11.0",
|
|
69
69
|
"@codeceptjs/reflection": "^0.5.2",
|
|
70
|
+
"@faker-js/faker": "^10.4.0",
|
|
70
71
|
"@inkjs/ui": "^2.0.0",
|
|
71
72
|
"@langfuse/otel": "^4.5.1",
|
|
72
73
|
"@openrouter/ai-sdk-provider": "^2.3.3",
|
|
@@ -78,7 +79,7 @@
|
|
|
78
79
|
"@opentelemetry/sdk-trace-base": "^2.2.0",
|
|
79
80
|
"@opentelemetry/semantic-conventions": "^1.38.0",
|
|
80
81
|
"@scalar/openapi-parser": "^0.25.6",
|
|
81
|
-
"@testomatio/reporter": "^2.7.9-beta.
|
|
82
|
+
"@testomatio/reporter": "^2.7.9-beta.3-markdown",
|
|
82
83
|
"ai": "^6.0.6",
|
|
83
84
|
"axe-core": "^4.11.1",
|
|
84
85
|
"bash-tool": "^1.3.15",
|
package/src/action.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
+
import { faker } from '@faker-js/faker';
|
|
3
4
|
import { context, trace } from '@opentelemetry/api';
|
|
4
5
|
import { highlight } from 'cli-highlight';
|
|
5
6
|
import { container, recorder } from 'codeceptjs';
|
|
@@ -255,8 +256,8 @@ class Action {
|
|
|
255
256
|
await asyncFn(page);
|
|
256
257
|
await sleep(this.config.action?.delay || 500);
|
|
257
258
|
} else {
|
|
258
|
-
const codeFunction = new Function('I', 'tryTo', 'retryTo', 'within', 'hopeThat', 'step', sanitizedCode);
|
|
259
|
-
codeFunction(this.actor, tryTo, retryTo, within, hopeThat, step);
|
|
259
|
+
const codeFunction = new Function('I', 'tryTo', 'retryTo', 'within', 'hopeThat', 'step', 'faker', sanitizedCode);
|
|
260
|
+
codeFunction(this.actor, tryTo, retryTo, within, hopeThat, step, faker);
|
|
260
261
|
await recorder.add(() => sleep(this.config.action?.delay || 500));
|
|
261
262
|
await recorder.promise();
|
|
262
263
|
}
|
package/src/ai/conversation.ts
CHANGED
|
@@ -19,6 +19,7 @@ export class Conversation {
|
|
|
19
19
|
messages: ModelMessage[];
|
|
20
20
|
model: any;
|
|
21
21
|
telemetryFunctionId?: string;
|
|
22
|
+
protectedPrefixCount = 0;
|
|
22
23
|
private autoTrimRules: Map<string, number>;
|
|
23
24
|
|
|
24
25
|
constructor(messages: ModelMessage[] = [], model?: any, telemetryFunctionId?: string) {
|
|
@@ -29,6 +30,10 @@ export class Conversation {
|
|
|
29
30
|
this.autoTrimRules = new Map();
|
|
30
31
|
}
|
|
31
32
|
|
|
33
|
+
protectPrefix(count: number): void {
|
|
34
|
+
this.protectedPrefixCount = count;
|
|
35
|
+
}
|
|
36
|
+
|
|
32
37
|
addUserText(text: string): void {
|
|
33
38
|
this.messages.push({
|
|
34
39
|
role: 'user',
|
|
@@ -85,9 +90,11 @@ export class Conversation {
|
|
|
85
90
|
const escapedTag = tagName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
86
91
|
const regex = new RegExp(`<${escapedTag}>[\\s\\S]*?<\\/${escapedTag}>`, 'g');
|
|
87
92
|
const replacementText = `<${tagName}>${replacement}</${tagName}>`;
|
|
93
|
+
const start = this.protectedPrefixCount;
|
|
88
94
|
|
|
89
95
|
if (keepLast === 0) {
|
|
90
|
-
for (
|
|
96
|
+
for (let i = start; i < this.messages.length; i++) {
|
|
97
|
+
const message = this.messages[i];
|
|
91
98
|
if (typeof message.content === 'string') {
|
|
92
99
|
message.content = message.content.replace(regex, replacementText);
|
|
93
100
|
}
|
|
@@ -96,7 +103,7 @@ export class Conversation {
|
|
|
96
103
|
}
|
|
97
104
|
|
|
98
105
|
const allMatches: Array<{ messageIndex: number; startIndex: number; endIndex: number }> = [];
|
|
99
|
-
for (let i =
|
|
106
|
+
for (let i = start; i < this.messages.length; i++) {
|
|
100
107
|
const message = this.messages[i];
|
|
101
108
|
if (typeof message.content === 'string') {
|
|
102
109
|
const matches = [...message.content.matchAll(new RegExp(`<${escapedTag}>[\\s\\S]*?<\\/${escapedTag}>`, 'g'))];
|
|
@@ -112,7 +119,7 @@ export class Conversation {
|
|
|
112
119
|
const keepMatches = allMatches.slice(-keepCount);
|
|
113
120
|
const keepSet = new Set(keepMatches.map((m) => `${m.messageIndex}:${m.startIndex}`));
|
|
114
121
|
|
|
115
|
-
for (let i =
|
|
122
|
+
for (let i = start; i < this.messages.length; i++) {
|
|
116
123
|
const message = this.messages[i];
|
|
117
124
|
if (typeof message.content === 'string') {
|
|
118
125
|
const matches = [...message.content.matchAll(new RegExp(`<${escapedTag}>[\\s\\S]*?<\\/${escapedTag}>`, 'g'))];
|
|
@@ -137,7 +144,7 @@ export class Conversation {
|
|
|
137
144
|
|
|
138
145
|
compactToolResults(keepLastN: number): void {
|
|
139
146
|
const toolMessageIndexes: number[] = [];
|
|
140
|
-
for (let i =
|
|
147
|
+
for (let i = this.protectedPrefixCount; i < this.messages.length; i++) {
|
|
141
148
|
if (this.messages[i].role === 'tool') toolMessageIndexes.push(i);
|
|
142
149
|
}
|
|
143
150
|
const compactUpTo = toolMessageIndexes.length - Math.max(0, keepLastN);
|
|
@@ -169,6 +176,16 @@ export class Conversation {
|
|
|
169
176
|
}
|
|
170
177
|
}
|
|
171
178
|
|
|
179
|
+
markLastMessageCacheable(): void {
|
|
180
|
+
const last = this.messages[this.messages.length - 1];
|
|
181
|
+
if (!last) return;
|
|
182
|
+
(last as any).providerOptions = {
|
|
183
|
+
...(last as any).providerOptions,
|
|
184
|
+
anthropic: { cacheControl: { type: 'ephemeral' } },
|
|
185
|
+
bedrock: { cachePoint: { type: 'default' } },
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
172
189
|
hasTag(tagName: string, lastN?: number): boolean {
|
|
173
190
|
const escapedTag = tagName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
174
191
|
const regex = new RegExp(`<${escapedTag}>`, 'g');
|
|
@@ -1,7 +1,14 @@
|
|
|
1
|
+
import { isDynamicId } from '../../utils/xpath.ts';
|
|
1
2
|
import type { ToolExecution } from '../conversation.ts';
|
|
2
3
|
|
|
3
4
|
export function isNonReusableCode(code: string): boolean {
|
|
4
|
-
|
|
5
|
+
if (/\bI\.clickXY\s*\(/.test(code)) return true;
|
|
6
|
+
|
|
7
|
+
for (const m of code.matchAll(/#([A-Za-z_][\w-]*)/g)) {
|
|
8
|
+
if (isDynamicId(m[1])) return true;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return false;
|
|
5
12
|
}
|
|
6
13
|
|
|
7
14
|
export function escapeString(str: string): string {
|