niceeval 0.6.1 → 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/INDEX.md +23 -23
- package/README.zh.md +6 -6
- package/dist/agents/types.d.ts +69 -7
- package/dist/context/types.d.ts +32 -12
- package/dist/i18n/en.d.ts +54 -0
- package/dist/i18n/zh-CN.d.ts +57 -3
- package/dist/o11y/types.d.ts +16 -2
- package/dist/report/aggregate.d.ts +32 -24
- package/dist/report/aggregate.js +158 -50
- package/dist/report/built-in/index.d.ts +2 -0
- package/dist/report/built-in/index.js +8 -0
- package/dist/report/components.d.ts +93 -160
- package/dist/report/components.js +377 -114
- package/dist/report/compute.d.ts +87 -81
- package/dist/report/compute.js +597 -417
- package/dist/report/flag.d.ts +32 -6
- package/dist/report/flag.js +92 -4
- package/dist/report/format.d.ts +19 -11
- package/dist/report/format.js +30 -13
- package/dist/report/index.d.ts +16 -16
- package/dist/report/index.js +20 -21
- package/dist/report/load.js +3 -2
- package/dist/report/locale.d.ts +57 -33
- package/dist/report/locale.js +122 -56
- package/dist/report/metrics.d.ts +23 -4
- package/dist/report/metrics.js +110 -25
- package/dist/report/primitives.d.ts +48 -15
- package/dist/report/primitives.js +135 -26
- package/dist/report/react/AttemptList.d.ts +9 -7
- package/dist/report/react/AttemptList.js +17 -10
- package/dist/report/react/DeltaTable.js +19 -18
- package/dist/report/react/EvalList.d.ts +4 -4
- package/dist/report/react/EvalList.js +0 -0
- package/dist/report/react/ExperimentComparison.d.ts +10 -0
- package/dist/report/react/ExperimentComparison.js +12 -0
- package/dist/report/react/ExperimentList.d.ts +4 -3
- package/dist/report/react/ExperimentList.js +17 -18
- package/dist/report/react/MetricBars.js +5 -4
- package/dist/report/react/MetricLine.js +12 -5
- package/dist/report/react/MetricMatrix.js +1 -1
- package/dist/report/react/MetricScatter.js +59 -28
- package/dist/report/react/MetricTable.js +2 -12
- package/dist/report/react/ScopeSummary.d.ts +10 -0
- package/dist/report/react/ScopeSummary.js +17 -0
- package/dist/report/react/Scoreboard.js +6 -6
- package/dist/report/react/cell.js +2 -2
- package/dist/report/react/chart-math.d.ts +23 -6
- package/dist/report/react/chart-math.js +71 -19
- package/dist/report/react/fixtures.d.ts +5 -9
- package/dist/report/react/fixtures.js +110 -147
- package/dist/report/react/index.d.ts +15 -5
- package/dist/report/react/index.js +18 -7
- package/dist/report/report.d.ts +137 -16
- package/dist/report/report.js +259 -28
- package/dist/report/text/faces.d.ts +17 -19
- package/dist/report/text/faces.js +253 -184
- package/dist/report/text/plot.js +1 -1
- package/dist/report/text/table.js +38 -7
- package/dist/report/tree.d.ts +90 -40
- package/dist/report/tree.js +252 -94
- package/dist/report/types.d.ts +247 -284
- package/dist/report/types.js +4 -3
- package/dist/report/web.d.ts +21 -5
- package/dist/report/web.js +42 -16
- package/dist/results/select.d.ts +38 -16
- package/dist/results/select.js +73 -25
- package/dist/results/types.d.ts +49 -14
- package/dist/runner/feedback/sink.d.ts +110 -0
- package/dist/runner/types.d.ts +513 -22
- package/dist/sandbox/docker.d.ts +23 -2
- package/dist/sandbox/e2b.d.ts +15 -1
- package/dist/sandbox/errors.d.ts +30 -3
- package/dist/sandbox/io-retry.d.ts +17 -0
- package/dist/sandbox/registry.d.ts +2 -0
- package/dist/sandbox/resolve.d.ts +18 -5
- package/dist/sandbox/retry.d.ts +11 -1
- package/dist/sandbox/types.d.ts +39 -5
- package/dist/sandbox/vercel.d.ts +7 -1
- package/dist/scoring/coverage.d.ts +30 -0
- package/dist/scoring/display.d.ts +21 -0
- package/dist/scoring/display.js +120 -0
- package/dist/scoring/types.d.ts +103 -20
- package/dist/shared/aggregate.d.ts +4 -2
- package/dist/shared/aggregate.js +8 -7
- package/dist/shared/types.d.ts +28 -0
- package/dist/tty-line.d.ts +0 -4
- package/dist/util.d.ts +23 -0
- package/docs-site/zh/README.md +44 -0
- package/docs-site/zh/examples/ai-agent-application.mdx +63 -0
- package/docs-site/zh/examples/coding-agent-extensions.mdx +57 -0
- package/docs-site/zh/examples/index.mdx +50 -0
- package/docs-site/zh/{concepts → explanation}/adapter.mdx +31 -13
- package/docs-site/zh/{concepts → explanation}/assert.mdx +7 -7
- package/docs-site/zh/{concepts → explanation}/drive.mdx +8 -8
- package/docs-site/zh/{concepts → explanation}/evals.mdx +4 -4
- package/docs-site/zh/{concepts → explanation}/experiment.mdx +8 -8
- package/docs-site/zh/{concepts → explanation}/hitl.mdx +8 -8
- package/docs-site/zh/{concepts → explanation}/judge.mdx +5 -5
- package/docs-site/zh/{concepts → explanation}/overview.mdx +11 -11
- package/docs-site/zh/{guides → explanation}/runner.mdx +18 -8
- package/docs-site/zh/{concepts → explanation}/tier.mdx +6 -6
- package/docs-site/zh/{guides → how-to}/agent-feedback-loop.mdx +35 -33
- package/docs-site/zh/{guides → how-to}/authoring.mdx +35 -2
- package/docs-site/zh/{guides → how-to}/ci-integration.mdx +23 -12
- package/docs-site/zh/{guides → how-to}/connect-otel.mdx +6 -6
- package/docs-site/zh/{guides → how-to}/connect-your-agent.mdx +47 -21
- package/docs-site/zh/{guides → how-to}/custom-reports.mdx +34 -39
- package/docs-site/zh/{guides → how-to}/dataset-fanout.mdx +25 -3
- package/docs-site/zh/{guides → how-to}/experiments.mdx +12 -5
- package/docs-site/zh/how-to/publish-report.mdx +105 -0
- package/docs-site/zh/{guides → how-to}/reporters.mdx +2 -2
- package/docs-site/zh/{guides → how-to}/sandbox-agent.mdx +56 -7
- package/docs-site/zh/how-to/sandbox-providers.mdx +350 -0
- package/docs-site/zh/{guides → how-to}/scoring-guide.mdx +4 -4
- package/docs-site/zh/{guides → how-to}/viewing-results.mdx +82 -39
- package/docs-site/zh/{guides → how-to}/write-experiment.mdx +6 -4
- package/docs-site/zh/{guides → how-to}/write-send.mdx +30 -14
- package/docs-site/zh/index.mdx +24 -26
- package/docs-site/zh/introduction.mdx +8 -8
- package/docs-site/zh/reference/builtin-agents.mdx +32 -5
- package/docs-site/zh/reference/capabilities.mdx +8 -8
- package/docs-site/zh/reference/cli.mdx +40 -12
- package/docs-site/zh/reference/define-agent.mdx +58 -5
- package/docs-site/zh/reference/define-config.mdx +1 -1
- package/docs-site/zh/reference/define-eval.mdx +42 -9
- package/docs-site/zh/reference/events.mdx +3 -3
- package/docs-site/zh/reference/expect.mdx +26 -1
- package/docs-site/zh/{guides → reference}/official-adapters.mdx +32 -8
- package/docs-site/zh/{guides → reference}/report-components.mdx +45 -33
- package/docs-site/zh/{guides → reference}/results-data.mdx +21 -13
- package/docs-site/zh/troubleshooting/debug-sandbox.mdx +57 -0
- package/docs-site/zh/troubleshooting/debugging.mdx +212 -0
- package/docs-site/zh/{quickstart.mdx → tutorials/quickstart.mdx} +5 -17
- package/package.json +10 -2
- package/src/agents/ai-sdk-otel.test.ts +1 -0
- package/src/agents/ai-sdk.test.ts +3 -0
- package/src/agents/ai-sdk.ts +3 -0
- package/src/agents/bub-install-spec.test.ts +34 -0
- package/src/agents/bub-install-spec.ts +32 -0
- package/src/agents/bub.ts +31 -32
- package/src/agents/claude-code.test.ts +130 -9
- package/src/agents/claude-code.ts +76 -4
- package/src/agents/codex.test.ts +189 -40
- package/src/agents/codex.ts +155 -14
- package/src/agents/coding-cli-versions.test.ts +15 -0
- package/src/agents/coding-cli-versions.ts +3 -0
- package/src/agents/index.ts +13 -2
- package/src/agents/langgraph.test.ts +204 -0
- package/src/agents/langgraph.ts +495 -0
- package/src/agents/marketplace.ts +85 -0
- package/src/agents/native-config.test.ts +179 -0
- package/src/agents/native-config.ts +267 -0
- package/src/agents/openai-compat.test.ts +1 -0
- package/src/agents/openai-compat.ts +1 -1
- package/src/agents/openclaw.test.ts +31 -0
- package/src/agents/openclaw.ts +171 -0
- package/src/agents/plugin-config.test.ts +1 -0
- package/src/agents/sdk-streams.test.ts +79 -0
- package/src/agents/sdk-streams.ts +55 -10
- package/src/agents/skills.test.ts +1 -0
- package/src/agents/streaming.test.ts +3 -9
- package/src/agents/streaming.ts +2 -2
- package/src/agents/types.ts +71 -8
- package/src/agents/ui-message-stream.test.ts +3 -0
- package/src/cli.ts +446 -124
- package/src/context/context.test.ts +51 -12
- package/src/context/context.ts +162 -30
- package/src/context/session.test.ts +2 -1
- package/src/context/session.ts +115 -7
- package/src/context/types.ts +30 -12
- package/src/define.test.ts +13 -8
- package/src/define.ts +25 -4
- package/src/expect/index.ts +53 -23
- package/src/i18n/en.ts +81 -17
- package/src/i18n/zh-CN.ts +80 -17
- package/src/o11y/cost.test.ts +1 -0
- package/src/o11y/execution-tree.test.ts +1 -20
- package/src/o11y/otlp/mappers/claude-code.test.ts +1 -0
- package/src/o11y/otlp/parse.test.ts +1 -0
- package/src/o11y/otlp/turn-otel.test.ts +1 -0
- package/src/o11y/parsers/bub.test.ts +1 -0
- package/src/o11y/parsers/claude-code.test.ts +1 -34
- package/src/o11y/parsers/openclaw.test.ts +154 -0
- package/src/o11y/parsers/openclaw.ts +310 -0
- package/src/o11y/prices.json +746 -311
- package/src/o11y/tool-names.test.ts +1 -0
- package/src/o11y/types.ts +16 -2
- package/src/report/aggregate.ts +178 -61
- package/src/report/built-in/index.tsx +9 -0
- package/src/report/components.tsx +625 -279
- package/src/report/compute.ts +723 -491
- package/src/report/dual-render.test.tsx +741 -1024
- package/src/report/flag.ts +104 -12
- package/src/report/format.ts +32 -12
- package/src/report/index.ts +119 -46
- package/src/report/load.ts +3 -2
- package/src/report/locale.ts +136 -65
- package/src/report/metrics.ts +108 -25
- package/src/report/primitives.tsx +196 -45
- package/src/report/react/AttemptList.tsx +30 -43
- package/src/report/react/DeltaTable.tsx +63 -45
- package/src/report/react/EvalList.tsx +0 -0
- package/src/report/react/ExperimentComparison.tsx +73 -0
- package/src/report/react/ExperimentList.tsx +50 -32
- package/src/report/react/MetricBars.tsx +5 -4
- package/src/report/react/MetricLine.tsx +13 -8
- package/src/report/react/MetricMatrix.tsx +2 -2
- package/src/report/react/MetricScatter.tsx +86 -34
- package/src/report/react/MetricTable.tsx +4 -76
- package/src/report/react/ScopeSummary.tsx +86 -0
- package/src/report/react/Scoreboard.tsx +28 -10
- package/src/report/react/cell.tsx +2 -2
- package/src/report/react/chart-math.test.ts +85 -0
- package/src/report/react/chart-math.ts +101 -22
- package/src/report/react/enhance.js +89 -5
- package/src/report/react/fixtures.ts +114 -154
- package/src/report/react/index.tsx +24 -39
- package/src/report/react/render.test.tsx +138 -158
- package/src/report/react/styles.css +243 -82
- package/src/report/report.test.ts +779 -841
- package/src/report/report.ts +423 -41
- package/src/report/text/faces.ts +290 -193
- package/src/report/text/plot.ts +1 -1
- package/src/report/text/table.ts +44 -7
- package/src/report/tree.ts +362 -104
- package/src/report/types.ts +261 -271
- package/src/report/web.ts +63 -20
- package/src/results/annotated-source.test.ts +62 -9
- package/src/results/annotated-source.ts +64 -6
- package/src/results/attempt-evidence.test.ts +13 -11
- package/src/results/attempt-evidence.ts +20 -13
- package/src/results/attempt-source.ts +6 -3
- package/src/results/copy.ts +150 -60
- package/src/results/host-equivalence.test.ts +34 -20
- package/src/results/index.ts +12 -4
- package/src/results/locator.test.ts +1 -22
- package/src/results/open.ts +15 -5
- package/src/results/publish.ts +149 -0
- package/src/results/results.test.ts +89 -54
- package/src/results/select.ts +104 -34
- package/src/results/truncate.ts +90 -0
- package/src/results/types.ts +43 -14
- package/src/results/writer.ts +31 -13
- package/src/runner/attempt.test.ts +138 -7
- package/src/runner/attempt.ts +603 -104
- package/src/runner/discover.test.ts +47 -0
- package/src/runner/discover.ts +36 -2
- package/src/runner/eval-source.test.ts +1 -27
- package/src/runner/feedback/agent.test.ts +504 -0
- package/src/runner/feedback/agent.ts +409 -0
- package/src/runner/feedback/ci.test.ts +562 -0
- package/src/runner/feedback/ci.ts +401 -0
- package/src/runner/feedback/coordinator.test.ts +317 -0
- package/src/runner/feedback/coordinator.ts +397 -0
- package/src/runner/feedback/failure.ts +40 -0
- package/src/runner/feedback/human.test.ts +616 -0
- package/src/runner/feedback/human.ts +535 -0
- package/src/runner/feedback/index.ts +66 -0
- package/src/runner/feedback/io.ts +78 -0
- package/src/runner/feedback/profile.test.ts +50 -0
- package/src/runner/feedback/profile.ts +58 -0
- package/src/runner/feedback/reducer.test.ts +395 -0
- package/src/runner/feedback/reducer.ts +260 -0
- package/src/runner/feedback/renderer.ts +82 -0
- package/src/runner/feedback/sink.ts +203 -0
- package/src/runner/feedback/testing.ts +106 -0
- package/src/runner/ledger.test.ts +230 -0
- package/src/runner/ledger.ts +329 -0
- package/src/runner/report.test.ts +128 -3
- package/src/runner/report.ts +33 -9
- package/src/runner/reporters/artifacts.ts +8 -2
- package/src/runner/reporters/braintrust.test.ts +8 -7
- package/src/runner/reporters/braintrust.ts +9 -2
- package/src/runner/reporters/index.ts +2 -2
- package/src/runner/reporters/json.test.ts +162 -0
- package/src/runner/reporters/json.ts +35 -8
- package/src/runner/reporters/shared.ts +1 -5
- package/src/runner/run.test.ts +760 -3
- package/src/runner/run.ts +243 -37
- package/src/runner/sandbox-prep.ts +3 -42
- package/src/runner/timing.ts +158 -0
- package/src/runner/types.ts +518 -22
- package/src/sandbox/checkpoint.test.ts +55 -0
- package/src/sandbox/checkpoint.ts +29 -8
- package/src/sandbox/cli-commands.ts +407 -0
- package/src/sandbox/docker.ts +115 -16
- package/src/sandbox/e2b-agent-template.test.ts +56 -0
- package/src/sandbox/e2b-agent-template.ts +94 -0
- package/src/sandbox/e2b.ts +74 -9
- package/src/sandbox/errors.ts +111 -4
- package/src/sandbox/index.ts +2 -0
- package/src/sandbox/io-retry.test.ts +58 -0
- package/src/sandbox/io-retry.ts +45 -0
- package/src/sandbox/keep-registry.test.ts +86 -0
- package/src/sandbox/keep-registry.ts +142 -0
- package/src/sandbox/keep.ts +178 -0
- package/src/sandbox/paths.test.ts +1 -0
- package/src/sandbox/paths.ts +19 -8
- package/src/sandbox/registry.ts +20 -3
- package/src/sandbox/resolve.ts +76 -11
- package/src/sandbox/retry.test.ts +70 -0
- package/src/sandbox/retry.ts +46 -4
- package/src/sandbox/types.ts +44 -6
- package/src/sandbox/vercel.ts +43 -20
- package/src/scoring/collector.ts +60 -17
- package/src/scoring/coverage.ts +95 -0
- package/src/scoring/diff.ts +81 -0
- package/src/scoring/display.test.ts +121 -0
- package/src/scoring/display.ts +133 -0
- package/src/scoring/evidence.test.ts +189 -0
- package/src/scoring/judge.test.ts +142 -0
- package/src/scoring/judge.ts +15 -18
- package/src/scoring/scoped.ts +217 -50
- package/src/scoring/types.ts +117 -20
- package/src/scoring/verdict.ts +16 -4
- package/src/shared/aggregate.ts +8 -6
- package/src/shared/types.ts +31 -0
- package/src/show/compose.ts +50 -67
- package/src/show/index.ts +127 -56
- package/src/show/render.ts +662 -131
- package/src/show/report-host.test.ts +188 -0
- package/src/show/report-host.ts +375 -0
- package/src/show/show.test.ts +320 -54
- package/src/tty-line.ts +8 -26
- package/src/util.test.ts +1 -0
- package/src/util.ts +41 -0
- package/src/view/app/App.test.tsx +69 -0
- package/src/view/app/App.tsx +144 -48
- package/src/view/app/components/AttemptModal.tsx +423 -11
- package/src/view/app/components/CodeView.tsx +41 -14
- package/src/view/app/components/CopyControls.tsx +2 -2
- package/src/view/app/i18n.ts +37 -17
- package/src/view/app/lib/attempt-route.test.ts +1 -0
- package/src/view/app/lib/verdict.ts +7 -9
- package/src/view/app/main.tsx +13 -8
- package/src/view/app/pages/{RunsPage.tsx → AttemptsPage.tsx} +6 -6
- package/src/view/app/types.ts +4 -1
- package/src/view/artifact-serving.test.ts +2 -1
- package/src/view/client-dist/app.css +1 -1
- package/src/view/client-dist/app.js +17 -17
- package/src/view/data.test.ts +10 -3
- package/src/view/data.ts +155 -49
- package/src/view/index.ts +56 -41
- package/src/view/server.ts +37 -15
- package/src/view/shared/types.ts +34 -5
- package/src/view/styles.css +227 -0
- package/src/view/view-report.test.ts +167 -62
- package/dist/report/built-ins/experiment-comparison.d.ts +0 -1
- package/dist/report/built-ins/experiment-comparison.js +0 -13
- package/dist/report/built-ins/index.d.ts +0 -1
- package/dist/report/built-ins/index.js +0 -2
- package/dist/report/react/GroupSummary.d.ts +0 -8
- package/dist/report/react/GroupSummary.js +0 -8
- package/dist/report/react/RunOverview.d.ts +0 -8
- package/dist/report/react/RunOverview.js +0 -12
- package/docs-site/zh/example/ai-agent-application.mdx +0 -152
- package/docs-site/zh/example/claude-code-codex-plugin.mdx +0 -167
- package/docs-site/zh/example/claude-code-codex-skill.mdx +0 -152
- package/docs-site/zh/example/showcase.mdx +0 -39
- package/docs-site/zh/guides/publish-report.mdx +0 -91
- package/docs-site/zh/guides/sandbox-providers.mdx +0 -102
- package/src/report/built-in-user-parity.test.tsx +0 -640
- package/src/report/built-ins/experiment-comparison.tsx +0 -19
- package/src/report/built-ins/index.ts +0 -2
- package/src/report/react/GroupSummary.tsx +0 -66
- package/src/report/react/RunOverview.tsx +0 -109
- package/src/runner/reporters/console.ts +0 -70
- package/src/runner/reporters/live.test.ts +0 -56
- package/src/runner/reporters/live.ts +0 -247
- package/src/runner/reporters/quiet.test.ts +0 -66
- package/src/runner/reporters/quiet.ts +0 -49
- package/src/runner/reporters/table.ts +0 -277
- /package/docs-site/zh/{example/tier1-ai-sdk-v7.mdx → examples/integrations/ai-sdk-v7.mdx} +0 -0
- /package/docs-site/zh/{example/tier1-claude-sdk.mdx → examples/integrations/claude-sdk.mdx} +0 -0
- /package/docs-site/zh/{example/tier1-codex-sdk.mdx → examples/integrations/codex-sdk.mdx} +0 -0
- /package/docs-site/zh/{example/tier1-langgraph.mdx → examples/integrations/langgraph.mdx} +0 -0
- /package/docs-site/zh/{example/tier1-pi-sdk.mdx → examples/integrations/pi-sdk.mdx} +0 -0
- /package/docs-site/zh/{guides → how-to}/fixtures.mdx +0 -0
|
@@ -0,0 +1,616 @@
|
|
|
1
|
+
// cases: docs/engineering/unit-tests/experiments-runner/cases.md
|
|
2
|
+
// Human renderer 测试:全部经真实 FeedbackCoordinator + createFakeFeedbackIO 驱动(不
|
|
3
|
+
// monkey-patch process/Date/setInterval),断言具体文本/结构属性而不是整段 ANSI snapshot diff——
|
|
4
|
+
// 逐条对应 plan section D 的 checklist(见文件内每个 describe 块的注释)。
|
|
5
|
+
//
|
|
6
|
+
// 只测「clear/append/redraw 按 coordinator 保证的顺序调用」这一层已经由 coordinator.test.ts
|
|
7
|
+
// 用一个 recordingRenderer 覆盖过;这里额外验证的是 human 渲染器自己产出的具体内容/宽高约束/
|
|
8
|
+
// 节流行为,以及它如何使用 clear/append/redraw 三个钩子。
|
|
9
|
+
|
|
10
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
11
|
+
import { createFeedbackCoordinator, type FeedbackCoordinator } from "./coordinator.ts";
|
|
12
|
+
import { createFakeFeedbackIO, type FakeFeedbackIO } from "./testing.ts";
|
|
13
|
+
import { activeFeedbackSinkCount } from "./sink.ts";
|
|
14
|
+
import { createHumanRenderer, formatElapsed, formatTokenCount, renderDurableLines } from "./human.ts";
|
|
15
|
+
import { createInitialRunFeedbackState } from "./reducer.ts";
|
|
16
|
+
import type { AttemptLocator } from "../../results/locator.ts";
|
|
17
|
+
import type { AttemptRef, RunCompletion, RunFeedbackPlan, RunSummary } from "../types.ts";
|
|
18
|
+
|
|
19
|
+
function locator(id: string): AttemptLocator {
|
|
20
|
+
return `@1${id}` as AttemptLocator;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function ref(evalId: string, attempt = 0, experimentId = "compare/bub-e2b"): AttemptRef {
|
|
24
|
+
return { experimentId, evalId, attempt };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function plan(overrides: Partial<RunFeedbackPlan["shape"]> = {}, reused = 0): RunFeedbackPlan {
|
|
28
|
+
return {
|
|
29
|
+
shape: { evals: 1, configs: 1, totalRuns: 1, maxConcurrency: 1, ...overrides },
|
|
30
|
+
reused,
|
|
31
|
+
reusedFailures: [],
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function summary(overrides: Partial<RunSummary> = {}): RunSummary {
|
|
36
|
+
return {
|
|
37
|
+
agent: "codex",
|
|
38
|
+
startedAt: "2026-07-13T00:00:00.000Z",
|
|
39
|
+
completedAt: "2026-07-13T00:03:21.000Z",
|
|
40
|
+
passed: 1,
|
|
41
|
+
failed: 0,
|
|
42
|
+
skipped: 0,
|
|
43
|
+
errored: 0,
|
|
44
|
+
durationMs: 201_000,
|
|
45
|
+
results: [],
|
|
46
|
+
...overrides,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function completion(overrides: Partial<RunCompletion> = {}): RunCompletion {
|
|
51
|
+
return { status: "complete", unstarted: 0, earlyExitUnstarted: 0, reporterErrors: [], ...overrides };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function flush(): Promise<void> {
|
|
55
|
+
return new Promise((resolve) => setTimeout(resolve, 0));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** ANSI escape 起始字节;非 TTY 输出必须永远不出现。 */
|
|
59
|
+
const ESC = "\x1B[";
|
|
60
|
+
|
|
61
|
+
function setupTty(overrides: { columns?: number; rows?: number } = {}): {
|
|
62
|
+
fake: FakeFeedbackIO;
|
|
63
|
+
coordinator: FeedbackCoordinator;
|
|
64
|
+
} {
|
|
65
|
+
const fake = createFakeFeedbackIO({ stderr: { isTTY: true, columns: overrides.columns ?? 100, rows: overrides.rows ?? 40 } });
|
|
66
|
+
const renderer = createHumanRenderer({ io: fake.io, command: "niceeval exp compare" });
|
|
67
|
+
const coordinator = createFeedbackCoordinator({ profile: "human", renderer, io: fake.io, tickIntervalMs: 250 });
|
|
68
|
+
return { fake, coordinator };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function setupPlain(): { fake: FakeFeedbackIO; coordinator: FeedbackCoordinator } {
|
|
72
|
+
const fake = createFakeFeedbackIO({ stderr: { isTTY: false } });
|
|
73
|
+
const renderer = createHumanRenderer({ io: fake.io, command: "niceeval exp compare" });
|
|
74
|
+
const coordinator = createFeedbackCoordinator({ profile: "human", renderer, io: fake.io, tickIntervalMs: 1000 });
|
|
75
|
+
return { fake, coordinator };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
afterEach(() => {
|
|
79
|
+
expect(activeFeedbackSinkCount()).toBe(0);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// ───────────────────────── 纯格式化 helper ─────────────────────────
|
|
83
|
+
|
|
84
|
+
describe("formatElapsed / formatTokenCount: 纯格式化", () => {
|
|
85
|
+
it("elapsed 用 'Xm Ys' / 'Ys' 风格,匹配 cli.md 的 dashboard/完成页示例", () => {
|
|
86
|
+
expect(formatElapsed(0)).toBe("0s");
|
|
87
|
+
expect(formatElapsed(54_000)).toBe("54s");
|
|
88
|
+
expect(formatElapsed(102_000)).toBe("1m 42s");
|
|
89
|
+
expect(formatElapsed(134_000)).toBe("2m 14s");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("token 计数超过 1000/1e6 分别用 k/M 后缀", () => {
|
|
93
|
+
expect(formatTokenCount(500)).toBe("500");
|
|
94
|
+
expect(formatTokenCount(3_400)).toBe("3.4k");
|
|
95
|
+
expect(formatTokenCount(1_200_000)).toBe("1.2M");
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// ───────────────────────── TTY dashboard: 静态符号 / 节流 / 同帧不写 ─────────────────────────
|
|
100
|
+
|
|
101
|
+
describe("TTY dashboard: 静态符号,不用 spinner 帧驱动重画", () => {
|
|
102
|
+
it("active 行恒用静态 ● 号,从不出现任何 spinner 字符", async () => {
|
|
103
|
+
const { fake, coordinator } = setupTty();
|
|
104
|
+
coordinator.start(plan());
|
|
105
|
+
coordinator.emit({ type: "attempt:start", at: 0, identity: ref("memory/a"), who: "bub-e2b", phase: "eval.run" });
|
|
106
|
+
fake.advance(1000);
|
|
107
|
+
await flush();
|
|
108
|
+
const all = fake.stderr.writes.join("");
|
|
109
|
+
expect(all).toContain("●");
|
|
110
|
+
// 老 live.ts 的 spinner 帧字符集,新实现绝不应再出现。
|
|
111
|
+
for (const frame of ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]) {
|
|
112
|
+
expect(all).not.toContain(frame);
|
|
113
|
+
}
|
|
114
|
+
await coordinator.finish({ summary: summary(), completion: completion(), paths: [] });
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe("TTY dashboard: 真实 state 变化合并渲染,同帧不写,elapsed 最多每秒变化一次", () => {
|
|
119
|
+
it("state 没有真实变化的 tick 不产生新的 ANSI 重画;elapsed 跨过整秒边界才重画", async () => {
|
|
120
|
+
const { fake, coordinator } = setupTty();
|
|
121
|
+
coordinator.start(plan());
|
|
122
|
+
coordinator.emit({ type: "attempt:start", at: 0, identity: ref("memory/a"), who: "bub-e2b", phase: "eval.run" });
|
|
123
|
+
await flush();
|
|
124
|
+
const ansiWriteCount = () => fake.stderr.writes.filter((w) => w.includes(ESC)).length;
|
|
125
|
+
const afterStart = ansiWriteCount();
|
|
126
|
+
|
|
127
|
+
fake.advance(250); // tick #1:activeOrder 刚变化(第一次画出这一行)→ 必然重画
|
|
128
|
+
await flush();
|
|
129
|
+
expect(ansiWriteCount()).toBe(afterStart + 1);
|
|
130
|
+
|
|
131
|
+
fake.advance(250); // tick #2 (t=500ms):elapsed 仍是 "0s",且 active 内容没变 → 不应重画
|
|
132
|
+
await flush();
|
|
133
|
+
expect(ansiWriteCount()).toBe(afterStart + 1);
|
|
134
|
+
|
|
135
|
+
fake.advance(250); // tick #3 (t=750ms):同上,仍 "0s"
|
|
136
|
+
await flush();
|
|
137
|
+
expect(ansiWriteCount()).toBe(afterStart + 1);
|
|
138
|
+
|
|
139
|
+
fake.advance(250); // tick #4 (t=1000ms):跨过整秒边界,elapsed 变成 "1s" → 必须重画
|
|
140
|
+
await flush();
|
|
141
|
+
expect(ansiWriteCount()).toBe(afterStart + 2);
|
|
142
|
+
|
|
143
|
+
await coordinator.finish({ summary: summary(), completion: completion(), paths: [] });
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// ───────────────────────── TTY dashboard: 高度/宽度硬上限 ─────────────────────────
|
|
148
|
+
|
|
149
|
+
describe("TTY dashboard: 高度以 stderr.rows 为硬上限,窄终端先减 active slots", () => {
|
|
150
|
+
it("active attempt 数超过可视预算时,先折叠成更少的行 + 一条 overflow 摘要,总行数不超过 rows", async () => {
|
|
151
|
+
const { fake, coordinator } = setupTty({ rows: 8, columns: 100 });
|
|
152
|
+
coordinator.start(plan({ totalRuns: 5 }));
|
|
153
|
+
for (let i = 0; i < 5; i++) {
|
|
154
|
+
coordinator.emit({ type: "attempt:start", at: 0, identity: ref(`memory/eval-${i}`), who: `who-${i}`, phase: "eval.run" });
|
|
155
|
+
}
|
|
156
|
+
fake.advance(250);
|
|
157
|
+
await flush();
|
|
158
|
+
|
|
159
|
+
const lastAnsiFrame = fake.stderr.writes.filter((w) => w.includes(ESC)).at(-1)!;
|
|
160
|
+
const lineCount = lastAnsiFrame.split("\n").filter((l) => l.length > 0).length;
|
|
161
|
+
expect(lineCount).toBeLessThanOrEqual(8);
|
|
162
|
+
expect(lastAnsiFrame).toMatch(/… \d+ more active/);
|
|
163
|
+
// 5 个 attempt 放不下,但至少展示了一部分(不是把全部都折叠成 0 行摘要)。
|
|
164
|
+
const shownRows = (lastAnsiFrame.match(/●/g) ?? []).length;
|
|
165
|
+
expect(shownRows).toBeGreaterThan(0);
|
|
166
|
+
expect(shownRows).toBeLessThan(5);
|
|
167
|
+
|
|
168
|
+
await coordinator.finish({ summary: summary(), completion: completion(), paths: [] });
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
describe("TTY dashboard: 宽度以 stderr.columns 为硬上限,截断消息而不是软换行", () => {
|
|
173
|
+
it("始终给 phase/detail 留出列宽,不能让身份列吞掉整行", async () => {
|
|
174
|
+
const { fake, coordinator } = setupTty({ rows: 40, columns: 180 });
|
|
175
|
+
coordinator.start(plan());
|
|
176
|
+
coordinator.emit({
|
|
177
|
+
type: "attempt:start",
|
|
178
|
+
at: 0,
|
|
179
|
+
identity: ref("memory/agent-029-use-cache-directive"),
|
|
180
|
+
who: "codex-e2b",
|
|
181
|
+
phase: "eval.run",
|
|
182
|
+
});
|
|
183
|
+
coordinator.emit({
|
|
184
|
+
type: "attempt:progress",
|
|
185
|
+
at: 0,
|
|
186
|
+
identity: ref("memory/agent-029-use-cache-directive"),
|
|
187
|
+
detail: "tool: pnpm test",
|
|
188
|
+
});
|
|
189
|
+
fake.advance(250);
|
|
190
|
+
await flush();
|
|
191
|
+
|
|
192
|
+
const lastAnsiFrame = fake.stderr.writes.filter((w) => w.includes(ESC)).at(-1)!;
|
|
193
|
+
expect(lastAnsiFrame).toContain("running eval: tool: pnpm test");
|
|
194
|
+
|
|
195
|
+
await coordinator.finish({ summary: summary(), completion: completion(), paths: [] });
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("窄终端下 active 行的纯文本长度不超过 columns,且不含内嵌换行", async () => {
|
|
199
|
+
const { fake, coordinator } = setupTty({ rows: 40, columns: 36 });
|
|
200
|
+
coordinator.start(plan());
|
|
201
|
+
coordinator.emit({
|
|
202
|
+
type: "attempt:start",
|
|
203
|
+
at: 0,
|
|
204
|
+
identity: ref("memory/a-very-long-eval-id-that-does-not-fit"),
|
|
205
|
+
who: "compare/a-very-long-agent-name",
|
|
206
|
+
phase: "eval.run",
|
|
207
|
+
});
|
|
208
|
+
coordinator.emit({
|
|
209
|
+
type: "attempt:progress",
|
|
210
|
+
at: 0,
|
|
211
|
+
identity: ref("memory/a-very-long-eval-id-that-does-not-fit"),
|
|
212
|
+
detail: "a very long detail string that should be truncated, not wrapped",
|
|
213
|
+
});
|
|
214
|
+
fake.advance(250);
|
|
215
|
+
await flush();
|
|
216
|
+
|
|
217
|
+
const lastAnsiFrame = fake.stderr.writes.filter((w) => w.includes(ESC)).at(-1)!;
|
|
218
|
+
// 逐个「视觉行」核对:按 \x1B[2K 切开(每个视觉行都以它开头),每段本身不应再含 "\n"
|
|
219
|
+
// (即行内容没有被软换行拆成两个终端行),且刨去 ANSI 控制序列后的可见宽度不超过 columns。
|
|
220
|
+
const rows = lastAnsiFrame.split(`${ESC}2K`).slice(1);
|
|
221
|
+
for (const row of rows) {
|
|
222
|
+
const visible = row.replace(/\n$/, "");
|
|
223
|
+
expect(visible.includes("\n")).toBe(false);
|
|
224
|
+
// eslint-disable-next-line no-control-regex
|
|
225
|
+
const stripped = visible.replace(/\x1B\[[0-9]*[A-Za-z]/g, "");
|
|
226
|
+
expect(stripped.length).toBeLessThanOrEqual(36);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
await coordinator.finish({ summary: summary(), completion: completion(), paths: [] });
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
// ───────────────────────── TTY dashboard: active slot 稳定 ─────────────────────────
|
|
234
|
+
|
|
235
|
+
describe("TTY dashboard: active slots 稳定,完成前不因其它 attempt 更新而换位", () => {
|
|
236
|
+
it("A 完成、C 加入后,B 仍保持在 C 之前(不重新排序成到达/字母序)", async () => {
|
|
237
|
+
const { fake, coordinator } = setupTty({ rows: 40, columns: 100 });
|
|
238
|
+
coordinator.start(plan({ totalRuns: 3 }));
|
|
239
|
+
coordinator.emit({ type: "attempt:start", at: 0, identity: ref("memory/a"), who: "who-a", phase: "eval.run" });
|
|
240
|
+
coordinator.emit({ type: "attempt:start", at: 0, identity: ref("memory/b"), who: "who-b", phase: "eval.run" });
|
|
241
|
+
fake.advance(250);
|
|
242
|
+
await flush();
|
|
243
|
+
|
|
244
|
+
coordinator.emit({ type: "attempt:complete", at: 0, identity: ref("memory/a"), who: "who-a", verdict: "passed" });
|
|
245
|
+
coordinator.emit({ type: "attempt:start", at: 0, identity: ref("memory/c"), who: "who-c", phase: "eval.run" });
|
|
246
|
+
fake.advance(250);
|
|
247
|
+
await flush();
|
|
248
|
+
|
|
249
|
+
const lastAnsiFrame = fake.stderr.writes.filter((w) => w.includes(ESC)).at(-1)!;
|
|
250
|
+
expect(lastAnsiFrame).not.toContain("memory/a"); // 已完成,退出 active
|
|
251
|
+
const idxB = lastAnsiFrame.indexOf("memory/b");
|
|
252
|
+
const idxC = lastAnsiFrame.indexOf("memory/c");
|
|
253
|
+
expect(idxB).toBeGreaterThan(-1);
|
|
254
|
+
expect(idxC).toBeGreaterThan(-1);
|
|
255
|
+
expect(idxB).toBeLessThan(idxC); // B 先于 C:C 是新到达的,追加在末尾,不插到 B 前面
|
|
256
|
+
|
|
257
|
+
await coordinator.finish({ summary: summary(), completion: completion(), paths: [] });
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
// ───────────────────────── TTY dashboard: 内容边界 ─────────────────────────
|
|
262
|
+
|
|
263
|
+
describe("TTY dashboard: 只含命令/elapsed/守恒计数/cost/active slots,不做会消失的诊断区块", () => {
|
|
264
|
+
it("diagnostic 永久事件的文本只出现在一次性追加行里,不进入持续重画的 dashboard 帧", async () => {
|
|
265
|
+
const { fake, coordinator } = setupTty();
|
|
266
|
+
coordinator.start(plan());
|
|
267
|
+
coordinator.diagnostic({ key: "memory-warmup-degraded", severity: "warning", message: "cold index (12 attempts)" });
|
|
268
|
+
fake.advance(250);
|
|
269
|
+
await flush();
|
|
270
|
+
|
|
271
|
+
const lastAnsiFrame = fake.stderr.writes.filter((w) => w.includes(ESC)).at(-1)!;
|
|
272
|
+
expect(lastAnsiFrame).not.toContain("memory-warmup-degraded");
|
|
273
|
+
expect(lastAnsiFrame).not.toContain("cold index");
|
|
274
|
+
// 但诊断本身确实被写过(非 ANSI 的那次纯文本追加)。
|
|
275
|
+
expect(fake.stderr.writes.some((w) => w.includes("memory-warmup-degraded"))).toBe(true);
|
|
276
|
+
|
|
277
|
+
await coordinator.finish({ summary: summary(), completion: completion(), paths: [] });
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
// ───────────────────────── failure 展开上限 ─────────────────────────
|
|
282
|
+
|
|
283
|
+
describe("failed/errored 默认展开前 10 条,超过后追加一次 suppressed 提示且不丢总数", () => {
|
|
284
|
+
it("12 条 failure 只展开 10 条,并给出 '… 2 more failures suppressed'", async () => {
|
|
285
|
+
const { fake, coordinator } = setupTty();
|
|
286
|
+
coordinator.start(plan({ totalRuns: 12 }));
|
|
287
|
+
for (let i = 0; i < 12; i++) {
|
|
288
|
+
coordinator.emit({
|
|
289
|
+
type: "failure",
|
|
290
|
+
at: i,
|
|
291
|
+
locator: locator(`fail${i}`),
|
|
292
|
+
identity: ref(`memory/eval-${i}`),
|
|
293
|
+
who: `who-${i}`,
|
|
294
|
+
verdict: "failed",
|
|
295
|
+
reason: "gate: cache tool not used",
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
await flush();
|
|
299
|
+
expect(coordinator.state.failures).toHaveLength(12); // 完整结果不丢
|
|
300
|
+
|
|
301
|
+
await coordinator.finish({
|
|
302
|
+
summary: summary({ passed: 0, failed: 12 }),
|
|
303
|
+
completion: completion(),
|
|
304
|
+
paths: [],
|
|
305
|
+
});
|
|
306
|
+
// 单条 failure 立即追加走 stderr;完成页的 FAILURES 汇总(含最终 suppressed 总数)属于
|
|
307
|
+
// "summary" 事件,走 stdout(见 human.ts 的 writeDurable/输出流边界测试)—— 这里两条流都要看。
|
|
308
|
+
const text = fake.stderr.writes.join("") + fake.stdout.writes.join("");
|
|
309
|
+
for (let i = 0; i < 10; i++) expect(text).toContain(`memory/eval-${i}`);
|
|
310
|
+
expect(text).not.toContain("memory/eval-10");
|
|
311
|
+
expect(text).not.toContain("memory/eval-11");
|
|
312
|
+
expect(text).toContain("… 2 more failures suppressed");
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
// ───────────────────────── 完成页 ─────────────────────────
|
|
317
|
+
|
|
318
|
+
describe("完成页:失败优先摘要 + locator + show/view 下一步 + 快照路径,不调用 renderRunReport 大表", () => {
|
|
319
|
+
it("有失败时给出 FAILED 摘要、locator、Inspect/Eval/Trace/Diff/Compare 下一步命令", async () => {
|
|
320
|
+
const { fake, coordinator } = setupTty();
|
|
321
|
+
coordinator.start(plan({ totalRuns: 45 }, 6));
|
|
322
|
+
coordinator.emit({
|
|
323
|
+
type: "failure",
|
|
324
|
+
at: 0,
|
|
325
|
+
locator: locator("7m2k9p"),
|
|
326
|
+
identity: ref("memory/commit0-cachetool"),
|
|
327
|
+
who: "compare/bub-e2b",
|
|
328
|
+
verdict: "failed",
|
|
329
|
+
reason: "failed",
|
|
330
|
+
assertion: {
|
|
331
|
+
severity: "gate",
|
|
332
|
+
assertion: "Issue 15193: selected proposal matches the accepted proposal",
|
|
333
|
+
matcher: "equals(4)",
|
|
334
|
+
expected: "4",
|
|
335
|
+
received: "3",
|
|
336
|
+
additionalFailures: 0,
|
|
337
|
+
},
|
|
338
|
+
});
|
|
339
|
+
await coordinator.finish({
|
|
340
|
+
summary: summary({ passed: 44, failed: 1, errored: 0, durationMs: 228_000 }),
|
|
341
|
+
completion: completion(),
|
|
342
|
+
paths: [".niceeval/compare/bub-e2b/2026-07-13T000000", ".niceeval/compare/codex/2026-07-13T000000"],
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
// 单条 failure 立即追加走 stderr;完成页(FAILED 摘要/Inspect 等下一步/Results 路径)属于
|
|
346
|
+
// "summary"/"saved" 事件,走 stdout(见 human.ts 的 writeDurable/输出流边界测试)。
|
|
347
|
+
const text = fake.stderr.writes.join("") + fake.stdout.writes.join("");
|
|
348
|
+
expect(text).toContain("FAILED");
|
|
349
|
+
expect(text).toContain("44 passed");
|
|
350
|
+
expect(text).toContain("1 failed");
|
|
351
|
+
expect(text).toContain("@17m2k9p");
|
|
352
|
+
expect(text).toContain("memory/commit0-cachetool");
|
|
353
|
+
expect(text).toContain("gate: Issue 15193: selected proposal matches the accepted proposal");
|
|
354
|
+
expect(text).toContain("equals(4) · expected 4 · received 3");
|
|
355
|
+
expect(text).toContain("Inspect: niceeval show @17m2k9p");
|
|
356
|
+
expect(text).toContain("Eval: niceeval show @17m2k9p --source");
|
|
357
|
+
expect(text).toContain("Trace: niceeval show @17m2k9p --execution");
|
|
358
|
+
expect(text).toContain("Diff: niceeval show @17m2k9p --diff");
|
|
359
|
+
expect(text).toContain("Compare: niceeval view compare");
|
|
360
|
+
expect(text).toContain(".niceeval/compare/bub-e2b/2026-07-13T000000");
|
|
361
|
+
// 不再是 renderRunReport() 的大表(表格标志性的竖线分隔表头在这里不应出现)。
|
|
362
|
+
expect(text).not.toMatch(/-\+-/);
|
|
363
|
+
});
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
describe("全通过:不显示空 FAILURES 区块;结果路径多时折叠", () => {
|
|
367
|
+
it("全部通过时不出现 FAILURES 字样", async () => {
|
|
368
|
+
const { fake, coordinator } = setupTty();
|
|
369
|
+
coordinator.start(plan({ totalRuns: 45 }));
|
|
370
|
+
await coordinator.finish({
|
|
371
|
+
summary: summary({ passed: 45, failed: 0, errored: 0, durationMs: 201_000, estimatedCostUSD: 1.22 }),
|
|
372
|
+
completion: completion(),
|
|
373
|
+
paths: [
|
|
374
|
+
".niceeval/compare/a/2026",
|
|
375
|
+
".niceeval/compare/b/2026",
|
|
376
|
+
".niceeval/compare/c/2026",
|
|
377
|
+
".niceeval/compare/d/2026",
|
|
378
|
+
".niceeval/compare/e/2026",
|
|
379
|
+
],
|
|
380
|
+
});
|
|
381
|
+
// 完成页(PASSED 摘要)属于 "summary" 事件,走 stdout(见 human.ts 的 writeDurable/
|
|
382
|
+
// 输出流边界测试)。
|
|
383
|
+
const text = fake.stdout.writes.join("");
|
|
384
|
+
expect(text).toContain("PASSED");
|
|
385
|
+
expect(text).not.toContain("FAILURES");
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
it("结果路径超过折叠上限时只展示前几个 + '… N more',不逐行刷满几十个", async () => {
|
|
389
|
+
const { fake, coordinator } = setupTty();
|
|
390
|
+
coordinator.start(plan());
|
|
391
|
+
const paths = Array.from({ length: 40 }, (_, i) => `.niceeval/compare/agent-${i}/2026`);
|
|
392
|
+
await coordinator.finish({ summary: summary(), completion: completion(), paths });
|
|
393
|
+
// 结果路径属于 "saved" 事件,走 stdout。
|
|
394
|
+
const text = fake.stdout.writes.join("");
|
|
395
|
+
expect(text).toContain("agent-0");
|
|
396
|
+
expect(text).not.toContain("agent-39"); // 第 40 个不应该被逐行列出
|
|
397
|
+
expect(text).toMatch(/… \d+ more/);
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
// required reporter(默认 artifacts、显式 --json/--junit)写失败必须让完成页判红,即便全部
|
|
401
|
+
// attempt 都通过——退出码已经因此非零(见 computeCiExitCode 对 reporterErrors 的同一条判断),
|
|
402
|
+
// 完成页不能反过来印一个会被误读成"全绿"的 PASSED(与 ci.ts 的 resultStatusWord() 同一契约)。
|
|
403
|
+
it("required reporter 写失败时即便全部 attempt 通过,也显示 FAILED 而不是 PASSED", async () => {
|
|
404
|
+
const { fake, coordinator } = setupTty();
|
|
405
|
+
coordinator.start(plan());
|
|
406
|
+
await coordinator.finish({
|
|
407
|
+
summary: summary({ passed: 45, failed: 0, errored: 0 }),
|
|
408
|
+
completion: completion({
|
|
409
|
+
reporterErrors: [{ reporter: "json", required: true, message: "EEXIST: mkdir failed" }],
|
|
410
|
+
}),
|
|
411
|
+
paths: [],
|
|
412
|
+
});
|
|
413
|
+
const text = fake.stdout.writes.join("");
|
|
414
|
+
expect(text).toContain("FAILED");
|
|
415
|
+
expect(text).not.toContain("PASSED");
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
// best-effort reporter(用户 config.reporters,如未显式 --json/--junit 的自定义出口)写失败
|
|
419
|
+
// 只折成 diagnostic,不影响退出码——完成页同样不应该判红,否则和实际退出码矛盾。
|
|
420
|
+
it("非 required reporter 写失败不影响完成页判定,仍显示 PASSED", async () => {
|
|
421
|
+
const { fake, coordinator } = setupTty();
|
|
422
|
+
coordinator.start(plan());
|
|
423
|
+
await coordinator.finish({
|
|
424
|
+
summary: summary({ passed: 45, failed: 0, errored: 0 }),
|
|
425
|
+
completion: completion({
|
|
426
|
+
reporterErrors: [{ reporter: "config-reporter-0", required: false, message: "network timeout" }],
|
|
427
|
+
}),
|
|
428
|
+
paths: [],
|
|
429
|
+
});
|
|
430
|
+
const text = fake.stdout.writes.join("");
|
|
431
|
+
expect(text).toContain("PASSED");
|
|
432
|
+
expect(text).not.toContain("FAILED");
|
|
433
|
+
});
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
describe("全量复用:只展示静态计划与终局,本次执行指标归零", () => {
|
|
437
|
+
it("不画 dashboard,历史失败进入 FAILURES,历史 usage/cost 不进入本次摘要", async () => {
|
|
438
|
+
const { fake, coordinator } = setupTty();
|
|
439
|
+
const carriedLocator = locator("carried-failure");
|
|
440
|
+
coordinator.start({
|
|
441
|
+
...plan({ totalRuns: 2, evals: 2 }, 2),
|
|
442
|
+
reusedFailures: [{
|
|
443
|
+
locator: carriedLocator,
|
|
444
|
+
identity: ref("memory/carried"),
|
|
445
|
+
who: "compare/bub-e2b",
|
|
446
|
+
verdict: "failed",
|
|
447
|
+
reason: "gate: carried assertion failed",
|
|
448
|
+
}],
|
|
449
|
+
});
|
|
450
|
+
await coordinator.finish({
|
|
451
|
+
summary: summary({
|
|
452
|
+
passed: 1,
|
|
453
|
+
failed: 1,
|
|
454
|
+
durationMs: 0,
|
|
455
|
+
usage: { inputTokens: 7_000, outputTokens: 40 },
|
|
456
|
+
estimatedCostUSD: 7.04,
|
|
457
|
+
}),
|
|
458
|
+
completion: completion(),
|
|
459
|
+
paths: [],
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
const stderr = fake.stderr.writes.join("");
|
|
463
|
+
const stdout = fake.stdout.writes.join("");
|
|
464
|
+
expect(stderr).toContain("Reuse: 2 of 2 carried in from cache · 0 to run");
|
|
465
|
+
expect(stderr).not.toContain("niceeval exp compare");
|
|
466
|
+
expect(stderr).not.toContain(ESC);
|
|
467
|
+
expect(stdout).toContain("FAILURES");
|
|
468
|
+
expect(stdout).toContain("(all 2 reused)");
|
|
469
|
+
expect(stdout).toContain(String(carriedLocator));
|
|
470
|
+
expect(stdout).toContain("0s · 0 new tok · $0.00");
|
|
471
|
+
expect(stdout).not.toContain("7.0k");
|
|
472
|
+
expect(stdout).not.toContain("$7.04");
|
|
473
|
+
});
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
// ───────────────────────── 输出流边界:stdout 只留给最终摘要与结果路径 ─────────────────────────
|
|
477
|
+
|
|
478
|
+
describe("输出流边界:human 的 stdout 只留给最终摘要与结果路径,其它永久事件都在 stderr", () => {
|
|
479
|
+
it("plan/failure/diagnostic 只出现在 stderr,完成页(FAILED/Results)只出现在 stdout", async () => {
|
|
480
|
+
const { fake, coordinator } = setupTty();
|
|
481
|
+
coordinator.start(plan({ totalRuns: 45 }, 6));
|
|
482
|
+
coordinator.diagnostic({ key: "memory-warmup-degraded", severity: "warning", message: "cold index" });
|
|
483
|
+
coordinator.emit({
|
|
484
|
+
type: "failure",
|
|
485
|
+
at: 0,
|
|
486
|
+
locator: locator("7m2k9p"),
|
|
487
|
+
identity: ref("memory/commit0-cachetool"),
|
|
488
|
+
who: "compare/bub-e2b",
|
|
489
|
+
verdict: "failed",
|
|
490
|
+
reason: "gate: cache tool not used",
|
|
491
|
+
});
|
|
492
|
+
await coordinator.finish({
|
|
493
|
+
summary: summary({ passed: 44, failed: 1, errored: 0 }),
|
|
494
|
+
completion: completion(),
|
|
495
|
+
paths: [".niceeval/compare/bub-e2b/2026-07-13T000000"],
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
const stderrText = fake.stderr.writes.join("");
|
|
499
|
+
const stdoutText = fake.stdout.writes.join("");
|
|
500
|
+
|
|
501
|
+
// 计划/诊断/失败的一次性追加行在 stderr,不在 stdout(否则 `niceeval exp > out.txt` 会把
|
|
502
|
+
// 过程噪音混进理应只含最终结论的文件)。
|
|
503
|
+
expect(stderrText).toContain("Plan:");
|
|
504
|
+
expect(stderrText).toContain("memory-warmup-degraded");
|
|
505
|
+
expect(stderrText).toContain("gate: cache tool not used");
|
|
506
|
+
expect(stdoutText).not.toContain("Plan:");
|
|
507
|
+
expect(stdoutText).not.toContain("memory-warmup-degraded");
|
|
508
|
+
|
|
509
|
+
// 完成页(FAILED 摘要 + 结果路径)在 stdout,不在 stderr。
|
|
510
|
+
expect(stdoutText).toContain("FAILED");
|
|
511
|
+
expect(stdoutText).toContain(".niceeval/compare/bub-e2b/2026-07-13T000000");
|
|
512
|
+
expect(stderrText).not.toContain("FAILED");
|
|
513
|
+
expect(stderrText).not.toContain(".niceeval/compare/bub-e2b/2026-07-13T000000");
|
|
514
|
+
});
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
// ───────────────────────── 非 TTY 退化流 ─────────────────────────
|
|
518
|
+
|
|
519
|
+
describe("显式 human + 非 TTY:无 ANSI,start + 永久事件 + 30 秒空闲 heartbeat,结束仍用 human 摘要", () => {
|
|
520
|
+
it("全程没有任何 ANSI 控制字符", async () => {
|
|
521
|
+
const { fake, coordinator } = setupPlain();
|
|
522
|
+
coordinator.start(plan());
|
|
523
|
+
coordinator.diagnostic({ key: "x", severity: "warning", message: "y" });
|
|
524
|
+
await coordinator.finish({ summary: summary(), completion: completion(), paths: [".niceeval/compare/a/2026"] });
|
|
525
|
+
// 非 TTY 退化流全程零 ANSI——不管落在 stderr(计划/诊断)还是 stdout(最终摘要/结果路径)。
|
|
526
|
+
expect(fake.stderr.writes.join("")).not.toContain(ESC);
|
|
527
|
+
expect(fake.stdout.writes.join("")).not.toContain(ESC);
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
it("start(plan) 立即追加一行;之后 29.9 秒无永久事件不 heartbeat,满 30 秒才追加一条", async () => {
|
|
531
|
+
const { fake, coordinator } = setupPlain();
|
|
532
|
+
coordinator.start(plan({ totalRuns: 5 }));
|
|
533
|
+
await flush();
|
|
534
|
+
expect(fake.stderr.writes.some((w) => w.includes("Plan:"))).toBe(true);
|
|
535
|
+
const afterPlan = fake.stderr.writes.length;
|
|
536
|
+
|
|
537
|
+
fake.advance(29_000);
|
|
538
|
+
await flush();
|
|
539
|
+
expect(fake.stderr.writes.length).toBe(afterPlan); // 还没到 30s,不 heartbeat
|
|
540
|
+
|
|
541
|
+
fake.advance(1_000); // 累计 30s
|
|
542
|
+
await flush();
|
|
543
|
+
expect(fake.stderr.writes.length).toBe(afterPlan + 1);
|
|
544
|
+
expect(fake.stderr.writes.at(-1)).toMatch(/elapsed/);
|
|
545
|
+
|
|
546
|
+
await coordinator.finish({ summary: summary(), completion: completion(), paths: [] });
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
it("永久事件(diagnostic)会重置空闲计时器,不会紧跟着再冒出一条冗余 heartbeat", async () => {
|
|
550
|
+
const { fake, coordinator } = setupPlain();
|
|
551
|
+
coordinator.start(plan());
|
|
552
|
+
await flush();
|
|
553
|
+
fake.advance(20_000);
|
|
554
|
+
coordinator.diagnostic({ key: "reset", severity: "warning", message: "resets the clock" });
|
|
555
|
+
await flush();
|
|
556
|
+
const afterDiagnostic = fake.stderr.writes.length;
|
|
557
|
+
|
|
558
|
+
fake.advance(29_000); // 距 diagnostic 才 29s,还没到 30s
|
|
559
|
+
await flush();
|
|
560
|
+
expect(fake.stderr.writes.length).toBe(afterDiagnostic);
|
|
561
|
+
|
|
562
|
+
fake.advance(1_000); // 距 diagnostic 满 30s
|
|
563
|
+
await flush();
|
|
564
|
+
expect(fake.stderr.writes.length).toBe(afterDiagnostic + 1);
|
|
565
|
+
|
|
566
|
+
await coordinator.finish({ summary: summary(), completion: completion(), paths: [] });
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
it("结束仍然是 human 文案摘要(不是 agent/ci 的 key=value envelope)", async () => {
|
|
570
|
+
const { fake, coordinator } = setupPlain();
|
|
571
|
+
coordinator.start(plan());
|
|
572
|
+
await coordinator.finish({
|
|
573
|
+
summary: summary({ passed: 0, failed: 1, errored: 0 }),
|
|
574
|
+
completion: completion(),
|
|
575
|
+
paths: [],
|
|
576
|
+
});
|
|
577
|
+
// 完成页(FAILED 摘要)属于 "summary" 事件,走 stdout。
|
|
578
|
+
const text = fake.stdout.writes.join("");
|
|
579
|
+
expect(text).toContain("FAILED");
|
|
580
|
+
expect(text).not.toContain("NICEEVAL"); // agent/ci envelope 的标志前缀,human 不应该出现
|
|
581
|
+
expect(text).not.toContain("=passed"); // ci 的 key=value 形态
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
it("非 TTY 不维护 active slot,不逐次展示阶段变化(不实现 onLifecycle/redrawDynamic)", async () => {
|
|
585
|
+
const { fake, coordinator } = setupPlain();
|
|
586
|
+
coordinator.start(plan());
|
|
587
|
+
coordinator.emit({ type: "attempt:start", at: 0, identity: ref("memory/a"), who: "bub-e2b", phase: "eval.run" });
|
|
588
|
+
coordinator.emit({ type: "attempt:phase", at: 1, identity: ref("memory/a"), phase: "agent.setup" });
|
|
589
|
+
await flush();
|
|
590
|
+
const text = fake.stderr.writes.join("");
|
|
591
|
+
expect(text).not.toContain("agent setup");
|
|
592
|
+
await coordinator.finish({ summary: summary(), completion: completion(), paths: [] });
|
|
593
|
+
});
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
// ───────────────────────── renderDurableLines:纯函数直接单测 ─────────────────────────
|
|
597
|
+
|
|
598
|
+
describe("renderDurableLines: 纯函数,不需要经过 coordinator 也能验证具体文案", () => {
|
|
599
|
+
it("plan 事件在有 reused 时只给出聚合 Reuse 行,不展开每个配置", () => {
|
|
600
|
+
const state = createInitialRunFeedbackState();
|
|
601
|
+
const lines = renderDurableLines(
|
|
602
|
+
{ type: "plan", at: 0, plan: plan({ totalRuns: 45, evals: 9, configs: 5, maxConcurrency: 19 }, 6) },
|
|
603
|
+
state,
|
|
604
|
+
);
|
|
605
|
+
expect(lines[0]).toContain("45 attempts");
|
|
606
|
+
expect(lines).toContain("Reuse: 6 of 45 carried in from cache · 39 to run");
|
|
607
|
+
expect(lines).toHaveLength(2);
|
|
608
|
+
expect(lines.some((l) => l.includes("compare/bub-e2b"))).toBe(false);
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
it("interrupted 事件复用现有 runner.interrupted 文案,不重新发明一份", () => {
|
|
612
|
+
const state = createInitialRunFeedbackState();
|
|
613
|
+
const lines = renderDurableLines({ type: "interrupted", at: 0 }, state);
|
|
614
|
+
expect(lines.join("\n")).toContain("interrupted");
|
|
615
|
+
});
|
|
616
|
+
});
|