@e2edev/e2e 0.5.0 → 0.6.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 +64 -25
- package/dist/agent/act.d.ts +2 -2
- package/dist/agent/act.js +14 -37
- package/dist/agent/act.js.map +1 -1
- package/dist/agent/actions.d.ts +2 -2
- package/dist/agent/actions.d.ts.map +1 -1
- package/dist/agent/actions.js +1 -1
- package/dist/agent/ai-sdk.d.ts +1 -2
- package/dist/agent/ai-sdk.d.ts.map +1 -1
- package/dist/agent/ai-sdk.js +1 -2
- package/dist/agent/ai-sdk.js.map +1 -1
- package/dist/agent/default-agent.d.ts +10 -32
- package/dist/agent/default-agent.d.ts.map +1 -1
- package/dist/agent/default-agent.js +6 -13
- package/dist/agent/default-agent.js.map +1 -1
- package/dist/agent/derived.d.ts +1 -1
- package/dist/agent/derived.js +1 -1
- package/dist/agent/error.d.ts +2 -4
- package/dist/agent/error.d.ts.map +1 -1
- package/dist/agent/error.js +3 -5
- package/dist/agent/error.js.map +1 -1
- package/dist/agent/executor.d.ts +23 -52
- package/dist/agent/executor.d.ts.map +1 -1
- package/dist/agent/executor.js +1 -2
- package/dist/agent/executor.js.map +1 -1
- package/dist/agent/index.d.ts +1 -1
- package/dist/agent/index.d.ts.map +1 -1
- package/dist/agent/index.js +10 -7
- package/dist/agent/index.js.map +1 -1
- package/dist/agent/invocation.d.ts +8 -6
- package/dist/agent/invocation.d.ts.map +1 -1
- package/dist/agent/invocation.js +9 -4
- package/dist/agent/invocation.js.map +1 -1
- package/dist/agent/ledger.d.ts +17 -12
- package/dist/agent/ledger.d.ts.map +1 -1
- package/dist/agent/ledger.js +6 -9
- package/dist/agent/ledger.js.map +1 -1
- package/dist/agent/model/adapter.d.ts +7 -4
- package/dist/agent/model/adapter.d.ts.map +1 -1
- package/dist/agent/model/adapter.js +2 -2
- package/dist/agent/model/adapter.js.map +1 -1
- package/dist/agent/model/router.d.ts +1 -1
- package/dist/agent/model/router.d.ts.map +1 -1
- package/dist/agent/model/router.js +1 -1
- package/dist/agent/model/router.js.map +1 -1
- package/dist/agent/model/sdk.d.ts +11 -1
- package/dist/agent/model/sdk.d.ts.map +1 -1
- package/dist/agent/model/sdk.js +54 -10
- package/dist/agent/model/sdk.js.map +1 -1
- package/dist/agent/observation.d.ts +7 -7
- package/dist/agent/observation.d.ts.map +1 -1
- package/dist/agent/observation.js +7 -7
- package/dist/agent/observation.js.map +1 -1
- package/dist/agent/phases.d.ts +3 -3
- package/dist/agent/phases.d.ts.map +1 -1
- package/dist/agent/phases.js +4 -4
- package/dist/agent/phases.js.map +1 -1
- package/dist/agent/primitives.d.ts +4 -25
- package/dist/agent/primitives.d.ts.map +1 -1
- package/dist/agent/primitives.js +3 -36
- package/dist/agent/primitives.js.map +1 -1
- package/dist/agent/prompts.d.ts +1 -1
- package/dist/agent/prompts.js +1 -1
- package/dist/agent/protocol.d.ts +2 -3
- package/dist/agent/protocol.d.ts.map +1 -1
- package/dist/agent/protocol.js +2 -3
- package/dist/agent/protocol.js.map +1 -1
- package/dist/agent/public.d.ts +8 -10
- package/dist/agent/public.d.ts.map +1 -1
- package/dist/agent/public.js +7 -8
- package/dist/agent/public.js.map +1 -1
- package/dist/agent/replay.d.ts +2 -2
- package/dist/agent/replay.d.ts.map +1 -1
- package/dist/agent/replay.js +1 -1
- package/dist/agent/secrets.d.ts +3 -3
- package/dist/agent/secrets.d.ts.map +1 -1
- package/dist/agent/secrets.js +4 -4
- package/dist/agent/secrets.js.map +1 -1
- package/dist/agent/step-cache.d.ts +1 -1
- package/dist/agent/step-cache.d.ts.map +1 -1
- package/dist/agent/step-cache.js +7 -1
- package/dist/agent/step-cache.js.map +1 -1
- package/dist/agent/tool-loop.d.ts +9 -33
- package/dist/agent/tool-loop.d.ts.map +1 -1
- package/dist/agent/tool-loop.js +15 -38
- package/dist/agent/tool-loop.js.map +1 -1
- package/dist/agent/tool.d.ts +4 -5
- package/dist/agent/tool.d.ts.map +1 -1
- package/dist/agent/tool.js +2 -7
- package/dist/agent/tool.js.map +1 -1
- package/dist/cache/anchors.d.ts +2 -2
- package/dist/cache/anchors.d.ts.map +1 -1
- package/dist/cache/anchors.js +1 -1
- package/dist/cache/context.d.ts +6 -1
- package/dist/cache/context.d.ts.map +1 -1
- package/dist/cache/context.js +2 -1
- package/dist/cache/context.js.map +1 -1
- package/dist/cache/decide.d.ts +1 -1
- package/dist/cache/decide.js +1 -1
- package/dist/cache/identity.d.ts +9 -9
- package/dist/cache/identity.d.ts.map +1 -1
- package/dist/cache/identity.js +6 -6
- package/dist/cache/identity.js.map +1 -1
- package/dist/cache/recorder.d.ts +4 -2
- package/dist/cache/recorder.d.ts.map +1 -1
- package/dist/cache/recorder.js +10 -1
- package/dist/cache/recorder.js.map +1 -1
- package/dist/cache/relocate.d.ts +2 -2
- package/dist/cache/relocate.d.ts.map +1 -1
- package/dist/cache/relocate.js +1 -1
- package/dist/cache/store.d.ts +1 -1
- package/dist/cache/store.js +1 -1
- package/dist/cache/trace.d.ts +16 -1
- package/dist/cache/trace.d.ts.map +1 -1
- package/dist/cache/trace.js +22 -1
- package/dist/cache/trace.js.map +1 -1
- package/dist/cli/bin.js +9 -1
- package/dist/cli/bin.js.map +1 -1
- package/dist/cli/cache.d.ts +25 -0
- package/dist/cli/cache.d.ts.map +1 -0
- package/dist/cli/cache.js +249 -0
- package/dist/cli/cache.js.map +1 -0
- package/dist/cli/docs-url.d.ts +3 -0
- package/dist/cli/docs-url.d.ts.map +1 -0
- package/dist/cli/docs-url.js +3 -0
- package/dist/cli/docs-url.js.map +1 -0
- package/dist/cli/guide.d.ts +4 -0
- package/dist/cli/guide.d.ts.map +1 -0
- package/dist/cli/guide.js +16 -0
- package/dist/cli/guide.js.map +1 -0
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +211 -42
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/init/agent-skill.d.ts +34 -0
- package/dist/cli/init/agent-skill.d.ts.map +1 -0
- package/dist/cli/init/agent-skill.js +44 -0
- package/dist/cli/init/agent-skill.js.map +1 -0
- package/dist/cli/init/{backends.d.ts → engines.d.ts} +39 -32
- package/dist/cli/init/engines.d.ts.map +1 -0
- package/dist/cli/init/{backends.js → engines.js} +43 -34
- package/dist/cli/init/engines.js.map +1 -0
- package/dist/cli/init/package.d.ts +19 -3
- package/dist/cli/init/package.d.ts.map +1 -1
- package/dist/cli/init/package.js +20 -20
- package/dist/cli/init/package.js.map +1 -1
- package/dist/cli/init/scaffold.d.ts +3 -3
- package/dist/cli/init/scaffold.d.ts.map +1 -1
- package/dist/cli/init/scaffold.js +13 -12
- package/dist/cli/init/scaffold.js.map +1 -1
- package/dist/cli/init.d.ts +14 -2
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +104 -25
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/signals.d.ts +5 -2
- package/dist/cli/signals.d.ts.map +1 -1
- package/dist/cli/signals.js +9 -3
- package/dist/cli/signals.js.map +1 -1
- package/dist/cli/skill.d.ts +16 -0
- package/dist/cli/skill.d.ts.map +1 -0
- package/dist/cli/skill.js +57 -0
- package/dist/cli/skill.js.map +1 -0
- package/dist/collect/collect.d.ts +40 -3
- package/dist/collect/collect.d.ts.map +1 -1
- package/dist/collect/collect.js +142 -19
- package/dist/collect/collect.js.map +1 -1
- package/dist/collect/registry.d.ts +1 -1
- package/dist/collect/registry.d.ts.map +1 -1
- package/dist/collect/registry.js +2 -2
- package/dist/collect/registry.js.map +1 -1
- package/dist/collect/select.d.ts +1 -1
- package/dist/collect/select.d.ts.map +1 -1
- package/dist/collect/select.js +81 -8
- package/dist/collect/select.js.map +1 -1
- package/dist/config/agent.d.ts +14 -2
- package/dist/config/agent.d.ts.map +1 -1
- package/dist/config/agent.js +36 -11
- package/dist/config/agent.js.map +1 -1
- package/dist/config/app.d.ts +76 -19
- package/dist/config/app.d.ts.map +1 -1
- package/dist/config/app.js +159 -104
- package/dist/config/app.js.map +1 -1
- package/dist/config/diagnose.d.ts +15 -0
- package/dist/config/diagnose.d.ts.map +1 -0
- package/dist/config/diagnose.js +122 -0
- package/dist/config/diagnose.js.map +1 -0
- package/dist/config/load.d.ts +9 -1
- package/dist/config/load.d.ts.map +1 -1
- package/dist/config/load.js +35 -3
- package/dist/config/load.js.map +1 -1
- package/dist/config/resolve.d.ts +8 -7
- package/dist/config/resolve.d.ts.map +1 -1
- package/dist/config/resolve.js +73 -40
- package/dist/config/resolve.js.map +1 -1
- package/dist/config/validate.d.ts +6 -2
- package/dist/config/validate.d.ts.map +1 -1
- package/dist/config/validate.js +20 -3
- package/dist/config/validate.js.map +1 -1
- package/dist/credentials.d.ts +1 -1
- package/dist/credentials.d.ts.map +1 -1
- package/dist/credentials.js +1 -1
- package/dist/credentials.js.map +1 -1
- package/dist/{backend → engine}/contract.d.ts +27 -19
- package/dist/engine/contract.d.ts.map +1 -0
- package/dist/{backend → engine}/contract.js +18 -18
- package/dist/engine/contract.js.map +1 -0
- package/dist/{backend → engine}/index.d.ts +141 -79
- package/dist/engine/index.d.ts.map +1 -0
- package/dist/{backend → engine}/index.js +62 -37
- package/dist/engine/index.js.map +1 -0
- package/dist/engine/session.d.ts +23 -0
- package/dist/engine/session.d.ts.map +1 -0
- package/dist/{backend → engine}/session.js +58 -51
- package/dist/engine/session.js.map +1 -0
- package/dist/{backend → engine}/surface.d.ts +11 -11
- package/dist/engine/surface.d.ts.map +1 -0
- package/dist/{backend → engine}/surface.js +2 -2
- package/dist/engine/surface.js.map +1 -0
- package/dist/engine/timing.d.ts.map +1 -0
- package/dist/engine/timing.js +8 -0
- package/dist/engine/timing.js.map +1 -0
- package/dist/expect/async.d.ts +1 -1
- package/dist/expect/async.d.ts.map +1 -1
- package/dist/expect/async.js +37 -1
- package/dist/expect/async.js.map +1 -1
- package/dist/expect/index.js +1 -1
- package/dist/expect/values.d.ts +1 -1
- package/dist/expect/values.d.ts.map +1 -1
- package/dist/expect/values.js +1 -1
- package/dist/expect/values.js.map +1 -1
- package/dist/index.d.ts +3 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -5
- package/dist/index.js.map +1 -1
- package/dist/internal/brands.d.ts +1 -1
- package/dist/internal/brands.d.ts.map +1 -1
- package/dist/internal/brands.js +1 -1
- package/dist/internal/brands.js.map +1 -1
- package/dist/internal/errors.d.ts +37 -19
- package/dist/internal/errors.d.ts.map +1 -1
- package/dist/internal/errors.js +94 -44
- package/dist/internal/errors.js.map +1 -1
- package/dist/internal/globs.d.ts +2 -2
- package/dist/internal/globs.d.ts.map +1 -1
- package/dist/internal/globs.js +2 -2
- package/dist/internal/globs.js.map +1 -1
- package/dist/internal/ids.d.ts +3 -3
- package/dist/internal/ids.d.ts.map +1 -1
- package/dist/internal/ids.js +3 -3
- package/dist/internal/ids.js.map +1 -1
- package/dist/internal/node-version.d.ts +10 -0
- package/dist/internal/node-version.d.ts.map +1 -0
- package/dist/internal/node-version.js +28 -0
- package/dist/internal/node-version.js.map +1 -0
- package/dist/internal/objects.d.ts +21 -0
- package/dist/internal/objects.d.ts.map +1 -0
- package/dist/internal/objects.js +14 -0
- package/dist/internal/objects.js.map +1 -0
- package/dist/internal/package-manager.d.ts +10 -0
- package/dist/internal/package-manager.d.ts.map +1 -0
- package/dist/internal/package-manager.js +43 -0
- package/dist/internal/package-manager.js.map +1 -0
- package/dist/internal/redact.d.ts +1 -1
- package/dist/internal/redact.d.ts.map +1 -1
- package/dist/internal/redact.js +1 -1
- package/dist/internal/redact.js.map +1 -1
- package/dist/internal/suggest.d.ts +16 -0
- package/dist/internal/suggest.d.ts.map +1 -0
- package/dist/internal/suggest.js +61 -0
- package/dist/internal/suggest.js.map +1 -0
- package/dist/internal/text.d.ts +1 -1
- package/dist/internal/text.d.ts.map +1 -1
- package/dist/internal/text.js +1 -1
- package/dist/internal/text.js.map +1 -1
- package/dist/internal/urls.d.ts +3 -3
- package/dist/internal/urls.d.ts.map +1 -1
- package/dist/internal/urls.js +8 -3
- package/dist/internal/urls.js.map +1 -1
- package/dist/locator/engine.d.ts +7 -7
- package/dist/locator/engine.d.ts.map +1 -1
- package/dist/locator/engine.js +14 -14
- package/dist/locator/engine.js.map +1 -1
- package/dist/locator/expression.d.ts +5 -3
- package/dist/locator/expression.d.ts.map +1 -1
- package/dist/locator/expression.js +25 -5
- package/dist/locator/expression.js.map +1 -1
- package/dist/locator/screen.d.ts +1 -1
- package/dist/locator/screen.d.ts.map +1 -1
- package/dist/locator/screen.js +3 -3
- package/dist/locator/screen.js.map +1 -1
- package/dist/report/build.d.ts +10 -9
- package/dist/report/build.d.ts.map +1 -1
- package/dist/report/build.js +25 -22
- package/dist/report/build.js.map +1 -1
- package/dist/report/code-frame.d.ts +8 -4
- package/dist/report/code-frame.d.ts.map +1 -1
- package/dist/report/code-frame.js +23 -18
- package/dist/report/code-frame.js.map +1 -1
- package/dist/report/debug-steps.js +1 -1
- package/dist/report/debug-steps.js.map +1 -1
- package/dist/report/format.d.ts +73 -0
- package/dist/report/format.d.ts.map +1 -0
- package/dist/report/format.js +147 -0
- package/dist/report/format.js.map +1 -0
- package/dist/report/junit.d.ts +1 -1
- package/dist/report/junit.d.ts.map +1 -1
- package/dist/report/junit.js +1 -1
- package/dist/report/junit.js.map +1 -1
- package/dist/report/list.d.ts +96 -47
- package/dist/report/list.d.ts.map +1 -1
- package/dist/report/list.js +489 -244
- package/dist/report/list.js.map +1 -1
- package/dist/report/live-window.d.ts +28 -0
- package/dist/report/live-window.d.ts.map +1 -0
- package/dist/report/live-window.js +102 -0
- package/dist/report/live-window.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 +1 -1
- package/dist/report/write.js.map +1 -1
- package/dist/run/budget.d.ts +2 -2
- package/dist/run/budget.js +2 -2
- package/dist/run/declared-processes.d.ts +30 -0
- package/dist/run/declared-processes.d.ts.map +1 -0
- package/dist/run/declared-processes.js +61 -0
- package/dist/run/declared-processes.js.map +1 -0
- package/dist/run/events.d.ts +30 -2
- package/dist/run/events.d.ts.map +1 -1
- package/dist/run/events.js.map +1 -1
- package/dist/run/execute.d.ts +19 -16
- package/dist/run/execute.d.ts.map +1 -1
- package/dist/run/execute.js +65 -67
- package/dist/run/execute.js.map +1 -1
- package/dist/run/fixture-recording.d.ts +10 -4
- package/dist/run/fixture-recording.d.ts.map +1 -1
- package/dist/run/fixture-recording.js +19 -6
- package/dist/run/fixture-recording.js.map +1 -1
- package/dist/run/fixtures.d.ts +2 -2
- package/dist/run/fixtures.d.ts.map +1 -1
- package/dist/run/fixtures.js +72 -44
- package/dist/run/fixtures.js.map +1 -1
- package/dist/run/in-process.d.ts +1 -1
- package/dist/run/in-process.js +1 -1
- package/dist/run/managed-process.d.ts +46 -9
- package/dist/run/managed-process.d.ts.map +1 -1
- package/dist/run/managed-process.js +136 -50
- package/dist/run/managed-process.js.map +1 -1
- package/dist/run/realm.d.ts +2 -2
- package/dist/run/realm.js +2 -2
- package/dist/run/runner.d.ts +23 -1
- package/dist/run/runner.d.ts.map +1 -1
- package/dist/run/runner.js +111 -40
- package/dist/run/runner.js.map +1 -1
- package/dist/run/scheduler.d.ts +5 -5
- package/dist/run/scheduler.d.ts.map +1 -1
- package/dist/run/scheduler.js +7 -7
- package/dist/run/scheduler.js.map +1 -1
- package/dist/run/serial.d.ts +10 -2
- package/dist/run/serial.d.ts.map +1 -1
- package/dist/run/serial.js +5 -3
- package/dist/run/serial.js.map +1 -1
- package/dist/run/sessions.d.ts +10 -10
- package/dist/run/sessions.d.ts.map +1 -1
- package/dist/run/sessions.js +11 -11
- package/dist/run/sessions.js.map +1 -1
- package/dist/run/steps.d.ts +3 -3
- package/dist/run/steps.d.ts.map +1 -1
- package/dist/run/steps.js +1 -1
- package/dist/run/steps.js.map +1 -1
- package/dist/run/units.d.ts +1 -1
- package/dist/run/units.js +1 -1
- package/dist/run/worker/entry.d.ts +1 -1
- package/dist/run/worker/entry.js +2 -2
- package/dist/run/worker/entry.js.map +1 -1
- package/dist/run/worker/protocol.d.ts +18 -6
- package/dist/run/worker/protocol.d.ts.map +1 -1
- package/dist/run/worker/protocol.js +1 -1
- package/dist/run/worker/protocol.js.map +1 -1
- package/dist/run/worker/session.d.ts +2 -2
- package/dist/run/worker/session.d.ts.map +1 -1
- package/dist/run/worker/session.js +5 -3
- package/dist/run/worker/session.js.map +1 -1
- package/dist/types.d.ts +58 -41
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +2 -2
- package/package.json +13 -12
- package/skills/e2e/SKILL.md +110 -0
- package/skills/e2e/references/agent.md +210 -0
- package/skills/e2e/references/debugging.md +81 -0
- package/skills/e2e/references/running.md +153 -0
- package/skills/e2e/references/setup.md +238 -0
- package/skills/e2e/references/writing-tests.md +289 -0
- package/dist/backend/contract.d.ts.map +0 -1
- package/dist/backend/contract.js.map +0 -1
- package/dist/backend/index.d.ts.map +0 -1
- package/dist/backend/index.js.map +0 -1
- package/dist/backend/session.d.ts +0 -23
- package/dist/backend/session.d.ts.map +0 -1
- package/dist/backend/session.js.map +0 -1
- package/dist/backend/surface.d.ts.map +0 -1
- package/dist/backend/surface.js.map +0 -1
- package/dist/backend/timing.d.ts.map +0 -1
- package/dist/backend/timing.js +0 -8
- package/dist/backend/timing.js.map +0 -1
- package/dist/cli/init/backends.d.ts.map +0 -1
- package/dist/cli/init/backends.js.map +0 -1
- package/dist/report/live-status.d.ts +0 -51
- package/dist/report/live-status.d.ts.map +0 -1
- package/dist/report/live-status.js +0 -163
- package/dist/report/live-status.js.map +0 -1
- package/dist/run/fixture-legacy.d.ts +0 -36
- package/dist/run/fixture-legacy.d.ts.map +0 -1
- package/dist/run/fixture-legacy.js +0 -143
- package/dist/run/fixture-legacy.js.map +0 -1
- /package/dist/{backend → engine}/timing.d.ts +0 -0
package/dist/report/list.js
CHANGED
|
@@ -1,85 +1,100 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* Human-readable list reporter, styled after vitest's
|
|
3
|
+
* default reporter: one block per test file and target, a `Failed Tests`
|
|
4
|
+
* section with code frames, and a padded summary. On a TTY a live window
|
|
5
|
+
* below the log shows the running files, their tests, and the counters.
|
|
6
|
+
*/
|
|
2
7
|
import path from 'node:path';
|
|
3
8
|
import picocolors from 'picocolors';
|
|
4
9
|
import { sanitizeText, truncateUtf8 } from '../internal/errors.js';
|
|
10
|
+
import { packageVersion } from '../internal/package-version.js';
|
|
5
11
|
import { collapseText } from '../internal/text.js';
|
|
6
12
|
import { codeFrame, userFrame } from './code-frame.js';
|
|
7
|
-
import {
|
|
8
|
-
|
|
13
|
+
import { addUsage, aiSegment, bounded, ellipsize, emptyCounters, emptyUsage, fileOutcome, formatClock, formatTime, formatTokens, padTitle, rule, stateString, statusBucket, stepsUsage, sumUsage, tally, terminalColumns, terminalRows, } from './format.js';
|
|
14
|
+
import { LiveWindow } from './live-window.js';
|
|
9
15
|
/** Step labels stay one glanceable line; the report holds the full text. */
|
|
10
16
|
const MAX_STEP_LABEL_BYTES = 72;
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
17
|
+
const F_POINTER = '❯';
|
|
18
|
+
const F_CHECK = '✓';
|
|
19
|
+
const F_CROSS = '×';
|
|
20
|
+
const F_DOWN = '↓';
|
|
21
|
+
const F_RIGHT = '→';
|
|
22
|
+
const F_DOWN_RIGHT = '↳';
|
|
23
|
+
const F_TREE_MIDDLE = '├──';
|
|
24
|
+
const F_TREE_END = '└──';
|
|
25
|
+
/** Indentation under a badge line, matching vitest's banner padding. */
|
|
26
|
+
const BADGE_PADDING = ' ';
|
|
27
|
+
/**
|
|
28
|
+
* Badge backgrounds, assigned to targets in declaration order. Bright variants
|
|
29
|
+
* because GitHub Actions renders plain yellow as dark brown, which swallows the
|
|
30
|
+
* black label.
|
|
31
|
+
*/
|
|
32
|
+
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
|
+
/** Indentation of the `→ first error line` under a failed test in a file block. */
|
|
41
|
+
const ERROR_GLANCE_INDENT = 7;
|
|
14
42
|
/** One-line, quoted step label bounded for the live view. */
|
|
15
43
|
function stepLabel(label) {
|
|
16
44
|
const flat = collapseText(label);
|
|
17
45
|
const shown = truncateUtf8(flat, MAX_STEP_LABEL_BYTES);
|
|
18
46
|
return `"${shown}${shown === flat ? '' : '…'}"`;
|
|
19
47
|
}
|
|
20
|
-
/** Human duration: milliseconds under a second, one decimal above. */
|
|
21
|
-
function formatDuration(ms) {
|
|
22
|
-
return ms < 1_000 ? `${ms}ms` : `${(ms / 1_000).toFixed(1)}s`;
|
|
23
|
-
}
|
|
24
|
-
/** Compact token count: plain under a thousand, `12.4k`, then `4.2M`. */
|
|
25
|
-
function formatTokens(count) {
|
|
26
|
-
if (count < 1_000)
|
|
27
|
-
return `${count}`;
|
|
28
|
-
if (count < 1_000_000)
|
|
29
|
-
return `${(count / 1_000).toFixed(1)}k`;
|
|
30
|
-
return `${(count / 1_000_000).toFixed(1)}M`;
|
|
31
|
-
}
|
|
32
|
-
/** USD with enough precision for sub-cent model calls. */
|
|
33
|
-
function formatCost(costUsd) {
|
|
34
|
-
return `$${costUsd.toFixed(costUsd < 0.1 ? 4 : 2)}`;
|
|
35
|
-
}
|
|
36
|
-
/** Accumulates the model usage of one step list into a running total. */
|
|
37
|
-
function addStepsUsage(usage, steps) {
|
|
38
|
-
for (const step of steps) {
|
|
39
|
-
if (step.model === undefined)
|
|
40
|
-
continue;
|
|
41
|
-
usage.calls += step.model.calls;
|
|
42
|
-
usage.tokens += step.model.inputTokens + step.model.outputTokens;
|
|
43
|
-
if (step.model.estimatedCostUsd !== undefined) {
|
|
44
|
-
usage.costUsd = (usage.costUsd ?? 0) + step.model.estimatedCostUsd;
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
48
|
/**
|
|
49
|
-
*
|
|
50
|
-
* members carry no attempts of their own; their usage arrives once per group
|
|
51
|
-
* through the `serial-group` event.
|
|
52
|
-
*/
|
|
53
|
-
function aiUsage(result) {
|
|
54
|
-
const usage = { calls: 0, tokens: 0, costUsd: undefined };
|
|
55
|
-
for (const attempt of result.attempts)
|
|
56
|
-
addStepsUsage(usage, attempt.steps);
|
|
57
|
-
return usage;
|
|
58
|
-
}
|
|
59
|
-
/** One dim `ai …` segment, or undefined when the test used no model. */
|
|
60
|
-
function aiSegment(usage) {
|
|
61
|
-
if (usage.calls === 0)
|
|
62
|
-
return undefined;
|
|
63
|
-
const cost = usage.costUsd === undefined ? '' : ` · ${formatCost(usage.costUsd)}`;
|
|
64
|
-
return `ai ${formatTokens(usage.tokens)} tokens${cost}`;
|
|
65
|
-
}
|
|
66
|
-
/**
|
|
67
|
-
* Live tail for one step event. Model and backend calls are the ones worth a
|
|
49
|
+
* Live tail for one step event. Model and engine calls are the ones worth a
|
|
68
50
|
* glance (`tool:` prefixes come from executor tool accounting); polls and
|
|
69
51
|
* policy decisions stay quiet.
|
|
70
52
|
*/
|
|
71
53
|
function eventTail(event) {
|
|
72
54
|
if (event.kind === 'model') {
|
|
73
55
|
const tokens = event.count !== undefined && event.count > 0 ? ` · ${formatTokens(event.count)} tokens` : '';
|
|
74
|
-
return `model turn ${
|
|
56
|
+
return `model turn ${formatTime(event.durationMs)}${tokens}`;
|
|
75
57
|
}
|
|
76
|
-
if (event.kind === '
|
|
77
|
-
const name = sanitizeText(event.name ?? '
|
|
78
|
-
const failed = event.status === 'passed' ? '' :
|
|
79
|
-
return `${truncateUtf8(name, 40)} ${
|
|
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}`;
|
|
80
62
|
}
|
|
81
63
|
return undefined;
|
|
82
64
|
}
|
|
65
|
+
/** The identity of one test-target pair across `test-started`, `step`, and `test-finished`. */
|
|
66
|
+
function pairKey(testId, target) {
|
|
67
|
+
return `${testId}@${target}`;
|
|
68
|
+
}
|
|
69
|
+
/** Details of an ordinary result: summed over its attempts, the error from the last. */
|
|
70
|
+
function attemptDetails(attempts) {
|
|
71
|
+
return {
|
|
72
|
+
durationMs: attempts.reduce((total, attempt) => total + attempt.durationMs, 0),
|
|
73
|
+
usage: stepsUsage(attempts.map((attempt) => attempt.steps)),
|
|
74
|
+
error: attempts[attempts.length - 1]?.error,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Details of one serial member, read from its group: member results carry no
|
|
79
|
+
* attempts of their own. Duration and usage sum over every group attempt the
|
|
80
|
+
* member ran in. The error is the member's own from the last attempt; a member
|
|
81
|
+
* that attempt never ran (skipped, or absent after a launch failure) inherits
|
|
82
|
+
* the attempt's error, since that is what stopped it.
|
|
83
|
+
*/
|
|
84
|
+
function serialMemberDetails(group, testId) {
|
|
85
|
+
const runs = group.attempts.map((attempt) => ({
|
|
86
|
+
attempt,
|
|
87
|
+
member: attempt.members.find((member) => member.testId === testId),
|
|
88
|
+
}));
|
|
89
|
+
const last = runs[runs.length - 1];
|
|
90
|
+
const own = last?.member;
|
|
91
|
+
const neverRan = own === undefined || own.status === 'skipped';
|
|
92
|
+
return {
|
|
93
|
+
durationMs: runs.reduce((total, run) => total + (run.member?.durationMs ?? 0), 0),
|
|
94
|
+
usage: stepsUsage(runs.map((run) => run.member?.steps ?? [])),
|
|
95
|
+
error: own?.error ?? (neverRan ? last?.attempt.error : undefined),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
83
98
|
const DEFAULT_OUTPUT = {
|
|
84
99
|
write: (line) => process.stdout.write(`${line}\n`),
|
|
85
100
|
raw: (text) => process.stdout.write(text),
|
|
@@ -91,31 +106,35 @@ const DEFAULT_OUTPUT = {
|
|
|
91
106
|
*/
|
|
92
107
|
export class ListReporter {
|
|
93
108
|
output;
|
|
94
|
-
passed = 0;
|
|
95
|
-
failed = 0;
|
|
96
|
-
flaky = 0;
|
|
97
|
-
skipped = 0;
|
|
98
|
-
projectRoot;
|
|
99
|
-
status;
|
|
100
|
-
/** Base live-status text of each running pair, keyed `${testId}@${target}`. */
|
|
101
|
-
liveBase = new Map();
|
|
102
|
-
/** Current step line of each running pair, rendered under its header. */
|
|
103
|
-
liveStep = new Map();
|
|
104
|
-
/** Rolling window of the running step's latest model and backend calls. */
|
|
105
|
-
liveEvents = new Map();
|
|
106
|
-
/** Pairs whose permanent header line has been written. */
|
|
107
|
-
headerPrinted = new Set();
|
|
108
|
-
/** Run-wide model usage, summed from every reported result. */
|
|
109
|
-
runAi = { calls: 0, tokens: 0, costUsd: undefined };
|
|
110
|
-
/** Colors follow live rendering: non-interactive sinks get plain text. */
|
|
111
109
|
pc;
|
|
110
|
+
window;
|
|
111
|
+
separator;
|
|
112
|
+
projectRoot;
|
|
113
|
+
/** Selected targets in declaration order; decides each badge's color. */
|
|
114
|
+
targets = [];
|
|
115
|
+
/** When the run began: the summary's `Start at` and the origin of `Duration`. */
|
|
116
|
+
startedAt = new Date();
|
|
117
|
+
/** File groups keyed `${target}\u0000${file}`, in first-seen order. */
|
|
118
|
+
groups = new Map();
|
|
119
|
+
/** Pairs executing right now, keyed by `pairKey`, in start order. */
|
|
120
|
+
running = new Map();
|
|
121
|
+
failures = [];
|
|
122
|
+
/**
|
|
123
|
+
* Serial groups whose members' results are still to come, by group id. The
|
|
124
|
+
* runner emits a group before its member results, and a member's details
|
|
125
|
+
* live only in the group.
|
|
126
|
+
*/
|
|
127
|
+
pendingSerial = new Map();
|
|
112
128
|
/** Run-level errors, printed once at the end; the stream carries them as they happen. */
|
|
113
129
|
errors = [];
|
|
130
|
+
/** Run-wide model usage, summed from every reported result and serial group. */
|
|
131
|
+
runUsage = emptyUsage();
|
|
114
132
|
constructor(output = DEFAULT_OUTPUT, options = {}) {
|
|
115
133
|
this.output = output;
|
|
116
134
|
const live = (options.live ?? process.stdout.isTTY === true) && output.raw !== undefined;
|
|
117
|
-
this.
|
|
118
|
-
this.
|
|
135
|
+
this.pc = picocolors.createColors(options.colors ?? (live || picocolors.isColorSupported));
|
|
136
|
+
this.separator = this.pc.dim(' > ');
|
|
137
|
+
this.window = new LiveWindow(live ? output.raw?.bind(output) : undefined, () => this.renderWindow());
|
|
119
138
|
}
|
|
120
139
|
/**
|
|
121
140
|
* The reporter as an event sink: one dispatch per fact. Bound, so it can be
|
|
@@ -127,10 +146,10 @@ export class ListReporter {
|
|
|
127
146
|
this.runStarted(event);
|
|
128
147
|
break;
|
|
129
148
|
case 'plan':
|
|
130
|
-
this.
|
|
149
|
+
this.plan(event);
|
|
131
150
|
break;
|
|
132
151
|
case 'notice':
|
|
133
|
-
this.
|
|
152
|
+
this.print(`${this.pc.dim('ℹ')} ${bounded(event.message)}`);
|
|
134
153
|
break;
|
|
135
154
|
case 'test-started':
|
|
136
155
|
this.testStarted(event);
|
|
@@ -155,233 +174,459 @@ export class ListReporter {
|
|
|
155
174
|
break;
|
|
156
175
|
}
|
|
157
176
|
};
|
|
177
|
+
/** Writes one permanent line without disturbing the live window. */
|
|
178
|
+
print(line) {
|
|
179
|
+
this.window.erase();
|
|
180
|
+
this.output.write(line);
|
|
181
|
+
this.window.redraw();
|
|
182
|
+
}
|
|
183
|
+
/** A target's colored badge, `|web|` when colors are off (vitest's convention). */
|
|
184
|
+
badge(target) {
|
|
185
|
+
const name = bounded(target);
|
|
186
|
+
if (!this.pc.isColorSupported)
|
|
187
|
+
return `|${name}|`;
|
|
188
|
+
const index = Math.max(0, this.targets.indexOf(target));
|
|
189
|
+
const color = BADGE_COLORS[index % BADGE_COLORS.length];
|
|
190
|
+
return this.pc.black(this.pc[color](` ${name} `));
|
|
191
|
+
}
|
|
192
|
+
/** A path relative to the project root when inside it, else as given. */
|
|
193
|
+
displayPath(target) {
|
|
194
|
+
const inside = this.projectRoot !== undefined && target.startsWith(`${this.projectRoot}${path.sep}`);
|
|
195
|
+
return bounded(inside ? path.relative(this.projectRoot, target) : target);
|
|
196
|
+
}
|
|
197
|
+
group(file, target) {
|
|
198
|
+
const key = `${target}\u0000${file}`;
|
|
199
|
+
let group = this.groups.get(key);
|
|
200
|
+
if (group === undefined) {
|
|
201
|
+
group = { file, target, planned: undefined, lines: [], streamed: false, printed: false };
|
|
202
|
+
this.groups.set(key, group);
|
|
203
|
+
}
|
|
204
|
+
return group;
|
|
205
|
+
}
|
|
206
|
+
runStarted(event) {
|
|
207
|
+
const { pc } = this;
|
|
208
|
+
this.projectRoot = event.projectRoot;
|
|
209
|
+
this.targets = event.targets;
|
|
210
|
+
this.startedAt = new Date();
|
|
211
|
+
const version = packageVersion(import.meta.url, '../../package.json', '0.0.0');
|
|
212
|
+
this.output.write('');
|
|
213
|
+
this.output.write(`${pc.bold(pc.black(pc.bgCyan(' RUN ')))} ${pc.cyan(`e2e v${version}`)} ${pc.gray(event.projectRoot)}`);
|
|
214
|
+
const details = [`run ${event.runId}`, `targets: ${event.targets.join(', ')}`];
|
|
215
|
+
if (event.ci)
|
|
216
|
+
details.push('CI');
|
|
217
|
+
this.output.write(BADGE_PADDING + pc.dim(details.join(' · ')));
|
|
218
|
+
this.output.write('');
|
|
219
|
+
this.window.start();
|
|
220
|
+
}
|
|
221
|
+
plan(event) {
|
|
222
|
+
for (const planned of event.files) {
|
|
223
|
+
this.group(planned.file, planned.target).planned = planned.tests;
|
|
224
|
+
}
|
|
225
|
+
this.window.redraw();
|
|
226
|
+
}
|
|
158
227
|
/** Acknowledged immediately, so a bounded teardown is not mistaken for a hang. */
|
|
159
228
|
runInterrupted(event) {
|
|
160
|
-
this.
|
|
229
|
+
this.print(this.pc.yellow(event.mode === 'graceful'
|
|
161
230
|
? 'interrupted: stopping the running test and tearing down (interrupt again to force)'
|
|
162
231
|
: 'interrupted again: tearing every worker down now'));
|
|
163
232
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
233
|
+
/**
|
|
234
|
+
* A worker began one test-target pair: show it in the live window. A serial
|
|
235
|
+
* member replaces the previous member of its group, which has finished
|
|
236
|
+
* executing even though its result only arrives with the whole group.
|
|
237
|
+
*/
|
|
169
238
|
testStarted(event) {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
if (this.liveBase.size > 1) {
|
|
177
|
-
for (const [other, otherBase] of this.liveBase)
|
|
178
|
-
this.redrawLive(other, otherBase);
|
|
239
|
+
if (event.serialId !== undefined) {
|
|
240
|
+
for (const [key, test] of this.running) {
|
|
241
|
+
if (test.serialId === event.serialId && test.group.target === event.target) {
|
|
242
|
+
this.running.delete(key);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
179
245
|
}
|
|
246
|
+
this.running.set(pairKey(event.testId, event.target), {
|
|
247
|
+
group: this.group(event.file, event.target),
|
|
248
|
+
serialId: event.serialId,
|
|
249
|
+
title: bounded(event.title),
|
|
250
|
+
startedMs: Date.now(),
|
|
251
|
+
step: undefined,
|
|
252
|
+
events: [],
|
|
253
|
+
streaming: false,
|
|
254
|
+
});
|
|
255
|
+
this.window.redraw();
|
|
180
256
|
}
|
|
181
257
|
/**
|
|
182
|
-
* Streams step progress of a running pair. With a single test running,
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
* latest calls transiently in
|
|
258
|
+
* Streams step progress of a running pair. With a single test running, its
|
|
259
|
+
* agent steps print permanently and chronologically under a header naming
|
|
260
|
+
* the file and test - the scrollback of a long agentic run reads without
|
|
261
|
+
* `--debug`. Deterministic steps are fast and many, so they only ever show
|
|
262
|
+
* in the live window. With parallel tests the stream would interleave, so
|
|
263
|
+
* each pair instead shows its current step and latest calls transiently in
|
|
264
|
+
* the live window, and finished agent steps print with the test they
|
|
265
|
+
* belong to.
|
|
188
266
|
*/
|
|
189
267
|
step(event) {
|
|
190
|
-
const
|
|
191
|
-
const
|
|
192
|
-
if (
|
|
268
|
+
const { pc } = this;
|
|
269
|
+
const running = this.running.get(pairKey(event.testId, event.target));
|
|
270
|
+
if (running === undefined)
|
|
193
271
|
return;
|
|
194
272
|
const { progress } = event;
|
|
195
273
|
switch (progress.phase) {
|
|
196
274
|
case 'start': {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
if (this.
|
|
200
|
-
|
|
201
|
-
|
|
275
|
+
running.step = `${progress.api} ${stepLabel(progress.label)}`;
|
|
276
|
+
running.events = [];
|
|
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
|
+
}
|
|
283
|
+
this.window.redraw();
|
|
202
284
|
break;
|
|
203
285
|
}
|
|
204
286
|
case 'event': {
|
|
205
287
|
const tail = eventTail(progress.event);
|
|
206
|
-
if (
|
|
288
|
+
if (running.step === undefined || tail === undefined)
|
|
207
289
|
break;
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
this.liveEvents.set(key, recent);
|
|
211
|
-
this.redrawLive(key, base);
|
|
290
|
+
running.events.push(tail);
|
|
291
|
+
this.window.redraw();
|
|
212
292
|
break;
|
|
213
293
|
}
|
|
214
294
|
case 'end': {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
295
|
+
running.step = undefined;
|
|
296
|
+
running.events = [];
|
|
297
|
+
if (progress.kind !== 'agent') {
|
|
298
|
+
this.window.redraw();
|
|
219
299
|
break;
|
|
220
|
-
|
|
300
|
+
}
|
|
301
|
+
const glyph = progress.status === 'passed' ? pc.green(F_CHECK) : pc.red(F_CROSS);
|
|
221
302
|
const calls = progress.modelCalls > 0
|
|
222
303
|
? ` · ${progress.modelCalls} model call${progress.modelCalls === 1 ? '' : 's'}`
|
|
223
304
|
: '';
|
|
224
305
|
const outcome = progress.status === 'passed' ? '' : ` ${progress.status}`;
|
|
225
|
-
|
|
226
|
-
|
|
306
|
+
const context = running.streaming ? '' : `${pc.dim(running.title)}${this.separator}`;
|
|
307
|
+
this.print(` ${glyph} ${context}${pc.dim(progress.api)} ${stepLabel(progress.label)} ` +
|
|
308
|
+
pc.dim(`${formatTime(progress.durationMs)}${calls}${outcome}`));
|
|
227
309
|
break;
|
|
228
310
|
}
|
|
229
311
|
}
|
|
230
312
|
}
|
|
231
|
-
/**
|
|
232
|
-
* Adds one serial group's model usage to the run totals. Member result
|
|
233
|
-
* records intentionally carry no attempts, so the group record is the one
|
|
234
|
-
* place their steps exist; summing here counts each member exactly once.
|
|
235
|
-
*/
|
|
313
|
+
/** Holds a serial group until each of its members' results has read its details. */
|
|
236
314
|
serialGroup(group) {
|
|
237
|
-
|
|
238
|
-
for (const member of attempt.members) {
|
|
239
|
-
addStepsUsage(this.runAi, member.steps);
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
/** Writes one permanent line without disturbing the live block. */
|
|
244
|
-
writeAboveStatus(line) {
|
|
245
|
-
this.status.erase();
|
|
246
|
-
this.output.write(line);
|
|
247
|
-
this.status.redraw();
|
|
315
|
+
this.pendingSerial.set(group.id, { group, remaining: group.memberTestIds.length });
|
|
248
316
|
}
|
|
249
317
|
/**
|
|
250
|
-
*
|
|
251
|
-
*
|
|
318
|
+
* The details behind one result. A serial member's come from its group; a
|
|
319
|
+
* group that never arrived (a stream out of contract) leaves them empty
|
|
320
|
+
* rather than failing the run.
|
|
252
321
|
*/
|
|
253
|
-
|
|
254
|
-
if (
|
|
255
|
-
return;
|
|
256
|
-
this.
|
|
257
|
-
|
|
322
|
+
detailsOf(result) {
|
|
323
|
+
if (result.serialGroupId === undefined)
|
|
324
|
+
return attemptDetails(result.attempts);
|
|
325
|
+
const pending = this.pendingSerial.get(result.serialGroupId);
|
|
326
|
+
if (pending === undefined)
|
|
327
|
+
return attemptDetails([]);
|
|
328
|
+
pending.remaining -= 1;
|
|
329
|
+
if (pending.remaining <= 0)
|
|
330
|
+
this.pendingSerial.delete(result.serialGroupId);
|
|
331
|
+
return serialMemberDetails(pending.group, result.test.id);
|
|
258
332
|
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
* test title. With parallel pairs the title stays as block context.
|
|
264
|
-
*/
|
|
265
|
-
redrawLive(key, base) {
|
|
266
|
-
const solo = this.liveBase.size === 1 && this.headerPrinted.has(key);
|
|
267
|
-
const step = this.liveStep.get(key);
|
|
268
|
-
if (step === undefined) {
|
|
269
|
-
this.status.start(key, solo ? undefined : base);
|
|
333
|
+
testFinished(result) {
|
|
334
|
+
// Unselected pairs are report-only: they never print and the plan never
|
|
335
|
+
// counted them.
|
|
336
|
+
if (!result.selected)
|
|
270
337
|
return;
|
|
338
|
+
this.running.delete(pairKey(result.test.id, result.target.name));
|
|
339
|
+
const group = this.group(result.test.file, result.target.name);
|
|
340
|
+
const { durationMs, usage, error } = this.detailsOf(result);
|
|
341
|
+
addUsage(this.runUsage, usage);
|
|
342
|
+
const title = bounded(result.test.titlePath.join(' > '));
|
|
343
|
+
group.lines.push({
|
|
344
|
+
title,
|
|
345
|
+
declarationIndex: result.test.declarationIndex,
|
|
346
|
+
status: result.status,
|
|
347
|
+
durationMs,
|
|
348
|
+
usage,
|
|
349
|
+
firstErrorLine: error === undefined ? undefined : bounded(error.message).split('\n')[0],
|
|
350
|
+
skipReason: result.skip === undefined ? undefined : bounded(result.skip.reason),
|
|
351
|
+
});
|
|
352
|
+
if (statusBucket(result.status) === 'failed') {
|
|
353
|
+
this.failures.push({ group, title, status: result.status, error });
|
|
271
354
|
}
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
const
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
355
|
+
if (group.planned !== undefined && group.lines.length >= group.planned)
|
|
356
|
+
this.printGroup(group);
|
|
357
|
+
this.window.redraw();
|
|
358
|
+
}
|
|
359
|
+
/** Counters over every reported result; the total is the plan's once it has arrived. */
|
|
360
|
+
testCounters() {
|
|
361
|
+
const counters = tally([...this.groups.values()].flatMap((group) => group.lines));
|
|
362
|
+
counters.total = 0;
|
|
363
|
+
for (const group of this.groups.values())
|
|
364
|
+
counters.total += group.planned ?? group.lines.length;
|
|
365
|
+
return counters;
|
|
366
|
+
}
|
|
367
|
+
/** Counters over test files; a file lands in an outcome once its block has printed. */
|
|
368
|
+
fileCounters() {
|
|
369
|
+
const counters = emptyCounters();
|
|
370
|
+
for (const group of this.groups.values()) {
|
|
371
|
+
counters.total += 1;
|
|
372
|
+
if (group.printed)
|
|
373
|
+
counters[fileOutcome(tally(group.lines))] += 1;
|
|
288
374
|
}
|
|
289
|
-
|
|
375
|
+
return counters;
|
|
290
376
|
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
377
|
+
/**
|
|
378
|
+
* Prints one file's block: the file line with its counts, then - when the
|
|
379
|
+
* file failed, had a flaky pass, streamed its steps, or is the run's only
|
|
380
|
+
* file - one line per test, as vitest lists tests for a failed module.
|
|
381
|
+
*/
|
|
382
|
+
printGroup(group) {
|
|
383
|
+
if (group.printed)
|
|
294
384
|
return;
|
|
385
|
+
group.printed = true;
|
|
386
|
+
const { pc } = this;
|
|
387
|
+
const counts = tally(group.lines);
|
|
388
|
+
const outcome = fileOutcome(counts);
|
|
389
|
+
const symbol = outcome === 'failed'
|
|
390
|
+
? pc.red(F_POINTER)
|
|
391
|
+
: outcome === 'skipped'
|
|
392
|
+
? pc.dim(pc.gray(F_DOWN))
|
|
393
|
+
: pc.green(F_CHECK);
|
|
394
|
+
const state = [
|
|
395
|
+
pc.dim(`${counts.total} test${counts.total === 1 ? '' : 's'}`),
|
|
396
|
+
counts.failed > 0 ? pc.red(`${counts.failed} failed`) : undefined,
|
|
397
|
+
counts.flaky > 0 ? pc.yellow(`${counts.flaky} flaky`) : undefined,
|
|
398
|
+
counts.skipped > 0 ? pc.yellow(`${counts.skipped} skipped`) : undefined,
|
|
399
|
+
]
|
|
400
|
+
.filter((part) => part !== undefined)
|
|
401
|
+
.join(pc.dim(' | '));
|
|
402
|
+
const durationMs = group.lines.reduce((total, line) => total + line.durationMs, 0);
|
|
403
|
+
const ai = aiSegment(sumUsage(group.lines));
|
|
404
|
+
const parts = [
|
|
405
|
+
` ${symbol} ${this.badge(group.target)} ${bounded(group.file)}`,
|
|
406
|
+
`${pc.dim('(')}${state}${pc.dim(')')}`,
|
|
407
|
+
];
|
|
408
|
+
// A file whose tests all skipped never ran; `0ms` would only invite a question.
|
|
409
|
+
if (durationMs > 0)
|
|
410
|
+
parts.push(pc.dim(formatTime(durationMs)));
|
|
411
|
+
if (ai !== undefined)
|
|
412
|
+
parts.push(pc.dim(ai));
|
|
413
|
+
this.print(parts.join(' '));
|
|
414
|
+
const verbose = counts.failed > 0 ||
|
|
415
|
+
counts.flaky > 0 ||
|
|
416
|
+
group.streamed ||
|
|
417
|
+
this.groups.size === 1;
|
|
418
|
+
if (!verbose)
|
|
419
|
+
return;
|
|
420
|
+
const ordered = group.lines.toSorted((a, b) => a.declarationIndex - b.declarationIndex);
|
|
421
|
+
for (const line of ordered) {
|
|
422
|
+
for (const rendered of this.testLine(line))
|
|
423
|
+
this.print(rendered);
|
|
295
424
|
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
this
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
const target = result.target.name;
|
|
305
|
-
const duration = result.attempts.reduce((total, attempt) => total + attempt.durationMs, 0);
|
|
306
|
-
const usage = aiUsage(result);
|
|
307
|
-
this.runAi.calls += usage.calls;
|
|
308
|
-
this.runAi.tokens += usage.tokens;
|
|
309
|
-
if (usage.costUsd !== undefined)
|
|
310
|
-
this.runAi.costUsd = (this.runAi.costUsd ?? 0) + usage.costUsd;
|
|
311
|
-
const ai = aiSegment(usage);
|
|
312
|
-
const aiSuffix = ai === undefined ? '' : ` \u00b7 ${ai}`;
|
|
313
|
-
switch (result.status) {
|
|
425
|
+
}
|
|
426
|
+
/** One test row under its file line, plus the first error line for a failure. */
|
|
427
|
+
testLine(line) {
|
|
428
|
+
const { pc } = this;
|
|
429
|
+
const duration = pc.dim(formatTime(line.durationMs));
|
|
430
|
+
const segment = aiSegment(line.usage);
|
|
431
|
+
const ai = segment === undefined ? '' : ` ${pc.dim(segment)}`;
|
|
432
|
+
switch (line.status) {
|
|
314
433
|
case 'passed':
|
|
315
|
-
|
|
316
|
-
this.output.write(`${this.pc.green('\u2713')} ${title} ${this.pc.dim(`[${target}] ${formatDuration(duration)}${aiSuffix}`)}`);
|
|
317
|
-
break;
|
|
434
|
+
return [` ${pc.green(F_CHECK)} ${line.title} ${duration}${ai}`];
|
|
318
435
|
case 'flaky':
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
436
|
+
return [` ${pc.yellow(F_CHECK)} ${line.title} ${pc.yellow('(flaky)')} ${duration}${ai}`];
|
|
437
|
+
case 'skipped': {
|
|
438
|
+
// `test.skip` without a message records the bare word; repeating it adds nothing.
|
|
439
|
+
const reason = line.skipReason === undefined || line.skipReason === 'skipped'
|
|
440
|
+
? ''
|
|
441
|
+
: pc.dim(pc.gray(` [${line.skipReason}]`));
|
|
442
|
+
return [` ${pc.dim(pc.gray(F_DOWN))} ${line.title}${reason}`];
|
|
443
|
+
}
|
|
326
444
|
default: {
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
}
|
|
334
|
-
this.writeFailureLocation(error.stack);
|
|
445
|
+
const status = line.status === 'failed' ? '' : pc.red(` (${line.status})`);
|
|
446
|
+
const rows = [` ${pc.red(`${F_CROSS} ${line.title}`)}${status} ${duration}${ai}`];
|
|
447
|
+
if (line.firstErrorLine !== undefined) {
|
|
448
|
+
// A glance line; the `Failed Tests` section carries the whole message.
|
|
449
|
+
const glance = ellipsize(line.firstErrorLine, terminalColumns() - ERROR_GLANCE_INDENT);
|
|
450
|
+
rows.push(` ${pc.red(`${F_RIGHT} ${glance}`)}`);
|
|
335
451
|
}
|
|
452
|
+
return rows;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* 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.
|
|
459
|
+
*/
|
|
460
|
+
summaryRows() {
|
|
461
|
+
const { pc } = this;
|
|
462
|
+
const files = this.fileCounters();
|
|
463
|
+
const tests = this.testCounters();
|
|
464
|
+
const rows = [
|
|
465
|
+
padTitle(pc, 'Test Files') + (files.total === 0 ? pc.dim('no test files') : stateString(pc, files)),
|
|
466
|
+
padTitle(pc, 'Tests') + (tests.total === 0 ? pc.dim('no tests executed') : stateString(pc, tests)),
|
|
467
|
+
];
|
|
468
|
+
const ai = aiSegment(this.runUsage);
|
|
469
|
+
if (ai !== undefined)
|
|
470
|
+
rows.push(padTitle(pc, 'AI') + `${ai} · ${this.runUsage.calls} model calls`);
|
|
471
|
+
if (this.errors.length > 0) {
|
|
472
|
+
const count = this.errors.length;
|
|
473
|
+
rows.push(padTitle(pc, 'Errors') + pc.bold(pc.red(`${count} error${count === 1 ? '' : 's'}`)));
|
|
474
|
+
}
|
|
475
|
+
rows.push(padTitle(pc, 'Start at') + formatClock(this.startedAt));
|
|
476
|
+
rows.push(padTitle(pc, 'Duration') + formatTime(Date.now() - this.startedAt.getTime()));
|
|
477
|
+
return rows;
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* The live window: running files and tests, then the summary. The block
|
|
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() {
|
|
487
|
+
const { pc } = this;
|
|
488
|
+
const now = Date.now();
|
|
489
|
+
const active = new Map();
|
|
490
|
+
for (const test of this.running.values()) {
|
|
491
|
+
const tests = active.get(test.group);
|
|
492
|
+
if (tests === undefined)
|
|
493
|
+
active.set(test.group, [test]);
|
|
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;
|
|
336
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
|
+
});
|
|
337
534
|
}
|
|
338
|
-
|
|
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;
|
|
339
541
|
}
|
|
340
|
-
/**
|
|
341
|
-
|
|
342
|
-
|
|
542
|
+
/** vitest's `Failed Tests` section: a banner, then each failure with its code frame. */
|
|
543
|
+
printFailures() {
|
|
544
|
+
const { pc } = this;
|
|
545
|
+
if (this.failures.length === 0)
|
|
343
546
|
return;
|
|
547
|
+
this.print('');
|
|
548
|
+
this.print(this.errorBanner(`Failed Tests ${this.failures.length}`));
|
|
549
|
+
this.print('');
|
|
550
|
+
this.failures.forEach(({ group, title, status, error }, index) => {
|
|
551
|
+
this.print(`${pc.bold(pc.bgRed(' FAIL '))} ${this.badge(group.target)} ${bounded(group.file)}${this.separator}${title}`);
|
|
552
|
+
if (error === undefined) {
|
|
553
|
+
this.print(pc.red(`${pc.bold(status)}: no error was recorded`));
|
|
554
|
+
}
|
|
555
|
+
else {
|
|
556
|
+
const [first = '', ...rest] = bounded(error.message).split('\n');
|
|
557
|
+
this.print(pc.red(`${pc.bold(bounded(error.code))}: ${first}`));
|
|
558
|
+
for (const line of rest)
|
|
559
|
+
this.print(pc.red(line));
|
|
560
|
+
this.printFailureLocation(error.stack);
|
|
561
|
+
}
|
|
562
|
+
const marker = `[${index + 1}/${this.failures.length}]`;
|
|
563
|
+
const { before, after } = rule(marker, 'right');
|
|
564
|
+
this.print('');
|
|
565
|
+
this.print(pc.red(pc.dim(`${before}${marker}${after}`)));
|
|
566
|
+
this.print('');
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
/** A full-width red rule with a centered `FAIL`-style label. */
|
|
570
|
+
errorBanner(message) {
|
|
571
|
+
const { pc } = this;
|
|
572
|
+
const label = pc.bold(pc.bgRed(` ${message} `));
|
|
573
|
+
const { before, after } = rule(label, 'center');
|
|
574
|
+
return `${pc.red(before)}${label}${pc.red(after)}`;
|
|
575
|
+
}
|
|
576
|
+
/** Names the user's failing line and renders a code frame around it. */
|
|
577
|
+
printFailureLocation(stack) {
|
|
578
|
+
const { pc } = this;
|
|
344
579
|
const frame = userFrame(stack, this.projectRoot);
|
|
345
580
|
if (frame === undefined)
|
|
346
581
|
return;
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
for (const line of codeFrame(frame))
|
|
351
|
-
this.output.write(` ${line}`);
|
|
582
|
+
this.print(pc.cyan(` ${pc.dim(F_POINTER)} ${bounded(this.displayPath(frame.file))}:${pc.dim(`${frame.line}:${frame.column}`)}`));
|
|
583
|
+
for (const line of codeFrame(frame, pc))
|
|
584
|
+
this.print(` ${line}`);
|
|
352
585
|
}
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
586
|
+
/**
|
|
587
|
+
* Run-level errors never arrive as results: they abort before, between, or
|
|
588
|
+
* after test execution. Without this the console shows only a bare exit
|
|
589
|
+
* code and the reason lives solely in report.json.
|
|
590
|
+
*/
|
|
591
|
+
printErrors() {
|
|
592
|
+
const { pc } = this;
|
|
593
|
+
if (this.errors.length === 0)
|
|
594
|
+
return;
|
|
595
|
+
this.print('');
|
|
596
|
+
this.print(this.errorBanner(`Run Errors ${this.errors.length}`));
|
|
597
|
+
this.print('');
|
|
358
598
|
for (const error of this.errors) {
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
}
|
|
599
|
+
const phase = error.phase === undefined ? '' : pc.dim(` (${bounded(error.phase)})`);
|
|
600
|
+
this.print(`${pc.bold(pc.bgRed(' ERROR '))} ${bounded(error.category)} error ${pc.dim(bounded(error.code))}${phase}`);
|
|
601
|
+
for (const line of bounded(error.message).split('\n'))
|
|
602
|
+
this.print(pc.red(line));
|
|
603
|
+
this.print('');
|
|
365
604
|
}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
this.
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
if (ai !== undefined) {
|
|
376
|
-
this.output.write(this.pc.dim(`${ai} \u00b7 ${this.runAi.calls} model calls`));
|
|
605
|
+
}
|
|
606
|
+
runFinished(event) {
|
|
607
|
+
const { pc } = this;
|
|
608
|
+
this.window.stop();
|
|
609
|
+
// An interrupted or crashed run leaves files without their full result
|
|
610
|
+
// set; print what they have so nothing that ran goes unreported.
|
|
611
|
+
for (const group of this.groups.values()) {
|
|
612
|
+
if (!group.printed && group.lines.length > 0)
|
|
613
|
+
this.printGroup(group);
|
|
377
614
|
}
|
|
378
|
-
this.
|
|
615
|
+
this.printFailures();
|
|
616
|
+
this.printErrors();
|
|
617
|
+
this.print('');
|
|
618
|
+
for (const row of this.summaryRows())
|
|
619
|
+
this.print(row);
|
|
620
|
+
this.print(padTitle(pc, 'Report') +
|
|
621
|
+
(event.reportPath === undefined ? pc.dim('(not written)') : this.displayPath(event.reportPath)));
|
|
379
622
|
if (event.junitPath !== undefined) {
|
|
380
|
-
this.
|
|
623
|
+
this.print(padTitle(pc, 'JUnit') + this.displayPath(event.junitPath));
|
|
381
624
|
}
|
|
382
625
|
if (event.aiTracePath !== undefined) {
|
|
383
|
-
this.
|
|
626
|
+
const shown = this.displayPath(event.aiTracePath);
|
|
627
|
+
this.print(padTitle(pc, 'AI trace') + `${shown} ${pc.dim(`(open with: npx unbox-ai ${shown})`)}`);
|
|
384
628
|
}
|
|
629
|
+
this.print('');
|
|
385
630
|
}
|
|
386
631
|
}
|
|
387
632
|
//# sourceMappingURL=list.js.map
|