explorbot 0.1.11 → 0.1.13
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/README.md +12 -2
- package/bin/explorbot-cli.ts +21 -21
- package/dist/bin/explorbot-cli.js +3 -3
- package/dist/package.json +4 -3
- package/dist/rules/researcher/container-rules.md +2 -0
- package/dist/src/action-result.js +2 -1
- package/dist/src/action.js +5 -10
- package/dist/src/ai/captain.js +0 -2
- package/dist/src/ai/driller.js +1108 -0
- package/dist/src/ai/historian/codeceptjs.js +2 -2
- package/dist/src/ai/historian/experience.js +1 -0
- package/dist/src/ai/historian/playwright.js +4 -4
- package/dist/src/ai/historian/screencast.js +121 -0
- package/dist/src/ai/historian.js +5 -3
- package/dist/src/ai/pilot.js +31 -22
- package/dist/src/ai/rules.js +3 -5
- package/dist/src/ai/session-analyst.js +117 -0
- package/dist/src/ai/tester.js +13 -2
- package/dist/src/commands/base-command.js +6 -6
- package/dist/src/commands/drill-command.js +3 -2
- package/dist/src/commands/exit-command.js +1 -0
- package/dist/src/commands/explore-command.js +20 -3
- package/dist/src/components/AddRule.js +1 -1
- package/dist/src/explorbot.js +52 -9
- package/dist/src/explorer.js +11 -9
- package/dist/src/reporter.js +68 -4
- package/dist/src/state-manager.js +4 -3
- package/dist/src/stats.js +5 -0
- package/dist/src/utils/aria.js +354 -529
- package/dist/src/utils/hooks-runner.js +2 -8
- package/dist/src/utils/html.js +371 -0
- package/dist/src/utils/strings.js +15 -0
- package/dist/src/utils/unique-names.js +12 -1
- package/dist/src/utils/url-matcher.js +6 -1
- package/dist/src/utils/web-element.js +27 -24
- package/dist/src/utils/xpath.js +1 -1
- package/package.json +4 -3
- package/rules/researcher/container-rules.md +2 -0
- package/src/action-result.ts +2 -1
- package/src/action.ts +5 -12
- package/src/ai/captain.ts +0 -2
- package/src/ai/driller.ts +1194 -0
- package/src/ai/historian/codeceptjs.ts +2 -2
- package/src/ai/historian/experience.ts +3 -2
- package/src/ai/historian/playwright.ts +5 -5
- package/src/ai/historian/screencast.ts +133 -0
- package/src/ai/historian.ts +7 -5
- package/src/ai/pilot.ts +31 -21
- package/src/ai/rules.ts +3 -5
- package/src/ai/session-analyst.ts +133 -0
- package/src/ai/tester.ts +15 -2
- package/src/commands/base-command.ts +6 -6
- package/src/commands/drill-command.ts +3 -2
- package/src/commands/exit-command.ts +1 -0
- package/src/commands/explore-command.ts +22 -3
- package/src/components/AddRule.tsx +1 -1
- package/src/config.ts +10 -0
- package/src/explorbot.ts +59 -11
- package/src/explorer.ts +11 -9
- package/src/reporter.ts +68 -4
- package/src/state-manager.ts +4 -3
- package/src/stats.ts +7 -0
- package/src/utils/aria.ts +367 -537
- package/src/utils/hooks-runner.ts +2 -6
- package/src/utils/html.ts +381 -0
- package/src/utils/strings.ts +17 -0
- package/src/utils/unique-names.ts +13 -0
- package/src/utils/url-matcher.ts +5 -1
- package/src/utils/web-element.ts +31 -28
- package/src/utils/xpath.ts +1 -1
- package/dist/src/ai/bosun.js +0 -456
- package/src/ai/bosun.ts +0 -571
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import figureSet from 'figures';
|
|
2
2
|
import { getStyles } from '../ai/planner/styles.js';
|
|
3
|
+
import { outputPath } from '../config.js';
|
|
3
4
|
import { Stats } from '../stats.js';
|
|
4
5
|
import type { Plan } from '../test-plan.js';
|
|
5
6
|
import { getCliName } from '../utils/cli-name.ts';
|
|
@@ -7,6 +8,7 @@ import { ErrorPageError } from '../utils/error-page.ts';
|
|
|
7
8
|
import { tag } from '../utils/logger.js';
|
|
8
9
|
import { jsonToTable } from '../utils/markdown-parser.js';
|
|
9
10
|
import { type NextStepSection, printNextSteps, relativeToCwd } from '../utils/next-steps.ts';
|
|
11
|
+
import { safeFilename } from '../utils/strings.ts';
|
|
10
12
|
import { BaseCommand, type Suggestion } from './base-command.js';
|
|
11
13
|
|
|
12
14
|
export class ExploreCommand extends BaseCommand {
|
|
@@ -71,6 +73,7 @@ export class ExploreCommand extends BaseCommand {
|
|
|
71
73
|
if (mainUrl) await this.explorBot.visit(mainUrl);
|
|
72
74
|
const savedPath = this.explorBot.savePlans(this.completedPlans);
|
|
73
75
|
this.printResults();
|
|
76
|
+
await this.explorBot.printSessionAnalysis();
|
|
74
77
|
this.printNextSteps(savedPath);
|
|
75
78
|
}
|
|
76
79
|
|
|
@@ -152,11 +155,27 @@ export class ExploreCommand extends BaseCommand {
|
|
|
152
155
|
}
|
|
153
156
|
|
|
154
157
|
const savedFiles = this.explorBot.agentHistorian().getSavedFiles();
|
|
155
|
-
|
|
156
|
-
|
|
158
|
+
const screencasts = savedFiles.filter((f) => f.endsWith('.webm'));
|
|
159
|
+
const testFiles = savedFiles.filter((f) => !f.endsWith('.webm'));
|
|
160
|
+
|
|
161
|
+
if (testFiles.length > 0) {
|
|
162
|
+
const commands = testFiles.map((f) => ({ label: '', command: `${cli} rerun ${relativeToCwd(f)}` }));
|
|
157
163
|
commands.push({ label: 'List tests', command: `${cli} runs` });
|
|
158
164
|
sections.push({
|
|
159
|
-
label: `Generated tests (${
|
|
165
|
+
label: `Generated tests (${testFiles.length})`,
|
|
166
|
+
commands,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (screencasts.length > 0) {
|
|
171
|
+
const commands = screencasts.map((f) => ({ label: '', command: relativeToCwd(f) }));
|
|
172
|
+
const screencastDir = relativeToCwd(outputPath('screencasts'));
|
|
173
|
+
const planSlugs = [...new Set(this.completedPlans.map((p) => safeFilename(p.title)).filter(Boolean))];
|
|
174
|
+
for (const slug of planSlugs) {
|
|
175
|
+
commands.push({ label: 'Browse plan', command: `ls ${screencastDir}/${slug}-*` });
|
|
176
|
+
}
|
|
177
|
+
sections.push({
|
|
178
|
+
label: `Screencasts (${screencasts.length})`,
|
|
160
179
|
commands,
|
|
161
180
|
});
|
|
162
181
|
}
|
|
@@ -5,7 +5,7 @@ import React, { useEffect, useState } from 'react';
|
|
|
5
5
|
import { AddRuleCommand } from '../commands/add-rule-command.js';
|
|
6
6
|
import InputReadline from './InputReadline.js';
|
|
7
7
|
|
|
8
|
-
const KNOWN_AGENTS = ['researcher', 'tester', 'planner', 'pilot', 'captain', '
|
|
8
|
+
const KNOWN_AGENTS = ['researcher', 'tester', 'planner', 'pilot', 'captain', 'driller', 'navigator'];
|
|
9
9
|
|
|
10
10
|
interface AddRuleProps {
|
|
11
11
|
initialAgent?: string;
|
package/src/config.ts
CHANGED
|
@@ -107,8 +107,14 @@ interface PlannerAgentConfig extends AgentConfig {
|
|
|
107
107
|
stylesDir?: string;
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
+
interface ScreencastConfig {
|
|
111
|
+
size?: { width: number; height: number };
|
|
112
|
+
quality?: number;
|
|
113
|
+
}
|
|
114
|
+
|
|
110
115
|
interface HistorianAgentConfig extends AgentConfig {
|
|
111
116
|
framework?: 'codeceptjs' | 'playwright';
|
|
117
|
+
screencast?: boolean | ScreencastConfig;
|
|
112
118
|
}
|
|
113
119
|
|
|
114
120
|
interface AgentsConfig {
|
|
@@ -117,6 +123,7 @@ interface AgentsConfig {
|
|
|
117
123
|
researcher?: ResearcherAgentConfig;
|
|
118
124
|
planner?: PlannerAgentConfig;
|
|
119
125
|
pilot?: PilotAgentConfig;
|
|
126
|
+
driller?: AgentConfig;
|
|
120
127
|
'experience-compactor'?: AgentConfig;
|
|
121
128
|
captain?: AgentConfig;
|
|
122
129
|
quartermaster?: AgentConfig;
|
|
@@ -125,6 +132,7 @@ interface AgentsConfig {
|
|
|
125
132
|
chief?: AgentConfig;
|
|
126
133
|
curler?: AgentConfig;
|
|
127
134
|
rerunner?: RerunnerAgentConfig;
|
|
135
|
+
analyst?: AgentConfig;
|
|
128
136
|
}
|
|
129
137
|
|
|
130
138
|
interface AIConfig {
|
|
@@ -173,6 +181,8 @@ interface ActionConfig {
|
|
|
173
181
|
interface ReporterConfig {
|
|
174
182
|
enabled?: boolean;
|
|
175
183
|
html?: boolean;
|
|
184
|
+
markdown?: boolean;
|
|
185
|
+
runGroup?: string | null;
|
|
176
186
|
}
|
|
177
187
|
|
|
178
188
|
type ApiHookFn = (ctx: { headers: Record<string, string>; baseEndpoint: string }) => Promise<Record<string, string> | undefined> | Record<string, string> | undefined;
|
package/src/explorbot.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { existsSync, mkdirSync } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { ActionResult } from './action-result.ts';
|
|
4
|
-
import { Bosun } from './ai/bosun.ts';
|
|
5
4
|
import { Captain } from './ai/captain.ts';
|
|
5
|
+
import { Driller } from './ai/driller.ts';
|
|
6
6
|
import { ExperienceCompactor } from './ai/experience-compactor.ts';
|
|
7
7
|
import { Fisherman } from './ai/fisherman.ts';
|
|
8
8
|
import { Historian } from './ai/historian.ts';
|
|
@@ -13,6 +13,7 @@ import { AIProvider } from './ai/provider.ts';
|
|
|
13
13
|
import { Quartermaster } from './ai/quartermaster.ts';
|
|
14
14
|
import { Rerunner } from './ai/rerunner.ts';
|
|
15
15
|
import { Researcher } from './ai/researcher.ts';
|
|
16
|
+
import { SessionAnalyst } from './ai/session-analyst.ts';
|
|
16
17
|
import { Tester } from './ai/tester.ts';
|
|
17
18
|
import { createAgentTools } from './ai/tools.ts';
|
|
18
19
|
import { ApiClient } from './api/api-client.ts';
|
|
@@ -25,8 +26,9 @@ import Explorer from './explorer.ts';
|
|
|
25
26
|
import { KnowledgeTracker } from './knowledge-tracker.ts';
|
|
26
27
|
import { WebPageState } from './state-manager.ts';
|
|
27
28
|
import type { Suite } from './suite.ts';
|
|
28
|
-
import { Plan } from './test-plan.ts';
|
|
29
|
+
import { Plan, type Test } from './test-plan.ts';
|
|
29
30
|
import { setVerboseMode, tag } from './utils/logger.ts';
|
|
31
|
+
import { relativeToCwd } from './utils/next-steps.ts';
|
|
30
32
|
import { sanitizeFilename } from './utils/strings.ts';
|
|
31
33
|
|
|
32
34
|
export interface ExplorBotOptions {
|
|
@@ -55,6 +57,8 @@ export class ExplorBot {
|
|
|
55
57
|
lastPlanError: Error | null = null;
|
|
56
58
|
lastSavedPlanPath: string | null = null;
|
|
57
59
|
private agents: Record<string, any> = {};
|
|
60
|
+
private sessionPlans: Plan[] = [];
|
|
61
|
+
private lastReportedTestCount = 0;
|
|
58
62
|
|
|
59
63
|
constructor(options: ExplorBotOptions = {}) {
|
|
60
64
|
this.options = options;
|
|
@@ -262,7 +266,10 @@ export class ExplorBot {
|
|
|
262
266
|
return (this.agents.historian ||= this.createAgent(({ ai, explorer, config }) => {
|
|
263
267
|
const experienceTracker = explorer.getStateManager().getExperienceTracker();
|
|
264
268
|
const reporter = explorer.getReporter();
|
|
265
|
-
return new Historian(ai, experienceTracker, reporter, explorer.getStateManager(), config,
|
|
269
|
+
return new Historian(ai, experienceTracker, reporter, explorer.getStateManager(), config, {
|
|
270
|
+
recorder: explorer.getPlaywrightRecorder(),
|
|
271
|
+
helper: explorer.playwrightHelper,
|
|
272
|
+
});
|
|
266
273
|
}));
|
|
267
274
|
}
|
|
268
275
|
|
|
@@ -281,15 +288,17 @@ export class ExplorBot {
|
|
|
281
288
|
return this.agents.rerunner;
|
|
282
289
|
}
|
|
283
290
|
|
|
284
|
-
|
|
285
|
-
return (this.agents.
|
|
286
|
-
const researcher = this.agentResearcher();
|
|
291
|
+
agentDriller(): Driller {
|
|
292
|
+
return (this.agents.driller ||= this.createAgent(({ ai, explorer }) => {
|
|
287
293
|
const navigator = this.agentNavigator();
|
|
288
|
-
|
|
289
|
-
return new Bosun(explorer, ai, researcher, navigator, tools);
|
|
294
|
+
return new Driller(explorer, ai, navigator);
|
|
290
295
|
}));
|
|
291
296
|
}
|
|
292
297
|
|
|
298
|
+
agentSessionAnalyst(): SessionAnalyst {
|
|
299
|
+
return (this.agents.sessionAnalyst ||= this.createAgent(({ ai }) => new SessionAnalyst(ai)));
|
|
300
|
+
}
|
|
301
|
+
|
|
293
302
|
agentFisherman(): Fisherman | null {
|
|
294
303
|
const fishermanConfig = this.config.ai?.agents?.fisherman;
|
|
295
304
|
const hasApiConfig = !!this.config.api;
|
|
@@ -362,7 +371,7 @@ export class ExplorBot {
|
|
|
362
371
|
}
|
|
363
372
|
this.lastPlanError = null;
|
|
364
373
|
try {
|
|
365
|
-
this.
|
|
374
|
+
this.setCurrentPlan(await planner.plan(feature, opts.style, opts.extend, opts.completedPlans));
|
|
366
375
|
} catch (err) {
|
|
367
376
|
this.lastPlanError = err instanceof Error ? err : new Error(String(err));
|
|
368
377
|
tag('warning').log(`Planning failed: ${this.lastPlanError.message}`);
|
|
@@ -433,11 +442,50 @@ export class ExplorBot {
|
|
|
433
442
|
throw new Error(`Plan file not found: ${planPath}`);
|
|
434
443
|
}
|
|
435
444
|
|
|
436
|
-
this.
|
|
437
|
-
return this.currentPlan
|
|
445
|
+
this.setCurrentPlan(Plan.fromMarkdown(planPath));
|
|
446
|
+
return this.currentPlan!;
|
|
438
447
|
}
|
|
439
448
|
|
|
440
449
|
setCurrentPlan(plan?: Plan): void {
|
|
441
450
|
this.currentPlan = plan;
|
|
451
|
+
if (plan && !this.sessionPlans.includes(plan)) {
|
|
452
|
+
this.sessionPlans.push(plan);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
getSessionTests(): Test[] {
|
|
457
|
+
return this.sessionPlans.flatMap((p) => p.tests.filter((t) => t.startTime != null));
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
async printSessionAnalysis(): Promise<void> {
|
|
461
|
+
const analystConfig = this.config.ai?.agents?.analyst;
|
|
462
|
+
if (analystConfig?.enabled === false) return;
|
|
463
|
+
|
|
464
|
+
const tests = this.getSessionTests();
|
|
465
|
+
if (tests.length === 0) return;
|
|
466
|
+
if (tests.length === this.lastReportedTestCount) return;
|
|
467
|
+
|
|
468
|
+
try {
|
|
469
|
+
const markdown = await this.agentSessionAnalyst().analyze(tests);
|
|
470
|
+
if (!markdown) {
|
|
471
|
+
this.lastReportedTestCount = tests.length;
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
tag('multiline').log(markdown);
|
|
476
|
+
|
|
477
|
+
const filePath = this.agentSessionAnalyst().writeReport(markdown);
|
|
478
|
+
tag('info').log(`Session report saved: ${relativeToCwd(filePath)}`);
|
|
479
|
+
|
|
480
|
+
const reporter = this.explorer?.getReporter();
|
|
481
|
+
if (reporter?.isEnabled()) {
|
|
482
|
+
await reporter.setRunDescription(markdown);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
this.lastReportedTestCount = tests.length;
|
|
486
|
+
} catch (error) {
|
|
487
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
488
|
+
tag('warning').log(`Session analysis failed: ${message}`);
|
|
489
|
+
}
|
|
442
490
|
}
|
|
443
491
|
}
|
package/src/explorer.ts
CHANGED
|
@@ -19,8 +19,9 @@ import { PlaywrightRecorder } from './playwright-recorder.ts';
|
|
|
19
19
|
import { Reporter } from './reporter.ts';
|
|
20
20
|
import { StateManager } from './state-manager.js';
|
|
21
21
|
import { Test } from './test-plan.ts';
|
|
22
|
+
import { ELEMENT_EXTRACTION_CONFIG, getElementDataExtractorSource } from './utils/html.ts';
|
|
22
23
|
import { createDebug, log, tag } from './utils/logger.js';
|
|
23
|
-
import { WebElement
|
|
24
|
+
import { WebElement } from './utils/web-element.ts';
|
|
24
25
|
|
|
25
26
|
declare global {
|
|
26
27
|
namespace NodeJS {
|
|
@@ -337,11 +338,11 @@ class Explorer {
|
|
|
337
338
|
async getEidxInContainer(containerCss: string | null): Promise<string[]> {
|
|
338
339
|
const page = this.playwrightHelper.page;
|
|
339
340
|
try {
|
|
340
|
-
const selector = containerCss ? `${containerCss} [
|
|
341
|
+
const selector = containerCss ? `${containerCss} [${ELEMENT_EXTRACTION_CONFIG.attrs.eidx}]` : `[${ELEMENT_EXTRACTION_CONFIG.attrs.eidx}]`;
|
|
341
342
|
const elements = await page.locator(selector).all();
|
|
342
343
|
const result: string[] = [];
|
|
343
344
|
for (const el of elements) {
|
|
344
|
-
const attr = await el.getAttribute(
|
|
345
|
+
const attr = await el.getAttribute(ELEMENT_EXTRACTION_CONFIG.attrs.eidx);
|
|
345
346
|
if (attr) result.push(attr);
|
|
346
347
|
}
|
|
347
348
|
return result;
|
|
@@ -359,7 +360,7 @@ class Explorer {
|
|
|
359
360
|
const page = this.playwrightHelper.page;
|
|
360
361
|
const base = container ? page.locator(container) : page;
|
|
361
362
|
const el = locator.startsWith('//') ? base.locator(`xpath=${locator}`) : base.locator(locator);
|
|
362
|
-
return await el.first().getAttribute(
|
|
363
|
+
return await el.first().getAttribute(ELEMENT_EXTRACTION_CONFIG.attrs.eidx);
|
|
363
364
|
} catch (error) {
|
|
364
365
|
if (this.isFatalBrowserError(error)) {
|
|
365
366
|
tag('warning').log(`getEidxByLocator: ${error instanceof Error ? error.message : error}`);
|
|
@@ -714,6 +715,7 @@ function toCodeceptjsTest(test: Test): any {
|
|
|
714
715
|
codeceptjsTest.fullTitle = () => `${parent.title} ${test.scenario}`;
|
|
715
716
|
codeceptjsTest.state = 'pending';
|
|
716
717
|
codeceptjsTest.notes = test.getPrintableNotes();
|
|
718
|
+
codeceptjsTest._explorbotTest = test;
|
|
717
719
|
return codeceptjsTest;
|
|
718
720
|
}
|
|
719
721
|
|
|
@@ -733,7 +735,7 @@ function parseAriaRefs(ariaSnapshot: string): Array<{ role: string; name: string
|
|
|
733
735
|
}
|
|
734
736
|
|
|
735
737
|
export async function annotatePageElements(page: any): Promise<{ ariaSnapshot: string; elements: WebElement[] }> {
|
|
736
|
-
const ariaSnapshot: string = await page.locator('body').ariaSnapshot({
|
|
738
|
+
const ariaSnapshot: string = await page.locator('body').ariaSnapshot({ mode: 'ai' });
|
|
737
739
|
const refEntries = parseAriaRefs(ariaSnapshot);
|
|
738
740
|
|
|
739
741
|
const byRole = new Map<string, Array<{ name: string; ref: string }>>();
|
|
@@ -750,20 +752,20 @@ export async function annotatePageElements(page: any): Promise<{ ariaSnapshot: s
|
|
|
750
752
|
for (const [role, entries] of byRole) {
|
|
751
753
|
try {
|
|
752
754
|
const rawList = await page.getByRole(role).evaluateAll(
|
|
753
|
-
(domElements: Element[], [data, extractFnStr]: [Array<{ name: string; ref: string }>, string]) => {
|
|
755
|
+
(domElements: Element[], [data, extractFnStr, config]: [Array<{ name: string; ref: string }>, string, typeof ELEMENT_EXTRACTION_CONFIG]) => {
|
|
754
756
|
const extract = new Function(`return ${extractFnStr}`)() as (el: Element) => any;
|
|
755
757
|
const results: any[] = [];
|
|
756
758
|
let ariaIdx = 0;
|
|
757
759
|
for (const el of domElements) {
|
|
758
760
|
if (ariaIdx >= data.length) break;
|
|
759
|
-
el.setAttribute(
|
|
760
|
-
const elData = extract(el);
|
|
761
|
+
el.setAttribute(config.attrs.eidx, data[ariaIdx].ref);
|
|
762
|
+
const elData = extract(el, config);
|
|
761
763
|
if (elData) results.push(elData);
|
|
762
764
|
ariaIdx++;
|
|
763
765
|
}
|
|
764
766
|
return results;
|
|
765
767
|
},
|
|
766
|
-
[entries,
|
|
768
|
+
[entries, getElementDataExtractorSource(), ELEMENT_EXTRACTION_CONFIG]
|
|
767
769
|
);
|
|
768
770
|
for (const raw of rawList) {
|
|
769
771
|
elements.push(WebElement.fromRawData(raw, role));
|
package/src/reporter.ts
CHANGED
|
@@ -33,8 +33,21 @@ export class Reporter {
|
|
|
33
33
|
this.configureHtmlPipe();
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
if (this.reporterEnabled && config?.markdown) {
|
|
37
|
+
this.configureMarkdownPipe();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (this.reporterEnabled) {
|
|
41
|
+
this.configureRunGroup(config?.runGroup);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
debugLog('Reporter initialized', {
|
|
45
|
+
enabled: this.reporterEnabled,
|
|
46
|
+
testomatio: Boolean(process.env.TESTOMATIO),
|
|
47
|
+
html: Boolean(process.env.TESTOMATIO_HTML_REPORT_SAVE),
|
|
48
|
+
markdown: Boolean(process.env.TESTOMATIO_MARKDOWN_REPORT_SAVE),
|
|
49
|
+
runGroup: process.env.TESTOMATIO_RUNGROUP_TITLE || null,
|
|
50
|
+
});
|
|
38
51
|
}
|
|
39
52
|
|
|
40
53
|
private buildTitle(): string {
|
|
@@ -56,7 +69,31 @@ export class Reporter {
|
|
|
56
69
|
private configureHtmlPipe(): void {
|
|
57
70
|
process.env.TESTOMATIO_HTML_REPORT_SAVE = '1';
|
|
58
71
|
process.env.TESTOMATIO_HTML_REPORT_FOLDER = outputPath('reports');
|
|
59
|
-
|
|
72
|
+
process.env.TESTOMATIO_HTML_FILENAME = `${Stats.sessionLabel()}.html`;
|
|
73
|
+
debugLog('HTML report pipe configured', {
|
|
74
|
+
folder: process.env.TESTOMATIO_HTML_REPORT_FOLDER,
|
|
75
|
+
filename: process.env.TESTOMATIO_HTML_FILENAME,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private configureMarkdownPipe(): void {
|
|
80
|
+
process.env.TESTOMATIO_MARKDOWN_REPORT_SAVE = '1';
|
|
81
|
+
process.env.TESTOMATIO_MARKDOWN_REPORT_FOLDER = outputPath('reports');
|
|
82
|
+
process.env.TESTOMATIO_MARKDOWN_FILENAME = `${Stats.sessionLabel()}-tests.md`;
|
|
83
|
+
debugLog('Markdown report pipe configured', {
|
|
84
|
+
folder: process.env.TESTOMATIO_MARKDOWN_REPORT_FOLDER,
|
|
85
|
+
filename: process.env.TESTOMATIO_MARKDOWN_FILENAME,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private configureRunGroup(runGroup: string | null | undefined): void {
|
|
90
|
+
if (process.env.TESTOMATIO_RUNGROUP_TITLE) return;
|
|
91
|
+
if (runGroup === null) return;
|
|
92
|
+
if (runGroup) {
|
|
93
|
+
process.env.TESTOMATIO_RUNGROUP_TITLE = runGroup;
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
process.env.TESTOMATIO_RUNGROUP_TITLE = `Explorbot ${new Date().toISOString().slice(0, 10)}`;
|
|
60
97
|
}
|
|
61
98
|
|
|
62
99
|
async startRun(): Promise<void> {
|
|
@@ -104,6 +141,7 @@ export class Reporter {
|
|
|
104
141
|
const noteEntries = Object.entries(test.notes)
|
|
105
142
|
.map(([timestampKey, note]) => ({
|
|
106
143
|
startTime: note.startTime,
|
|
144
|
+
endTime: note.endTime,
|
|
107
145
|
message: note.message,
|
|
108
146
|
status: note.status,
|
|
109
147
|
screenshot: note.screenshot,
|
|
@@ -135,7 +173,7 @@ export class Reporter {
|
|
|
135
173
|
const step: Step = {
|
|
136
174
|
category: 'user',
|
|
137
175
|
title: noteEntry.message,
|
|
138
|
-
duration: 0,
|
|
176
|
+
duration: Math.max(0, Math.round(noteEntry.endTime - noteEntry.startTime)),
|
|
139
177
|
status: noteEntry.status || 'none',
|
|
140
178
|
steps: noteSteps.length > 0 ? noteSteps : undefined,
|
|
141
179
|
};
|
|
@@ -182,6 +220,7 @@ export class Reporter {
|
|
|
182
220
|
}
|
|
183
221
|
|
|
184
222
|
const steps = this.combineStepsAndNotes(test, screenshotFile);
|
|
223
|
+
const durationMs = test.getDurationMs();
|
|
185
224
|
|
|
186
225
|
const testData = {
|
|
187
226
|
rid: test.id,
|
|
@@ -197,6 +236,7 @@ export class Reporter {
|
|
|
197
236
|
files: Object.values(test.artifacts) || [],
|
|
198
237
|
message: test.summary || this.extractLastNoteMessage(test) || '',
|
|
199
238
|
meta,
|
|
239
|
+
time: durationMs != null ? Math.round(durationMs) : 0,
|
|
200
240
|
};
|
|
201
241
|
|
|
202
242
|
debugLog(testData);
|
|
@@ -226,6 +266,30 @@ export class Reporter {
|
|
|
226
266
|
return this.isRunStarted;
|
|
227
267
|
}
|
|
228
268
|
|
|
269
|
+
async setRunDescription(text: string): Promise<void> {
|
|
270
|
+
if (!this.isRunStarted) return;
|
|
271
|
+
if (!process.env.TESTOMATIO) return;
|
|
272
|
+
const runId = this.client.runId;
|
|
273
|
+
if (!runId) return;
|
|
274
|
+
|
|
275
|
+
const baseUrl = process.env.TESTOMATIO_URL || 'https://app.testomat.io';
|
|
276
|
+
const url = `${baseUrl}/api/reporter/${runId}`;
|
|
277
|
+
try {
|
|
278
|
+
const response = await fetch(url, {
|
|
279
|
+
method: 'PUT',
|
|
280
|
+
headers: { 'Content-Type': 'application/json' },
|
|
281
|
+
body: JSON.stringify({ api_key: process.env.TESTOMATIO, description: text }),
|
|
282
|
+
});
|
|
283
|
+
if (!response.ok) {
|
|
284
|
+
debugLog('Run description update failed:', response.status, response.statusText);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
debugLog('Run description updated');
|
|
288
|
+
} catch (error) {
|
|
289
|
+
debugLog('Failed to update run description:', error);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
229
293
|
private extractLastNoteMessage(test: Test): string {
|
|
230
294
|
const notes = Object.values(test.notes);
|
|
231
295
|
if (notes.length === 0) return '';
|
package/src/state-manager.ts
CHANGED
|
@@ -142,8 +142,8 @@ export class StateManager {
|
|
|
142
142
|
|
|
143
143
|
/**
|
|
144
144
|
* Extract state path from full URL
|
|
145
|
-
* Removes domain, port, protocol
|
|
146
|
-
* Keeps path and hash: /path/to/page#section
|
|
145
|
+
* Removes domain, port, protocol
|
|
146
|
+
* Keeps path, query, and hash: /path/to/page?tab=users#section
|
|
147
147
|
*/
|
|
148
148
|
/**
|
|
149
149
|
* Update current state from ActionResult and record transition if state changed
|
|
@@ -549,7 +549,8 @@ export class StateManager {
|
|
|
549
549
|
export function normalizeUrl(url: string): string {
|
|
550
550
|
try {
|
|
551
551
|
const parsed = new URL(url, 'http://localhost');
|
|
552
|
-
|
|
552
|
+
const path = parsed.pathname.replace(/^\/+|\/+$/g, '');
|
|
553
|
+
return `${path}${parsed.search}${parsed.hash}`;
|
|
553
554
|
} catch {
|
|
554
555
|
return url.replace(/^\/+|\/+$/g, '');
|
|
555
556
|
}
|
package/src/stats.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { uniqExplorationName } from './utils/unique-names.ts';
|
|
2
|
+
|
|
1
3
|
interface TokenUsage {
|
|
2
4
|
input: number;
|
|
3
5
|
output: number;
|
|
@@ -8,6 +10,7 @@ export type ExplorbotMode = 'explore' | 'test' | 'freesail' | 'tui';
|
|
|
8
10
|
|
|
9
11
|
export class Stats {
|
|
10
12
|
static startTime = Date.now();
|
|
13
|
+
static sessionName = uniqExplorationName();
|
|
11
14
|
static researches = 0;
|
|
12
15
|
static tests = 0;
|
|
13
16
|
static plans = 0;
|
|
@@ -54,4 +57,8 @@ export class Stats {
|
|
|
54
57
|
const totalTokens = Object.values(Stats.models).reduce((sum, m) => sum + m.total, 0);
|
|
55
58
|
return totalTokens > 0;
|
|
56
59
|
}
|
|
60
|
+
|
|
61
|
+
static sessionLabel(): string {
|
|
62
|
+
return `${Stats.mode || 'session'}-${Stats.sessionName}`;
|
|
63
|
+
}
|
|
57
64
|
}
|