mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-09-11 04:00:38 +00:00
test(code-runtime-python): drive the inherited SIGXCPU path with a wrapper; fix the zh outer wire sentence
The review's two follow-ups on the inherited-SIGXCPU fix: (1) a discriminating case — pythonBin points at a wrapper that ignores SIGXCPU before exec'ing python3, so the child genuinely inherits the ignore; with cpuSeconds: 1 the busy loop must end as timeout (the bootstrap reset restored SIG_DFL), and reverting the reset leaves it running to the wall — verified red. (2) The zh README's OUTER wire section now carries the truncation-exception sentence (the previous commit had duplicated it in the inner section instead); the duplicate is removed, and the settlement note registers the inherited-SIGXCPU reset.
This commit is contained in:
@@ -3,4 +3,4 @@
|
||||
# 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: 2e22a5122f891b8317e13ee395ce222805b01b29
|
||||
README.zh.md: 028d2f5f7b57f1ca4da46557ba66511992227379
|
||||
README.zh.md: a0372bbe1148ca259f1fc6849060d13fcec24947
|
||||
|
||||
@@ -33,7 +33,7 @@ kind: "package-reference"
|
||||
|
||||
### wire
|
||||
|
||||
帧在子进程 fd 3 上以 JSON-lines 传输——每行一个对象——因此 stdout/stderr 留给程序自己的输出。子进程 → 宿主:`boot-ack`、`call`、`log`、`done`。宿主 → 子进程:`boot`(首帧,携带全部上限与命名空间声明)、`run`(`boot-ack` 之后,只携带程序体)与每个 `call` 一个 `reply`。伪造帧可在 `done` 上同时携带 `value` 与 `error`,因此消费方必须先检查 `error`,在它存在时忽略 `value`。`log` 帧的 `open` 标志标记由显式 flush 提交的未结束行:宿主把下一个 log 帧追加到同一条目,因此 `print('a', end='', flush=True); print('b')` 读回为一条 `'ab'` 条目而不是假换行。
|
||||
帧在子进程 fd 3 上以 JSON-lines 传输——每行一个对象——因此 stdout/stderr 留给程序自己的输出。子进程 → 宿主:`boot-ack`、`call`、`log`、`done`。宿主 → 子进程:`boot`(首帧,携带全部上限与命名空间声明)、`run`(`boot-ack` 之后,只携带程序体)与每个 `call` 一个 `reply`。伪造帧可在 `done` 上同时携带 `value` 与 `error`,因此消费方必须先检查 `error`,在它存在时忽略 `value`。`log` 帧的 `open` 标志标记由显式 flush 提交的未结束行:宿主把下一个 log 帧追加到同一条目,因此 `print('a', end='', flush=True); print('b')` 读回为一条 `'ab'` 条目而不是假换行。合并的唯一例外是截断:当后续超预算帧触发账本时,已计费的前缀作为独立条目先提交,截断 marker 跟在后面(marker 保持末位,无重复计费)。
|
||||
|
||||
### 可能出错的地方
|
||||
|
||||
@@ -55,7 +55,7 @@ kind: "package-reference"
|
||||
|
||||
### wire 契约
|
||||
|
||||
帧为 `boot`/`run`(宿主 → 子进程)与 `boot-ack`/`call`/`log`/`done` 加每个 call 一个 `reply`(子进程 → 宿主)。`log` 帧的 `truncated` 标志标记的就是子进程账本自己的截断标记帧,因此宿主在与子进程相同的点停止捕获,而不是从自己的预算推断。`log` 帧的 `open` 标志标记由显式 flush 提交的未结束行:宿主把下一个 log 帧合并进同一条目,因此 `print('a', end='', flush=True); print('b')` 读回为一条 `'ab'` 条目而不是假换行(拆分计费算术在 fd-3 协议 Agent Note 的 wire-contract 段)。合并的唯一例外是截断:当后续超预算帧触发账本时,已计费的前缀作为独立条目先提交,截断 marker 跟在后面(marker 保持末位,无重复计费)。合并的唯一例外是截断:当后续超预算帧触发账本时,已计费的前缀作为独立条目先提交,截断 marker 跟在后面(marker 保持末位,无重复计费)。`done.error.kind` 为 `exception`、`invalid-output`、`output-limit` 之一;墙钟/CPU 预算、中止与基底死亡在宿主侧观察,不以帧形式携带。
|
||||
帧为 `boot`/`run`(宿主 → 子进程)与 `boot-ack`/`call`/`log`/`done` 加每个 call 一个 `reply`(子进程 → 宿主)。`log` 帧的 `truncated` 标志标记的就是子进程账本自己的截断标记帧,因此宿主在与子进程相同的点停止捕获,而不是从自己的预算推断。`log` 帧的 `open` 标志标记由显式 flush 提交的未结束行:宿主把下一个 log 帧合并进同一条目,因此 `print('a', end='', flush=True); print('b')` 读回为一条 `'ab'` 条目而不是假换行(拆分计费算术在 fd-3 协议 Agent Note 的 wire-contract 段)。合并的唯一例外是截断:当后续超预算帧触发账本时,已计费的前缀作为独立条目先提交,截断 marker 跟在后面(marker 保持末位,无重复计费)。`done.error.kind` 为 `exception`、`invalid-output`、`output-limit` 之一;墙钟/CPU 预算、中止与基底死亡在宿主侧观察,不以帧形式携带。
|
||||
|
||||
### 无损 JSON 跨越
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { existsSync, readdirSync, realpathSync, statSync } from 'node:fs'
|
||||
import { existsSync, readdirSync, realpathSync, rmSync, statSync, writeFileSync } from 'node:fs'
|
||||
import { mkdtemp, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { basename, dirname, join } from 'node:path'
|
||||
@@ -638,6 +638,26 @@ describe('PythonCodeRuntime — inherited resource limits', () => {
|
||||
expect(result.value).toBeUndefined()
|
||||
}, 20_000)
|
||||
|
||||
it('reports a timeout when the interpreter was started with SIGXCPU ignored (inherited state)', async () => {
|
||||
// The child inherits the host's SIGXCPU disposition: a wrapper that
|
||||
// ignores SIGXCPU before exec'ing python3 hands the child a soft
|
||||
// RLIMIT_CPU that cannot stop it. The bootstrap resets SIGXCPU to SIG_DFL
|
||||
// before model code runs, so a busy loop still ends as a timeout rather
|
||||
// than running to the hard limit and being misclassified as worker-exit.
|
||||
const wrapper = join(tmpdir(), `dsh-xcpu-ignore-${process.pid}.sh`)
|
||||
writeFileSync(wrapper, '#!/bin/sh\ntrap "" XCPU\nexec python3 "$@"\n', { mode: 0o755 })
|
||||
try {
|
||||
const { runtime } = await setup({ maxWallMs: 30_000, cpuSeconds: 1, pythonBin: wrapper })
|
||||
const result = await runtime.run({
|
||||
program: ['while True: pass'].join('\n'),
|
||||
bindings: [],
|
||||
})
|
||||
expect(result.error?.kind).toBe('timeout')
|
||||
} finally {
|
||||
rmSync(wrapper, { force: true })
|
||||
}
|
||||
}, 20_000)
|
||||
|
||||
it('reports a timeout when a program traps AND masks SIGXCPU and returns past the soft limit', async () => {
|
||||
// The mask-only case exercises the unblock; the trap+mask combination is
|
||||
// the harder one: a program that installed a custom handler AND masked the
|
||||
|
||||
Reference in New Issue
Block a user