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
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
type ExperimentHookInput,
|
|
29
29
|
type ExperimentProgressInput,
|
|
30
30
|
type PrecheckInput,
|
|
31
|
+
type LockWaitInput,
|
|
31
32
|
type FailureInput,
|
|
32
33
|
type FeedbackSink,
|
|
33
34
|
type KeptInput,
|
|
@@ -198,6 +199,7 @@ export function createFeedbackCoordinator(options: FeedbackCoordinatorOptions):
|
|
|
198
199
|
type: "diagnostic",
|
|
199
200
|
at: io.clock.now(),
|
|
200
201
|
key: input.key,
|
|
202
|
+
...(input.code !== undefined ? { code: input.code } : {}),
|
|
201
203
|
severity: input.severity,
|
|
202
204
|
message: input.message,
|
|
203
205
|
identity: input.identity,
|
|
@@ -272,6 +274,22 @@ export function createFeedbackCoordinator(options: FeedbackCoordinatorOptions):
|
|
|
272
274
|
});
|
|
273
275
|
}
|
|
274
276
|
|
|
277
|
+
function lockWait(input: LockWaitInput): void {
|
|
278
|
+
emit({
|
|
279
|
+
type: "lock-wait",
|
|
280
|
+
at: io.clock.now(),
|
|
281
|
+
experimentId: input.experimentId,
|
|
282
|
+
evalId: input.evalId,
|
|
283
|
+
status: input.status,
|
|
284
|
+
...(input.holderPid !== undefined ? { holderPid: input.holderPid } : {}),
|
|
285
|
+
...(input.holderHost !== undefined ? { holderHost: input.holderHost } : {}),
|
|
286
|
+
...(input.attempts !== undefined ? { attempts: input.attempts } : {}),
|
|
287
|
+
...(input.carried !== undefined ? { carried: input.carried } : {}),
|
|
288
|
+
...(input.dispatched !== undefined ? { dispatched: input.dispatched } : {}),
|
|
289
|
+
...(input.waitedMs !== undefined ? { waitedMs: input.waitedMs } : {}),
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
275
293
|
function reporterError(input: { reporter: string; required: boolean; message: string }): void {
|
|
276
294
|
emit({
|
|
277
295
|
type: "reporter-error",
|
|
@@ -313,6 +331,7 @@ export function createFeedbackCoordinator(options: FeedbackCoordinatorOptions):
|
|
|
313
331
|
experimentHook,
|
|
314
332
|
experimentProgress,
|
|
315
333
|
precheck,
|
|
334
|
+
lockWait,
|
|
316
335
|
lifecycle,
|
|
317
336
|
});
|
|
318
337
|
emit({ type: "plan", at: startedAtMs, plan });
|
|
@@ -378,6 +397,7 @@ export function createFeedbackCoordinator(options: FeedbackCoordinatorOptions):
|
|
|
378
397
|
experimentHook,
|
|
379
398
|
experimentProgress,
|
|
380
399
|
precheck,
|
|
400
|
+
lockWait,
|
|
381
401
|
lifecycle,
|
|
382
402
|
stopDynamic,
|
|
383
403
|
finish,
|
|
@@ -404,8 +424,10 @@ function fallbackTextFor(event: DurableFeedbackEvent): string | undefined {
|
|
|
404
424
|
case "saved":
|
|
405
425
|
case "experiment-hook":
|
|
406
426
|
case "precheck":
|
|
407
|
-
|
|
408
|
-
//
|
|
427
|
+
case "lock-wait":
|
|
428
|
+
// 钩子/预检/锁等待起止都不是失败证据:setup 失败的每条 attempt 另有 "failure" 事件
|
|
429
|
+
// 兜底,预检失败以既有错误路径中止运行,锁等待解决与否不改变任何 verdict——
|
|
430
|
+
// renderer 崩溃丢一行起止不丢数据。
|
|
409
431
|
return undefined;
|
|
410
432
|
}
|
|
411
433
|
}
|
|
@@ -40,12 +40,15 @@ export interface EvalConclusionRow {
|
|
|
40
40
|
rate?: number;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
/** `diagnostics` 里
|
|
44
|
-
* 的记录(理论上不应出现——fail-fast 诊断恒带 identity,见 run.ts)不计入任何 key。
|
|
43
|
+
/** `diagnostics` 里 fail-fast 记录按 (experiment, eval) 求和的未派发次数;没有 `identity`
|
|
44
|
+
* 的记录(理论上不应出现——fail-fast 诊断恒带 identity,见 run.ts)不计入任何 key。
|
|
45
|
+
* 归类按稳定词法 `code`(省略时回落到 key 首段,缺省 key 恒是 `${code}:${identity}`),不按
|
|
46
|
+
* `key` 前缀:key 里编着折叠身份,拿它匹配会在身份编码变动时静默失配,减不掉的那部分
|
|
47
|
+
* fail-fast 份额会当成真实的 earlyExit 省略数报出去。 */
|
|
45
48
|
function failFastByEval(diagnostics: readonly DiagnosticNotice[]): ReadonlyMap<string, number> {
|
|
46
49
|
const result = new Map<string, number>();
|
|
47
50
|
for (const d of diagnostics) {
|
|
48
|
-
if (
|
|
51
|
+
if ((d.code ?? d.key.split(":", 1)[0]) !== "fail-fast" || !d.identity) continue;
|
|
49
52
|
const key = evalConclusionKey(d.identity);
|
|
50
53
|
result.set(key, (result.get(key) ?? 0) + d.count);
|
|
51
54
|
}
|
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
// boxed 能力下产生可识别的框线字符与正确的面板顺序/分隔,plain/非 TTY 下不产生任何框字符。
|
|
6
6
|
|
|
7
7
|
import { afterEach, describe, expect, it } from "vitest";
|
|
8
|
-
import { createHumanRenderer, renderDurableLines } from "./human.ts";
|
|
8
|
+
import { createHumanRenderer, renderDurableLines, renderHumanDryPlan } from "./human.ts";
|
|
9
9
|
import { createFakeFeedbackIO } from "./testing.ts";
|
|
10
|
-
import { createInitialRunFeedbackState } from "./reducer.ts";
|
|
11
|
-
import { encodeAttemptKey } from "../types.ts";
|
|
10
|
+
import { createInitialRunFeedbackState, reduceRunFeedback } from "./reducer.ts";
|
|
11
|
+
import { encodeAttemptKey, HALT_DIAGNOSTIC_CODE } from "../types.ts";
|
|
12
12
|
import { stringWidth } from "../../report/model/text-layout.ts";
|
|
13
13
|
import type { DurableFeedbackEvent, InvocationCompletion, InvocationSummary, RunFeedbackPlan, RunFeedbackState } from "../types.ts";
|
|
14
14
|
import type { AttemptLocator } from "../../results/locator.ts";
|
|
@@ -182,7 +182,8 @@ describe("live dashboard — 接线到 panel.ts", () => {
|
|
|
182
182
|
reused: 6,
|
|
183
183
|
running: 19,
|
|
184
184
|
queued: 12,
|
|
185
|
-
|
|
185
|
+
passed: 6,
|
|
186
|
+
failed: 2,
|
|
186
187
|
elapsedMs: 134_000,
|
|
187
188
|
estimatedCostUSD: 0.84,
|
|
188
189
|
active: new Map([[key, { identity, who: "compare/bub-e2b", phase: "eval.run", phaseStartedAt: 0 }]]),
|
|
@@ -260,7 +261,8 @@ describe("live dashboard — 宽终端下 ACTIVE 行与身份列分配", () => {
|
|
|
260
261
|
reused: 6,
|
|
261
262
|
running: 19,
|
|
262
263
|
queued: 12,
|
|
263
|
-
|
|
264
|
+
passed: 6,
|
|
265
|
+
failed: 2,
|
|
264
266
|
elapsedMs: 134_000,
|
|
265
267
|
active: new Map([
|
|
266
268
|
[key, { identity, who: "compare/bub-e2b", phase: "eval.run", phaseStartedAt: 0, detail: longDetail }],
|
|
@@ -358,7 +360,7 @@ describe("live dashboard — 宽终端下 ACTIVE 行与身份列分配", () => {
|
|
|
358
360
|
...createInitialRunFeedbackState(),
|
|
359
361
|
total: 2,
|
|
360
362
|
running: 1,
|
|
361
|
-
|
|
363
|
+
passed: 1,
|
|
362
364
|
active: new Map([[shortKey, { identity: shortIdentity, who, phase: "eval.run", phaseStartedAt: 1 }]]),
|
|
363
365
|
};
|
|
364
366
|
renderer.onLifecycle?.(
|
|
@@ -418,3 +420,295 @@ describe("live dashboard — 宽终端下 ACTIVE 行与身份列分配", () => {
|
|
|
418
420
|
for (const l of framedLines) expect(stringWidth(l)).toBe(100);
|
|
419
421
|
});
|
|
420
422
|
});
|
|
423
|
+
|
|
424
|
+
// cases: docs/engineering/testing/unit/experiments-runner.md「用例锁与并发 Invocation」——
|
|
425
|
+
// 字节级精确渲染归 E2E · CLI「反馈输出格式」;这里只做与 precheck/experiment-hook 同等级别
|
|
426
|
+
// 的最小 smoke 断言(行是否出现、关键子串是否存在),不断言列宽算术。
|
|
427
|
+
describe("用例锁等待(elsewhere)的显示", () => {
|
|
428
|
+
it("TTY:等待期间显示运行级行,面板首行的 elsewhere 计数非零", () => {
|
|
429
|
+
const { io, stderr } = createFakeFeedbackIO({ stderr: { isTTY: true, columns: 100, rows: 30 } });
|
|
430
|
+
const renderer = createHumanRenderer({ io, command: "niceeval exp compare/codex" });
|
|
431
|
+
const state: RunFeedbackState = {
|
|
432
|
+
...createInitialRunFeedbackState(),
|
|
433
|
+
total: 3,
|
|
434
|
+
elsewhere: 2,
|
|
435
|
+
queued: 1,
|
|
436
|
+
lockWaits: new Map([
|
|
437
|
+
[
|
|
438
|
+
"compare/codex",
|
|
439
|
+
{
|
|
440
|
+
experimentId: "compare/codex",
|
|
441
|
+
waiting: new Map([
|
|
442
|
+
["memory/a", { startedAt: 0, holderPid: 41267, holderHost: "mba.local" }],
|
|
443
|
+
["memory/b", { startedAt: 5, holderPid: 41267, holderHost: "mba.local" }],
|
|
444
|
+
]),
|
|
445
|
+
resolvedCarried: 0,
|
|
446
|
+
resolvedDispatched: 0,
|
|
447
|
+
},
|
|
448
|
+
],
|
|
449
|
+
]),
|
|
450
|
+
};
|
|
451
|
+
renderer.redrawDynamic?.(state);
|
|
452
|
+
|
|
453
|
+
const plain = stripAnsi(stderr.writes.join(""));
|
|
454
|
+
expect(plain).toMatch(/├─ ACTIVE ─+┤/);
|
|
455
|
+
expect(plain).toContain("waiting on another run");
|
|
456
|
+
expect(plain).toContain("compare/codex");
|
|
457
|
+
expect(plain).toContain("2 evals");
|
|
458
|
+
expect(plain).toContain("pid 41267");
|
|
459
|
+
expect(plain).toContain("2 elsewhere");
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
it("TTY appendDurable 对 lock-wait 直接返回,不写 scrollback 永久行(运行级行由 state.lockWaits 驱动)", () => {
|
|
463
|
+
const { io, stdout, stderr } = createFakeFeedbackIO({ stderr: { isTTY: true, columns: 100, rows: 30 } });
|
|
464
|
+
const renderer = createHumanRenderer({ io, command: "niceeval exp compare/codex" });
|
|
465
|
+
const state: RunFeedbackState = { ...createInitialRunFeedbackState(), total: 1, elsewhere: 1 };
|
|
466
|
+
renderer.appendDurable(
|
|
467
|
+
{ type: "lock-wait", at: 0, experimentId: "compare/codex", evalId: "memory/a", status: "started", attempts: 1, holderPid: 1, holderHost: "h" },
|
|
468
|
+
state,
|
|
469
|
+
);
|
|
470
|
+
expect(stdout.writes.join("") + stderr.writes.join("")).toBe("");
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
it("非 TTY:started 只在窗口第一次打开(唯一等待用例)时追加一行,中途加入的用例不逐条刷屏", () => {
|
|
474
|
+
const { io, stdout } = createFakeFeedbackIO({ stderr: { isTTY: false } });
|
|
475
|
+
const renderer = createHumanRenderer({ io, command: "niceeval exp compare/codex" });
|
|
476
|
+
const firstState: RunFeedbackState = {
|
|
477
|
+
...createInitialRunFeedbackState(),
|
|
478
|
+
total: 2,
|
|
479
|
+
elsewhere: 1,
|
|
480
|
+
lockWaits: new Map([
|
|
481
|
+
[
|
|
482
|
+
"compare/codex",
|
|
483
|
+
{
|
|
484
|
+
experimentId: "compare/codex",
|
|
485
|
+
waiting: new Map([["memory/a", { startedAt: 0, holderPid: 41267 }]]),
|
|
486
|
+
resolvedCarried: 0,
|
|
487
|
+
resolvedDispatched: 0,
|
|
488
|
+
},
|
|
489
|
+
],
|
|
490
|
+
]),
|
|
491
|
+
};
|
|
492
|
+
renderer.appendDurable(
|
|
493
|
+
{ type: "lock-wait", at: 0, experimentId: "compare/codex", evalId: "memory/a", status: "started", attempts: 1, holderPid: 41267, holderHost: "h" },
|
|
494
|
+
firstState,
|
|
495
|
+
);
|
|
496
|
+
const secondState: RunFeedbackState = {
|
|
497
|
+
...firstState,
|
|
498
|
+
elsewhere: 2,
|
|
499
|
+
lockWaits: new Map([
|
|
500
|
+
[
|
|
501
|
+
"compare/codex",
|
|
502
|
+
{
|
|
503
|
+
...firstState.lockWaits.get("compare/codex")!,
|
|
504
|
+
waiting: new Map([
|
|
505
|
+
["memory/a", { startedAt: 0, holderPid: 41267 }],
|
|
506
|
+
["memory/b", { startedAt: 1, holderPid: 41267 }],
|
|
507
|
+
]),
|
|
508
|
+
},
|
|
509
|
+
],
|
|
510
|
+
]),
|
|
511
|
+
};
|
|
512
|
+
renderer.appendDurable(
|
|
513
|
+
{ type: "lock-wait", at: 1, experimentId: "compare/codex", evalId: "memory/b", status: "started", attempts: 1, holderPid: 41267, holderHost: "h" },
|
|
514
|
+
secondState,
|
|
515
|
+
);
|
|
516
|
+
|
|
517
|
+
const out = stdout.writes.join("");
|
|
518
|
+
expect(out).toContain("waiting on another run · compare/codex");
|
|
519
|
+
// 只出现一次:第二条(memory/b 加入)是同一窗口内的非首条,静默不刷屏。
|
|
520
|
+
expect(out.split("waiting on another run").length - 1).toBe(1);
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
it("非 TTY:resolved 只在窗口最后一次关闭(全部等待用例都已解决)时追加聚合收尾行", () => {
|
|
524
|
+
const { io, stdout } = createFakeFeedbackIO({ stderr: { isTTY: false } });
|
|
525
|
+
const renderer = createHumanRenderer({ io, command: "niceeval exp compare/codex" });
|
|
526
|
+
// 还剩一个用例没解决:窗口未关闭,静默。
|
|
527
|
+
const stillWaitingState: RunFeedbackState = {
|
|
528
|
+
...createInitialRunFeedbackState(),
|
|
529
|
+
lockWaits: new Map([
|
|
530
|
+
[
|
|
531
|
+
"compare/codex",
|
|
532
|
+
{
|
|
533
|
+
experimentId: "compare/codex",
|
|
534
|
+
waiting: new Map([["memory/b", { startedAt: 1, holderPid: 1 }]]),
|
|
535
|
+
resolvedCarried: 2,
|
|
536
|
+
resolvedDispatched: 0,
|
|
537
|
+
},
|
|
538
|
+
],
|
|
539
|
+
]),
|
|
540
|
+
};
|
|
541
|
+
renderer.appendDurable(
|
|
542
|
+
{ type: "lock-wait", at: 5, experimentId: "compare/codex", evalId: "memory/a", status: "resolved", carried: 2, dispatched: 0, waitedMs: 5_000 },
|
|
543
|
+
stillWaitingState,
|
|
544
|
+
);
|
|
545
|
+
expect(stdout.writes.join("")).toBe("");
|
|
546
|
+
|
|
547
|
+
// 最后一个也解决了:窗口关闭,打印聚合收尾行(carried + dispatched 混合的措辞两面都要覆盖)。
|
|
548
|
+
const closedState: RunFeedbackState = {
|
|
549
|
+
...createInitialRunFeedbackState(),
|
|
550
|
+
lockWaits: new Map([
|
|
551
|
+
[
|
|
552
|
+
"compare/codex",
|
|
553
|
+
{
|
|
554
|
+
experimentId: "compare/codex",
|
|
555
|
+
waiting: new Map(),
|
|
556
|
+
resolvedCarried: 2,
|
|
557
|
+
resolvedDispatched: 1,
|
|
558
|
+
},
|
|
559
|
+
],
|
|
560
|
+
]),
|
|
561
|
+
};
|
|
562
|
+
renderer.appendDurable(
|
|
563
|
+
{ type: "lock-wait", at: 94_000, experimentId: "compare/codex", evalId: "memory/b", status: "resolved", carried: 0, dispatched: 1, waitedMs: 94_000 },
|
|
564
|
+
closedState,
|
|
565
|
+
);
|
|
566
|
+
const out = stdout.writes.join("");
|
|
567
|
+
expect(out).toContain("lock wait resolved · compare/codex");
|
|
568
|
+
expect(out).toContain("2 carried");
|
|
569
|
+
expect(out).toContain("1 to run");
|
|
570
|
+
});
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
describe("诊断行:标题是「阶段标签 · code」,止损闸落闸是一行 error 级通知", () => {
|
|
574
|
+
/** 把同一条诊断喂 N 次(emitter 刷新 data.unstarted 时就是这个形状),返回每一次的渲染行。 */
|
|
575
|
+
function replayDiagnostic(event: DurableFeedbackEvent & { type: "diagnostic" }, times: number): string[][] {
|
|
576
|
+
let state = createInitialRunFeedbackState();
|
|
577
|
+
const out: string[][] = [];
|
|
578
|
+
for (let i = 0; i < times; i++) {
|
|
579
|
+
state = reduceRunFeedback(state, event);
|
|
580
|
+
out.push(renderDurableLines(event, state, { mode: "plain", width: 100 }));
|
|
581
|
+
}
|
|
582
|
+
return out;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
it("普通诊断的标题用 code,不把编了身份的去重 key 甩进人读的一行", () => {
|
|
586
|
+
const [lines] = replayDiagnostic(
|
|
587
|
+
{
|
|
588
|
+
type: "diagnostic",
|
|
589
|
+
at: 0,
|
|
590
|
+
key: "lock-taken-over:compare/codex|memory/retention",
|
|
591
|
+
code: "lock-taken-over",
|
|
592
|
+
severity: "warning",
|
|
593
|
+
message: "took over a stale lock from pid 41267",
|
|
594
|
+
},
|
|
595
|
+
1,
|
|
596
|
+
);
|
|
597
|
+
expect(lines![0]).toBe("! lock-taken-over");
|
|
598
|
+
expect(lines![1]).toContain("took over a stale lock");
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
it("同一 key 再次出现时标题带 ×N 折叠计数(非止损闸诊断保持既有形态)", () => {
|
|
602
|
+
const rounds = replayDiagnostic(
|
|
603
|
+
{ type: "diagnostic", at: 0, key: "memory-warmup-degraded", severity: "warning", message: "cold index" },
|
|
604
|
+
3,
|
|
605
|
+
);
|
|
606
|
+
expect(rounds[0]![0]).toBe("! memory-warmup-degraded");
|
|
607
|
+
expect(rounds[2]![0]).toBe("! memory-warmup-degraded (3 attempts)");
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
it("attempt 级诊断的标题是「阶段标签 · code」,阶段标签与失败行同一投影", () => {
|
|
611
|
+
// 失败行已有的 ` · <阶段标签>` 段是同一个投影的既有出口:拿它当参照,断言诊断行没有
|
|
612
|
+
// 另写一份阶段词表(不硬编码语言,en/zh-CN 两种 locale 下都成立)。
|
|
613
|
+
const failureLines = renderDurableLines(
|
|
614
|
+
{
|
|
615
|
+
type: "failure",
|
|
616
|
+
at: 0,
|
|
617
|
+
locator: locator("@1bwcxxiy"),
|
|
618
|
+
identity: { experimentId: "compare/codex", evalId: "memory/x", attempt: 1 },
|
|
619
|
+
who: "codex",
|
|
620
|
+
verdict: "errored",
|
|
621
|
+
reason: "boom",
|
|
622
|
+
phase: "sandbox.setup",
|
|
623
|
+
},
|
|
624
|
+
createInitialRunFeedbackState(),
|
|
625
|
+
{ mode: "plain", width: 100 },
|
|
626
|
+
);
|
|
627
|
+
const label = failureLines[0]!.split("\n")[0]!.split(" · ")[1]!;
|
|
628
|
+
|
|
629
|
+
const rounds = replayDiagnostic(
|
|
630
|
+
{
|
|
631
|
+
type: "diagnostic",
|
|
632
|
+
at: 0,
|
|
633
|
+
// 作者没传 dedupeKey 时 attempt.ts 折出来的 key —— 编了身份,不能出现在人读标题里。
|
|
634
|
+
key: "memory-warmup-degraded:compare/codex|memory/x|1",
|
|
635
|
+
code: "memory-warmup-degraded",
|
|
636
|
+
severity: "warning",
|
|
637
|
+
message: "Memory warmup failed; continuing with a cold index",
|
|
638
|
+
identity: { experimentId: "compare/codex", evalId: "memory/x", attempt: 1 },
|
|
639
|
+
data: { phase: "sandbox.setup" },
|
|
640
|
+
},
|
|
641
|
+
12,
|
|
642
|
+
);
|
|
643
|
+
expect(rounds[0]![0]).toBe(`! ${label} · memory-warmup-degraded`);
|
|
644
|
+
expect(rounds[11]![0]).toBe(`! ${label} · memory-warmup-degraded (12 attempts)`);
|
|
645
|
+
expect(rounds[11]![1]).toBe(" Memory warmup failed; continuing with a cold index");
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
it("运行级诊断没有 phase:标题只有 code,不留空的 · 分隔符", () => {
|
|
649
|
+
const [lines] = replayDiagnostic(
|
|
650
|
+
{
|
|
651
|
+
type: "diagnostic",
|
|
652
|
+
at: 0,
|
|
653
|
+
key: "budget-unenforceable:compare/codex",
|
|
654
|
+
code: "budget-unenforceable",
|
|
655
|
+
severity: "warning",
|
|
656
|
+
message: "no cost data; budget cannot be enforced",
|
|
657
|
+
data: { experimentId: "compare/codex" },
|
|
658
|
+
},
|
|
659
|
+
1,
|
|
660
|
+
);
|
|
661
|
+
expect(lines![0]).toBe("! budget-unenforceable");
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
it("实验闸落闸:一行 error 级通知,文案就是契约字面,不再多一行标题", () => {
|
|
665
|
+
const [lines] = replayDiagnostic(
|
|
666
|
+
{
|
|
667
|
+
type: "diagnostic",
|
|
668
|
+
at: 0,
|
|
669
|
+
key: "dispatch-halted:experiment:compare/codex",
|
|
670
|
+
code: HALT_DIAGNOSTIC_CODE,
|
|
671
|
+
severity: "error",
|
|
672
|
+
message: "experiment halted (dispatch-halted): shared service is down; restart the tunnel",
|
|
673
|
+
data: { experimentId: "compare/codex", scope: "experiment", phase: "eval.run", unstarted: 0 },
|
|
674
|
+
},
|
|
675
|
+
1,
|
|
676
|
+
);
|
|
677
|
+
expect(lines).toEqual(["✗ experiment halted (dispatch-halted): shared service is down; restart the tunnel"]);
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
it("每条未派发 attempt 刷一次的后续声明零输出:被中止的等待集不逐条刷屏,数量归完成状态的 unstarted", () => {
|
|
681
|
+
const rounds = replayDiagnostic(
|
|
682
|
+
{
|
|
683
|
+
type: "diagnostic",
|
|
684
|
+
at: 0,
|
|
685
|
+
key: "dispatch-halted:eval:compare/codex|memory/retention",
|
|
686
|
+
code: HALT_DIAGNOSTIC_CODE,
|
|
687
|
+
severity: "error",
|
|
688
|
+
message: "eval halted: fixture db is empty; run scripts/seed.ts",
|
|
689
|
+
data: { experimentId: "compare/codex", scope: "eval", evalId: "memory/retention", phase: "eval.run", unstarted: 4 },
|
|
690
|
+
},
|
|
691
|
+
5,
|
|
692
|
+
);
|
|
693
|
+
expect(rounds[0]).toEqual(["✗ eval halted: fixture db is empty; run scripts/seed.ts"]);
|
|
694
|
+
expect(rounds.slice(1).flat()).toEqual([]);
|
|
695
|
+
});
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
describe("renderHumanDryPlan: locked 标注", () => {
|
|
699
|
+
it("locked 为 true 的行尾标注 locked;false/省略的行不受影响", () => {
|
|
700
|
+
const text = renderHumanDryPlan({
|
|
701
|
+
totalAttempts: 2,
|
|
702
|
+
evals: 2,
|
|
703
|
+
configs: 1,
|
|
704
|
+
runs: 1,
|
|
705
|
+
rows: [
|
|
706
|
+
{ experimentId: "compare/codex", evalId: "memory/a", locked: true },
|
|
707
|
+
{ experimentId: "compare/codex", evalId: "memory/b" },
|
|
708
|
+
],
|
|
709
|
+
});
|
|
710
|
+
const lines = text.trim().split("\n");
|
|
711
|
+
expect(lines.find((l) => l.includes("memory/a"))).toContain("locked");
|
|
712
|
+
expect(lines.find((l) => l.includes("memory/b"))).not.toContain("locked");
|
|
713
|
+
});
|
|
714
|
+
});
|