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 { type Plan, type Test, TestResult } from '../test-plan.js';
|
|
|
7
7
|
import { getCliName } from '../utils/cli-name.ts';
|
|
8
8
|
import { ErrorPageError } from '../utils/error-page.ts';
|
|
9
9
|
import { tag } from '../utils/logger.js';
|
|
10
|
-
import { jsonToTable } from '../utils/markdown-parser.js';
|
|
11
10
|
import { type NextStepSection, printNextSteps, relativeToCwd } from '../utils/next-steps.ts';
|
|
12
11
|
import { safeFilename } from '../utils/strings.ts';
|
|
13
12
|
import { BaseCommand, type Suggestion } from './base-command.js';
|
|
@@ -88,14 +87,14 @@ export class ExploreCommand extends BaseCommand {
|
|
|
88
87
|
const t = tests[i];
|
|
89
88
|
lines.push(` ${String(i + 1).padStart(2)}. [${this.originLabel(t)}] [${t.priority.padEnd(9)}] ${t.scenario}`);
|
|
90
89
|
}
|
|
91
|
-
tag('multiline').log(lines.join('\n'));
|
|
90
|
+
tag('multiline').log(lines.join('\n'), { maxLines: 24 });
|
|
92
91
|
}
|
|
93
92
|
|
|
94
93
|
private async runFreshMode(mainUrl: string | undefined, feature: string | undefined, styles?: string[]): Promise<void> {
|
|
95
94
|
await this.runAllStyles(mainUrl, feature, undefined, undefined, styles);
|
|
95
|
+
this.rememberCurrentPlan();
|
|
96
96
|
const mainPlan = this.explorBot.getCurrentPlan();
|
|
97
97
|
if (!mainPlan) return;
|
|
98
|
-
this.completedPlans.push(mainPlan);
|
|
99
98
|
|
|
100
99
|
if (feature || this.isLimitReached()) return;
|
|
101
100
|
|
|
@@ -270,6 +269,7 @@ export class ExploreCommand extends BaseCommand {
|
|
|
270
269
|
const styleList = styles ?? Object.keys(getStyles());
|
|
271
270
|
let fresh = true;
|
|
272
271
|
for (const style of styleList) {
|
|
272
|
+
if (this.isLimitReached()) break;
|
|
273
273
|
if (!fresh && pageUrl && !this.dryRun) {
|
|
274
274
|
await this.explorBot.visit(pageUrl);
|
|
275
275
|
}
|
|
@@ -278,10 +278,19 @@ export class ExploreCommand extends BaseCommand {
|
|
|
278
278
|
if (this.dryRun) opts.noSave = true;
|
|
279
279
|
await this.planWithRetry(feature, opts, pageUrl);
|
|
280
280
|
await this.runPendingTests();
|
|
281
|
+
this.rememberCurrentPlan();
|
|
281
282
|
fresh = false;
|
|
282
283
|
}
|
|
283
284
|
}
|
|
284
285
|
|
|
286
|
+
private rememberCurrentPlan(): void {
|
|
287
|
+
const plan = this.explorBot.getCurrentPlan();
|
|
288
|
+
if (!plan) return;
|
|
289
|
+
if (this.completedPlans.includes(plan)) return;
|
|
290
|
+
if (plan.tests.every((test) => test.startTime == null)) return;
|
|
291
|
+
this.completedPlans.push(plan);
|
|
292
|
+
}
|
|
293
|
+
|
|
285
294
|
private async planWithRetry(feature: string | undefined, opts: { fresh: boolean; style: string; extend?: Plan; completedPlans?: Plan[]; noSave?: boolean }, pageUrl?: string): Promise<void> {
|
|
286
295
|
const before = new Set(this.explorBot.getCurrentPlan()?.tests ?? []);
|
|
287
296
|
|
|
@@ -401,34 +410,63 @@ export class ExploreCommand extends BaseCommand {
|
|
|
401
410
|
|
|
402
411
|
if (allTests.length === 0) return;
|
|
403
412
|
|
|
404
|
-
const hasSubPages = this.completedPlans.
|
|
413
|
+
const hasSubPages = new Set(this.completedPlans.map((plan) => plan.title)).size > 1;
|
|
405
414
|
const hasOrigin = this.oldTestRefs.size > 0;
|
|
406
|
-
const
|
|
415
|
+
const completed = allTests.map(({ test, planTitle }, index) => {
|
|
407
416
|
const durationMs = test.getDurationMs();
|
|
408
417
|
const duration = durationMs != null ? `${(durationMs / 1000).toFixed(1)}s` : '-';
|
|
409
418
|
let status = 'failed';
|
|
410
419
|
if (test.isSuccessful) status = 'passed';
|
|
411
420
|
else if (test.isSkipped) status = 'skipped';
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
421
|
+
return {
|
|
422
|
+
index: index + 1,
|
|
423
|
+
status,
|
|
424
|
+
title: test.scenario.replace(/\s+/g, ' ').trim(),
|
|
425
|
+
priority: test.priority,
|
|
426
|
+
duration,
|
|
427
|
+
durationMs: durationMs ?? 0,
|
|
428
|
+
steps: Object.keys(test.notes).length,
|
|
429
|
+
origin: hasOrigin ? this.originLabel(test) : '',
|
|
430
|
+
planTitle: hasSubPages ? planTitle : '',
|
|
419
431
|
};
|
|
420
|
-
|
|
421
|
-
|
|
432
|
+
});
|
|
433
|
+
const passed = completed.filter((t) => t.status === 'passed').length;
|
|
434
|
+
const failed = completed.filter((t) => t.status === 'failed').length;
|
|
435
|
+
const skipped = completed.filter((t) => t.status === 'skipped').length;
|
|
436
|
+
const totalSeconds = completed.reduce((sum, t) => sum + t.durationMs, 0) / 1000;
|
|
437
|
+
const lines = [`Results: ${passed} passed, ${failed} failed, ${skipped} skipped - ${formatDuration(totalSeconds)}`];
|
|
438
|
+
|
|
439
|
+
const failedTests = completed.filter((t) => t.status === 'failed');
|
|
440
|
+
if (failedTests.length > 0) {
|
|
441
|
+
lines.push('', 'Failed tests:');
|
|
442
|
+
for (const test of failedTests) {
|
|
443
|
+
lines.push(` #${test.index} [${test.priority}] ${test.title} (${test.duration}, ${test.steps} steps)`);
|
|
422
444
|
}
|
|
423
|
-
|
|
424
|
-
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const slowTests = completed
|
|
448
|
+
.filter((t) => t.durationMs >= 1000)
|
|
449
|
+
.sort((a, b) => b.durationMs - a.durationMs)
|
|
450
|
+
.slice(0, 3);
|
|
451
|
+
if (slowTests.length > 0) {
|
|
452
|
+
lines.push('', 'Slowest tests:');
|
|
453
|
+
for (const test of slowTests) {
|
|
454
|
+
lines.push(` #${test.index} ${test.duration} - ${test.title}`);
|
|
425
455
|
}
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
const
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const detailLines = completed
|
|
459
|
+
.map((test) => {
|
|
460
|
+
const details = [test.origin, test.planTitle].filter(Boolean).join(' - ');
|
|
461
|
+
return details ? ` #${test.index} ${details}` : '';
|
|
462
|
+
})
|
|
463
|
+
.filter(Boolean);
|
|
464
|
+
if (detailLines.length > 0) {
|
|
465
|
+
lines.push('', 'Details:');
|
|
466
|
+
lines.push(...detailLines);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
tag('multiline').log(lines.join('\n'));
|
|
432
470
|
tag('info').log(`${figureSet.tick} ${allTests.length} tests completed`);
|
|
433
471
|
}
|
|
434
472
|
|
|
@@ -463,8 +501,8 @@ export class ExploreCommand extends BaseCommand {
|
|
|
463
501
|
}
|
|
464
502
|
|
|
465
503
|
if (screencasts.length > 0) {
|
|
466
|
-
const commands = screencasts.map((f) => ({ label: '', command: relativeToCwd(f) }));
|
|
467
504
|
const screencastDir = relativeToCwd(outputPath('screencasts'));
|
|
505
|
+
const commands = [{ label: 'Folder', command: screencastDir }];
|
|
468
506
|
const planSlugs = [...new Set(this.completedPlans.map((p) => safeFilename(p.title)).filter(Boolean))];
|
|
469
507
|
for (const slug of planSlugs) {
|
|
470
508
|
commands.push({ label: 'Browse plan', command: `ls ${screencastDir}/${slug}-*` });
|
|
@@ -529,3 +567,10 @@ function parseRatio(s: string): number | null {
|
|
|
529
567
|
if (Number.isNaN(n) || n < 0 || n > 1) return null;
|
|
530
568
|
return n;
|
|
531
569
|
}
|
|
570
|
+
|
|
571
|
+
function formatDuration(seconds: number): string {
|
|
572
|
+
if (seconds < 60) return `${seconds.toFixed(1)}s`;
|
|
573
|
+
const minutes = Math.floor(seconds / 60);
|
|
574
|
+
const remainingSeconds = Math.round(seconds % 60);
|
|
575
|
+
return `${minutes}m ${remainingSeconds}s`;
|
|
576
|
+
}
|
|
@@ -69,7 +69,8 @@ export class TestCommand extends BaseCommand {
|
|
|
69
69
|
|
|
70
70
|
tag('info').log(`Launching ${toExecute.length} test scenario(s).`);
|
|
71
71
|
const tester = this.explorBot.agentTester();
|
|
72
|
-
for (const test of toExecute) {
|
|
72
|
+
for (const [index, test] of toExecute.entries()) {
|
|
73
|
+
tag('info').log(`Starting test ${index + 1}/${toExecute.length}: ${test.scenario}`);
|
|
73
74
|
await tester.test(test);
|
|
74
75
|
}
|
|
75
76
|
tag('success').log('Test execution finished');
|
package/src/components/App.tsx
CHANGED
|
@@ -14,7 +14,6 @@ import InputPane from './InputPane.js';
|
|
|
14
14
|
import InputReadline from './InputReadline.js';
|
|
15
15
|
import LogPane from './LogPane.js';
|
|
16
16
|
import PlanEditor from './PlanEditor.js';
|
|
17
|
-
import PlanPane, { type PlanSummary } from './PlanPane.js';
|
|
18
17
|
import SessionTimer from './SessionTimer.js';
|
|
19
18
|
import StateTransitionPane from './StateTransitionPane.js';
|
|
20
19
|
import TaskPane, { WINDOW_SIZE } from './TaskPane.js';
|
|
@@ -158,42 +157,20 @@ export function App({ explorBot, initialShowInput = false, exitOnEmptyInput = fa
|
|
|
158
157
|
|
|
159
158
|
const planRef = useRef<ReturnType<typeof explorBot.getCurrentPlan>>(undefined);
|
|
160
159
|
const unsubscribeRef = useRef<(() => void) | undefined>(undefined);
|
|
161
|
-
const [completedPlans, setCompletedPlans] = useState<PlanSummary[]>([]);
|
|
162
|
-
const [activePlanInfo, setActivePlanInfo] = useState<PlanSummary | null>(null);
|
|
163
160
|
|
|
164
161
|
useEffect(() => {
|
|
165
|
-
const makeSummary = (plan: NonNullable<ReturnType<typeof explorBot.getCurrentPlan>>): PlanSummary => {
|
|
166
|
-
const enabled = plan.tests.filter((t) => t.enabled);
|
|
167
|
-
return {
|
|
168
|
-
title: plan.title,
|
|
169
|
-
testCount: enabled.length,
|
|
170
|
-
passed: enabled.filter((t) => t.isSuccessful).length,
|
|
171
|
-
failed: enabled.filter((t) => t.hasFailed).length,
|
|
172
|
-
};
|
|
173
|
-
};
|
|
174
|
-
|
|
175
162
|
const subscribeToPlan = (plan: NonNullable<ReturnType<typeof explorBot.getCurrentPlan>>) => {
|
|
176
163
|
if (unsubscribeRef.current) unsubscribeRef.current();
|
|
177
164
|
|
|
178
|
-
if (planRef.current && planRef.current !== plan && planRef.current.tests.length > 0) {
|
|
179
|
-
const summary = makeSummary(planRef.current);
|
|
180
|
-
setCompletedPlans((prev) => {
|
|
181
|
-
if (prev.some((p) => p.title === summary.title)) return prev;
|
|
182
|
-
return [...prev, summary];
|
|
183
|
-
});
|
|
184
|
-
}
|
|
185
|
-
|
|
186
165
|
planRef.current = plan;
|
|
187
166
|
tasksRef.current = [...plan.tests];
|
|
188
167
|
setTasks(tasksRef.current);
|
|
189
168
|
setTaskScrollOffset(0);
|
|
190
|
-
setActivePlanInfo(makeSummary(plan));
|
|
191
169
|
|
|
192
170
|
let lastInProgressIdx = -1;
|
|
193
171
|
unsubscribeRef.current = plan.onTestsChange((updatedTests) => {
|
|
194
172
|
tasksRef.current = [...updatedTests];
|
|
195
173
|
setTasks(tasksRef.current);
|
|
196
|
-
setActivePlanInfo(makeSummary(plan));
|
|
197
174
|
const inProgressIdx = updatedTests.findIndex((t) => t.status === 'in_progress' && t.enabled);
|
|
198
175
|
if (inProgressIdx >= 0 && inProgressIdx !== lastInProgressIdx) {
|
|
199
176
|
lastInProgressIdx = inProgressIdx;
|
|
@@ -211,17 +188,9 @@ export function App({ explorBot, initialShowInput = false, exitOnEmptyInput = fa
|
|
|
211
188
|
subscribeToPlan(currentPlan);
|
|
212
189
|
} else if (!currentPlan && planRef.current) {
|
|
213
190
|
if (unsubscribeRef.current) unsubscribeRef.current();
|
|
214
|
-
if (planRef.current.tests.length > 0) {
|
|
215
|
-
const summary = makeSummary(planRef.current);
|
|
216
|
-
setCompletedPlans((prev) => {
|
|
217
|
-
if (prev.some((p) => p.title === summary.title)) return prev;
|
|
218
|
-
return [...prev, summary];
|
|
219
|
-
});
|
|
220
|
-
}
|
|
221
191
|
planRef.current = undefined;
|
|
222
192
|
tasksRef.current = [];
|
|
223
193
|
setTasks([]);
|
|
224
|
-
setActivePlanInfo(null);
|
|
225
194
|
}
|
|
226
195
|
}, 2000);
|
|
227
196
|
|
|
@@ -389,8 +358,6 @@ export function App({ explorBot, initialShowInput = false, exitOnEmptyInput = fa
|
|
|
389
358
|
)}
|
|
390
359
|
<Autocomplete />
|
|
391
360
|
</Box>
|
|
392
|
-
|
|
393
|
-
<PlanPane completedPlans={completedPlans} activePlan={activePlanInfo} />
|
|
394
361
|
</Box>
|
|
395
362
|
);
|
|
396
363
|
}
|
|
@@ -22,7 +22,6 @@ const LogPane: React.FC<LogPaneProps> = React.memo(({ verboseMode }) => {
|
|
|
22
22
|
const pendingLogsRef = React.useRef<LogEntry[]>([]);
|
|
23
23
|
const flushTimeoutRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
24
24
|
|
|
25
|
-
const MAX_MULTILINE_LINES = 16;
|
|
26
25
|
const MAX_STEP_LINES = 8;
|
|
27
26
|
const MAX_SUBSTEP_LINES = 6;
|
|
28
27
|
|
|
@@ -115,6 +114,8 @@ const LogPane: React.FC<LogPaneProps> = React.memo(({ verboseMode }) => {
|
|
|
115
114
|
return { color: 'yellow' as const };
|
|
116
115
|
case 'debug':
|
|
117
116
|
return { color: 'gray' as const, dimColor: true };
|
|
117
|
+
case 'operation':
|
|
118
|
+
return { color: 'gray' as const, dimColor: true };
|
|
118
119
|
case 'substep':
|
|
119
120
|
return { color: 'gray' as const, dimColor: true };
|
|
120
121
|
case 'step':
|
|
@@ -146,7 +147,8 @@ const LogPane: React.FC<LogPaneProps> = React.memo(({ verboseMode }) => {
|
|
|
146
147
|
const cleaned = stripAnsi(dedent(log.content));
|
|
147
148
|
const parsed = parseMarkdownToTerminal(cleaned);
|
|
148
149
|
const lines = parsed.split('\n');
|
|
149
|
-
const
|
|
150
|
+
const maxLines = log.maxLines || 16;
|
|
151
|
+
const truncated = lines.length > maxLines ? `${lines.slice(0, maxLines).join('\n')}\n... (${lines.length - maxLines} more lines)` : parsed;
|
|
150
152
|
return (
|
|
151
153
|
<Box key={index} borderStyle="classic" borderLeft={false} borderRight={false} marginY={1} padding={1} borderColor="dim" overflow="hidden">
|
|
152
154
|
<Text color="gray" dimColor>
|
|
@@ -163,6 +165,7 @@ const LogPane: React.FC<LogPaneProps> = React.memo(({ verboseMode }) => {
|
|
|
163
165
|
type: 'multiline',
|
|
164
166
|
content: `HTML Content:\n\n${markdown}`,
|
|
165
167
|
timestamp: log.timestamp,
|
|
168
|
+
maxLines: 10,
|
|
166
169
|
};
|
|
167
170
|
|
|
168
171
|
return renderLogEntry(multilineLog, index);
|
|
@@ -182,6 +185,18 @@ const LogPane: React.FC<LogPaneProps> = React.memo(({ verboseMode }) => {
|
|
|
182
185
|
);
|
|
183
186
|
}
|
|
184
187
|
|
|
188
|
+
if (log.type === 'operation') {
|
|
189
|
+
return (
|
|
190
|
+
<Box key={index} marginLeft={2} flexDirection="column">
|
|
191
|
+
{lines.map((line, lineIndex) => (
|
|
192
|
+
<Text key={`${index}-${lineIndex}`} {...styles}>
|
|
193
|
+
{lineIndex === 0 ? `· ${line}` : ` ${line}`}
|
|
194
|
+
</Text>
|
|
195
|
+
))}
|
|
196
|
+
</Box>
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
185
200
|
if (log.type === 'step') {
|
|
186
201
|
return (
|
|
187
202
|
<Box key={index} flexDirection="column" paddingLeft={2}>
|
|
@@ -212,7 +227,7 @@ const LogPane: React.FC<LogPaneProps> = React.memo(({ verboseMode }) => {
|
|
|
212
227
|
);
|
|
213
228
|
};
|
|
214
229
|
|
|
215
|
-
const maxLogs =
|
|
230
|
+
const maxLogs = 80;
|
|
216
231
|
const visibleLogs = logs.length > maxLogs ? logs.slice(-maxLogs) : logs;
|
|
217
232
|
return <Box flexDirection="column">{visibleLogs.map((log, index) => renderLogEntry(log, index)).filter(Boolean)}</Box>;
|
|
218
233
|
});
|
|
@@ -188,7 +188,7 @@ export class ExperienceTracker {
|
|
|
188
188
|
const updatedContent = `${newEntry}\n\n${content}`;
|
|
189
189
|
this.writeExperienceFile(stateHash, updatedContent, data);
|
|
190
190
|
|
|
191
|
-
tag('
|
|
191
|
+
tag('operation').log(`Added ACTION to: ${stateHash}.md`);
|
|
192
192
|
}
|
|
193
193
|
|
|
194
194
|
writeFlow(state: ActionResult, body: string, relatedUrls?: string[]): void {
|
|
@@ -218,7 +218,7 @@ export class ExperienceTracker {
|
|
|
218
218
|
const updatedContent = `${body}\n${content}`;
|
|
219
219
|
this.writeExperienceFile(stateHash, updatedContent, data);
|
|
220
220
|
|
|
221
|
-
tag('
|
|
221
|
+
tag('operation').log(`Added FLOW to: ${stateHash}.md`);
|
|
222
222
|
}
|
|
223
223
|
|
|
224
224
|
getAllExperience(): ExperienceFile[] {
|
package/src/explorbot.ts
CHANGED
|
@@ -487,7 +487,7 @@ export class ExplorBot {
|
|
|
487
487
|
return;
|
|
488
488
|
}
|
|
489
489
|
|
|
490
|
-
tag('multiline').log(markdown);
|
|
490
|
+
tag('multiline').log(markdown, { maxLines: 22 });
|
|
491
491
|
|
|
492
492
|
const filePath = this.agentSessionAnalyst().writeReport(markdown);
|
|
493
493
|
tag('info').log(`Session report saved: ${relativeToCwd(filePath)}`);
|
package/src/reporter.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
2
|
import { Client } from '@testomatio/reporter';
|
|
3
3
|
import type { Step } from '@testomatio/reporter/types/types.js';
|
|
4
|
-
import {
|
|
4
|
+
import { outputPath } from './config.js';
|
|
5
5
|
import type { ReporterConfig } from './config.js';
|
|
6
6
|
import type { StateManager } from './state-manager.js';
|
|
7
7
|
import { Stats } from './stats.js';
|
|
@@ -103,11 +103,13 @@ export class Reporter {
|
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
try {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
106
|
+
const result = await withQuietReporterLogs(async () => {
|
|
107
|
+
this.client = new Client({ apiKey: process.env.TESTOMATIO || '', title: this.buildTitle() });
|
|
108
|
+
const timeoutMs = Number(process.env.TESTOMATIO_TIMEOUT_MS || '15000');
|
|
109
|
+
const timeoutPromise = new Promise<'timeout'>((resolve) => setTimeout(() => resolve('timeout'), timeoutMs));
|
|
109
110
|
|
|
110
|
-
|
|
111
|
+
return await Promise.race([this.client.createRun({ configuration: { exploratory: true } }).then(() => 'success' as const), timeoutPromise]);
|
|
112
|
+
});
|
|
111
113
|
|
|
112
114
|
if (result === 'timeout') {
|
|
113
115
|
debugLog('Reporter run creation timed out');
|
|
@@ -294,7 +296,9 @@ export class Reporter {
|
|
|
294
296
|
}
|
|
295
297
|
|
|
296
298
|
try {
|
|
297
|
-
await
|
|
299
|
+
await withQuietReporterLogs(async () => {
|
|
300
|
+
await this.client.updateRunStatus('finished');
|
|
301
|
+
});
|
|
298
302
|
this.isRunStarted = false;
|
|
299
303
|
debugLog('Testomat.io run finished');
|
|
300
304
|
} catch (error) {
|
|
@@ -340,3 +344,17 @@ export class Reporter {
|
|
|
340
344
|
return;
|
|
341
345
|
}
|
|
342
346
|
}
|
|
347
|
+
|
|
348
|
+
async function withQuietReporterLogs<T>(fn: () => Promise<T>): Promise<T> {
|
|
349
|
+
const previousLevel = process.env.TESTOMATIO_LOG_LEVEL;
|
|
350
|
+
process.env.TESTOMATIO_LOG_LEVEL = 'ERROR';
|
|
351
|
+
try {
|
|
352
|
+
return await fn();
|
|
353
|
+
} finally {
|
|
354
|
+
if (previousLevel === undefined) {
|
|
355
|
+
process.env.TESTOMATIO_LOG_LEVEL = undefined;
|
|
356
|
+
} else {
|
|
357
|
+
process.env.TESTOMATIO_LOG_LEVEL = previousLevel;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export class RecentStepFilter {
|
|
2
|
+
private recentStepKeys = new Map<string, number>();
|
|
3
|
+
|
|
4
|
+
constructor(private ttlMs = 15000) {}
|
|
5
|
+
|
|
6
|
+
shouldSuppress(content: string, now = Date.now()): boolean {
|
|
7
|
+
const key = normalizeStepCommand(content);
|
|
8
|
+
if (!key) return false;
|
|
9
|
+
|
|
10
|
+
for (const [existingKey, timestamp] of this.recentStepKeys) {
|
|
11
|
+
if (now - timestamp > this.ttlMs) {
|
|
12
|
+
this.recentStepKeys.delete(existingKey);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (this.recentStepKeys.has(key)) return true;
|
|
17
|
+
this.recentStepKeys.set(key, now);
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function normalizeStepCommand(content: string): string | null {
|
|
23
|
+
const normalized = content.replace(/\s+/g, ' ').trim();
|
|
24
|
+
if (!normalized.startsWith('I.')) return null;
|
|
25
|
+
return normalized.toLowerCase();
|
|
26
|
+
}
|
package/src/utils/logger.ts
CHANGED
|
@@ -8,9 +8,10 @@ import { marked } from 'marked';
|
|
|
8
8
|
import stripAnsi from 'strip-ansi';
|
|
9
9
|
import { ConfigParser } from '../config.js';
|
|
10
10
|
import { Observability } from '../observability.ts';
|
|
11
|
+
import { RecentStepFilter } from './log-filters.ts';
|
|
11
12
|
import { parseMarkdownToTerminal } from './markdown-terminal.ts';
|
|
12
13
|
|
|
13
|
-
export type LogType = 'info' | 'success' | 'error' | 'warning' | 'debug' | 'substep' | 'step' | 'multiline' | 'html' | 'input';
|
|
14
|
+
export type LogType = 'info' | 'success' | 'error' | 'warning' | 'debug' | 'substep' | 'operation' | 'step' | 'multiline' | 'html' | 'input';
|
|
14
15
|
|
|
15
16
|
export interface TaggedLogEntry {
|
|
16
17
|
type: LogType;
|
|
@@ -18,6 +19,7 @@ export interface TaggedLogEntry {
|
|
|
18
19
|
timestamp?: Date;
|
|
19
20
|
originalArgs?: any[];
|
|
20
21
|
namespace?: string;
|
|
22
|
+
maxLines?: number;
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
type LogEntry = TaggedLogEntry;
|
|
@@ -77,6 +79,7 @@ const debugFilter = new DebugFilter();
|
|
|
77
79
|
class ConsoleDestination implements LogDestination {
|
|
78
80
|
private verboseMode = false;
|
|
79
81
|
private forceEnabled = false;
|
|
82
|
+
private recentSteps = new RecentStepFilter();
|
|
80
83
|
|
|
81
84
|
isEnabled(): boolean {
|
|
82
85
|
return this.forceEnabled || !process.env.INK_RUNNING;
|
|
@@ -93,6 +96,8 @@ class ConsoleDestination implements LogDestination {
|
|
|
93
96
|
write(entry: TaggedLogEntry): void {
|
|
94
97
|
if (entry.type === 'debug') return;
|
|
95
98
|
if (entry.type === 'html') return;
|
|
99
|
+
if (entry.type === 'operation' && !this.verboseMode) return;
|
|
100
|
+
if (entry.type === 'step' && !this.verboseMode && this.recentSteps.shouldSuppress(entry.content)) return;
|
|
96
101
|
let content = entry.content;
|
|
97
102
|
if (entry.type === 'multiline') {
|
|
98
103
|
const cleaned = stripAnsi(dedent(entry.content));
|
|
@@ -107,6 +112,8 @@ class ConsoleDestination implements LogDestination {
|
|
|
107
112
|
content = chalk.yellow(content);
|
|
108
113
|
} else if (entry.type === 'step') {
|
|
109
114
|
content = chalk.gray(` ${content}`);
|
|
115
|
+
} else if (entry.type === 'operation') {
|
|
116
|
+
content = chalk.gray(` · ${content}`);
|
|
110
117
|
} else if (entry.type === 'substep') {
|
|
111
118
|
content = chalk.gray(` > ${content}`);
|
|
112
119
|
}
|
|
@@ -247,6 +254,7 @@ class ReactDestination implements LogDestination {
|
|
|
247
254
|
}
|
|
248
255
|
|
|
249
256
|
private shouldWrite(entry: TaggedLogEntry): boolean {
|
|
257
|
+
if (entry.type === 'operation' && !this.debugMode) return false;
|
|
250
258
|
if (entry.type !== 'debug') return true;
|
|
251
259
|
if (this.debugMode) return true;
|
|
252
260
|
if (!entry.namespace) return true;
|
|
@@ -306,7 +314,7 @@ class CaptainDestination implements LogDestination {
|
|
|
306
314
|
|
|
307
315
|
stopCapture(): string[] {
|
|
308
316
|
this.capturing = false;
|
|
309
|
-
const logs = this.entries.filter((e) => e.type !== 'debug' && e.type !== 'html' && e.type !== 'multiline').map((e) => `[${e.type}] ${e.content}`);
|
|
317
|
+
const logs = this.entries.filter((e) => e.type !== 'debug' && e.type !== 'html' && e.type !== 'multiline' && e.type !== 'operation').map((e) => `[${e.type}] ${e.content}`);
|
|
310
318
|
this.entries = [];
|
|
311
319
|
return logs;
|
|
312
320
|
}
|
|
@@ -432,6 +440,7 @@ class Logger {
|
|
|
432
440
|
return;
|
|
433
441
|
}
|
|
434
442
|
|
|
443
|
+
const options = this.extractLogOptions(type, args);
|
|
435
444
|
let content = this.processArgs(args);
|
|
436
445
|
if (type === 'step' && args[0]?.toCode) {
|
|
437
446
|
content = args[0].toCode();
|
|
@@ -441,6 +450,7 @@ class Logger {
|
|
|
441
450
|
content,
|
|
442
451
|
timestamp: new Date(),
|
|
443
452
|
originalArgs: args,
|
|
453
|
+
maxLines: options?.maxLines,
|
|
444
454
|
};
|
|
445
455
|
|
|
446
456
|
if (this.file.isEnabled()) this.file.write(entry);
|
|
@@ -480,6 +490,18 @@ class Logger {
|
|
|
480
490
|
multiline(...args: any[]): void {
|
|
481
491
|
this.log('multiline', ...args);
|
|
482
492
|
}
|
|
493
|
+
|
|
494
|
+
private extractLogOptions(type: LogType, args: any[]): { maxLines?: number } | null {
|
|
495
|
+
if (type !== 'multiline') return null;
|
|
496
|
+
const last = args[args.length - 1];
|
|
497
|
+
if (!last || typeof last !== 'object' || Array.isArray(last)) return null;
|
|
498
|
+
if (!('maxLines' in last)) return null;
|
|
499
|
+
|
|
500
|
+
args.pop();
|
|
501
|
+
const maxLines = Number(last.maxLines);
|
|
502
|
+
if (!Number.isFinite(maxLines) || maxLines <= 0) return null;
|
|
503
|
+
return { maxLines };
|
|
504
|
+
}
|
|
483
505
|
}
|
|
484
506
|
|
|
485
507
|
const logger = Logger.getInstance();
|
package/src/utils/next-steps.ts
CHANGED
|
@@ -42,10 +42,5 @@ export function printNextSteps(sections: NextStepSection[]): void {
|
|
|
42
42
|
blocks.push(lines.join('\n'));
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
if (i > 0) tag('info').log('');
|
|
47
|
-
for (const line of blocks[i].split('\n')) {
|
|
48
|
-
tag('info').log(line);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
45
|
+
tag('multiline').log(blocks.join('\n\n'), { maxLines: 18 });
|
|
51
46
|
}
|