niceeval 0.10.0 → 0.10.3-canary.1
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 +4 -1
- package/README.md +1 -1
- package/README.zh.md +1 -1
- package/dist/report/components/attempt-detail/AttemptAssertions.js +2 -1
- package/dist/report/components/attempt-detail/AttemptConversation.d.ts +5 -1
- package/dist/report/components/attempt-detail/AttemptConversation.js +42 -2
- package/dist/report/components/attempt-detail/AttemptSource.js +84 -13
- package/dist/report/components/attempt-detail/compute.js +44 -1
- package/dist/report/components/attempt-detail/faces.js +4 -0
- package/dist/report/components/attempt-detail/index.d.ts +2 -2
- package/dist/report/components/attempt-detail/index.js +24 -4
- package/dist/report/components/entity-lists/ExperimentList.d.ts +1 -2
- package/dist/report/components/entity-lists/ExperimentList.js +6 -5
- package/dist/report/components/entity-lists/faces.d.ts +1 -1
- package/dist/report/components/entity-lists/faces.js +10 -9
- package/dist/report/components/entity-lists/index.d.ts +5 -7
- package/dist/report/components/entity-lists/index.js +7 -3
- package/dist/report/components/metric-views/MetricScatter.js +2 -38
- package/dist/report/index.d.ts +1 -1
- package/dist/report/model/format.d.ts +5 -5
- package/dist/report/model/format.js +34 -8
- package/dist/report/model/types.d.ts +16 -2
- package/dist/report/react/index.d.ts +1 -1
- package/dist/runner/types.d.ts +2 -1
- package/dist/sandbox/e2b.d.ts +0 -1
- package/dist/scoring/display.d.ts +7 -1
- package/dist/scoring/display.js +22 -2
- package/docs-site/zh/examples/integrations/ai-sdk-v7.mdx +5 -5
- package/docs-site/zh/explanation/runner.mdx +10 -2
- package/docs-site/zh/index.mdx +1 -1
- package/docs-site/zh/reference/cli.mdx +2 -2
- package/docs-site/zh/reference/expect.mdx +21 -1
- package/docs-site/zh/reference/report-components.mdx +3 -1
- package/docs-site/zh/troubleshooting/debugging.mdx +8 -8
- package/docs-site/zh/troubleshooting/recover-after-kill.mdx +62 -0
- package/docs-site/zh/tutorials/agent-onboarding.mdx +14 -2
- package/docs-site/zh/tutorials/experiments.mdx +1 -2
- package/docs-site/zh/tutorials/quickstart.mdx +2 -2
- package/docs-site/zh/tutorials/sandbox-providers.mdx +27 -0
- package/docs-site/zh/tutorials/viewing-results.mdx +13 -10
- package/docs-site/zh/tutorials/write-experiment.mdx +1 -3
- package/package.json +3 -4
- package/src/cli.ts +10 -2
- package/src/expect/index.test.ts +39 -0
- package/src/expect/index.ts +33 -0
- package/src/o11y/prices.json +176 -99
- package/src/report/assets/styles.css +822 -0
- package/src/report/components/attempt-detail/AttemptAssertions.tsx +4 -3
- package/src/report/components/attempt-detail/AttemptConversation.tsx +55 -7
- package/src/report/components/attempt-detail/AttemptSource.tsx +187 -42
- package/src/report/components/attempt-detail/attempt-components.test.tsx +82 -4
- package/src/report/components/attempt-detail/compute.ts +50 -1
- package/src/report/components/attempt-detail/faces.ts +3 -0
- package/src/report/components/attempt-detail/index.tsx +33 -16
- package/src/report/components/attempt-detail/validate.test.ts +19 -2
- package/src/report/components/entity-lists/ExperimentList.tsx +13 -10
- package/src/report/components/entity-lists/faces.ts +10 -9
- package/src/report/components/entity-lists/index.tsx +7 -10
- package/src/report/components/metric-views/MetricScatter.tsx +2 -38
- package/src/report/components/render.test.tsx +19 -9
- package/src/report/index.ts +2 -0
- package/src/report/model/format.ts +33 -8
- package/src/report/model/types.ts +18 -2
- package/src/report/react/index.tsx +2 -0
- package/src/report/runtime/dual-render.test.tsx +43 -10
- package/src/runner/types.ts +2 -1
- package/src/sandbox/e2b-reconcile.test.ts +126 -0
- package/src/sandbox/e2b.ts +38 -2
- package/src/scoring/display.test.ts +26 -0
- package/src/scoring/display.ts +24 -2
- package/src/view/view-report.test.ts +3 -1
package/src/sandbox/e2b.ts
CHANGED
|
@@ -14,7 +14,11 @@ import type {
|
|
|
14
14
|
SourceFiles,
|
|
15
15
|
ReadSourceFilesOptions,
|
|
16
16
|
} from "../types.ts";
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
classifyProvisionErrorFallback,
|
|
19
|
+
isRetryableProvisionError,
|
|
20
|
+
type SandboxProvisionErrorKind,
|
|
21
|
+
} from "./errors.ts";
|
|
18
22
|
import { classifySandboxIoError } from "./errors.ts";
|
|
19
23
|
import { readSourceFilesByList } from "./source-files.ts";
|
|
20
24
|
import { collectLocalFiles } from "./local-files.ts";
|
|
@@ -30,6 +34,13 @@ const DEFAULT_COMMAND_TIMEOUT_MS = 600_000;
|
|
|
30
34
|
const SESSION_TIMEOUT_MS = 1_800_000;
|
|
31
35
|
|
|
32
36
|
/** e2b 的限流错误是 SDK 原生的 RateLimitError(HTTP 429 映射而来);见 resolve.ts 的 withProvisionRetry。 */
|
|
37
|
+
// 对账本身只有一次机会:retry.ts 的 withProvisionRetry 对账失败就直接放弃重试、抛回原始
|
|
38
|
+
// create() 错误(见那边的注释)。对账走的这次 list 请求跟刚失败的 create() 往往挨得很近,
|
|
39
|
+
// 大概率处在同一段网络抖动里——不给它自己的重试,一次瞬时失败就会把本可能自愈的 attempt
|
|
40
|
+
// 判死。这里给 nextItems() 单独包一层短重试,只吃与 create() 侧同一套分类下的瞬时错误。
|
|
41
|
+
const RECONCILE_LIST_MAX_ATTEMPTS = 3;
|
|
42
|
+
const RECONCILE_LIST_RETRY_DELAY_MS = 500;
|
|
43
|
+
|
|
33
44
|
/**
|
|
34
45
|
* Provisioning 重试前的对账:按 metadata 里的 provision token 检索远端实例,查到即 kill。
|
|
35
46
|
* 检索或销毁失败必须抛出——对账是重试的硬前置,静默放行等于盲重试,会复制计费实例
|
|
@@ -43,7 +54,7 @@ export async function reconcileProvision(token: string): Promise<void> {
|
|
|
43
54
|
// 命中的实例数极少,通常一页打完。
|
|
44
55
|
const paginator = E2BSdkSandbox.list({ apiKey, query: { metadata: { "niceeval-provision-token": token } } });
|
|
45
56
|
while (paginator.hasNext) {
|
|
46
|
-
const sandboxes = await paginator
|
|
57
|
+
const sandboxes = await fetchNextItemsWithRetry(paginator);
|
|
47
58
|
for (const info of sandboxes) {
|
|
48
59
|
try {
|
|
49
60
|
await E2BSdkSandbox.kill(info.sandboxId, { apiKey });
|
|
@@ -54,6 +65,31 @@ export async function reconcileProvision(token: string): Promise<void> {
|
|
|
54
65
|
}
|
|
55
66
|
}
|
|
56
67
|
|
|
68
|
+
/**
|
|
69
|
+
* `nextItems()` 按类型契约总是 resolve 成数组(SDK 内部对空响应也兜了 `?? []`),但对账这条
|
|
70
|
+
* 路径线上真实撞见过它 resolve 成非数组的一次——没能复现出确切成因,不排它,原样让下面的
|
|
71
|
+
* `for...of` 抛出,但换一句能定位的诊断,而不是留一条裸的 "X is not iterable"。
|
|
72
|
+
*/
|
|
73
|
+
async function fetchNextItemsWithRetry(
|
|
74
|
+
paginator: ReturnType<typeof E2BSdkSandbox.list>,
|
|
75
|
+
): Promise<Awaited<ReturnType<typeof paginator.nextItems>>> {
|
|
76
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
77
|
+
try {
|
|
78
|
+
const items = await paginator.nextItems();
|
|
79
|
+
if (!Array.isArray(items)) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
`e2b Sandbox.list() 分页器 nextItems() 返回了非数组(${typeof items}),不是 SDK 类型契约里的 SandboxInfo[]`,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
return items;
|
|
85
|
+
} catch (e) {
|
|
86
|
+
const kind = classifyProvisionErrorFallback(e);
|
|
87
|
+
if (attempt >= RECONCILE_LIST_MAX_ATTEMPTS - 1 || !isRetryableProvisionError(kind)) throw e;
|
|
88
|
+
await new Promise((resolve) => setTimeout(resolve, RECONCILE_LIST_RETRY_DELAY_MS * 2 ** attempt));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
57
93
|
export function classifyProvisionError(e: unknown): SandboxProvisionErrorKind {
|
|
58
94
|
// SDK 原生限流先归拒绝类;没认出的过与文件 IO 共用的保守瞬时兜底分类器
|
|
59
95
|
// (真实跑分里出现过 create 阶段 `fetch failed · other side closed`,属歧义类)。
|
|
@@ -6,8 +6,34 @@ import {
|
|
|
6
6
|
compactAssertionSummary,
|
|
7
7
|
fitCompactAssertionSummary,
|
|
8
8
|
primaryAssertionSummary,
|
|
9
|
+
stripControl,
|
|
10
|
+
summaryText,
|
|
9
11
|
} from "./display.ts";
|
|
10
12
|
|
|
13
|
+
// 用字符码构造真实控制字节,避免源码里嵌裸控制字符。
|
|
14
|
+
const ESC = String.fromCharCode(0x1b);
|
|
15
|
+
const BEL = String.fromCharCode(0x07);
|
|
16
|
+
const BS = String.fromCharCode(0x08);
|
|
17
|
+
|
|
18
|
+
describe("stripControl / summaryText 控制字节收口", () => {
|
|
19
|
+
it("剥 ANSI/OSC/裸控制字节,保留可打印字符与结构性换行", () => {
|
|
20
|
+
const colored = `${ESC}[2m 28 |${ESC}[22m code ${ESC}[31m✕${ESC}[39m Tests: 2 failed`;
|
|
21
|
+
const stripped = stripControl(colored);
|
|
22
|
+
expect(stripped).not.toContain(ESC);
|
|
23
|
+
expect(stripped).toContain("✕"); // jest 合法 glyph 保留
|
|
24
|
+
expect(stripControl(`${ESC}]0;title${BEL}kept`)).toBe("kept"); // OSC 连 payload 去除
|
|
25
|
+
expect(stripControl(`a${BS}b`)).toBe("ab"); // 裸 BS 去除
|
|
26
|
+
expect(stripControl(`${ESC}[2mline1${ESC}[22m\nline2`)).toBe("line1\nline2"); // 保留换行
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("summaryText 先剥控制字节再折单行,输出不含任何 ESC", () => {
|
|
30
|
+
const s = summaryText(`exit 1 · "${ESC}[2m 28 |${ESC}[22m\n ${ESC}[31m✕${ESC}[39m failed"`);
|
|
31
|
+
expect(s).not.toContain(ESC);
|
|
32
|
+
expect(s).not.toContain("\n");
|
|
33
|
+
expect(s).toContain("✕");
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
11
37
|
describe("primaryAssertionSummary", () => {
|
|
12
38
|
it("选择第一条失败 gate,保留领域 group、matcher 与 expected/received,并只计数其余 gate", () => {
|
|
13
39
|
const assertions: AssertionResult[] = [
|
package/src/scoring/display.ts
CHANGED
|
@@ -16,9 +16,31 @@ const SUMMARY_TEXT_MAX_CHARS = 240;
|
|
|
16
16
|
*/
|
|
17
17
|
const DETAIL_LINE_MAX_CHARS = 100;
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
// 捕获内容(received=命令输出 / expected=源码 / evidence)常带被测工具的着色:jest/vitest 的
|
|
20
|
+
// 代码帧、行号、✕ 都由 ANSI 转义(ESC[…m 等)上色。这些 ESC(U+001B)不是 \s,若原样落进任何
|
|
21
|
+
// 面,终端会重新解释它们(被单行截断从序列中间切开时尤其乱),HTML 报告则把 ESC[2m28|ESC[22m
|
|
22
|
+
// 当字面文本渲染。所以任何展示面在渲染捕获内容前先剥控制字节;剥的是展示投影,不改存进
|
|
23
|
+
// AssertionResult / artifact 的原始字节(完整证据仍在 events.json / diff.json)。
|
|
24
|
+
// CSI(ESC[…,含 SGR 着色 / 光标控制)与 OSC(ESC]…,以 BEL 或 ST 收尾);OSC 的 payload 一并吃掉,
|
|
25
|
+
// 不让它作为裸文本泄漏。没配成序列的裸 ESC 由 OTHER_CONTROL 兜底。
|
|
26
|
+
// eslint-disable-next-line no-control-regex
|
|
27
|
+
const ANSI_ESCAPE = /\u001B(?:\[[0-9;:?]*[ -/]*[@-~]|\][^\u0007\u001B]*(?:\u0007|\u001B\\))/g;
|
|
28
|
+
// 其余不可打印 C0/C1(含裸 ESC);保留 \t\n\f\r 交给下游折空白规则,不在这里塌成空。
|
|
29
|
+
// eslint-disable-next-line no-control-regex
|
|
30
|
+
const OTHER_CONTROL = /[\u0000-\u0008\u000B\u000E-\u001F\u007F-\u009F]/g;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 剥离 ANSI 转义与其余不可打印控制字节,保留可打印字符与结构性空白(换行 / 制表)。给需要
|
|
34
|
+
* 完整多行值的面(报告详情)直接用;`summaryText` 在此基础上再折单行 + 截断。jest 合法打印的
|
|
35
|
+
* `✕ ✓ › ❯ ↓ │`(均 ≥ U+2020)在保留范围内,不误删。
|
|
36
|
+
*/
|
|
37
|
+
export function stripControl(value: string): string {
|
|
38
|
+
return value.replace(ANSI_ESCAPE, "").replace(OTHER_CONTROL, "");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** 摘要面的单值收口:剥控制字节 + 折单行 + 240 字符上限。任何把断言事实放进「行」里的面共用这一条。 */
|
|
20
42
|
export function summaryText(value: string): string {
|
|
21
|
-
const singleLine = value.replace(/\s+/g, " ").trim();
|
|
43
|
+
const singleLine = stripControl(value).replace(/\s+/g, " ").trim();
|
|
22
44
|
return singleLine.length <= SUMMARY_TEXT_MAX_CHARS
|
|
23
45
|
? singleLine
|
|
24
46
|
: `${singleLine.slice(0, SUMMARY_TEXT_MAX_CHARS - 1)}…`;
|
|
@@ -577,7 +577,7 @@ describe("buildView · attempt/<locator>.html", () => {
|
|
|
577
577
|
expect(existsSync(join(out, "attempt", `${droppedLocator}.html`))).toBe(false);
|
|
578
578
|
});
|
|
579
579
|
|
|
580
|
-
it("
|
|
580
|
+
it("直接读取该文档即可见完整内容(cases.md Attempt 参数化 page);样式与视觉归 e2e 报告域,不在本层断言", async () => {
|
|
581
581
|
const root = await makeRoot();
|
|
582
582
|
const writer = createResultsWriter(root, { producer: { name: "niceeval", version: "1.0.0" } });
|
|
583
583
|
const snap = await writer.snapshot({ experimentId: "compare/bub", agent: "bub", startedAt: "2026-07-01T08:00:00.000Z" });
|
|
@@ -642,5 +642,7 @@ describe("buildView · attempt/<locator>.html", () => {
|
|
|
642
642
|
]) {
|
|
643
643
|
expect(enBlock, needle).toContain(needle);
|
|
644
644
|
}
|
|
645
|
+
// 有 source 时对话住在源码行内,不再渲染独立 AttemptConversation。
|
|
646
|
+
expect(enBlock).not.toContain("nre-attempt-conversation");
|
|
645
647
|
});
|
|
646
648
|
});
|