@e2edev/e2e 0.6.0 → 0.7.0
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 +7 -8
- package/dist/agent/act-validation.d.ts +4 -1
- package/dist/agent/act-validation.d.ts.map +1 -1
- package/dist/agent/act-validation.js +7 -3
- package/dist/agent/act-validation.js.map +1 -1
- package/dist/agent/act.d.ts.map +1 -1
- package/dist/agent/act.js +16 -2
- package/dist/agent/act.js.map +1 -1
- package/dist/agent/actions.d.ts +14 -14
- package/dist/agent/actions.d.ts.map +1 -1
- package/dist/agent/actions.js +12 -2
- package/dist/agent/actions.js.map +1 -1
- package/dist/agent/executor.d.ts +7 -0
- package/dist/agent/executor.d.ts.map +1 -1
- package/dist/agent/executor.js.map +1 -1
- package/dist/agent/invocation.d.ts.map +1 -1
- package/dist/agent/invocation.js +2 -1
- package/dist/agent/invocation.js.map +1 -1
- package/dist/agent/model/sdk.js +1 -1
- package/dist/agent/model/sdk.js.map +1 -1
- package/dist/agent/step-cache.d.ts +8 -8
- package/dist/agent/step-cache.d.ts.map +1 -1
- package/dist/agent/step-cache.js +14 -15
- package/dist/agent/step-cache.js.map +1 -1
- package/dist/agent/tool-loop.d.ts.map +1 -1
- package/dist/agent/tool-loop.js +1 -0
- package/dist/agent/tool-loop.js.map +1 -1
- package/dist/agent/usage.d.ts +6 -1
- package/dist/agent/usage.d.ts.map +1 -1
- package/dist/agent/usage.js +12 -0
- package/dist/agent/usage.js.map +1 -1
- package/dist/cache/anchors.d.ts.map +1 -1
- package/dist/cache/anchors.js +21 -1
- package/dist/cache/anchors.js.map +1 -1
- package/dist/cache/decide.d.ts +9 -0
- package/dist/cache/decide.d.ts.map +1 -1
- package/dist/cache/decide.js +35 -0
- package/dist/cache/decide.js.map +1 -1
- package/dist/cache/relocate.d.ts +11 -1
- package/dist/cache/relocate.d.ts.map +1 -1
- package/dist/cache/relocate.js +48 -17
- package/dist/cache/relocate.js.map +1 -1
- package/dist/cache/store.d.ts.map +1 -1
- package/dist/cache/store.js +3 -14
- package/dist/cache/store.js.map +1 -1
- package/dist/cache/trace.d.ts +12 -0
- package/dist/cache/trace.d.ts.map +1 -1
- package/dist/cache/trace.js +21 -0
- package/dist/cache/trace.js.map +1 -1
- package/dist/cli/docs-url.d.ts +1 -1
- package/dist/cli/docs-url.d.ts.map +1 -1
- package/dist/cli/docs-url.js +1 -1
- package/dist/cli/docs-url.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +58 -9
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/init/scaffold.d.ts.map +1 -1
- package/dist/cli/init/scaffold.js +3 -0
- package/dist/cli/init/scaffold.js.map +1 -1
- package/dist/cli/telemetry.d.ts +12 -0
- package/dist/cli/telemetry.d.ts.map +1 -0
- package/dist/cli/telemetry.js +40 -0
- package/dist/cli/telemetry.js.map +1 -0
- package/dist/collect/select.d.ts +2 -0
- package/dist/collect/select.d.ts.map +1 -1
- package/dist/collect/select.js +15 -11
- package/dist/collect/select.js.map +1 -1
- package/dist/config/agent.d.ts.map +1 -1
- package/dist/config/agent.js +41 -6
- package/dist/config/agent.js.map +1 -1
- package/dist/config/diagnose.d.ts.map +1 -1
- package/dist/config/diagnose.js +1 -0
- package/dist/config/diagnose.js.map +1 -1
- package/dist/config/resolve.d.ts.map +1 -1
- package/dist/config/resolve.js +2 -5
- package/dist/config/resolve.js.map +1 -1
- package/dist/engine/index.d.ts +5 -2
- package/dist/engine/index.d.ts.map +1 -1
- package/dist/engine/index.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/internal/ai-trace.d.ts +1 -5
- package/dist/internal/ai-trace.d.ts.map +1 -1
- package/dist/internal/ai-trace.js +82 -11
- package/dist/internal/ai-trace.js.map +1 -1
- package/dist/internal/atomic-write.d.ts +17 -0
- package/dist/internal/atomic-write.d.ts.map +1 -0
- package/dist/internal/atomic-write.js +44 -0
- package/dist/internal/atomic-write.js.map +1 -0
- package/dist/internal/env.d.ts +6 -0
- package/dist/internal/env.d.ts.map +1 -0
- package/dist/internal/env.js +12 -0
- package/dist/internal/env.js.map +1 -0
- package/dist/report/build.d.ts.map +1 -1
- package/dist/report/build.js +9 -4
- package/dist/report/build.js.map +1 -1
- package/dist/report/format.d.ts +9 -1
- package/dist/report/format.d.ts.map +1 -1
- package/dist/report/format.js +17 -4
- package/dist/report/format.js.map +1 -1
- package/dist/report/list-model.d.ts +85 -0
- package/dist/report/list-model.d.ts.map +1 -0
- package/dist/report/list-model.js +10 -0
- package/dist/report/list-model.js.map +1 -0
- package/dist/report/list-steps.d.ts +27 -0
- package/dist/report/list-steps.d.ts.map +1 -0
- package/dist/report/list-steps.js +65 -0
- package/dist/report/list-steps.js.map +1 -0
- package/dist/report/list.d.ts +73 -27
- package/dist/report/list.d.ts.map +1 -1
- package/dist/report/list.js +201 -171
- package/dist/report/list.js.map +1 -1
- package/dist/report/live-window.d.ts +11 -0
- package/dist/report/live-window.d.ts.map +1 -1
- package/dist/report/live-window.js +12 -5
- package/dist/report/live-window.js.map +1 -1
- package/dist/report/running-tree.d.ts +46 -0
- package/dist/report/running-tree.d.ts.map +1 -0
- package/dist/report/running-tree.js +186 -0
- package/dist/report/running-tree.js.map +1 -0
- package/dist/report/write.d.ts +1 -1
- package/dist/report/write.d.ts.map +1 -1
- package/dist/report/write.js +3 -6
- package/dist/report/write.js.map +1 -1
- package/dist/run/events.d.ts +61 -8
- package/dist/run/events.d.ts.map +1 -1
- package/dist/run/events.js.map +1 -1
- package/dist/run/execute.d.ts +7 -0
- package/dist/run/execute.d.ts.map +1 -1
- package/dist/run/execute.js +6 -0
- package/dist/run/execute.js.map +1 -1
- package/dist/run/fixtures.d.ts +3 -0
- package/dist/run/fixtures.d.ts.map +1 -1
- package/dist/run/fixtures.js +5 -3
- package/dist/run/fixtures.js.map +1 -1
- package/dist/run/managed-process.d.ts +8 -2
- package/dist/run/managed-process.d.ts.map +1 -1
- package/dist/run/managed-process.js +12 -0
- package/dist/run/managed-process.js.map +1 -1
- package/dist/run/public.d.ts +1 -1
- package/dist/run/public.d.ts.map +1 -1
- package/dist/run/records.d.ts +2 -1
- package/dist/run/records.d.ts.map +1 -1
- package/dist/run/runner.d.ts +3 -2
- package/dist/run/runner.d.ts.map +1 -1
- package/dist/run/runner.js +129 -48
- package/dist/run/runner.js.map +1 -1
- package/dist/run/scheduler.d.ts +2 -0
- package/dist/run/scheduler.d.ts.map +1 -1
- package/dist/run/scheduler.js +4 -0
- package/dist/run/scheduler.js.map +1 -1
- package/dist/run/sessions.d.ts.map +1 -1
- package/dist/run/sessions.js +3 -4
- package/dist/run/sessions.js.map +1 -1
- package/dist/run/steps.d.ts +9 -1
- package/dist/run/steps.d.ts.map +1 -1
- package/dist/run/steps.js +2 -0
- package/dist/run/steps.js.map +1 -1
- package/dist/run/worker/protocol.d.ts +10 -1
- package/dist/run/worker/protocol.d.ts.map +1 -1
- package/dist/run/worker/protocol.js.map +1 -1
- package/dist/run/worker/session.d.ts.map +1 -1
- package/dist/run/worker/session.js +4 -0
- package/dist/run/worker/session.js.map +1 -1
- package/dist/run/worker-models.d.ts +27 -0
- package/dist/run/worker-models.d.ts.map +1 -0
- package/dist/run/worker-models.js +49 -0
- package/dist/run/worker-models.js.map +1 -0
- package/dist/telemetry/environment.d.ts +33 -0
- package/dist/telemetry/environment.d.ts.map +1 -0
- package/dist/telemetry/environment.js +96 -0
- package/dist/telemetry/environment.js.map +1 -0
- package/dist/telemetry/events.d.ts +25 -0
- package/dist/telemetry/events.d.ts.map +1 -0
- package/dist/telemetry/events.js +107 -0
- package/dist/telemetry/events.js.map +1 -0
- package/dist/telemetry/posthog.d.ts +31 -0
- package/dist/telemetry/posthog.d.ts.map +1 -0
- package/dist/telemetry/posthog.js +29 -0
- package/dist/telemetry/posthog.js.map +1 -0
- package/dist/telemetry/project.d.ts +18 -0
- package/dist/telemetry/project.d.ts.map +1 -0
- package/dist/telemetry/project.js +55 -0
- package/dist/telemetry/project.js.map +1 -0
- package/dist/telemetry/store.d.ts +53 -0
- package/dist/telemetry/store.d.ts.map +1 -0
- package/dist/telemetry/store.js +153 -0
- package/dist/telemetry/store.js.map +1 -0
- package/dist/telemetry/telemetry.d.ts +90 -0
- package/dist/telemetry/telemetry.d.ts.map +1 -0
- package/dist/telemetry/telemetry.js +188 -0
- package/dist/telemetry/telemetry.js.map +1 -0
- package/dist/types.d.ts +6 -9
- package/dist/types.d.ts.map +1 -1
- package/package.json +5 -5
- package/skills/e2e/references/agent.md +13 -3
- package/skills/e2e/references/debugging.md +1 -1
- package/skills/e2e/references/running.md +3 -2
- package/skills/e2e/references/setup.md +6 -1
package/dist/report/list.js
CHANGED
|
@@ -1,67 +1,35 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Human-readable list reporter, styled after vitest's
|
|
3
|
-
*
|
|
4
|
-
* section with code frames, and a padded summary. On a TTY a
|
|
5
|
-
* below the log shows the running
|
|
2
|
+
* Human-readable list reporter, styled after vitest's default reporter: one
|
|
3
|
+
* block per test file and target with each test's agent steps nested, a
|
|
4
|
+
* `Failed Tests` section with code frames, and a padded summary. On a TTY a
|
|
5
|
+
* live window below the log shows the running tree (`RunningTree`) and the
|
|
6
|
+
* counters.
|
|
6
7
|
*/
|
|
7
8
|
import path from 'node:path';
|
|
8
9
|
import picocolors from 'picocolors';
|
|
9
|
-
import { sanitizeText, truncateUtf8 } from '../internal/errors.js';
|
|
10
10
|
import { packageVersion } from '../internal/package-version.js';
|
|
11
|
-
import { collapseText } from '../internal/text.js';
|
|
12
11
|
import { codeFrame, userFrame } from './code-frame.js';
|
|
13
|
-
import { addUsage, aiSegment, bounded, ellipsize, emptyCounters, emptyUsage, fileOutcome, formatClock, formatTime,
|
|
12
|
+
import { addUsage, aiSegment, bounded, ellipsize, emptyCounters, emptyUsage, F_CHECK, F_CROSS, F_POINTER, fileOutcome, formatClock, formatTime, padTitle, rule, stateString, statusBucket, stepsUsage, sumUsage, tally, terminalColumns, usageText, } from './format.js';
|
|
13
|
+
import { isShownEvent } from './list-model.js';
|
|
14
|
+
import { stepLine } from './list-steps.js';
|
|
14
15
|
import { LiveWindow } from './live-window.js';
|
|
15
|
-
|
|
16
|
-
const MAX_STEP_LABEL_BYTES = 72;
|
|
17
|
-
const F_POINTER = '❯';
|
|
18
|
-
const F_CHECK = '✓';
|
|
19
|
-
const F_CROSS = '×';
|
|
16
|
+
import { RunningTree } from './running-tree.js';
|
|
20
17
|
const F_DOWN = '↓';
|
|
21
18
|
const F_RIGHT = '→';
|
|
22
|
-
const F_DOWN_RIGHT = '↳';
|
|
23
|
-
const F_TREE_MIDDLE = '├──';
|
|
24
|
-
const F_TREE_END = '└──';
|
|
25
19
|
/** Indentation under a badge line, matching vitest's banner padding. */
|
|
26
20
|
const BADGE_PADDING = ' ';
|
|
21
|
+
/** Indentation of a test line under its file line. */
|
|
22
|
+
const TEST_INDENT = ' ';
|
|
23
|
+
/** Indentation of a finished agent step under its test line in a file block. */
|
|
24
|
+
const STEP_INDENT = ' ';
|
|
27
25
|
/**
|
|
28
26
|
* Badge backgrounds, assigned to targets in declaration order. Bright variants
|
|
29
27
|
* because GitHub Actions renders plain yellow as dark brown, which swallows the
|
|
30
28
|
* black label.
|
|
31
29
|
*/
|
|
32
30
|
const BADGE_COLORS = ['bgYellowBright', 'bgCyanBright', 'bgGreenBright', 'bgMagentaBright'];
|
|
33
|
-
/**
|
|
34
|
-
* Rows the live window spends outside the running tests and the summary: its
|
|
35
|
-
* blank lines, the `… more running` marker, and a margin above the prompt.
|
|
36
|
-
*/
|
|
37
|
-
const WINDOW_CHROME_ROWS = 6;
|
|
38
|
-
/** Rows one running test takes before its calls: the test line and its current step. */
|
|
39
|
-
const WINDOW_ROWS_PER_TEST = 2;
|
|
40
31
|
/** Indentation of the `→ first error line` under a failed test in a file block. */
|
|
41
32
|
const ERROR_GLANCE_INDENT = 7;
|
|
42
|
-
/** One-line, quoted step label bounded for the live view. */
|
|
43
|
-
function stepLabel(label) {
|
|
44
|
-
const flat = collapseText(label);
|
|
45
|
-
const shown = truncateUtf8(flat, MAX_STEP_LABEL_BYTES);
|
|
46
|
-
return `"${shown}${shown === flat ? '' : '…'}"`;
|
|
47
|
-
}
|
|
48
|
-
/**
|
|
49
|
-
* Live tail for one step event. Model and engine calls are the ones worth a
|
|
50
|
-
* glance (`tool:` prefixes come from executor tool accounting); polls and
|
|
51
|
-
* policy decisions stay quiet.
|
|
52
|
-
*/
|
|
53
|
-
function eventTail(event) {
|
|
54
|
-
if (event.kind === 'model') {
|
|
55
|
-
const tokens = event.count !== undefined && event.count > 0 ? ` · ${formatTokens(event.count)} tokens` : '';
|
|
56
|
-
return `model turn ${formatTime(event.durationMs)}${tokens}`;
|
|
57
|
-
}
|
|
58
|
-
if (event.kind === 'engine') {
|
|
59
|
-
const name = sanitizeText(event.name ?? 'engine').replace(/^tool:/, '');
|
|
60
|
-
const failed = event.status === 'passed' ? '' : ` ${F_CROSS}`;
|
|
61
|
-
return `${truncateUtf8(name, 40)} ${formatTime(event.durationMs)}${failed}`;
|
|
62
|
-
}
|
|
63
|
-
return undefined;
|
|
64
|
-
}
|
|
65
33
|
/** The identity of one test-target pair across `test-started`, `step`, and `test-finished`. */
|
|
66
34
|
function pairKey(testId, target) {
|
|
67
35
|
return `${testId}@${target}`;
|
|
@@ -108,16 +76,25 @@ export class ListReporter {
|
|
|
108
76
|
output;
|
|
109
77
|
pc;
|
|
110
78
|
window;
|
|
79
|
+
tree;
|
|
111
80
|
separator;
|
|
112
81
|
projectRoot;
|
|
113
82
|
/** Selected targets in declaration order; decides each badge's color. */
|
|
114
83
|
targets = [];
|
|
115
|
-
/** When the run
|
|
116
|
-
|
|
84
|
+
/** When the run was launched (`run-started`): what a run that never reached `plan` reports. */
|
|
85
|
+
launchedAt = new Date();
|
|
86
|
+
/**
|
|
87
|
+
* When the run began executing (`plan`): the summary's `Start at` and the
|
|
88
|
+
* origin of `Duration`. Undefined until then, so the clock never ticks
|
|
89
|
+
* while a first-run download narrates above the live window.
|
|
90
|
+
*/
|
|
91
|
+
startedAt;
|
|
117
92
|
/** File groups keyed `${target}\u0000${file}`, in first-seen order. */
|
|
118
93
|
groups = new Map();
|
|
119
|
-
/** Pairs
|
|
120
|
-
|
|
94
|
+
/** Pairs that started and whose result is still to come, keyed by `pairKey`, in start order. */
|
|
95
|
+
pairs = new Map();
|
|
96
|
+
/** Whether a live window paints; without one, finished steps stream permanently. */
|
|
97
|
+
live;
|
|
121
98
|
failures = [];
|
|
122
99
|
/**
|
|
123
100
|
* Serial groups whose members' results are still to come, by group id. The
|
|
@@ -127,13 +104,29 @@ export class ListReporter {
|
|
|
127
104
|
pendingSerial = new Map();
|
|
128
105
|
/** Run-level errors, printed once at the end; the stream carries them as they happen. */
|
|
129
106
|
errors = [];
|
|
107
|
+
/**
|
|
108
|
+
* Whether the run was cut short. An interrupt during service or app startup
|
|
109
|
+
* lands before discovery, so the counters stay at zero; the summary must not
|
|
110
|
+
* read that as "no test files".
|
|
111
|
+
*/
|
|
112
|
+
interrupted = false;
|
|
113
|
+
/**
|
|
114
|
+
* The setup step in flight, ticking in the live window. `narrated` once a
|
|
115
|
+
* notice printed under it: a step that said something (a browser download)
|
|
116
|
+
* prints its total when done, a silent one (an instant `prepare`) does not.
|
|
117
|
+
*/
|
|
118
|
+
inFlight;
|
|
119
|
+
/** Time spent starting services and app commands, split out of the summary's `Duration`. */
|
|
120
|
+
startupMs = 0;
|
|
130
121
|
/** Run-wide model usage, summed from every reported result and serial group. */
|
|
131
122
|
runUsage = emptyUsage();
|
|
132
123
|
constructor(output = DEFAULT_OUTPUT, options = {}) {
|
|
133
124
|
this.output = output;
|
|
134
125
|
const live = (options.live ?? process.stdout.isTTY === true) && output.raw !== undefined;
|
|
126
|
+
this.live = live;
|
|
135
127
|
this.pc = picocolors.createColors(options.colors ?? (live || picocolors.isColorSupported));
|
|
136
128
|
this.separator = this.pc.dim(' > ');
|
|
129
|
+
this.tree = new RunningTree(this.pc, (target) => this.badge(target));
|
|
137
130
|
this.window = new LiveWindow(live ? output.raw?.bind(output) : undefined, () => this.renderWindow());
|
|
138
131
|
}
|
|
139
132
|
/**
|
|
@@ -149,8 +142,13 @@ export class ListReporter {
|
|
|
149
142
|
this.plan(event);
|
|
150
143
|
break;
|
|
151
144
|
case 'notice':
|
|
145
|
+
if (this.inFlight !== undefined)
|
|
146
|
+
this.inFlight.narrated = true;
|
|
152
147
|
this.print(`${this.pc.dim('ℹ')} ${bounded(event.message)}`);
|
|
153
148
|
break;
|
|
149
|
+
case 'setup':
|
|
150
|
+
this.setupStep(event);
|
|
151
|
+
break;
|
|
154
152
|
case 'test-started':
|
|
155
153
|
this.testStarted(event);
|
|
156
154
|
break;
|
|
@@ -198,7 +196,7 @@ export class ListReporter {
|
|
|
198
196
|
const key = `${target}\u0000${file}`;
|
|
199
197
|
let group = this.groups.get(key);
|
|
200
198
|
if (group === undefined) {
|
|
201
|
-
group = { file, target, planned: undefined, lines: [],
|
|
199
|
+
group = { file, target, planned: undefined, lines: [], printed: false };
|
|
202
200
|
this.groups.set(key, group);
|
|
203
201
|
}
|
|
204
202
|
return group;
|
|
@@ -207,7 +205,7 @@ export class ListReporter {
|
|
|
207
205
|
const { pc } = this;
|
|
208
206
|
this.projectRoot = event.projectRoot;
|
|
209
207
|
this.targets = event.targets;
|
|
210
|
-
this.
|
|
208
|
+
this.launchedAt = new Date();
|
|
211
209
|
const version = packageVersion(import.meta.url, '../../package.json', '0.0.0');
|
|
212
210
|
this.output.write('');
|
|
213
211
|
this.output.write(`${pc.bold(pc.black(pc.bgCyan(' RUN ')))} ${pc.cyan(`e2e v${version}`)} ${pc.gray(event.projectRoot)}`);
|
|
@@ -215,97 +213,144 @@ export class ListReporter {
|
|
|
215
213
|
if (event.ci)
|
|
216
214
|
details.push('CI');
|
|
217
215
|
this.output.write(BADGE_PADDING + pc.dim(details.join(' · ')));
|
|
216
|
+
if (event.model !== undefined) {
|
|
217
|
+
this.output.write(BADGE_PADDING + pc.dim(`model ${bounded(event.model)}`));
|
|
218
|
+
}
|
|
218
219
|
this.output.write('');
|
|
219
220
|
this.window.start();
|
|
220
221
|
}
|
|
222
|
+
/**
|
|
223
|
+
* The plan is settled once collection and every target's `prepare` are
|
|
224
|
+
* done, so its arrival is when the run starts executing and the clock
|
|
225
|
+
* starts: a browser downloaded moments earlier is not on it.
|
|
226
|
+
*/
|
|
221
227
|
plan(event) {
|
|
228
|
+
this.startedAt = new Date();
|
|
222
229
|
for (const planned of event.files) {
|
|
223
230
|
this.group(planned.file, planned.target).planned = planned.tests;
|
|
224
231
|
}
|
|
225
232
|
this.window.redraw();
|
|
226
233
|
}
|
|
227
|
-
/**
|
|
234
|
+
/**
|
|
235
|
+
* The setup stretch before any test: collection, each target's `prepare`,
|
|
236
|
+
* then the services and app commands. Without this the window showed only
|
|
237
|
+
* zero counters while a database booted or a browser downloaded. The step
|
|
238
|
+
* in flight ticks in the live window; a finished process prints once with
|
|
239
|
+
* its wait, a finished collection or provisioning only when it narrated;
|
|
240
|
+
* the process waits sum into the summary's startup split.
|
|
241
|
+
*/
|
|
242
|
+
setupStep(event) {
|
|
243
|
+
const { pc } = this;
|
|
244
|
+
const { verb, subject, done } = setupWording(event.step);
|
|
245
|
+
if (event.state === 'started') {
|
|
246
|
+
this.inFlight = { step: event.step, verb, subject, startedMs: Date.now(), narrated: false };
|
|
247
|
+
this.window.redraw();
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
const narrated = this.inFlight?.narrated ?? false;
|
|
251
|
+
this.inFlight = undefined;
|
|
252
|
+
const process = event.step.kind === 'service' || event.step.kind === 'app';
|
|
253
|
+
if (process)
|
|
254
|
+
this.startupMs += event.durationMs;
|
|
255
|
+
if (!process && !narrated) {
|
|
256
|
+
this.window.redraw();
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
const outcome = event.outcome ?? done;
|
|
260
|
+
this.print(` ${pc.green(F_CHECK)} ${subject} ${pc.dim(`${outcome} ${formatTime(event.durationMs)}`)}`);
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Acknowledged immediately, so a bounded teardown is not mistaken for a
|
|
264
|
+
* hang. An interrupt that lands during setup names the step it cut short
|
|
265
|
+
* (`interrupted while starting service "postgres"`), since no test was
|
|
266
|
+
* running to stop.
|
|
267
|
+
*/
|
|
228
268
|
runInterrupted(event) {
|
|
229
|
-
this.
|
|
230
|
-
|
|
231
|
-
|
|
269
|
+
this.interrupted = true;
|
|
270
|
+
const during = this.inFlight === undefined ? undefined : `${this.inFlight.verb} ${this.inFlight.subject}`;
|
|
271
|
+
this.print(this.pc.yellow(event.mode === 'forced'
|
|
272
|
+
? 'interrupted again: tearing every worker down now'
|
|
273
|
+
: during === undefined
|
|
274
|
+
? 'interrupted: stopping the running test and tearing down (interrupt again to force)'
|
|
275
|
+
: `interrupted while ${during}: tearing down (interrupt again to force)`));
|
|
232
276
|
}
|
|
233
277
|
/**
|
|
234
278
|
* A worker began one test-target pair: show it in the live window. A serial
|
|
235
279
|
* member replaces the previous member of its group, which has finished
|
|
236
|
-
* executing even though its result only arrives with the whole group.
|
|
280
|
+
* executing even though its result only arrives with the whole group. A
|
|
281
|
+
* serial group retries as a whole, so a member starts once per group
|
|
282
|
+
* attempt; its steps from earlier attempts stay with it, as an ordinary
|
|
283
|
+
* test's do.
|
|
237
284
|
*/
|
|
238
285
|
testStarted(event) {
|
|
239
286
|
if (event.serialId !== undefined) {
|
|
240
|
-
for (const
|
|
241
|
-
if (test.serialId === event.serialId && test.group.target === event.target)
|
|
242
|
-
|
|
243
|
-
}
|
|
287
|
+
for (const test of this.pairs.values()) {
|
|
288
|
+
if (test.serialId === event.serialId && test.group.target === event.target)
|
|
289
|
+
test.executing = false;
|
|
244
290
|
}
|
|
245
291
|
}
|
|
246
|
-
|
|
292
|
+
const key = pairKey(event.testId, event.target);
|
|
293
|
+
this.pairs.set(key, {
|
|
247
294
|
group: this.group(event.file, event.target),
|
|
248
295
|
serialId: event.serialId,
|
|
249
296
|
title: bounded(event.title),
|
|
250
297
|
startedMs: Date.now(),
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
298
|
+
executing: true,
|
|
299
|
+
current: undefined,
|
|
300
|
+
steps: this.pairs.get(key)?.steps ?? [],
|
|
254
301
|
});
|
|
255
302
|
this.window.redraw();
|
|
256
303
|
}
|
|
257
304
|
/**
|
|
258
|
-
*
|
|
259
|
-
* agent steps
|
|
260
|
-
*
|
|
261
|
-
*
|
|
262
|
-
*
|
|
263
|
-
*
|
|
264
|
-
* the live window, and finished agent steps print with the test they
|
|
265
|
-
* belong to.
|
|
305
|
+
* Tracks step progress of a running pair. With a live window the pair's
|
|
306
|
+
* finished agent steps accumulate under its row there and print once, nested
|
|
307
|
+
* under its line, when the file block prints - so every name appears exactly
|
|
308
|
+
* once in the scrollback. Without a window (CI logs) nothing is transient, so
|
|
309
|
+
* each finished agent step prints at once, prefixed with its test. Deterministic
|
|
310
|
+
* steps are fast and many, so they only ever show in the live window.
|
|
266
311
|
*/
|
|
267
312
|
step(event) {
|
|
268
|
-
const
|
|
269
|
-
const running = this.running.get(pairKey(event.testId, event.target));
|
|
313
|
+
const running = this.pairs.get(pairKey(event.testId, event.target));
|
|
270
314
|
if (running === undefined)
|
|
271
315
|
return;
|
|
272
316
|
const { progress } = event;
|
|
273
317
|
switch (progress.phase) {
|
|
274
318
|
case 'start': {
|
|
275
|
-
|
|
276
|
-
running.
|
|
277
|
-
if (progress.kind === 'agent' && this.running.size === 1 && !running.streaming) {
|
|
278
|
-
running.streaming = true;
|
|
279
|
-
const { group } = running;
|
|
280
|
-
group.streamed = true;
|
|
281
|
-
this.print(` ${pc.yellow(F_POINTER)} ${this.badge(group.target)} ${pc.dim(bounded(group.file))}${this.separator}${running.title}`);
|
|
282
|
-
}
|
|
319
|
+
const { api, label, kind } = progress;
|
|
320
|
+
running.current = { api, label, kind, events: [], turnStart: 0 };
|
|
283
321
|
this.window.redraw();
|
|
284
322
|
break;
|
|
285
323
|
}
|
|
286
324
|
case 'event': {
|
|
287
|
-
const
|
|
288
|
-
if (
|
|
325
|
+
const { current } = running;
|
|
326
|
+
if (current === undefined || !isShownEvent(progress.event))
|
|
289
327
|
break;
|
|
290
|
-
|
|
328
|
+
if (progress.event.kind === 'model') {
|
|
329
|
+
current.events.splice(current.turnStart, 0, progress.event);
|
|
330
|
+
current.turnStart = current.events.length;
|
|
331
|
+
}
|
|
332
|
+
else {
|
|
333
|
+
current.events.push(progress.event);
|
|
334
|
+
}
|
|
291
335
|
this.window.redraw();
|
|
292
336
|
break;
|
|
293
337
|
}
|
|
294
338
|
case 'end': {
|
|
295
|
-
running.
|
|
296
|
-
running.events = [];
|
|
339
|
+
running.current = undefined;
|
|
297
340
|
if (progress.kind !== 'agent') {
|
|
298
341
|
this.window.redraw();
|
|
299
342
|
break;
|
|
300
343
|
}
|
|
301
|
-
const
|
|
302
|
-
const
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
pc.dim(
|
|
344
|
+
const { api, label, status, durationMs, modelCalls } = progress;
|
|
345
|
+
const step = { api, label, status, durationMs, modelCalls };
|
|
346
|
+
if (this.live) {
|
|
347
|
+
running.steps.push(step);
|
|
348
|
+
this.window.redraw();
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
351
|
+
const context = `${this.pc.dim(running.title)}${this.separator}`;
|
|
352
|
+
this.print(`${TEST_INDENT}${stepLine(this.pc, step, { context })}`);
|
|
353
|
+
}
|
|
309
354
|
break;
|
|
310
355
|
}
|
|
311
356
|
}
|
|
@@ -335,12 +380,14 @@ export class ListReporter {
|
|
|
335
380
|
// counted them.
|
|
336
381
|
if (!result.selected)
|
|
337
382
|
return;
|
|
338
|
-
|
|
383
|
+
const key = pairKey(result.test.id, result.target.name);
|
|
384
|
+
const steps = this.pairs.get(key)?.steps ?? [];
|
|
385
|
+
this.pairs.delete(key);
|
|
339
386
|
const group = this.group(result.test.file, result.target.name);
|
|
340
387
|
const { durationMs, usage, error } = this.detailsOf(result);
|
|
341
388
|
addUsage(this.runUsage, usage);
|
|
342
389
|
const title = bounded(result.test.titlePath.join(' > '));
|
|
343
|
-
|
|
390
|
+
const line = {
|
|
344
391
|
title,
|
|
345
392
|
declarationIndex: result.test.declarationIndex,
|
|
346
393
|
status: result.status,
|
|
@@ -348,7 +395,9 @@ export class ListReporter {
|
|
|
348
395
|
usage,
|
|
349
396
|
firstErrorLine: error === undefined ? undefined : bounded(error.message).split('\n')[0],
|
|
350
397
|
skipReason: result.skip === undefined ? undefined : bounded(result.skip.reason),
|
|
351
|
-
|
|
398
|
+
steps,
|
|
399
|
+
};
|
|
400
|
+
group.lines.push(line);
|
|
352
401
|
if (statusBucket(result.status) === 'failed') {
|
|
353
402
|
this.failures.push({ group, title, status: result.status, error });
|
|
354
403
|
}
|
|
@@ -376,8 +425,11 @@ export class ListReporter {
|
|
|
376
425
|
}
|
|
377
426
|
/**
|
|
378
427
|
* Prints one file's block: the file line with its counts, then - when the
|
|
379
|
-
* file failed, had a flaky pass,
|
|
380
|
-
*
|
|
428
|
+
* file failed, had a flaky pass, or is the run's only file - one line per
|
|
429
|
+
* test with its finished agent steps nested, as vitest lists tests for a
|
|
430
|
+
* failed module. With a live window the block is also the only permanent
|
|
431
|
+
* record of a file's agent steps, so a file that has any lists its tests
|
|
432
|
+
* too; without one the steps already printed as they finished.
|
|
381
433
|
*/
|
|
382
434
|
printGroup(group) {
|
|
383
435
|
if (group.printed)
|
|
@@ -413,14 +465,16 @@ export class ListReporter {
|
|
|
413
465
|
this.print(parts.join(' '));
|
|
414
466
|
const verbose = counts.failed > 0 ||
|
|
415
467
|
counts.flaky > 0 ||
|
|
416
|
-
|
|
417
|
-
this.
|
|
468
|
+
this.groups.size === 1 ||
|
|
469
|
+
(this.live && group.lines.some((line) => line.steps.length > 0));
|
|
418
470
|
if (!verbose)
|
|
419
471
|
return;
|
|
420
472
|
const ordered = group.lines.toSorted((a, b) => a.declarationIndex - b.declarationIndex);
|
|
421
473
|
for (const line of ordered) {
|
|
422
474
|
for (const rendered of this.testLine(line))
|
|
423
475
|
this.print(rendered);
|
|
476
|
+
for (const step of line.steps)
|
|
477
|
+
this.print(`${STEP_INDENT}${stepLine(pc, step)}`);
|
|
424
478
|
}
|
|
425
479
|
}
|
|
426
480
|
/** One test row under its file line, plus the first error line for a failure. */
|
|
@@ -455,89 +509,43 @@ export class ListReporter {
|
|
|
455
509
|
}
|
|
456
510
|
/**
|
|
457
511
|
* vitest's padded summary, shared by the live window and the final report:
|
|
458
|
-
* files, tests, model usage, run errors, start time, and elapsed time.
|
|
512
|
+
* files, tests, model usage, run errors, start time, and elapsed time. The
|
|
513
|
+
* clock rows wait for `plan`; the final summary of a run that never got
|
|
514
|
+
* there (a config or collection failure, a failed download) counts from
|
|
515
|
+
* the launch instead, so the time it took to fail is still on record.
|
|
459
516
|
*/
|
|
460
|
-
summaryRows() {
|
|
517
|
+
summaryRows(final) {
|
|
461
518
|
const { pc } = this;
|
|
462
519
|
const files = this.fileCounters();
|
|
463
520
|
const tests = this.testCounters();
|
|
464
521
|
const rows = [
|
|
465
|
-
padTitle(pc, 'Test Files') + (files.total === 0 ?
|
|
466
|
-
padTitle(pc, 'Tests') + (tests.total === 0 ?
|
|
522
|
+
padTitle(pc, 'Test Files') + (files.total === 0 ? this.emptyState('no test files', 'none started') : stateString(pc, files)),
|
|
523
|
+
padTitle(pc, 'Tests') + (tests.total === 0 ? this.emptyState('no tests executed', 'none executed') : stateString(pc, tests)),
|
|
467
524
|
];
|
|
468
|
-
const ai =
|
|
525
|
+
const ai = usageText(this.runUsage);
|
|
469
526
|
if (ai !== undefined)
|
|
470
527
|
rows.push(padTitle(pc, 'AI') + `${ai} · ${this.runUsage.calls} model calls`);
|
|
471
528
|
if (this.errors.length > 0) {
|
|
472
529
|
const count = this.errors.length;
|
|
473
530
|
rows.push(padTitle(pc, 'Errors') + pc.bold(pc.red(`${count} error${count === 1 ? '' : 's'}`)));
|
|
474
531
|
}
|
|
475
|
-
|
|
476
|
-
|
|
532
|
+
const startedAt = this.startedAt ?? (final ? this.launchedAt : undefined);
|
|
533
|
+
if (startedAt !== undefined) {
|
|
534
|
+
rows.push(padTitle(pc, 'Start at') + formatClock(startedAt));
|
|
535
|
+
const startup = this.startupMs > 0 ? pc.dim(` (startup ${formatTime(this.startupMs)})`) : '';
|
|
536
|
+
rows.push(padTitle(pc, 'Duration') + formatTime(Date.now() - startedAt.getTime()) + startup);
|
|
537
|
+
}
|
|
477
538
|
return rows;
|
|
478
539
|
}
|
|
479
|
-
/**
|
|
480
|
-
|
|
481
|
-
* must fit the screen, because repainting more rows than the terminal has
|
|
482
|
-
* breaks the cursor-up erase: rows left after the summary go to the running
|
|
483
|
-
* tests, each test's calls share what remains once every test has its own
|
|
484
|
-
* line, and tests that still do not fit fold into one `more running` marker.
|
|
485
|
-
*/
|
|
486
|
-
renderWindow() {
|
|
540
|
+
/** A zero counter's label: the plain text, or the interrupt when that is why nothing is counted. */
|
|
541
|
+
emptyState(none, cut) {
|
|
487
542
|
const { pc } = this;
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
else
|
|
495
|
-
tests.push(test);
|
|
496
|
-
}
|
|
497
|
-
const summary = this.summaryRows();
|
|
498
|
-
let budget = terminalRows() - summary.length - WINDOW_CHROME_ROWS;
|
|
499
|
-
const spare = budget - active.size - this.running.size * WINDOW_ROWS_PER_TEST;
|
|
500
|
-
const maxEvents = Math.max(0, Math.floor(spare / Math.max(1, this.running.size)));
|
|
501
|
-
const lines = [''];
|
|
502
|
-
let hidden = 0;
|
|
503
|
-
for (const [group, tests] of active) {
|
|
504
|
-
if (budget < 2) {
|
|
505
|
-
hidden += tests.length;
|
|
506
|
-
continue;
|
|
507
|
-
}
|
|
508
|
-
const progress = pc.dim(` ${group.lines.length}/${group.planned ?? '?'}`);
|
|
509
|
-
lines.push(`${pc.bold(pc.yellow(` ${F_POINTER} `))}${this.badge(group.target)} ${bounded(group.file)}${progress}`);
|
|
510
|
-
budget -= 1;
|
|
511
|
-
tests.forEach((test, index) => {
|
|
512
|
-
if (budget < 1) {
|
|
513
|
-
hidden += 1;
|
|
514
|
-
return;
|
|
515
|
-
}
|
|
516
|
-
const glyph = index === tests.length - 1 ? F_TREE_END : F_TREE_MIDDLE;
|
|
517
|
-
const elapsed = pc.bold(pc.yellow(formatTime(Math.max(0, now - test.startedMs))));
|
|
518
|
-
lines.push(`${pc.bold(pc.yellow(` ${glyph} `))}${test.title} ${elapsed}`);
|
|
519
|
-
budget -= 1;
|
|
520
|
-
if (test.step === undefined)
|
|
521
|
-
return;
|
|
522
|
-
const detail = [` ${pc.dim(`${F_DOWN_RIGHT} ${test.step}`)}`];
|
|
523
|
-
const overflow = test.events.length - maxEvents;
|
|
524
|
-
if (overflow > 0) {
|
|
525
|
-
detail.push(` ${pc.dim(`… ${overflow} earlier call${overflow === 1 ? '' : 's'}`)}`);
|
|
526
|
-
}
|
|
527
|
-
for (const tail of overflow > 0 ? test.events.slice(overflow) : test.events) {
|
|
528
|
-
detail.push(` ${pc.dim(tail)}`);
|
|
529
|
-
}
|
|
530
|
-
const shown = detail.slice(0, budget);
|
|
531
|
-
lines.push(...shown);
|
|
532
|
-
budget -= shown.length;
|
|
533
|
-
});
|
|
534
|
-
}
|
|
535
|
-
if (hidden > 0)
|
|
536
|
-
lines.push(pc.dim(` … ${hidden} more running`));
|
|
537
|
-
if (active.size > 0)
|
|
538
|
-
lines.push('');
|
|
539
|
-
lines.push(...summary, '');
|
|
540
|
-
return lines;
|
|
543
|
+
return this.interrupted ? pc.yellow(`${cut} (interrupted)`) : pc.dim(none);
|
|
544
|
+
}
|
|
545
|
+
/** The live window: the running tree, then the summary. */
|
|
546
|
+
renderWindow() {
|
|
547
|
+
const running = [...this.pairs.values()].filter((test) => test.executing);
|
|
548
|
+
return this.tree.render(running, this.summaryRows(false), Date.now(), this.inFlight);
|
|
541
549
|
}
|
|
542
550
|
/** vitest's `Failed Tests` section: a banner, then each failure with its code frame. */
|
|
543
551
|
printFailures() {
|
|
@@ -605,6 +613,8 @@ export class ListReporter {
|
|
|
605
613
|
}
|
|
606
614
|
runFinished(event) {
|
|
607
615
|
const { pc } = this;
|
|
616
|
+
if (event.status === 'interrupted')
|
|
617
|
+
this.interrupted = true;
|
|
608
618
|
this.window.stop();
|
|
609
619
|
// An interrupted or crashed run leaves files without their full result
|
|
610
620
|
// set; print what they have so nothing that ran goes unreported.
|
|
@@ -615,7 +625,7 @@ export class ListReporter {
|
|
|
615
625
|
this.printFailures();
|
|
616
626
|
this.printErrors();
|
|
617
627
|
this.print('');
|
|
618
|
-
for (const row of this.summaryRows())
|
|
628
|
+
for (const row of this.summaryRows(true))
|
|
619
629
|
this.print(row);
|
|
620
630
|
this.print(padTitle(pc, 'Report') +
|
|
621
631
|
(event.reportPath === undefined ? pc.dim('(not written)') : this.displayPath(event.reportPath)));
|
|
@@ -629,4 +639,24 @@ export class ListReporter {
|
|
|
629
639
|
this.print('');
|
|
630
640
|
}
|
|
631
641
|
}
|
|
642
|
+
/**
|
|
643
|
+
* How the reporter phrases one setup step: the verb and subject while it
|
|
644
|
+
* runs (`starting service "postgres"`), and the word for its completion
|
|
645
|
+
* (`ready`); a reused process says `reused` instead.
|
|
646
|
+
*/
|
|
647
|
+
function setupWording(step) {
|
|
648
|
+
switch (step.kind) {
|
|
649
|
+
case 'collect':
|
|
650
|
+
return { verb: 'collecting', subject: 'tests', done: 'collected' };
|
|
651
|
+
case 'prepare':
|
|
652
|
+
return {
|
|
653
|
+
verb: 'preparing',
|
|
654
|
+
subject: `${bounded(step.engine)} engine for target "${bounded(step.target)}"`,
|
|
655
|
+
done: 'prepared',
|
|
656
|
+
};
|
|
657
|
+
case 'service':
|
|
658
|
+
case 'app':
|
|
659
|
+
return { verb: 'starting', subject: bounded(step.label), done: 'ready' };
|
|
660
|
+
}
|
|
661
|
+
}
|
|
632
662
|
//# sourceMappingURL=list.js.map
|