mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-09-11 04:00:38 +00:00
fix(code-runtime-python): correct the log-budget floor to 64 and record the marker envelope bound
The review found the 62 floor off by two (the marker's fixed prefix is 51 characters counting both square brackets, so marker(62) serializes to 63) and the constructor error over-claiming a bound the marker-as-envelope design does not deliver. Fixes: - MIN_LOG_BYTES is 64 (marker-only serialization fits with one byte of room); the JSDoc arithmetic counts the brackets; the rejection test pins 63; the forged-frame test uses 11 NULs (69 escaped) at 64. - The constructor error now states the marker-only guarantee, and the README Known Limitations (en + zh) records the real bound: a truncated run with admitted entries serializes its logs to maxLogBytes + marker + envelope. - The SIGXCPU-mask tests burn with time.process_time() instead of wall-clock perf_counter, so a contended CI runner cannot under-burn the budget. - The settlement note (en + zh) records the 64 floor and the marker envelope bound, including the zh pre-encode section that the earlier pass missed. - The README constructor-rejection list names the maxLogBytes floor. Pairings re-recorded; corpus-wide verify-translation-pairing passes 1004.
This commit is contained in:
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/code-runtime/code-runtime-python/README.md
|
||||
README.md: 18c784394705e7fc10f956bcbf7e576d6a0b4c83
|
||||
README.zh.md: 64424b7d2de8b96452469c3e65e47ed3d9a91adb
|
||||
README.md: 25211993e1ebac7b940d4f373391819c15cab5c0
|
||||
README.zh.md: ca260b8c8d543545ce927f2c48e4676b34622786
|
||||
|
||||
@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
|
||||
|
||||
CPython-subprocess implementation of the [`@deepseek-ai/dsh-code-runtime`](../code-runtime/README.md) seam. Companion to [`@deepseek-ai/dsh-code-runtime-worker-thread`](../code-runtime-worker-thread/README.md); trades the Node worker thread for a fresh `python3` subprocess so model code is Python instead of TypeScript.
|
||||
|
||||
The package owns the wire protocol for that seam: the host-side frame codec and the Python-side mirror of the same message vocabulary. On top of that protocol it ships `PythonCodeRuntime` (the plugin's default export), which registers as `codeRuntime` with `language: 'python'` and `isolation: 'process'`. Each `run()` spawns a fresh `python3 -I` process, sends a boot frame and the program over fd 3, and resolves a `CodeRunResult` for every program outcome — `run()` rejects only for seam misuse, such as a malformed binding namespace or a call on a runtime whose fiber was already disposed. Configuration is rejected earlier, when the plugin loads: a non-Unix platform, a non-positive or non-integer budget, a timer value `setTimeout` would clamp, a budget larger than one fd-3 frame can carry, and an `addressSpaceMb`/output-budget pair whose worst-case peak would breach `RLIMIT_AS` all throw from the constructor, so a misconfiguration fails at assembly rather than on a later run. The child runs the program as the body of an async function, so top-level `await` and `return` both work; binding calls travel back over fd 3 as JSON-lines. Containment (not a security boundary — model code has bash-equivalent trust) comes from an empty environment, `RLIMIT_CPU`/`RLIMIT_AS`, a wall-clock ceiling, and a `SIGTERM`→grace→`SIGKILL` teardown on the child's process group.
|
||||
The package owns the wire protocol for that seam: the host-side frame codec and the Python-side mirror of the same message vocabulary. On top of that protocol it ships `PythonCodeRuntime` (the plugin's default export), which registers as `codeRuntime` with `language: 'python'` and `isolation: 'process'`. Each `run()` spawns a fresh `python3 -I` process, sends a boot frame and the program over fd 3, and resolves a `CodeRunResult` for every program outcome — `run()` rejects only for seam misuse, such as a malformed binding namespace or a call on a runtime whose fiber was already disposed. Configuration is rejected earlier, when the plugin loads: a non-Unix platform, a non-positive or non-integer budget, a `maxLogBytes` below the truncation-marker floor (64), a timer value `setTimeout` would clamp, a budget larger than one fd-3 frame can carry, and an `addressSpaceMb`/output-budget pair whose worst-case peak would breach `RLIMIT_AS` all throw from the constructor, so a misconfiguration fails at assembly rather than on a later run. The child runs the program as the body of an async function, so top-level `await` and `return` both work; binding calls travel back over fd 3 as JSON-lines. Containment (not a security boundary — model code has bash-equivalent trust) comes from an empty environment, `RLIMIT_CPU`/`RLIMIT_AS`, a wall-clock ceiling, and a `SIGTERM`→grace→`SIGKILL` teardown on the child's process group.
|
||||
|
||||
## Wire protocol
|
||||
|
||||
@@ -37,6 +37,7 @@ No direct invalidation; the named consumer owns any request-prefix changes.
|
||||
- **The cross-language guard covers executed values and frame field sets, not field types** — `tests/protocol-mirror.e2e.ts` compares `PROTOCOL_FD`, the log truncation marker, and each `TypedDict`'s required and optional fields against a real `python3`. Comparing field types across TypeScript and Python has no mechanical equivalent here, so review plus the backend's real-subprocess suite owns type-level drift.
|
||||
- **`RLIMIT_AS` is not enforced on macOS** — the dyld shared cache mapped into every process at exec exceeds any practical address-space cap, and the kernel rejects the `setrlimit` call, so `addressSpaceMb` is skipped there. `cpuSeconds` and `maxWallMs` still bound every run.
|
||||
- **PID-reuse protection is inert on macOS** — `readProcessStart` reads `/proc/<pid>/stat`, which Darwin does not provide, so the identity re-check that guards `killGroup` against signalling a recycled pgid always passes there; the guard degrades to the pre-existing behavior rather than paying a `ps` fork on a teardown path. The process-group teardown and the `closeDeadline` bound still contain the run.
|
||||
- **A truncated log's serialized array runs to `maxLogBytes` plus the marker.** The truncation marker is envelope, not payload — it rides uncharged so it can always be emitted — and the outer-array envelope is reserved one byte in the ledger. A truncated run with admitted entries therefore serializes its `logs` array to at most `maxLogBytes + marker + 1`; the marker alone fits any admissible budget (the 64-byte floor guarantees it).
|
||||
- **A descendant that calls `setsid()` / `start_new_session=True` escapes teardown.** Termination signals the child's process group with `kill(-pid)`; a descendant that moves itself into a fresh session is no longer in that group and no signal reaches it. If it also releases the inherited stdout/stderr/fd-3 pipes, the leader's `close` still settles the run, and after the `closeDeadline` bound the fiber goes quiescent while that orphan keeps running. This is the containment boundary, not a security one — model code has bash-equivalent trust, and a bash tool can `setsid` away just the same. Reaching such an orphan would require tracking every descendant pid (as the bash-local backend's process-inspector does) and is deferred; the process-group teardown reaps everything that stays in the group.
|
||||
- **A combined log-and-value peak is not modelled by the load gate.** Each budget is checked against `addressSpaceMb` on its own. A model daemon thread that keeps writing while the completion value is metered and framed can refill the log pending toward `maxLogBytes` during that window, so the two peaks add in a way no gate admits or rejects. A gate over `(maxLogBytes + maxValueBytes)` was considered and deferred: its discriminating case cannot be scheduled deterministically under `RLIMIT_AS`, so the gate would only prove its own arithmetic. When the combined peak is reached the run dies as `worker-exit` -- containment holds and only the failure classification is degraded.
|
||||
- **A 1-second dual-limit `ulimit -t 1` CPU overrun is reported as `worker-exit`, not a timeout.** When the host starts under a hard CPU limit equal to the soft (`ulimit -t N` sets both) and that limit is 1, `_clamped` cannot lower the soft to 0, so the kernel SIGKILLs the busy loop in the same tick and SIGXCPU is never delivered. The host classifies a CPU overrun only on `signal === 'SIGXCPU'`, so the overrun is reported as `worker-exit`. For a dual limit of 2 or more the soft is lowered by one unit, SIGXCPU fires, and the run is a timeout. Containment holds in both cases; only the classification is degraded.
|
||||
|
||||
@@ -9,7 +9,7 @@ kind: "package-reference"
|
||||
|
||||
[`@deepseek-ai/dsh-code-runtime`](../code-runtime/README.zh.md) seam 的 CPython 子进程实现。与 [`@deepseek-ai/dsh-code-runtime-worker-thread`](../code-runtime-worker-thread/README.zh.md) 配套;以全新的 `python3` 子进程取代 Node worker 线程,让模型代码从 TypeScript 换成 Python。
|
||||
|
||||
本包持有该 seam 的 wire protocol:host 侧的帧编解码,以及 Python 侧对同一套消息词汇的镜像。在该协议之上,本包交付 `PythonCodeRuntime`(插件的默认导出),它以 `language: 'python'`、`isolation: 'process'` 注册为 `codeRuntime`。每次 `run()` 启动一个全新的 `python3 -I` 进程,通过 fd 3 发送 boot 帧和程序,并为每个程序结果 resolve 一个 `CodeRunResult`——`run()` 仅在 seam 被误用时才 reject,例如 binding 命名空间不合法,或对 fiber 已被 dispose 的 runtime 发起调用。配置错误在更早的插件加载期被拒绝:非 Unix 平台、非正或非整数的预算、会被 `setTimeout` 截断的定时器值、超过单个 fd-3 帧承载能力的预算,以及最坏峰值会突破 `RLIMIT_AS` 的 `addressSpaceMb`/输出预算组合,都从构造器抛出,因此配置错误在装配时就失败,而不是等到之后某次运行。子进程把程序作为 async 函数体运行,因此顶层 `await` 与 `return` 都可用;binding 调用经 fd 3 以 JSON-lines 回传。containment 不是安全边界——模型代码具有等同 bash 的信任级别;空环境、`RLIMIT_CPU`/`RLIMIT_AS`、墙钟上限与对子进程进程组的 `SIGTERM`→grace→`SIGKILL` 拆卸共同提供 containment。
|
||||
本包持有该 seam 的 wire protocol:host 侧的帧编解码,以及 Python 侧对同一套消息词汇的镜像。在该协议之上,本包交付 `PythonCodeRuntime`(插件的默认导出),它以 `language: 'python'`、`isolation: 'process'` 注册为 `codeRuntime`。每次 `run()` 启动一个全新的 `python3 -I` 进程,通过 fd 3 发送 boot 帧和程序,并为每个程序结果 resolve 一个 `CodeRunResult`——`run()` 仅在 seam 被误用时才 reject,例如 binding 命名空间不合法,或对 fiber 已被 dispose 的 runtime 发起调用。配置错误在更早的插件加载期被拒绝:非 Unix 平台、非正或非整数的预算、低于截断标记下限(64)的 `maxLogBytes`、会被 `setTimeout` 截断的定时器值、超过单个 fd-3 帧承载能力的预算,以及最坏峰值会突破 `RLIMIT_AS` 的 `addressSpaceMb`/输出预算组合,都从构造器抛出,因此配置错误在装配时就失败,而不是等到之后某次运行。子进程把程序作为 async 函数体运行,因此顶层 `await` 与 `return` 都可用;binding 调用经 fd 3 以 JSON-lines 回传。containment 不是安全边界——模型代码具有等同 bash 的信任级别;空环境、`RLIMIT_CPU`/`RLIMIT_AS`、墙钟上限与对子进程进程组的 `SIGTERM`→grace→`SIGKILL` 拆卸共同提供 containment。
|
||||
|
||||
## Wire protocol
|
||||
|
||||
@@ -37,6 +37,7 @@ host 与 CPython 子进程在子进程的 fd 3 上交换一个无版本号的 JS
|
||||
- **跨语言 guard 覆盖执行值与帧字段集,但不覆盖字段类型** —— `tests/protocol-mirror.e2e.ts` 使用真实 `python3` 比较 `PROTOCOL_FD`、日志截断标记,以及每个 `TypedDict` 的必填和可选字段。跨 TypeScript 与 Python 比较字段类型在此没有机械等价物,因此类型级漂移由 review 加后端真子进程套件负责。
|
||||
- **`RLIMIT_AS` 在 macOS 上不施加** —— 在 exec 时映射进每个进程的 dyld 共享缓存超过任何实际的地址空间上限,内核会拒绝该 `setrlimit` 调用,故 `addressSpaceMb` 在那里被跳过。`cpuSeconds` 与 `maxWallMs` 仍约束每一次运行。
|
||||
- **PID 复用防护在 macOS 上失效** —— `readProcessStart` 读取 `/proc/<pid>/stat`,Darwin 不提供它,因此防止 `killGroup` 对已回收的 pgid 发信号的同一性复检在那里恒通过;该防护退化为既有行为,而非在拆卸路径上付出一次 `ps` fork。进程组拆卸与 `closeDeadline` 上界仍约束该次运行。
|
||||
- **截断日志的序列化数组会到 `maxLogBytes` 加标记为止。** 截断标记是 envelope 而非 payload——它不计费地随行,因此总能发出——而外层数组外壳在账本中预留了一字节。因此带已放行条目的截断运行,其 `logs` 数组序列化后至多为 `maxLogBytes + marker + 1`;标记单独能放进任何可接受的预算(64 字节下限保证这一点)。
|
||||
- **调用 `setsid()` / `start_new_session=True` 的后代会逃出 teardown。** 终止是用 `kill(-pid)` 向子进程的进程组发信号;一个把自己移入新会话的后代已不在该进程组内,任何信号都到不了它。若它同时释放了继承而来的 stdout/stderr/fd-3 管道,leader 的 `close` 仍会结算该次运行,在 `closeDeadline` 到界之后 fiber 变为完全停稳,而那个孤儿仍在运行。这是 containment 边界,而非安全边界——模型代码具有等同 bash 的信任级别,一个 bash 工具同样能 `setsid` 逃逸。要够到这样的孤儿需要追踪每一个后代 pid(如 bash-local 后端的 process-inspector 所做),此项已推迟;进程组 teardown 会回收所有留在组内的进程。
|
||||
- **日志与完成值的叠加峰值未被加载门建模。** 每项预算都是各自对照 `addressSpaceMb` 检查的。模型的 daemon 线程可以在完成值被计量并分帧的窗口内持续写入、把日志 pending 重填到接近 `maxLogBytes`,于是两个峰值以任何门都不曾放行也不曾拒绝的方式相加。对 `(maxLogBytes + maxValueBytes)` 设门的方案经评估后推迟:它的判别用例无法在 `RLIMIT_AS` 之下确定性地构造出来,因此该门只能证明自己的算术。叠加峰值被触及时该次运行死为 `worker-exit`——containment 仍然成立,只是失败分类失真。
|
||||
- **1 秒双限 `ulimit -t 1` 下的 CPU 超限会被报告为 `worker-exit`,而非超时。** 当宿主在一个硬 CPU 限制等于软限制(`ulimit -t N` 同时设置两者)且该限制为 1 的环境下启动时,`_clamped` 无法把软限制降到 0,因此内核在同一 tick 直接 SIGKILL 忙循环,SIGXCPU 永不送达。宿主只在 `signal === 'SIGXCPU'` 时把 CPU 超限分类为超时,因此该超限被报告为 `worker-exit`。当双限为 2 或更大时,软限制会被降低一个单位,SIGXCPU 触发,该次运行成为超时。两种情况 containment 都成立;只是分类被降级。
|
||||
|
||||
@@ -227,18 +227,22 @@ const MAX_PENDING_CHUNKS = 1024
|
||||
const FRAME_ENVELOPE_BYTES = 64
|
||||
|
||||
/**
|
||||
* Smallest `maxLogBytes` the backend can honor. The log ledger's truncation
|
||||
* marker (`logTruncationMarker`) plus the serialized outer-array envelope must
|
||||
* fit the budget, or a truncated run returns more than the configured cap: the
|
||||
* marker text is `[dsh-code-runtime-python] log capture truncated at <N>
|
||||
* bytes` — 49 fixed characters plus the digits of N plus 6 — serialized with
|
||||
* quotes and brackets adds 4, so the smallest N that admits its own marker is
|
||||
* 61 (49 + 2 + 6 + 4); 62 is the floor with one byte of room. `maxValueBytes`
|
||||
* has no floor beyond the positive-integer requirement: a completion can be as
|
||||
* small as a single byte (`1`), and the done-frame envelope is seam protocol
|
||||
* cost, not the advertised completion budget.
|
||||
* Smallest `maxLogBytes` the backend can honor. The truncation marker alone
|
||||
* (`logTruncationMarker`) must serialize within the budget, or a marker-only
|
||||
* truncated run returns more than the configured cap: the marker text is
|
||||
* `[dsh-code-runtime-python] log capture truncated at <N> bytes` — 51 fixed
|
||||
* characters (the bracketed prefix `[dsh-code-runtime-python] log capture
|
||||
* truncated at ` counts both square brackets) plus the digits of N plus 6 —
|
||||
* and its serialized form adds 4 (two quotes, two array brackets), so the
|
||||
* smallest N that admits its own marker is 63 (51 + 2 + 6 + 4 = 63); 64 is the
|
||||
* floor with one byte of room. The marker itself remains envelope, not
|
||||
* payload, so a truncated run with admitted entries serializes to at most
|
||||
* `maxLogBytes + marker + envelope`; that bound is recorded in the README.
|
||||
* `maxValueBytes` has no floor beyond the positive-integer requirement: a
|
||||
* completion can be as small as a single byte (`1`), and the done-frame
|
||||
* envelope is seam protocol cost, not the advertised completion budget.
|
||||
*/
|
||||
const MIN_LOG_BYTES = 62
|
||||
const MIN_LOG_BYTES = 64
|
||||
|
||||
/**
|
||||
* Extra time added to `graceMs` before the post-kill close-deadline force-settles
|
||||
@@ -781,11 +785,14 @@ export class PythonCodeRuntime extends CodeRuntime {
|
||||
if (this.config[key] > limit) {
|
||||
throw new Error(`dsh-code-runtime-python: config.${key} must not exceed ${limit} (a payload that large cannot cross the ${FRAME_CEILING_BYTES}-byte fd-3 frame ceiling, so the run would fail as worker-exit rather than output-limit), got ${String(this.config[key])}`)
|
||||
}
|
||||
// Reject a log budget too small to honor: the ledger must fit its
|
||||
// truncation marker plus the serialized outer-array envelope, or a
|
||||
// truncated run returns more than the configured cap.
|
||||
// Reject a log budget too small to honor: the truncation marker alone
|
||||
// must serialize within the budget, or a marker-only truncated run
|
||||
// returns more than the configured cap. (With admitted entries the
|
||||
// marker is envelope, so the serialized logs run to
|
||||
// `maxLogBytes + marker + envelope`; that bound is recorded in the
|
||||
// README's Known Limitations.)
|
||||
if (key === 'maxLogBytes' && this.config[key] < MIN_LOG_BYTES) {
|
||||
throw new Error(`dsh-code-runtime-python: config.maxLogBytes must be at least ${MIN_LOG_BYTES} (a smaller budget cannot serialize the truncation marker plus the outer-array envelope, so the run would return more than the configured cap), got ${String(this.config[key])}`)
|
||||
throw new Error(`dsh-code-runtime-python: config.maxLogBytes must be at least ${MIN_LOG_BYTES} (a smaller budget cannot serialize the truncation marker itself, so a marker-only truncated run would return more than the configured cap), got ${String(this.config[key])}`)
|
||||
}
|
||||
}
|
||||
// The child builds, charges, and frames a `maxLogBytes` log entry or a
|
||||
|
||||
@@ -555,8 +555,8 @@ describe('PythonCodeRuntime — inherited resource limits', () => {
|
||||
'import signal, time',
|
||||
'if hasattr(signal, "pthread_sigmask"):',
|
||||
' signal.pthread_sigmask(signal.SIG_BLOCK, {signal.SIGXCPU})',
|
||||
'end = time.perf_counter() + 1.05',
|
||||
'while time.perf_counter() < end:',
|
||||
'end = time.process_time() + 1.05',
|
||||
'while time.process_time() < end:',
|
||||
' pass',
|
||||
'return "escaped"',
|
||||
].join('\n'),
|
||||
@@ -585,8 +585,8 @@ describe('PythonCodeRuntime — inherited resource limits', () => {
|
||||
' signal.pthread_sigmask(signal.SIG_BLOCK, {signal.SIGXCPU})',
|
||||
' signal.signal(signal.SIGXCPU, h)',
|
||||
' signal.pthread_sigmask(signal.SIG_BLOCK, {signal.SIGXCPU})',
|
||||
' end = time.perf_counter() + 1.05',
|
||||
' while time.perf_counter() < end:',
|
||||
' end = time.process_time() + 1.05',
|
||||
' while time.process_time() < end:',
|
||||
' pass',
|
||||
'return "escaped"',
|
||||
].join('\n'),
|
||||
@@ -3922,10 +3922,11 @@ describe('PythonCodeRuntime — hostile peer', () => {
|
||||
// the serialized outer logs array adds one more byte of envelope (two
|
||||
// brackets and n-1 commas). The ledgers reserve that byte, so a result that
|
||||
// exactly exhausts the ledger still serializes within the configured cap.
|
||||
// At the 64-byte floor: ledger 63, a 60-character line serializes as
|
||||
// `"aaa...a"` (62 bytes) + 1 separator = 63, exactly exhausting the ledger
|
||||
// and serializing as `["aaa...a"]` = 64 = the cap; a 61-character line
|
||||
// costs 64 > 63 and truncates to the marker alone.
|
||||
// At the 64-byte floor (the smallest admissible maxLogBytes): ledger 63,
|
||||
// a 60-character line serializes as `"aaa...a"` (62 bytes) + 1 separator
|
||||
// = 63, exactly exhausting the ledger and serializing as `["aaa...a"]`
|
||||
// = 64 = the cap; a 61-character line costs 64 > 63 and truncates. The
|
||||
// marker rides envelope, so the serialized logs run to cap + marker.
|
||||
const { runtime } = await setup({ maxLogBytes: 64, maxWallMs: 10_000 })
|
||||
const result = await runtime.run({
|
||||
program: ['print("a" * 60 + "\\n" + "b" * 61, end="")', 'return "done"'].join('\n'),
|
||||
@@ -3941,11 +3942,11 @@ describe('PythonCodeRuntime — hostile peer', () => {
|
||||
}, 15_000)
|
||||
|
||||
it('rejects a log budget too small to serialize the truncation marker', async () => {
|
||||
// A maxLogBytes below 62 cannot serialize the truncation marker plus the
|
||||
// outer-array envelope; it is rejected at construction so a tiny config
|
||||
// cannot report more than the public cap. maxValueBytes keeps no floor
|
||||
// beyond the positive-integer requirement (a completion can be 1 byte).
|
||||
await expect(setup({ maxLogBytes: 61, maxWallMs: 10_000 })).rejects.toThrow(/must be at least 62/)
|
||||
// A maxLogBytes below 64 cannot serialize the truncation marker itself;
|
||||
// it is rejected at construction so a marker-only truncated run cannot
|
||||
// report more than the public cap. maxValueBytes keeps no floor beyond the
|
||||
// positive-integer requirement (a completion can be 1 byte).
|
||||
await expect(setup({ maxLogBytes: 63, maxWallMs: 10_000 })).rejects.toThrow(/must be at least 64/)
|
||||
}, 15_000)
|
||||
|
||||
it('charges the JSON-escaped cost of control characters against the log ledger', async () => {
|
||||
@@ -4399,20 +4400,20 @@ describe('PythonCodeRuntime — hostile peer', () => {
|
||||
it('charges a forged log frame its escaped cost once past the code-unit lower bound', async () => {
|
||||
// The cheap lower bound only rejects what cannot possibly fit; a SHORT
|
||||
// control-heavy frame clears it and must still be charged what it costs on
|
||||
// the wire. Ten NULs are 13 against the 32-byte lower bound but 63 escaped
|
||||
// the wire. Eleven NULs are 14 against the 64-byte ledger's cheap bound
|
||||
// (six bytes each, two quotes, one separator), so the full charge truncates.
|
||||
const { runtime } = await setup({ maxLogBytes: 63 })
|
||||
const { runtime } = await setup({ maxLogBytes: 64 })
|
||||
const result = await runtime.run({
|
||||
program: [
|
||||
'import os',
|
||||
'os.write(3, b\'{"type":"log","text":"\' + b"\\\\u0000" * 10 + b\'"}\\n\')',
|
||||
'os.write(3, b\'{"type":"log","text":"\' + b"\\\\u0000" * 11 + b\'"}\\n\')',
|
||||
'return "settled"',
|
||||
].join('\n'),
|
||||
bindings: [],
|
||||
})
|
||||
expect(result.error).toBeUndefined()
|
||||
expect(result.value).toBe('settled')
|
||||
expect(result.logs).toEqual([logTruncationMarker(63)])
|
||||
expect(result.logs).toEqual([logTruncationMarker(64)])
|
||||
}, 8000)
|
||||
|
||||
it('caps a forged done error.message from its code-unit prefix, never encoding the whole message', async () => {
|
||||
|
||||
Reference in New Issue
Block a user