explorbot 0.1.25 → 0.1.27
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 +1 -1
- package/dist/src/ai/historian/codeceptjs.js +1 -1
- package/dist/src/ai/historian/experience.js +1 -1
- package/dist/src/ai/historian/playwright.js +1 -1
- package/dist/src/ai/historian/screencast.js +3 -3
- package/dist/src/ai/historian.js +1 -1
- package/dist/src/ai/navigator.js +4 -4
- package/dist/src/ai/pilot.js +8 -5
- package/dist/src/ai/planner.js +2 -4
- package/dist/src/ai/provider.js +5 -3
- package/dist/src/ai/researcher/cache.js +8 -0
- package/dist/src/ai/researcher/deep-analysis.js +146 -61
- package/dist/src/ai/researcher/locators.js +1 -1
- package/dist/src/ai/researcher.js +4 -4
- package/dist/src/ai/session-analyst.js +4 -1
- package/dist/src/ai/task-agent.js +2 -2
- package/dist/src/commands/context-aria-command.js +1 -1
- package/dist/src/commands/explore-command.js +67 -25
- package/dist/src/commands/test-command.js +2 -1
- package/dist/src/components/App.js +1 -33
- package/dist/src/components/LogPane.js +9 -3
- package/dist/src/experience-tracker.js +2 -2
- package/dist/src/explorbot.js +1 -1
- package/dist/src/reporter.js +24 -5
- package/dist/src/utils/log-filters.js +27 -0
- package/dist/src/utils/logger.js +28 -1
- package/dist/src/utils/next-steps.js +1 -7
- package/package.json +1 -1
- package/src/ai/historian/codeceptjs.ts +1 -1
- package/src/ai/historian/experience.ts +1 -1
- package/src/ai/historian/playwright.ts +1 -1
- package/src/ai/historian/screencast.ts +3 -3
- package/src/ai/historian.ts +1 -1
- package/src/ai/navigator.ts +4 -4
- package/src/ai/pilot.ts +8 -5
- package/src/ai/planner.ts +1 -3
- package/src/ai/provider.ts +5 -3
- package/src/ai/researcher/cache.ts +7 -0
- package/src/ai/researcher/deep-analysis.ts +166 -68
- package/src/ai/researcher/locators.ts +1 -1
- package/src/ai/researcher.ts +4 -4
- package/src/ai/session-analyst.ts +4 -1
- package/src/ai/task-agent.ts +2 -2
- package/src/commands/context-aria-command.ts +1 -1
- package/src/commands/explore-command.ts +68 -23
- package/src/commands/test-command.ts +2 -1
- package/src/components/App.tsx +0 -33
- package/src/components/LogPane.tsx +18 -3
- package/src/experience-tracker.ts +2 -2
- package/src/explorbot.ts +1 -1
- package/src/reporter.ts +24 -6
- package/src/utils/log-filters.ts +26 -0
- package/src/utils/logger.ts +24 -2
- package/src/utils/next-steps.ts +1 -6
|
@@ -7,7 +7,6 @@ import { TestResult } from '../test-plan.js';
|
|
|
7
7
|
import { getCliName } from "../utils/cli-name.js";
|
|
8
8
|
import { ErrorPageError } from "../utils/error-page.js";
|
|
9
9
|
import { tag } from '../utils/logger.js';
|
|
10
|
-
import { jsonToTable } from '../utils/markdown-parser.js';
|
|
11
10
|
import { printNextSteps, relativeToCwd } from "../utils/next-steps.js";
|
|
12
11
|
import { safeFilename } from "../utils/strings.js";
|
|
13
12
|
import { BaseCommand } from './base-command.js';
|
|
@@ -85,14 +84,14 @@ export class ExploreCommand extends BaseCommand {
|
|
|
85
84
|
const t = tests[i];
|
|
86
85
|
lines.push(` ${String(i + 1).padStart(2)}. [${this.originLabel(t)}] [${t.priority.padEnd(9)}] ${t.scenario}`);
|
|
87
86
|
}
|
|
88
|
-
tag('multiline').log(lines.join('\n'));
|
|
87
|
+
tag('multiline').log(lines.join('\n'), { maxLines: 24 });
|
|
89
88
|
}
|
|
90
89
|
async runFreshMode(mainUrl, feature, styles) {
|
|
91
90
|
await this.runAllStyles(mainUrl, feature, undefined, undefined, styles);
|
|
91
|
+
this.rememberCurrentPlan();
|
|
92
92
|
const mainPlan = this.explorBot.getCurrentPlan();
|
|
93
93
|
if (!mainPlan)
|
|
94
94
|
return;
|
|
95
|
-
this.completedPlans.push(mainPlan);
|
|
96
95
|
if (feature || this.isLimitReached())
|
|
97
96
|
return;
|
|
98
97
|
await this.discoverNewSubPages(mainPlan, mainUrl, styles, new Set());
|
|
@@ -264,6 +263,8 @@ export class ExploreCommand extends BaseCommand {
|
|
|
264
263
|
const styleList = styles ?? Object.keys(getStyles());
|
|
265
264
|
let fresh = true;
|
|
266
265
|
for (const style of styleList) {
|
|
266
|
+
if (this.isLimitReached())
|
|
267
|
+
break;
|
|
267
268
|
if (!fresh && pageUrl && !this.dryRun) {
|
|
268
269
|
await this.explorBot.visit(pageUrl);
|
|
269
270
|
}
|
|
@@ -274,9 +275,20 @@ export class ExploreCommand extends BaseCommand {
|
|
|
274
275
|
opts.noSave = true;
|
|
275
276
|
await this.planWithRetry(feature, opts, pageUrl);
|
|
276
277
|
await this.runPendingTests();
|
|
278
|
+
this.rememberCurrentPlan();
|
|
277
279
|
fresh = false;
|
|
278
280
|
}
|
|
279
281
|
}
|
|
282
|
+
rememberCurrentPlan() {
|
|
283
|
+
const plan = this.explorBot.getCurrentPlan();
|
|
284
|
+
if (!plan)
|
|
285
|
+
return;
|
|
286
|
+
if (this.completedPlans.includes(plan))
|
|
287
|
+
return;
|
|
288
|
+
if (plan.tests.every((test) => test.startTime == null))
|
|
289
|
+
return;
|
|
290
|
+
this.completedPlans.push(plan);
|
|
291
|
+
}
|
|
280
292
|
async planWithRetry(feature, opts, pageUrl) {
|
|
281
293
|
const before = new Set(this.explorBot.getCurrentPlan()?.tests ?? []);
|
|
282
294
|
await this.explorBot.plan(feature, opts);
|
|
@@ -394,9 +406,9 @@ export class ExploreCommand extends BaseCommand {
|
|
|
394
406
|
const allTests = this.completedPlans.flatMap((plan) => plan.tests.filter((t) => t.startTime != null).map((test) => ({ test, planTitle: plan.title }))).sort((a, b) => (a.test.startTime ?? 0) - (b.test.startTime ?? 0));
|
|
395
407
|
if (allTests.length === 0)
|
|
396
408
|
return;
|
|
397
|
-
const hasSubPages = this.completedPlans.
|
|
409
|
+
const hasSubPages = new Set(this.completedPlans.map((plan) => plan.title)).size > 1;
|
|
398
410
|
const hasOrigin = this.oldTestRefs.size > 0;
|
|
399
|
-
const
|
|
411
|
+
const completed = allTests.map(({ test, planTitle }, index) => {
|
|
400
412
|
const durationMs = test.getDurationMs();
|
|
401
413
|
const duration = durationMs != null ? `${(durationMs / 1000).toFixed(1)}s` : '-';
|
|
402
414
|
let status = 'failed';
|
|
@@ -404,28 +416,51 @@ export class ExploreCommand extends BaseCommand {
|
|
|
404
416
|
status = 'passed';
|
|
405
417
|
else if (test.isSkipped)
|
|
406
418
|
status = 'skipped';
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
419
|
+
return {
|
|
420
|
+
index: index + 1,
|
|
421
|
+
status,
|
|
422
|
+
title: test.scenario.replace(/\s+/g, ' ').trim(),
|
|
423
|
+
priority: test.priority,
|
|
424
|
+
duration,
|
|
425
|
+
durationMs: durationMs ?? 0,
|
|
426
|
+
steps: Object.keys(test.notes).length,
|
|
427
|
+
origin: hasOrigin ? this.originLabel(test) : '',
|
|
428
|
+
planTitle: hasSubPages ? planTitle : '',
|
|
414
429
|
};
|
|
415
|
-
|
|
416
|
-
|
|
430
|
+
});
|
|
431
|
+
const passed = completed.filter((t) => t.status === 'passed').length;
|
|
432
|
+
const failed = completed.filter((t) => t.status === 'failed').length;
|
|
433
|
+
const skipped = completed.filter((t) => t.status === 'skipped').length;
|
|
434
|
+
const totalSeconds = completed.reduce((sum, t) => sum + t.durationMs, 0) / 1000;
|
|
435
|
+
const lines = [`Results: ${passed} passed, ${failed} failed, ${skipped} skipped - ${formatDuration(totalSeconds)}`];
|
|
436
|
+
const failedTests = completed.filter((t) => t.status === 'failed');
|
|
437
|
+
if (failedTests.length > 0) {
|
|
438
|
+
lines.push('', 'Failed tests:');
|
|
439
|
+
for (const test of failedTests) {
|
|
440
|
+
lines.push(` #${test.index} [${test.priority}] ${test.title} (${test.duration}, ${test.steps} steps)`);
|
|
417
441
|
}
|
|
418
|
-
|
|
419
|
-
|
|
442
|
+
}
|
|
443
|
+
const slowTests = completed
|
|
444
|
+
.filter((t) => t.durationMs >= 1000)
|
|
445
|
+
.sort((a, b) => b.durationMs - a.durationMs)
|
|
446
|
+
.slice(0, 3);
|
|
447
|
+
if (slowTests.length > 0) {
|
|
448
|
+
lines.push('', 'Slowest tests:');
|
|
449
|
+
for (const test of slowTests) {
|
|
450
|
+
lines.push(` #${test.index} ${test.duration} - ${test.title}`);
|
|
420
451
|
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
452
|
+
}
|
|
453
|
+
const detailLines = completed
|
|
454
|
+
.map((test) => {
|
|
455
|
+
const details = [test.origin, test.planTitle].filter(Boolean).join(' - ');
|
|
456
|
+
return details ? ` #${test.index} ${details}` : '';
|
|
457
|
+
})
|
|
458
|
+
.filter(Boolean);
|
|
459
|
+
if (detailLines.length > 0) {
|
|
460
|
+
lines.push('', 'Details:');
|
|
461
|
+
lines.push(...detailLines);
|
|
462
|
+
}
|
|
463
|
+
tag('multiline').log(lines.join('\n'));
|
|
429
464
|
tag('info').log(`${figureSet.tick} ${allTests.length} tests completed`);
|
|
430
465
|
}
|
|
431
466
|
printNextSteps(savedPlanPath) {
|
|
@@ -455,8 +490,8 @@ export class ExploreCommand extends BaseCommand {
|
|
|
455
490
|
});
|
|
456
491
|
}
|
|
457
492
|
if (screencasts.length > 0) {
|
|
458
|
-
const commands = screencasts.map((f) => ({ label: '', command: relativeToCwd(f) }));
|
|
459
493
|
const screencastDir = relativeToCwd(outputPath('screencasts'));
|
|
494
|
+
const commands = [{ label: 'Folder', command: screencastDir }];
|
|
460
495
|
const planSlugs = [...new Set(this.completedPlans.map((p) => safeFilename(p.title)).filter(Boolean))];
|
|
461
496
|
for (const slug of planSlugs) {
|
|
462
497
|
commands.push({ label: 'Browse plan', command: `ls ${screencastDir}/${slug}-*` });
|
|
@@ -513,3 +548,10 @@ function parseRatio(s) {
|
|
|
513
548
|
return null;
|
|
514
549
|
return n;
|
|
515
550
|
}
|
|
551
|
+
function formatDuration(seconds) {
|
|
552
|
+
if (seconds < 60)
|
|
553
|
+
return `${seconds.toFixed(1)}s`;
|
|
554
|
+
const minutes = Math.floor(seconds / 60);
|
|
555
|
+
const remainingSeconds = Math.round(seconds % 60);
|
|
556
|
+
return `${minutes}m ${remainingSeconds}s`;
|
|
557
|
+
}
|
|
@@ -68,7 +68,8 @@ export class TestCommand extends BaseCommand {
|
|
|
68
68
|
}
|
|
69
69
|
tag('info').log(`Launching ${toExecute.length} test scenario(s).`);
|
|
70
70
|
const tester = this.explorBot.agentTester();
|
|
71
|
-
for (const test of toExecute) {
|
|
71
|
+
for (const [index, test] of toExecute.entries()) {
|
|
72
|
+
tag('info').log(`Starting test ${index + 1}/${toExecute.length}: ${test.scenario}`);
|
|
72
73
|
await tester.test(test);
|
|
73
74
|
}
|
|
74
75
|
tag('success').log('Test execution finished');
|
|
@@ -8,7 +8,6 @@ import Autocomplete from './Autocomplete.js';
|
|
|
8
8
|
import InputReadline from './InputReadline.js';
|
|
9
9
|
import LogPane from './LogPane.js';
|
|
10
10
|
import PlanEditor from './PlanEditor.js';
|
|
11
|
-
import PlanPane from './PlanPane.js';
|
|
12
11
|
import SessionTimer from './SessionTimer.js';
|
|
13
12
|
import StateTransitionPane from './StateTransitionPane.js';
|
|
14
13
|
import TaskPane, { WINDOW_SIZE } from './TaskPane.js';
|
|
@@ -124,39 +123,18 @@ export function App({ explorBot, initialShowInput = false, exitOnEmptyInput = fa
|
|
|
124
123
|
}, [explorBot, inputCallbackReady]);
|
|
125
124
|
const planRef = useRef(undefined);
|
|
126
125
|
const unsubscribeRef = useRef(undefined);
|
|
127
|
-
const [completedPlans, setCompletedPlans] = useState([]);
|
|
128
|
-
const [activePlanInfo, setActivePlanInfo] = useState(null);
|
|
129
126
|
useEffect(() => {
|
|
130
|
-
const makeSummary = (plan) => {
|
|
131
|
-
const enabled = plan.tests.filter((t) => t.enabled);
|
|
132
|
-
return {
|
|
133
|
-
title: plan.title,
|
|
134
|
-
testCount: enabled.length,
|
|
135
|
-
passed: enabled.filter((t) => t.isSuccessful).length,
|
|
136
|
-
failed: enabled.filter((t) => t.hasFailed).length,
|
|
137
|
-
};
|
|
138
|
-
};
|
|
139
127
|
const subscribeToPlan = (plan) => {
|
|
140
128
|
if (unsubscribeRef.current)
|
|
141
129
|
unsubscribeRef.current();
|
|
142
|
-
if (planRef.current && planRef.current !== plan && planRef.current.tests.length > 0) {
|
|
143
|
-
const summary = makeSummary(planRef.current);
|
|
144
|
-
setCompletedPlans((prev) => {
|
|
145
|
-
if (prev.some((p) => p.title === summary.title))
|
|
146
|
-
return prev;
|
|
147
|
-
return [...prev, summary];
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
130
|
planRef.current = plan;
|
|
151
131
|
tasksRef.current = [...plan.tests];
|
|
152
132
|
setTasks(tasksRef.current);
|
|
153
133
|
setTaskScrollOffset(0);
|
|
154
|
-
setActivePlanInfo(makeSummary(plan));
|
|
155
134
|
let lastInProgressIdx = -1;
|
|
156
135
|
unsubscribeRef.current = plan.onTestsChange((updatedTests) => {
|
|
157
136
|
tasksRef.current = [...updatedTests];
|
|
158
137
|
setTasks(tasksRef.current);
|
|
159
|
-
setActivePlanInfo(makeSummary(plan));
|
|
160
138
|
const inProgressIdx = updatedTests.findIndex((t) => t.status === 'in_progress' && t.enabled);
|
|
161
139
|
if (inProgressIdx >= 0 && inProgressIdx !== lastInProgressIdx) {
|
|
162
140
|
lastInProgressIdx = inProgressIdx;
|
|
@@ -175,18 +153,9 @@ export function App({ explorBot, initialShowInput = false, exitOnEmptyInput = fa
|
|
|
175
153
|
else if (!currentPlan && planRef.current) {
|
|
176
154
|
if (unsubscribeRef.current)
|
|
177
155
|
unsubscribeRef.current();
|
|
178
|
-
if (planRef.current.tests.length > 0) {
|
|
179
|
-
const summary = makeSummary(planRef.current);
|
|
180
|
-
setCompletedPlans((prev) => {
|
|
181
|
-
if (prev.some((p) => p.title === summary.title))
|
|
182
|
-
return prev;
|
|
183
|
-
return [...prev, summary];
|
|
184
|
-
});
|
|
185
|
-
}
|
|
186
156
|
planRef.current = undefined;
|
|
187
157
|
tasksRef.current = [];
|
|
188
158
|
setTasks([]);
|
|
189
|
-
setActivePlanInfo(null);
|
|
190
159
|
}
|
|
191
160
|
}, 2000);
|
|
192
161
|
return () => {
|
|
@@ -325,6 +294,5 @@ export function App({ explorBot, initialShowInput = false, exitOnEmptyInput = fa
|
|
|
325
294
|
React.createElement(StateTransitionPane, { currentState: currentState }))),
|
|
326
295
|
tasks.length > 0 && (React.createElement(Box, { width: currentState ? '50%' : '100%' },
|
|
327
296
|
React.createElement(TaskPane, { tasks: tasks, scrollOffset: taskScrollOffset }))),
|
|
328
|
-
React.createElement(Autocomplete, null))
|
|
329
|
-
React.createElement(PlanPane, { completedPlans: completedPlans, activePlan: activePlanInfo })));
|
|
297
|
+
React.createElement(Autocomplete, null))));
|
|
330
298
|
}
|
|
@@ -10,7 +10,6 @@ const LogPane = React.memo(({ verboseMode }) => {
|
|
|
10
10
|
const [logs, setLogs] = useState([]);
|
|
11
11
|
const pendingLogsRef = React.useRef([]);
|
|
12
12
|
const flushTimeoutRef = React.useRef(null);
|
|
13
|
-
const MAX_MULTILINE_LINES = 16;
|
|
14
13
|
const MAX_STEP_LINES = 8;
|
|
15
14
|
const MAX_SUBSTEP_LINES = 6;
|
|
16
15
|
const formatCollapsedContent = useCallback((lines, collapsedCount, label) => {
|
|
@@ -88,6 +87,8 @@ const LogPane = React.memo(({ verboseMode }) => {
|
|
|
88
87
|
return { color: 'yellow' };
|
|
89
88
|
case 'debug':
|
|
90
89
|
return { color: 'gray', dimColor: true };
|
|
90
|
+
case 'operation':
|
|
91
|
+
return { color: 'gray', dimColor: true };
|
|
91
92
|
case 'substep':
|
|
92
93
|
return { color: 'gray', dimColor: true };
|
|
93
94
|
case 'step':
|
|
@@ -116,7 +117,8 @@ const LogPane = React.memo(({ verboseMode }) => {
|
|
|
116
117
|
const cleaned = stripAnsi(dedent(log.content));
|
|
117
118
|
const parsed = parseMarkdownToTerminal(cleaned);
|
|
118
119
|
const lines = parsed.split('\n');
|
|
119
|
-
const
|
|
120
|
+
const maxLines = log.maxLines || 16;
|
|
121
|
+
const truncated = lines.length > maxLines ? `${lines.slice(0, maxLines).join('\n')}\n... (${lines.length - maxLines} more lines)` : parsed;
|
|
120
122
|
return (React.createElement(Box, { key: index, borderStyle: "classic", borderLeft: false, borderRight: false, marginY: 1, padding: 1, borderColor: "dim", overflow: "hidden" },
|
|
121
123
|
React.createElement(Text, { color: "gray", dimColor: true }, truncated)));
|
|
122
124
|
}
|
|
@@ -127,6 +129,7 @@ const LogPane = React.memo(({ verboseMode }) => {
|
|
|
127
129
|
type: 'multiline',
|
|
128
130
|
content: `HTML Content:\n\n${markdown}`,
|
|
129
131
|
timestamp: log.timestamp,
|
|
132
|
+
maxLines: 10,
|
|
130
133
|
};
|
|
131
134
|
return renderLogEntry(multilineLog, index);
|
|
132
135
|
}
|
|
@@ -134,6 +137,9 @@ const LogPane = React.memo(({ verboseMode }) => {
|
|
|
134
137
|
if (log.type === 'substep') {
|
|
135
138
|
return (React.createElement(Box, { key: index, marginLeft: 2, flexDirection: "column" }, lines.map((line, lineIndex) => (React.createElement(Text, { key: `${index}-${lineIndex}`, ...styles }, lineIndex === 0 ? `> ${line}` : ` ${line}`)))));
|
|
136
139
|
}
|
|
140
|
+
if (log.type === 'operation') {
|
|
141
|
+
return (React.createElement(Box, { key: index, marginLeft: 2, flexDirection: "column" }, lines.map((line, lineIndex) => (React.createElement(Text, { key: `${index}-${lineIndex}`, ...styles }, lineIndex === 0 ? `· ${line}` : ` ${line}`)))));
|
|
142
|
+
}
|
|
137
143
|
if (log.type === 'step') {
|
|
138
144
|
return (React.createElement(Box, { key: index, flexDirection: "column", paddingLeft: 2 }, lines.map((line, lineIndex) => (React.createElement(Text, { key: `${index}-${lineIndex}`, ...styles }, line)))));
|
|
139
145
|
}
|
|
@@ -145,7 +151,7 @@ const LogPane = React.memo(({ verboseMode }) => {
|
|
|
145
151
|
icon && React.createElement(Text, { ...styles }, icon),
|
|
146
152
|
React.createElement(Box, { flexDirection: "column" }, lines.map((line, lineIndex) => (React.createElement(Text, { key: `${index}-${lineIndex}`, ...styles }, line))))));
|
|
147
153
|
};
|
|
148
|
-
const maxLogs =
|
|
154
|
+
const maxLogs = 80;
|
|
149
155
|
const visibleLogs = logs.length > maxLogs ? logs.slice(-maxLogs) : logs;
|
|
150
156
|
return React.createElement(Box, { flexDirection: "column" }, visibleLogs.map((log, index) => renderLogEntry(log, index)).filter(Boolean));
|
|
151
157
|
});
|
|
@@ -164,7 +164,7 @@ export class ExperienceTracker {
|
|
|
164
164
|
const newEntry = generateActionContent(title, filteredCode, action.explanation);
|
|
165
165
|
const updatedContent = `${newEntry}\n\n${content}`;
|
|
166
166
|
this.writeExperienceFile(stateHash, updatedContent, data);
|
|
167
|
-
tag('
|
|
167
|
+
tag('operation').log(`Added ACTION to: ${stateHash}.md`);
|
|
168
168
|
}
|
|
169
169
|
writeFlow(state, body, relatedUrls) {
|
|
170
170
|
if (this.disabled || this.isWritingDisabled(state))
|
|
@@ -190,7 +190,7 @@ export class ExperienceTracker {
|
|
|
190
190
|
}
|
|
191
191
|
const updatedContent = `${body}\n${content}`;
|
|
192
192
|
this.writeExperienceFile(stateHash, updatedContent, data);
|
|
193
|
-
tag('
|
|
193
|
+
tag('operation').log(`Added FLOW to: ${stateHash}.md`);
|
|
194
194
|
}
|
|
195
195
|
getAllExperience() {
|
|
196
196
|
const allFiles = [];
|
package/dist/src/explorbot.js
CHANGED
|
@@ -422,7 +422,7 @@ export class ExplorBot {
|
|
|
422
422
|
this.lastReportedTestCount = tests.length;
|
|
423
423
|
return;
|
|
424
424
|
}
|
|
425
|
-
tag('multiline').log(markdown);
|
|
425
|
+
tag('multiline').log(markdown, { maxLines: 22 });
|
|
426
426
|
const filePath = this.agentSessionAnalyst().writeReport(markdown);
|
|
427
427
|
tag('info').log(`Session report saved: ${relativeToCwd(filePath)}`);
|
|
428
428
|
const reporter = this.explorer?.getReporter();
|
package/dist/src/reporter.js
CHANGED
|
@@ -82,10 +82,12 @@ export class Reporter {
|
|
|
82
82
|
return;
|
|
83
83
|
}
|
|
84
84
|
try {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
85
|
+
const result = await withQuietReporterLogs(async () => {
|
|
86
|
+
this.client = new Client({ apiKey: process.env.TESTOMATIO || '', title: this.buildTitle() });
|
|
87
|
+
const timeoutMs = Number(process.env.TESTOMATIO_TIMEOUT_MS || '15000');
|
|
88
|
+
const timeoutPromise = new Promise((resolve) => setTimeout(() => resolve('timeout'), timeoutMs));
|
|
89
|
+
return await Promise.race([this.client.createRun({ configuration: { exploratory: true } }).then(() => 'success'), timeoutPromise]);
|
|
90
|
+
});
|
|
89
91
|
if (result === 'timeout') {
|
|
90
92
|
debugLog('Reporter run creation timed out');
|
|
91
93
|
return;
|
|
@@ -252,7 +254,9 @@ export class Reporter {
|
|
|
252
254
|
return;
|
|
253
255
|
}
|
|
254
256
|
try {
|
|
255
|
-
await
|
|
257
|
+
await withQuietReporterLogs(async () => {
|
|
258
|
+
await this.client.updateRunStatus('finished');
|
|
259
|
+
});
|
|
256
260
|
this.isRunStarted = false;
|
|
257
261
|
debugLog('Testomat.io run finished');
|
|
258
262
|
}
|
|
@@ -299,3 +303,18 @@ export class Reporter {
|
|
|
299
303
|
return;
|
|
300
304
|
}
|
|
301
305
|
}
|
|
306
|
+
async function withQuietReporterLogs(fn) {
|
|
307
|
+
const previousLevel = process.env.TESTOMATIO_LOG_LEVEL;
|
|
308
|
+
process.env.TESTOMATIO_LOG_LEVEL = 'ERROR';
|
|
309
|
+
try {
|
|
310
|
+
return await fn();
|
|
311
|
+
}
|
|
312
|
+
finally {
|
|
313
|
+
if (previousLevel === undefined) {
|
|
314
|
+
process.env.TESTOMATIO_LOG_LEVEL = undefined;
|
|
315
|
+
}
|
|
316
|
+
else {
|
|
317
|
+
process.env.TESTOMATIO_LOG_LEVEL = previousLevel;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export class RecentStepFilter {
|
|
2
|
+
ttlMs;
|
|
3
|
+
recentStepKeys = new Map();
|
|
4
|
+
constructor(ttlMs = 15000) {
|
|
5
|
+
this.ttlMs = ttlMs;
|
|
6
|
+
}
|
|
7
|
+
shouldSuppress(content, now = Date.now()) {
|
|
8
|
+
const key = normalizeStepCommand(content);
|
|
9
|
+
if (!key)
|
|
10
|
+
return false;
|
|
11
|
+
for (const [existingKey, timestamp] of this.recentStepKeys) {
|
|
12
|
+
if (now - timestamp > this.ttlMs) {
|
|
13
|
+
this.recentStepKeys.delete(existingKey);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
if (this.recentStepKeys.has(key))
|
|
17
|
+
return true;
|
|
18
|
+
this.recentStepKeys.set(key, now);
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function normalizeStepCommand(content) {
|
|
23
|
+
const normalized = content.replace(/\s+/g, ' ').trim();
|
|
24
|
+
if (!normalized.startsWith('I.'))
|
|
25
|
+
return null;
|
|
26
|
+
return normalized.toLowerCase();
|
|
27
|
+
}
|
package/dist/src/utils/logger.js
CHANGED
|
@@ -7,6 +7,7 @@ import dedent from 'dedent';
|
|
|
7
7
|
import stripAnsi from 'strip-ansi';
|
|
8
8
|
import { ConfigParser } from '../config.js';
|
|
9
9
|
import { Observability } from "../observability.js";
|
|
10
|
+
import { RecentStepFilter } from "./log-filters.js";
|
|
10
11
|
import { parseMarkdownToTerminal } from "./markdown-terminal.js";
|
|
11
12
|
class DebugFilter {
|
|
12
13
|
patterns = [];
|
|
@@ -51,6 +52,7 @@ const debugFilter = new DebugFilter();
|
|
|
51
52
|
class ConsoleDestination {
|
|
52
53
|
verboseMode = false;
|
|
53
54
|
forceEnabled = false;
|
|
55
|
+
recentSteps = new RecentStepFilter();
|
|
54
56
|
isEnabled() {
|
|
55
57
|
return this.forceEnabled || !process.env.INK_RUNNING;
|
|
56
58
|
}
|
|
@@ -65,6 +67,10 @@ class ConsoleDestination {
|
|
|
65
67
|
return;
|
|
66
68
|
if (entry.type === 'html')
|
|
67
69
|
return;
|
|
70
|
+
if (entry.type === 'operation' && !this.verboseMode)
|
|
71
|
+
return;
|
|
72
|
+
if (entry.type === 'step' && !this.verboseMode && this.recentSteps.shouldSuppress(entry.content))
|
|
73
|
+
return;
|
|
68
74
|
let content = entry.content;
|
|
69
75
|
if (entry.type === 'multiline') {
|
|
70
76
|
const cleaned = stripAnsi(dedent(entry.content));
|
|
@@ -84,6 +90,9 @@ class ConsoleDestination {
|
|
|
84
90
|
else if (entry.type === 'step') {
|
|
85
91
|
content = chalk.gray(` ${content}`);
|
|
86
92
|
}
|
|
93
|
+
else if (entry.type === 'operation') {
|
|
94
|
+
content = chalk.gray(` · ${content}`);
|
|
95
|
+
}
|
|
87
96
|
else if (entry.type === 'substep') {
|
|
88
97
|
content = chalk.gray(` > ${content}`);
|
|
89
98
|
}
|
|
@@ -213,6 +222,8 @@ class ReactDestination {
|
|
|
213
222
|
return this.debugMode;
|
|
214
223
|
}
|
|
215
224
|
shouldWrite(entry) {
|
|
225
|
+
if (entry.type === 'operation' && !this.debugMode)
|
|
226
|
+
return false;
|
|
216
227
|
if (entry.type !== 'debug')
|
|
217
228
|
return true;
|
|
218
229
|
if (this.debugMode)
|
|
@@ -269,7 +280,7 @@ class CaptainDestination {
|
|
|
269
280
|
}
|
|
270
281
|
stopCapture() {
|
|
271
282
|
this.capturing = false;
|
|
272
|
-
const logs = this.entries.filter((e) => e.type !== 'debug' && e.type !== 'html' && e.type !== 'multiline').map((e) => `[${e.type}] ${e.content}`);
|
|
283
|
+
const logs = this.entries.filter((e) => e.type !== 'debug' && e.type !== 'html' && e.type !== 'multiline' && e.type !== 'operation').map((e) => `[${e.type}] ${e.content}`);
|
|
273
284
|
this.entries = [];
|
|
274
285
|
return logs;
|
|
275
286
|
}
|
|
@@ -377,6 +388,7 @@ class Logger {
|
|
|
377
388
|
}
|
|
378
389
|
return;
|
|
379
390
|
}
|
|
391
|
+
const options = this.extractLogOptions(type, args);
|
|
380
392
|
let content = this.processArgs(args);
|
|
381
393
|
if (type === 'step' && args[0]?.toCode) {
|
|
382
394
|
content = args[0].toCode();
|
|
@@ -386,6 +398,7 @@ class Logger {
|
|
|
386
398
|
content,
|
|
387
399
|
timestamp: new Date(),
|
|
388
400
|
originalArgs: args,
|
|
401
|
+
maxLines: options?.maxLines,
|
|
389
402
|
};
|
|
390
403
|
if (this.file.isEnabled())
|
|
391
404
|
this.file.write(entry);
|
|
@@ -421,6 +434,20 @@ class Logger {
|
|
|
421
434
|
multiline(...args) {
|
|
422
435
|
this.log('multiline', ...args);
|
|
423
436
|
}
|
|
437
|
+
extractLogOptions(type, args) {
|
|
438
|
+
if (type !== 'multiline')
|
|
439
|
+
return null;
|
|
440
|
+
const last = args[args.length - 1];
|
|
441
|
+
if (!last || typeof last !== 'object' || Array.isArray(last))
|
|
442
|
+
return null;
|
|
443
|
+
if (!('maxLines' in last))
|
|
444
|
+
return null;
|
|
445
|
+
args.pop();
|
|
446
|
+
const maxLines = Number(last.maxLines);
|
|
447
|
+
if (!Number.isFinite(maxLines) || maxLines <= 0)
|
|
448
|
+
return null;
|
|
449
|
+
return { maxLines };
|
|
450
|
+
}
|
|
424
451
|
}
|
|
425
452
|
const logger = Logger.getInstance();
|
|
426
453
|
let stepSpanParent = null;
|
|
@@ -27,11 +27,5 @@ export function printNextSteps(sections) {
|
|
|
27
27
|
}
|
|
28
28
|
blocks.push(lines.join('\n'));
|
|
29
29
|
}
|
|
30
|
-
|
|
31
|
-
if (i > 0)
|
|
32
|
-
tag('info').log('');
|
|
33
|
-
for (const line of blocks[i].split('\n')) {
|
|
34
|
-
tag('info').log(line);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
30
|
+
tag('multiline').log(blocks.join('\n\n'), { maxLines: 18 });
|
|
37
31
|
}
|
package/package.json
CHANGED
|
@@ -102,7 +102,7 @@ export function WithCodeceptJS<T extends Constructor>(Base: T) {
|
|
|
102
102
|
writeFileSync(filePath, lines.join('\n'));
|
|
103
103
|
this.savedFiles.add(filePath);
|
|
104
104
|
|
|
105
|
-
tag('
|
|
105
|
+
tag('operation').log(`Saved plan tests to: ${relativeToCwd(filePath)}`);
|
|
106
106
|
return filePath;
|
|
107
107
|
}
|
|
108
108
|
|
|
@@ -56,7 +56,7 @@ export function WithExperience<T extends Constructor>(Base: T) {
|
|
|
56
56
|
|
|
57
57
|
await this.stopScreencast();
|
|
58
58
|
|
|
59
|
-
tag('
|
|
59
|
+
tag('operation').log(`Historian saved session for: ${task.description}`);
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
private async reportSession(test: Test, steps: SessionStep[]): Promise<void> {
|
|
@@ -140,7 +140,7 @@ export function WithPlaywright<T extends Constructor>(Base: T) {
|
|
|
140
140
|
writeFileSync(filePath, lines.join('\n'));
|
|
141
141
|
this.savedFiles.add(filePath);
|
|
142
142
|
|
|
143
|
-
tag('
|
|
143
|
+
tag('operation').log(`Saved plan tests to: ${relativeToCwd(filePath)}`);
|
|
144
144
|
return filePath;
|
|
145
145
|
}
|
|
146
146
|
|
|
@@ -92,7 +92,7 @@ export function WithScreencast<T extends Constructor>(Base: T) {
|
|
|
92
92
|
this.screencastTask = test?._explorbotTest || null;
|
|
93
93
|
this.screencastLastChapter = null;
|
|
94
94
|
} catch (err) {
|
|
95
|
-
tag('
|
|
95
|
+
tag('operation').log(`Screencast start failed: ${(err as Error).message}`);
|
|
96
96
|
}
|
|
97
97
|
}
|
|
98
98
|
|
|
@@ -116,7 +116,7 @@ export function WithScreencast<T extends Constructor>(Base: T) {
|
|
|
116
116
|
try {
|
|
117
117
|
await this.screencastPage.screencast.stop();
|
|
118
118
|
} catch (err) {
|
|
119
|
-
tag('
|
|
119
|
+
tag('operation').log(`Screencast stop failed: ${(err as Error).message}`);
|
|
120
120
|
}
|
|
121
121
|
this.screencastActive = false;
|
|
122
122
|
this.screencastPage = null;
|
|
@@ -126,7 +126,7 @@ export function WithScreencast<T extends Constructor>(Base: T) {
|
|
|
126
126
|
if (path) {
|
|
127
127
|
this.savedFiles.add(path);
|
|
128
128
|
task?.addArtifact?.(path);
|
|
129
|
-
tag('
|
|
129
|
+
tag('operation').log(`Saved screencast: ${relativeToCwd(path)}`);
|
|
130
130
|
}
|
|
131
131
|
}
|
|
132
132
|
};
|
package/src/ai/historian.ts
CHANGED
|
@@ -62,6 +62,6 @@ export class Historian extends HistorianBase {
|
|
|
62
62
|
|
|
63
63
|
writeFileSync(filePath, content);
|
|
64
64
|
this.savedFiles.add(filePath);
|
|
65
|
-
tag('
|
|
65
|
+
tag('operation').log(`Updated test file with healed steps: ${relativeToCwd(filePath)}`);
|
|
66
66
|
}
|
|
67
67
|
}
|
package/src/ai/navigator.ts
CHANGED
|
@@ -206,7 +206,7 @@ class Navigator implements Agent {
|
|
|
206
206
|
if (!actionResult.isInsideIframe) {
|
|
207
207
|
const successful = this.experienceTracker.getSuccessfulExperience(actionResult);
|
|
208
208
|
if (successful.length > 0) {
|
|
209
|
-
tag('
|
|
209
|
+
tag('operation').log(`Found ${successful.length} experience ${pluralize(successful.length, 'file')} for: ${actionResult.url}`);
|
|
210
210
|
experience = `<experience>\nPast successful recipes recorded from prior runs for this page. Prefer these solutions first if they match the goal.\n\n${successful.join('\n\n')}\n</experience>`;
|
|
211
211
|
}
|
|
212
212
|
}
|
|
@@ -307,7 +307,7 @@ class Navigator implements Agent {
|
|
|
307
307
|
stop();
|
|
308
308
|
return;
|
|
309
309
|
}
|
|
310
|
-
tag('
|
|
310
|
+
tag('operation').log('Feeding failures back to AI for a new batch...');
|
|
311
311
|
let contextMsg = 'Previous solutions did not work. Analyze the failures and try DIFFERENT strategies (not syntactic variants of the same locator).\n\n';
|
|
312
312
|
if (batchFailures.length > 0) {
|
|
313
313
|
const lines = batchFailures
|
|
@@ -633,7 +633,7 @@ class Navigator implements Agent {
|
|
|
633
633
|
|
|
634
634
|
const cachedVerification = actionResult.getVerification(message);
|
|
635
635
|
if (cachedVerification !== null) {
|
|
636
|
-
tag('
|
|
636
|
+
tag('operation').log(`Reusing cached verification: ${cachedVerification ? 'PASS' : 'FAIL'}`);
|
|
637
637
|
return { verified: cachedVerification, successfulCodes: [], assertionSteps: [], totalAttempted: 0 };
|
|
638
638
|
}
|
|
639
639
|
|
|
@@ -654,7 +654,7 @@ class Navigator implements Agent {
|
|
|
654
654
|
const toc = this.experienceTracker.getExperienceTableOfContents(actionResult);
|
|
655
655
|
if (toc.length > 0) {
|
|
656
656
|
const totalSections = toc.reduce((sum, entry) => sum + entry.sections.length, 0);
|
|
657
|
-
tag('
|
|
657
|
+
tag('operation').log(`Found ${toc.length} experience ${pluralize(toc.length, 'file')} (${totalSections} sections) for: ${actionResult.url}`);
|
|
658
658
|
experience = renderExperienceToc(toc);
|
|
659
659
|
}
|
|
660
660
|
}
|