niceeval 0.10.3-canary.6 → 0.11.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/bin/niceeval.js +9 -4
- package/dist/agents/types.d.ts +5 -4
- package/dist/context/turn-errors.d.ts +27 -23
- package/dist/i18n/en.d.ts +15 -0
- package/dist/i18n/en.js +16 -1
- package/dist/i18n/zh-CN.d.ts +16 -1
- package/dist/i18n/zh-CN.js +16 -1
- package/dist/o11y/derive.js +28 -24
- package/dist/o11y/types.d.ts +8 -5
- package/dist/report/components/attempt-detail/UsageTable.js +4 -7
- package/dist/report/components/attempt-detail/compute.d.ts +2 -2
- package/dist/report/components/attempt-detail/compute.js +2 -6
- package/dist/report/components/attempt-detail/faces.js +7 -7
- package/dist/report/components/attempt-detail/index.js +0 -3
- package/dist/report/components/entity-lists/EvalList.js +0 -0
- package/dist/report/components/metric-views/compute.js +1 -1
- package/dist/report/model/types.d.ts +3 -4
- package/dist/results/locator.js +0 -0
- package/dist/results/select.d.ts +6 -0
- package/dist/results/select.js +8 -0
- package/dist/runner/feedback/sink.d.ts +26 -1
- package/dist/runner/fingerprint.d.ts +23 -0
- package/dist/runner/types.d.ts +105 -7
- package/dist/sandbox/errors.d.ts +29 -0
- package/dist/sandbox/resolve.d.ts +9 -0
- package/dist/shared/failure-class.d.ts +91 -0
- package/dist/types.d.ts +1 -0
- package/dist/util.d.ts +3 -2
- package/dist/util.js +31 -5
- package/docs-site/zh/explanation/runner.mdx +35 -0
- package/docs-site/zh/reference/cli.mdx +10 -1
- package/docs-site/zh/reference/events.mdx +4 -4
- package/docs-site/zh/troubleshooting/debugging.mdx +11 -0
- package/docs-site/zh/tutorials/viewing-results.mdx +35 -3
- package/package.json +28 -24
- package/src/agents/ai-sdk.test.ts +26 -0
- package/src/agents/ai-sdk.ts +7 -4
- package/src/agents/index.ts +5 -3
- package/src/agents/langgraph.test.ts +30 -0
- package/src/agents/langgraph.ts +5 -2
- package/src/agents/openai-compat.test.ts +35 -0
- package/src/agents/openai-compat.ts +16 -4
- package/src/agents/sdk-streams.test.ts +66 -0
- package/src/agents/sdk-streams.ts +3 -1
- package/src/agents/types.ts +5 -4
- package/src/cli.ts +81 -16
- package/src/context/context.test.ts +34 -0
- package/src/context/context.ts +14 -5
- package/src/context/send-retry.test.ts +86 -0
- package/src/context/send-retry.ts +37 -12
- package/src/context/session.ts +24 -0
- package/src/context/turn-errors.test.ts +124 -17
- package/src/context/turn-errors.ts +60 -50
- package/src/define.ts +5 -0
- package/src/i18n/en.ts +22 -1
- package/src/i18n/zh-CN.ts +22 -1
- package/src/index.ts +8 -0
- package/src/o11y/cost.ts +4 -2
- package/src/o11y/derive.test.ts +40 -0
- package/src/o11y/derive.ts +28 -22
- package/src/o11y/otlp/sandbox-receiver.test.ts +201 -0
- package/src/o11y/otlp/sandbox-receiver.ts +73 -27
- package/src/o11y/parsers/bub.test.ts +30 -0
- package/src/o11y/parsers/bub.ts +5 -2
- package/src/o11y/parsers/codex.test.ts +19 -0
- package/src/o11y/parsers/codex.ts +5 -2
- package/src/o11y/types.ts +8 -5
- package/src/report/components/attempt-detail/UsageTable.tsx +4 -6
- package/src/report/components/attempt-detail/attempt-components.test.tsx +5 -8
- package/src/report/components/attempt-detail/compute.ts +2 -7
- package/src/report/components/attempt-detail/faces.ts +7 -7
- package/src/report/components/attempt-detail/index.tsx +0 -3
- package/src/report/components/entity-lists/EvalList.tsx +0 -0
- package/src/report/components/metric-views/compute.ts +1 -1
- package/src/report/model/types.ts +3 -4
- package/src/results/format.ts +9 -2
- package/src/results/index.ts +2 -0
- package/src/results/locator.ts +0 -0
- package/src/results/open.ts +132 -21
- package/src/results/select.ts +12 -0
- package/src/results/skipped-notice.ts +0 -0
- package/src/runner/attempt.test.ts +116 -0
- package/src/runner/attempt.ts +89 -8
- package/src/runner/discover.ts +21 -3
- package/src/runner/feedback/coordinator.ts +24 -2
- package/src/runner/feedback/eval-conclusions.ts +6 -3
- package/src/runner/feedback/human.test.ts +300 -6
- package/src/runner/feedback/human.ts +132 -30
- package/src/runner/feedback/json.test.ts +127 -2
- package/src/runner/feedback/json.ts +51 -3
- package/src/runner/feedback/reducer.test.ts +328 -29
- package/src/runner/feedback/reducer.ts +84 -9
- package/src/runner/feedback/sink.ts +39 -1
- package/src/runner/fingerprint.ts +49 -19
- package/src/runner/gate-lease.test.ts +510 -0
- package/src/runner/gate-lease.ts +350 -0
- package/src/runner/lock.test.ts +454 -0
- package/src/runner/lock.ts +288 -0
- package/src/runner/report.test.ts +1 -0
- package/src/runner/run.test.ts +2044 -9
- package/src/runner/run.ts +825 -61
- package/src/runner/teardown-registry.ts +20 -78
- package/src/runner/types.ts +103 -7
- package/src/sandbox/errors.test.ts +72 -0
- package/src/sandbox/errors.ts +83 -0
- package/src/sandbox/keep-registry.ts +22 -46
- package/src/sandbox/resolve.test.ts +100 -0
- package/src/sandbox/resolve.ts +84 -27
- package/src/shared/entry-file-store.test.ts +149 -0
- package/src/shared/entry-file-store.ts +117 -0
- package/src/shared/failure-class.test.ts +137 -0
- package/src/shared/failure-class.ts +175 -0
- package/src/show/index.ts +5 -6
- package/src/show/render.test.ts +116 -15
- package/src/show/render.ts +124 -35
- package/src/types.ts +9 -0
- package/src/util.ts +31 -4
- package/src/view/app/App.tsx +5 -1
- package/src/view/client-dist/app.js +1 -1
- package/src/view/data.ts +5 -9
- package/src/view/shared/types.ts +7 -1
- package/src/view/view-report.test.ts +54 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// cases: docs/engineering/testing/unit/experiments-runner.md
|
|
2
2
|
// 表驱动测试:RunFeedbackEvent 序列 → RunFeedbackState。核心断言是「任何时刻」都满足
|
|
3
|
-
// total = reused + running + queued +
|
|
3
|
+
// total = reused + running + elsewhere + queued + passed + failed + errored + skipped
|
|
4
|
+
//(见 docs/feature/experiments/cli.md 的守恒公式)——
|
|
4
5
|
// 每处理一个事件就断言一次,不是只在序列末尾断言。辅助不变量 active.size === running 一并核对
|
|
5
6
|
// (active map 按设计只存「正在运行」的 attempt,见 ../types.ts 的 ActiveAttempt 注释)。
|
|
6
7
|
|
|
@@ -17,6 +18,9 @@ function ref(evalId: string, attempt = 0, experimentId?: string): AttemptRef {
|
|
|
17
18
|
return experimentId ? { experimentId, evalId, attempt } : { evalId, attempt };
|
|
18
19
|
}
|
|
19
20
|
|
|
21
|
+
/** 恒等式的八个互斥桶。 */
|
|
22
|
+
const BUCKETS = ["reused", "running", "elsewhere", "queued", "passed", "failed", "errored", "skipped"] as const;
|
|
23
|
+
|
|
20
24
|
/** 依次喂事件,每一步都断言守恒公式与 active/running 一致,返回最终状态供调用方追加断言。 */
|
|
21
25
|
function replay(events: readonly RunFeedbackEvent[]): RunFeedbackState {
|
|
22
26
|
let state = createInitialRunFeedbackState();
|
|
@@ -24,24 +28,32 @@ function replay(events: readonly RunFeedbackEvent[]): RunFeedbackState {
|
|
|
24
28
|
state = reduceRunFeedback(state, event);
|
|
25
29
|
expect(
|
|
26
30
|
state.total,
|
|
27
|
-
`after event #${i} (${event.type}): total should equal reused+running+queued+
|
|
28
|
-
).toBe(
|
|
31
|
+
`after event #${i} (${event.type}): total should equal reused+running+elsewhere+queued+passed+failed+errored+skipped`,
|
|
32
|
+
).toBe(
|
|
33
|
+
state.reused +
|
|
34
|
+
state.running +
|
|
35
|
+
state.elsewhere +
|
|
36
|
+
state.queued +
|
|
37
|
+
state.passed +
|
|
38
|
+
state.failed +
|
|
39
|
+
state.errored +
|
|
40
|
+
state.skipped,
|
|
41
|
+
);
|
|
29
42
|
expect(
|
|
30
43
|
state.active.size,
|
|
31
44
|
`after event #${i} (${event.type}): active map size should equal running count`,
|
|
32
45
|
).toBe(state.running);
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
expect(state.completed, `after event #${i}: completed must not go negative`).toBeGreaterThanOrEqual(0);
|
|
46
|
+
// 八个桶本身永远非负 —— 负数意味着某个事件把不存在的 attempt 又"完成"(或"迁移")了一次,
|
|
47
|
+
// 是 reducer 或 emitter 的 bug,不该被吞掉。
|
|
48
|
+
for (const bucket of BUCKETS) {
|
|
49
|
+
expect(state[bucket], `after event #${i}: ${bucket} must not go negative`).toBeGreaterThanOrEqual(0);
|
|
50
|
+
}
|
|
39
51
|
}
|
|
40
52
|
return state;
|
|
41
53
|
}
|
|
42
54
|
|
|
43
55
|
describe("reduceRunFeedback: 守恒公式", () => {
|
|
44
|
-
it("普通运行:plan → start × N → complete × N,计数逐步从 queued 转移到
|
|
56
|
+
it("普通运行:plan → start × N → complete × N,计数逐步从 queued 转移到 verdict 结局项", () => {
|
|
45
57
|
const a = ref("memory/a");
|
|
46
58
|
const b = ref("memory/b");
|
|
47
59
|
const c = ref("memory/c");
|
|
@@ -56,7 +68,7 @@ describe("reduceRunFeedback: 守恒公式", () => {
|
|
|
56
68
|
{ type: "attempt:complete", at: 5, identity: b, who: "codex", verdict: "passed", tokenCount: 25, estimatedCostUSD: 0.2 },
|
|
57
69
|
{ type: "attempt:complete", at: 6, identity: c, who: "codex", verdict: "passed", estimatedCostUSD: 0.05 },
|
|
58
70
|
]);
|
|
59
|
-
expect(state).toMatchObject({ total: 3, reused: 0, running: 0, queued: 0,
|
|
71
|
+
expect(state).toMatchObject({ total: 3, reused: 0, running: 0, queued: 0, passed: 3, failed: 0, errored: 0, skipped: 0 });
|
|
60
72
|
expect(state.estimatedCostUSD).toBeCloseTo(0.35, 5);
|
|
61
73
|
expect(state.newTokenCount).toBe(40);
|
|
62
74
|
expect(state.active.size).toBe(0);
|
|
@@ -110,8 +122,11 @@ describe("reduceRunFeedback: 守恒公式", () => {
|
|
|
110
122
|
},
|
|
111
123
|
});
|
|
112
124
|
// plan 之后立刻(在任何 attempt 事件之前)守恒公式就应成立:2 个 reused + 3 个 queued。
|
|
113
|
-
|
|
114
|
-
expect(state1).
|
|
125
|
+
// 携入结果的 verdict 留在 reused,不摊进四项结局:plan 带 reusedFailures 也一样。
|
|
126
|
+
expect(state1.total).toBe(
|
|
127
|
+
state1.reused + state1.running + state1.queued + state1.passed + state1.failed + state1.errored + state1.skipped,
|
|
128
|
+
);
|
|
129
|
+
expect(state1).toMatchObject({ total: 5, reused: 2, running: 0, queued: 3, passed: 0, failed: 0, errored: 0, skipped: 0 });
|
|
115
130
|
|
|
116
131
|
const a = ref("memory/a", 0, "compare/bub-e2b");
|
|
117
132
|
const b = ref("memory/b", 0, "compare/bub-e2b");
|
|
@@ -125,7 +140,7 @@ describe("reduceRunFeedback: 守恒公式", () => {
|
|
|
125
140
|
{ type: "attempt:complete", at: 5, identity: b, who: "bub-e2b", verdict: "passed" },
|
|
126
141
|
{ type: "attempt:complete", at: 5, identity: c, who: "bub-e2b", verdict: "passed" },
|
|
127
142
|
]);
|
|
128
|
-
expect(final).toMatchObject({ total: 5, reused: 2, running: 0, queued: 0,
|
|
143
|
+
expect(final).toMatchObject({ total: 5, reused: 2, running: 0, queued: 0, passed: 3, failed: 0, errored: 0, skipped: 0 });
|
|
129
144
|
});
|
|
130
145
|
|
|
131
146
|
it("并发完成:多个 active attempt 以任意顺序结束,active map 逐条精确摘除", () => {
|
|
@@ -154,15 +169,31 @@ describe("reduceRunFeedback: 守恒公式", () => {
|
|
|
154
169
|
];
|
|
155
170
|
for (const { identity, verdict } of order) {
|
|
156
171
|
state = reduceRunFeedback(state, { type: "attempt:complete", at: 2, identity, who: "codex", verdict });
|
|
157
|
-
expect(state.total).toBe(
|
|
172
|
+
expect(state.total).toBe(
|
|
173
|
+
state.reused + state.running + state.queued + state.passed + state.failed + state.errored + state.skipped,
|
|
174
|
+
);
|
|
158
175
|
expect(state.active.size).toBe(state.running);
|
|
159
176
|
// 每次完成后,已完成的那个 identity 必须已经从 active 里摘除,其余仍在的必须还在。
|
|
160
177
|
expect(state.active.has(encodeAttemptKey(identity))).toBe(false);
|
|
161
178
|
}
|
|
162
|
-
|
|
179
|
+
// 落项由事件携带的 verdict 决定:同一批完成事件里换 verdict,落项跟着变,不是恒落同一项。
|
|
180
|
+
expect(state).toMatchObject({ running: 0, queued: 0, passed: 2, failed: 1, errored: 1, skipped: 0 });
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("attempt:complete 的 skipped verdict 落 skipped,不并进 passed", () => {
|
|
184
|
+
const a = ref("memory/a");
|
|
185
|
+
const b = ref("memory/b");
|
|
186
|
+
const state = replay([
|
|
187
|
+
{ type: "plan", at: 0, plan: { shape: { evals: 2, configs: 1, totalAttempts: 2, maxConcurrency: 2 }, reused: 0, reusedFailures: [] } },
|
|
188
|
+
{ type: "attempt:start", at: 1, identity: a, who: "codex", phase: "eval.run" },
|
|
189
|
+
{ type: "attempt:start", at: 1, identity: b, who: "codex", phase: "eval.run" },
|
|
190
|
+
{ type: "attempt:complete", at: 2, identity: a, who: "codex", verdict: "skipped" },
|
|
191
|
+
{ type: "attempt:complete", at: 3, identity: b, who: "codex", verdict: "passed" },
|
|
192
|
+
]);
|
|
193
|
+
expect(state).toMatchObject({ total: 2, running: 0, queued: 0, passed: 1, failed: 0, errored: 0, skipped: 1 });
|
|
163
194
|
});
|
|
164
195
|
|
|
165
|
-
it("early exit:未派发的重复轮次折进
|
|
196
|
+
it("early exit:未派发的重复轮次折进 skipped(不冒充 passed),不产生 failures/diagnostics;按 (experiment, eval) 累计 earlyExitByEval", () => {
|
|
166
197
|
const first = ref("memory/retry", 0);
|
|
167
198
|
const retry1 = ref("memory/retry", 1);
|
|
168
199
|
const retry2 = ref("memory/retry", 2);
|
|
@@ -173,7 +204,8 @@ describe("reduceRunFeedback: 守恒公式", () => {
|
|
|
173
204
|
{ type: "attempt:early-exit", at: 3, identity: retry1, who: "codex" },
|
|
174
205
|
{ type: "attempt:early-exit", at: 4, identity: retry2, who: "codex" },
|
|
175
206
|
]);
|
|
176
|
-
|
|
207
|
+
// 1 条真跑出 passed,2 条省略的轮次进 skipped —— 省了多少轮直接读得出来。
|
|
208
|
+
expect(state).toMatchObject({ total: 3, reused: 0, running: 0, queued: 0, passed: 1, failed: 0, errored: 0, skipped: 2 });
|
|
177
209
|
expect(state.failures).toEqual([]);
|
|
178
210
|
expect(state.diagnostics).toEqual([]);
|
|
179
211
|
// 无 experimentId 的裸 run:evalConclusionKey 用 "" 占位空 experimentId 段。
|
|
@@ -227,7 +259,7 @@ describe("reduceRunFeedback: 守恒公式", () => {
|
|
|
227
259
|
});
|
|
228
260
|
expect(state.failures).toHaveLength(1);
|
|
229
261
|
expect(state.failures[0]?.reason).toContain("sandbox-rate-limit");
|
|
230
|
-
expect(state).toMatchObject({ total: 1, running: 0,
|
|
262
|
+
expect(state).toMatchObject({ total: 1, running: 0, queued: 0, passed: 0, failed: 0, errored: 1, skipped: 0 });
|
|
231
263
|
|
|
232
264
|
// 同一 locator 再来一次(比如 coordinator 重放/去抖动),覆盖而不是变成两条。
|
|
233
265
|
state = reduceRunFeedback(state, {
|
|
@@ -244,7 +276,7 @@ describe("reduceRunFeedback: 守恒公式", () => {
|
|
|
244
276
|
expect(state.failures[0]?.reason).toBe("updated reason");
|
|
245
277
|
});
|
|
246
278
|
|
|
247
|
-
it("budget:每次因预算跳过一个 attempt 就折进
|
|
279
|
+
it("budget:每次因预算跳过一个 attempt 就折进 skipped,诊断按 experimentId 去重并累加 count", () => {
|
|
248
280
|
const a = ref("memory/a", 0, "regression/codex");
|
|
249
281
|
const b = ref("memory/b", 0, "regression/codex");
|
|
250
282
|
let state = createInitialRunFeedbackState();
|
|
@@ -256,7 +288,7 @@ describe("reduceRunFeedback: 守恒公式", () => {
|
|
|
256
288
|
state = reduceRunFeedback(state, { type: "attempt:start", at: 1, identity: a, who: "regression/codex", phase: "eval.run" });
|
|
257
289
|
state = reduceRunFeedback(state, { type: "attempt:start", at: 1, identity: b, who: "regression/codex", phase: "eval.run" });
|
|
258
290
|
state = reduceRunFeedback(state, { type: "attempt:complete", at: 2, identity: a, who: "regression/codex", verdict: "passed" });
|
|
259
|
-
expect(state).toMatchObject({ total: 4, running: 1, queued: 2,
|
|
291
|
+
expect(state).toMatchObject({ total: 4, running: 1, queued: 2, passed: 1, skipped: 0 });
|
|
260
292
|
|
|
261
293
|
state = reduceRunFeedback(state, {
|
|
262
294
|
type: "budget-exhausted",
|
|
@@ -265,7 +297,7 @@ describe("reduceRunFeedback: 守恒公式", () => {
|
|
|
265
297
|
spent: 25.1,
|
|
266
298
|
unstarted: 1,
|
|
267
299
|
});
|
|
268
|
-
expect(state).toMatchObject({ total: 4, running: 1, queued: 1,
|
|
300
|
+
expect(state).toMatchObject({ total: 4, running: 1, queued: 1, passed: 1, skipped: 1 });
|
|
269
301
|
expect(state.diagnostics).toHaveLength(1);
|
|
270
302
|
expect(state.diagnostics[0]).toMatchObject({ key: "budget-exhausted:regression/codex", count: 1, severity: "warning" });
|
|
271
303
|
expect(state.diagnostics[0]?.data).toMatchObject({ experimentId: "regression/codex", spent: 25.1, unstarted: 1 });
|
|
@@ -277,14 +309,14 @@ describe("reduceRunFeedback: 守恒公式", () => {
|
|
|
277
309
|
spent: 25.31,
|
|
278
310
|
unstarted: 2,
|
|
279
311
|
});
|
|
280
|
-
expect(state).toMatchObject({ total: 4, running: 1, queued: 0,
|
|
312
|
+
expect(state).toMatchObject({ total: 4, running: 1, queued: 0, passed: 1, skipped: 2 });
|
|
281
313
|
// 去重:同一个 experimentId 仍然只有一条诊断,count 累加到 2,data 更新到最新一次的值。
|
|
282
314
|
expect(state.diagnostics).toHaveLength(1);
|
|
283
315
|
expect(state.diagnostics[0]).toMatchObject({ count: 2 });
|
|
284
316
|
expect(state.diagnostics[0]?.data).toMatchObject({ spent: 25.31, unstarted: 2 });
|
|
285
317
|
|
|
286
318
|
state = reduceRunFeedback(state, { type: "attempt:complete", at: 5, identity: b, who: "regression/codex", verdict: "passed" });
|
|
287
|
-
expect(state).toMatchObject({ total: 4, reused: 0, running: 0, queued: 0,
|
|
319
|
+
expect(state).toMatchObject({ total: 4, reused: 0, running: 0, queued: 0, passed: 2, failed: 0, errored: 0, skipped: 2 });
|
|
288
320
|
});
|
|
289
321
|
|
|
290
322
|
it("interrupted:只追加一条去重诊断,不擅自改变 running/queued 计数(中断时的进行中 attempt 保持原状)", () => {
|
|
@@ -299,14 +331,17 @@ describe("reduceRunFeedback: 守恒公式", () => {
|
|
|
299
331
|
state = reduceRunFeedback(state, { type: "attempt:start", at: 1, identity: a, who: "codex", phase: "eval.run" });
|
|
300
332
|
const beforeInterrupt = state;
|
|
301
333
|
state = reduceRunFeedback(state, { type: "interrupted", at: 2 });
|
|
302
|
-
expect(state).toMatchObject({ total: 3, running: 1, queued: 2,
|
|
334
|
+
expect(state).toMatchObject({ total: 3, running: 1, queued: 2, passed: 0, failed: 0, errored: 0, skipped: 0 });
|
|
303
335
|
expect(state.diagnostics).toHaveLength(1);
|
|
304
336
|
expect(state.diagnostics[0]).toMatchObject({ key: "interrupted", severity: "warning", count: 1 });
|
|
305
337
|
// interrupted 只追加诊断,counts/active 与中断前完全一致(除了 diagnostics 数组本身)。
|
|
306
338
|
expect(state.total).toBe(beforeInterrupt.total);
|
|
307
339
|
expect(state.running).toBe(beforeInterrupt.running);
|
|
308
340
|
expect(state.queued).toBe(beforeInterrupt.queued);
|
|
309
|
-
expect(state.
|
|
341
|
+
expect(state.passed).toBe(beforeInterrupt.passed);
|
|
342
|
+
expect(state.failed).toBe(beforeInterrupt.failed);
|
|
343
|
+
expect(state.errored).toBe(beforeInterrupt.errored);
|
|
344
|
+
expect(state.skipped).toBe(beforeInterrupt.skipped);
|
|
310
345
|
expect(state.active).toBe(beforeInterrupt.active);
|
|
311
346
|
|
|
312
347
|
// 再来一次 interrupted(理论上不该发生,防御性验证不会重复追加成两条)。
|
|
@@ -390,6 +425,25 @@ describe("reduceRunFeedback: 守恒公式", () => {
|
|
|
390
425
|
expect(state.diagnostics[0]).toMatchObject({ key: "memory-warmup-degraded", count: 3 });
|
|
391
426
|
});
|
|
392
427
|
|
|
428
|
+
it("diagnostic:code 原样带进 DiagnosticNotice;折叠仍按 key —— 同 code 不同身份是两条", () => {
|
|
429
|
+
let state = createInitialRunFeedbackState();
|
|
430
|
+
for (const evalId of ["memory/a", "memory/b", "memory/a"]) {
|
|
431
|
+
state = reduceRunFeedback(state, {
|
|
432
|
+
type: "diagnostic",
|
|
433
|
+
at: 1,
|
|
434
|
+
key: `dispatch-halted:eval:compare/codex|${evalId}`,
|
|
435
|
+
code: "dispatch-halted",
|
|
436
|
+
severity: "error",
|
|
437
|
+
message: `eval halted: ${evalId} needs a seeded fixture`,
|
|
438
|
+
data: { experimentId: "compare/codex", scope: "eval", evalId },
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
expect(state.diagnostics).toHaveLength(2);
|
|
442
|
+
expect(state.diagnostics.map((d) => d.code)).toEqual(["dispatch-halted", "dispatch-halted"]);
|
|
443
|
+
expect(state.diagnostics[0]).toMatchObject({ key: "dispatch-halted:eval:compare/codex|memory/a", count: 2 });
|
|
444
|
+
expect(state.diagnostics[1]).toMatchObject({ key: "dispatch-halted:eval:compare/codex|memory/b", count: 1 });
|
|
445
|
+
});
|
|
446
|
+
|
|
393
447
|
it("phase 变化清空上一个 phase 的 detail,不把旧阶段的次要文本带进新阶段", () => {
|
|
394
448
|
const a = ref("memory/a");
|
|
395
449
|
let state = createInitialRunFeedbackState();
|
|
@@ -418,7 +472,7 @@ describe("reduceRunFeedback: judge 预检运行级行", () => {
|
|
|
418
472
|
it("started 建行、done 清行;预检期间 attempt 全程保持 queued(计数不变量不受预检影响)", () => {
|
|
419
473
|
const afterStart = replay([plan, { type: "precheck", at: 1, status: "started" }]);
|
|
420
474
|
// 复现用户看到的「0 running · 1 queued」——预检不动计数,这行才是它停在 queued 的解释。
|
|
421
|
-
expect(afterStart).toMatchObject({ total: 1, running: 0, queued: 1,
|
|
475
|
+
expect(afterStart).toMatchObject({ total: 1, running: 0, queued: 1, passed: 0, failed: 0, errored: 0, skipped: 0 });
|
|
422
476
|
expect(afterStart.activePrecheck).toEqual({ startedAt: 1 });
|
|
423
477
|
|
|
424
478
|
const afterDone = replay([
|
|
@@ -427,7 +481,7 @@ describe("reduceRunFeedback: judge 预检运行级行", () => {
|
|
|
427
481
|
{ type: "precheck", at: 15, status: "done", durationMs: 14 },
|
|
428
482
|
]);
|
|
429
483
|
expect(afterDone.activePrecheck).toBeUndefined();
|
|
430
|
-
expect(afterDone).toMatchObject({ total: 1, running: 0, queued: 1,
|
|
484
|
+
expect(afterDone).toMatchObject({ total: 1, running: 0, queued: 1, passed: 0, failed: 0, errored: 0, skipped: 0 });
|
|
431
485
|
});
|
|
432
486
|
|
|
433
487
|
it("plan 事件清空残留的预检行(reducer 复用于多次 run 时不带上一次的预检状态)", () => {
|
|
@@ -452,7 +506,7 @@ describe("reduceRunFeedback: 实验级钩子", () => {
|
|
|
452
506
|
plan,
|
|
453
507
|
{ type: "experiment-hook", at: 1, experimentId: "compare/bub-e2b", hook: "setup", status: "started" },
|
|
454
508
|
]);
|
|
455
|
-
expect(afterStart).toMatchObject({ total: 2, running: 0, queued: 2,
|
|
509
|
+
expect(afterStart).toMatchObject({ total: 2, running: 0, queued: 2, passed: 0, failed: 0, errored: 0, skipped: 0 });
|
|
456
510
|
expect(afterStart.experimentHooks.get("compare/bub-e2b")).toMatchObject({
|
|
457
511
|
experimentId: "compare/bub-e2b",
|
|
458
512
|
hook: "setup",
|
|
@@ -503,3 +557,248 @@ describe("reduceRunFeedback: 实验级钩子", () => {
|
|
|
503
557
|
expect(state.experimentHooks.size).toBe(0);
|
|
504
558
|
});
|
|
505
559
|
});
|
|
560
|
+
|
|
561
|
+
// cases: docs/engineering/testing/unit/experiments-runner.md「用例锁与并发 Invocation」——
|
|
562
|
+
// 「等待语义……计入独立的 elsewhere 计数且与 queued 互斥、五项计数恒等式成立」与
|
|
563
|
+
// 「释放后续接……指纹匹配携入且计数 elsewhere 迁 reused、不匹配转 queued 自跑、runs 部分携入
|
|
564
|
+
// 部分补跑,三面都要有区分力场景」。lock.ts 本身(取锁/心跳/接管)与 run.ts 的调度接线不在
|
|
565
|
+
// 这里覆盖(run.test.ts / lock.test.ts 各自的范畴),这里只证 reducer 的纯状态归约。
|
|
566
|
+
describe("reduceRunFeedback: 用例锁等待(elsewhere)", () => {
|
|
567
|
+
const plan: RunFeedbackEvent = {
|
|
568
|
+
type: "plan",
|
|
569
|
+
at: 0,
|
|
570
|
+
plan: { shape: { evals: 2, configs: 1, totalAttempts: 5, maxConcurrency: 5 }, reused: 0, reusedFailures: [] },
|
|
571
|
+
};
|
|
572
|
+
|
|
573
|
+
it("started 把撞锁的 attempt 数从 queued 移入 elsewhere,与 queued 互斥;五项恒等式成立", () => {
|
|
574
|
+
const afterStart = replay([
|
|
575
|
+
plan,
|
|
576
|
+
{
|
|
577
|
+
type: "lock-wait",
|
|
578
|
+
at: 1,
|
|
579
|
+
experimentId: "compare/codex",
|
|
580
|
+
evalId: "memory/retention",
|
|
581
|
+
status: "started",
|
|
582
|
+
holderPid: 41267,
|
|
583
|
+
holderHost: "mba.local",
|
|
584
|
+
attempts: 3,
|
|
585
|
+
},
|
|
586
|
+
]);
|
|
587
|
+
expect(afterStart).toMatchObject({ total: 5, reused: 0, running: 0, elsewhere: 3, queued: 2, passed: 0, failed: 0, errored: 0, skipped: 0 });
|
|
588
|
+
expect(afterStart.lockWaits.get("compare/codex")?.waiting.get("memory/retention")).toMatchObject({
|
|
589
|
+
startedAt: 1,
|
|
590
|
+
holderPid: 41267,
|
|
591
|
+
holderHost: "mba.local",
|
|
592
|
+
});
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
it("resolved 全部携入(carried):elsewhere 迁入 reused,不落回 queued", () => {
|
|
596
|
+
const afterResolved = replay([
|
|
597
|
+
plan,
|
|
598
|
+
{
|
|
599
|
+
type: "lock-wait",
|
|
600
|
+
at: 1,
|
|
601
|
+
experimentId: "compare/codex",
|
|
602
|
+
evalId: "memory/retention",
|
|
603
|
+
status: "started",
|
|
604
|
+
attempts: 3,
|
|
605
|
+
holderPid: 1,
|
|
606
|
+
holderHost: "h",
|
|
607
|
+
},
|
|
608
|
+
{
|
|
609
|
+
type: "lock-wait",
|
|
610
|
+
at: 20,
|
|
611
|
+
experimentId: "compare/codex",
|
|
612
|
+
evalId: "memory/retention",
|
|
613
|
+
status: "resolved",
|
|
614
|
+
carried: 3,
|
|
615
|
+
dispatched: 0,
|
|
616
|
+
waitedMs: 19_000,
|
|
617
|
+
},
|
|
618
|
+
]);
|
|
619
|
+
expect(afterResolved).toMatchObject({ total: 5, reused: 3, running: 0, elsewhere: 0, queued: 2, passed: 0, failed: 0, errored: 0, skipped: 0 });
|
|
620
|
+
// 等待条目从 waiting 里摘除,但窗口聚合状态(供非 TTY 收尾行读取)仍保留在 map 里。
|
|
621
|
+
expect(afterResolved.lockWaits.get("compare/codex")?.waiting.size).toBe(0);
|
|
622
|
+
expect(afterResolved.lockWaits.get("compare/codex")).toMatchObject({ resolvedCarried: 3, resolvedDispatched: 0 });
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
it("resolved 转自跑(dispatched):elsewhere 迁入 queued,照常经 running 落进 verdict 结局项", () => {
|
|
626
|
+
const a = ref("memory/retention", 0, "compare/codex");
|
|
627
|
+
const state = replay([
|
|
628
|
+
plan,
|
|
629
|
+
{
|
|
630
|
+
type: "lock-wait",
|
|
631
|
+
at: 1,
|
|
632
|
+
experimentId: "compare/codex",
|
|
633
|
+
evalId: "memory/retention",
|
|
634
|
+
status: "started",
|
|
635
|
+
attempts: 3,
|
|
636
|
+
holderPid: 1,
|
|
637
|
+
holderHost: "h",
|
|
638
|
+
},
|
|
639
|
+
{
|
|
640
|
+
type: "lock-wait",
|
|
641
|
+
at: 20,
|
|
642
|
+
experimentId: "compare/codex",
|
|
643
|
+
evalId: "memory/retention",
|
|
644
|
+
status: "resolved",
|
|
645
|
+
carried: 0,
|
|
646
|
+
dispatched: 3,
|
|
647
|
+
waitedMs: 19_000,
|
|
648
|
+
},
|
|
649
|
+
{ type: "attempt:start", at: 21, identity: a, who: "compare/codex", phase: "eval.run" },
|
|
650
|
+
{ type: "attempt:complete", at: 25, identity: a, who: "compare/codex", verdict: "passed" },
|
|
651
|
+
]);
|
|
652
|
+
// plan 初始 5 - 3(撞锁的)= 2 个 queued;3 个从 elsewhere 转来的 attempt 也都进了 queued
|
|
653
|
+
// (合计 5),其中 1 个真的被派发跑完,4 个仍在 queued。
|
|
654
|
+
expect(state).toMatchObject({ total: 5, reused: 0, elsewhere: 0, queued: 4, running: 0, passed: 1, failed: 0, errored: 0, skipped: 0 });
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
it("runs 部分携入部分补跑:同一 resolved 事件里 carried 与 dispatched 同时非零", () => {
|
|
658
|
+
const afterResolved = replay([
|
|
659
|
+
plan,
|
|
660
|
+
{
|
|
661
|
+
type: "lock-wait",
|
|
662
|
+
at: 1,
|
|
663
|
+
experimentId: "compare/codex",
|
|
664
|
+
evalId: "memory/retention",
|
|
665
|
+
status: "started",
|
|
666
|
+
attempts: 3,
|
|
667
|
+
holderPid: 1,
|
|
668
|
+
holderHost: "h",
|
|
669
|
+
},
|
|
670
|
+
{
|
|
671
|
+
type: "lock-wait",
|
|
672
|
+
at: 20,
|
|
673
|
+
experimentId: "compare/codex",
|
|
674
|
+
evalId: "memory/retention",
|
|
675
|
+
status: "resolved",
|
|
676
|
+
carried: 2,
|
|
677
|
+
dispatched: 1,
|
|
678
|
+
waitedMs: 19_000,
|
|
679
|
+
},
|
|
680
|
+
]);
|
|
681
|
+
// 原本 5 - 3(撞锁的)= 2 个 queued,加上这次转自跑的 1 个 = 3。
|
|
682
|
+
expect(afterResolved).toMatchObject({ elsewhere: 0, reused: 2, queued: 3, total: 5 });
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
it("同一实验多个用例各自撞锁:elsewhere 累加,各自 resolved 各自结算", () => {
|
|
686
|
+
const state = replay([
|
|
687
|
+
plan,
|
|
688
|
+
{
|
|
689
|
+
type: "lock-wait",
|
|
690
|
+
at: 1,
|
|
691
|
+
experimentId: "compare/codex",
|
|
692
|
+
evalId: "memory/a",
|
|
693
|
+
status: "started",
|
|
694
|
+
attempts: 1,
|
|
695
|
+
holderPid: 1,
|
|
696
|
+
holderHost: "h",
|
|
697
|
+
},
|
|
698
|
+
{
|
|
699
|
+
type: "lock-wait",
|
|
700
|
+
at: 2,
|
|
701
|
+
experimentId: "compare/codex",
|
|
702
|
+
evalId: "memory/b",
|
|
703
|
+
status: "started",
|
|
704
|
+
attempts: 1,
|
|
705
|
+
holderPid: 1,
|
|
706
|
+
holderHost: "h",
|
|
707
|
+
},
|
|
708
|
+
]);
|
|
709
|
+
expect(state.elsewhere).toBe(2);
|
|
710
|
+
expect(state.lockWaits.get("compare/codex")?.waiting.size).toBe(2);
|
|
711
|
+
|
|
712
|
+
const afterOneResolves = replay([
|
|
713
|
+
plan,
|
|
714
|
+
{
|
|
715
|
+
type: "lock-wait",
|
|
716
|
+
at: 1,
|
|
717
|
+
experimentId: "compare/codex",
|
|
718
|
+
evalId: "memory/a",
|
|
719
|
+
status: "started",
|
|
720
|
+
attempts: 1,
|
|
721
|
+
holderPid: 1,
|
|
722
|
+
holderHost: "h",
|
|
723
|
+
},
|
|
724
|
+
{
|
|
725
|
+
type: "lock-wait",
|
|
726
|
+
at: 2,
|
|
727
|
+
experimentId: "compare/codex",
|
|
728
|
+
evalId: "memory/b",
|
|
729
|
+
status: "started",
|
|
730
|
+
attempts: 1,
|
|
731
|
+
holderPid: 1,
|
|
732
|
+
holderHost: "h",
|
|
733
|
+
},
|
|
734
|
+
{ type: "lock-wait", at: 5, experimentId: "compare/codex", evalId: "memory/a", status: "resolved", carried: 1, dispatched: 0, waitedMs: 4_000 },
|
|
735
|
+
]);
|
|
736
|
+
// 只有 memory/a 解决了;memory/b 仍在等,窗口(waiting)非空。
|
|
737
|
+
expect(afterOneResolves.elsewhere).toBe(1);
|
|
738
|
+
expect(afterOneResolves.reused).toBe(1);
|
|
739
|
+
expect(afterOneResolves.lockWaits.get("compare/codex")?.waiting.size).toBe(1);
|
|
740
|
+
expect(afterOneResolves.lockWaits.get("compare/codex")?.waiting.has("memory/b")).toBe(true);
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
it("resolved 事件在没有对应等待条目时静默忽略(防御,不把计数推负)", () => {
|
|
744
|
+
const state = replay([plan, { type: "lock-wait", at: 1, experimentId: "compare/codex", evalId: "memory/ghost", status: "resolved", carried: 1, dispatched: 0 }]);
|
|
745
|
+
expect(state).toMatchObject({ elsewhere: 0, reused: 0, queued: 5 });
|
|
746
|
+
});
|
|
747
|
+
|
|
748
|
+
it("plan 事件清空残留的 elsewhere 计数与等待行(reducer 复用于多次 run 时不带上一次的状态)", () => {
|
|
749
|
+
let state = createInitialRunFeedbackState();
|
|
750
|
+
state = reduceRunFeedback(state, plan);
|
|
751
|
+
state = reduceRunFeedback(state, {
|
|
752
|
+
type: "lock-wait",
|
|
753
|
+
at: 1,
|
|
754
|
+
experimentId: "compare/codex",
|
|
755
|
+
evalId: "memory/retention",
|
|
756
|
+
status: "started",
|
|
757
|
+
attempts: 1,
|
|
758
|
+
holderPid: 1,
|
|
759
|
+
holderHost: "h",
|
|
760
|
+
});
|
|
761
|
+
expect(state.elsewhere).toBe(1);
|
|
762
|
+
state = reduceRunFeedback(state, plan);
|
|
763
|
+
expect(state.elsewhere).toBe(0);
|
|
764
|
+
expect(state.lockWaits.size).toBe(0);
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
it("窗口关闭后重新撞锁:非 TTY 聚合计数(resolvedCarried/resolvedDispatched)从零重新累计", () => {
|
|
768
|
+
let state = createInitialRunFeedbackState();
|
|
769
|
+
state = reduceRunFeedback(state, plan);
|
|
770
|
+
state = reduceRunFeedback(state, {
|
|
771
|
+
type: "lock-wait",
|
|
772
|
+
at: 1,
|
|
773
|
+
experimentId: "compare/codex",
|
|
774
|
+
evalId: "memory/a",
|
|
775
|
+
status: "started",
|
|
776
|
+
attempts: 1,
|
|
777
|
+
holderPid: 1,
|
|
778
|
+
holderHost: "h",
|
|
779
|
+
});
|
|
780
|
+
state = reduceRunFeedback(state, {
|
|
781
|
+
type: "lock-wait",
|
|
782
|
+
at: 5,
|
|
783
|
+
experimentId: "compare/codex",
|
|
784
|
+
evalId: "memory/a",
|
|
785
|
+
status: "resolved",
|
|
786
|
+
carried: 1,
|
|
787
|
+
dispatched: 0,
|
|
788
|
+
waitedMs: 4_000,
|
|
789
|
+
});
|
|
790
|
+
expect(state.lockWaits.get("compare/codex")).toMatchObject({ resolvedCarried: 1, resolvedDispatched: 0 });
|
|
791
|
+
// 新的一批用例再次撞锁:上一窗口的累计不该带进这一窗口。
|
|
792
|
+
state = reduceRunFeedback(state, {
|
|
793
|
+
type: "lock-wait",
|
|
794
|
+
at: 10,
|
|
795
|
+
experimentId: "compare/codex",
|
|
796
|
+
evalId: "memory/b",
|
|
797
|
+
status: "started",
|
|
798
|
+
attempts: 1,
|
|
799
|
+
holderPid: 1,
|
|
800
|
+
holderHost: "h",
|
|
801
|
+
});
|
|
802
|
+
expect(state.lockWaits.get("compare/codex")).toMatchObject({ resolvedCarried: 0, resolvedDispatched: 0 });
|
|
803
|
+
});
|
|
804
|
+
});
|