diff --git a/.agents/notes/implemented/testing/2026-09-10-hosted-image-test-assumptions.i18n.yaml b/.agents/notes/implemented/testing/2026-09-10-hosted-image-test-assumptions.i18n.yaml index 52a4c5dd7f..241a9e9d5f 100644 --- a/.agents/notes/implemented/testing/2026-09-10-hosted-image-test-assumptions.i18n.yaml +++ b/.agents/notes/implemented/testing/2026-09-10-hosted-image-test-assumptions.i18n.yaml @@ -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 .agents/notes/implemented/testing/2026-09-10-hosted-image-test-assumptions.md -2026-09-10-hosted-image-test-assumptions.md: e6cd1ef37dce318920d75545f816006a98989076 -2026-09-10-hosted-image-test-assumptions.zh.md: e60910aebdc2952a2bfb6ab25963a973ea41ac87 +2026-09-10-hosted-image-test-assumptions.md: 5cf3b14503162d67d0fbb27743228cb1a8d7ffe2 +2026-09-10-hosted-image-test-assumptions.zh.md: db82566c11b50b9203d30f7be6aec1378512f94b diff --git a/.agents/notes/implemented/testing/2026-09-10-hosted-image-test-assumptions.md b/.agents/notes/implemented/testing/2026-09-10-hosted-image-test-assumptions.md index e6cd1ef37d..5cf3b14503 100644 --- a/.agents/notes/implemented/testing/2026-09-10-hosted-image-test-assumptions.md +++ b/.agents/notes/implemented/testing/2026-09-10-hosted-image-test-assumptions.md @@ -20,6 +20,8 @@ Terminal cases that drive a mocked PTY exit pin the containment they need (`inte Both illegal-UTF-8 residual cases in `packages/experimental/code-runtime-python/tests/runtime.spec.ts` pace their writes with `time.sleep(0.001)`: `os.sched_yield()` lets a loaded reader coalesce the writes into one chunk, and the coalesced chunk is what the wrapped `Buffer.concat` measures (the hosted image measured 2563 against the 2048 bound with a correct implementation). Their payloads stay above that bound — 3200 bytes for the `0xFF` case and 1100 `ED A0 80` sequences, 3300 raw bytes, for the CESU-8 case, past the 3072-byte budget a raw-byte undercount reaches — so the undercount still flushes above 2048. Each carries a 20s case budget for the paced writes plus the interpreter start. +The stray-output sealing test in `packages/experimental/code-runtime-python/tests/stray-fragments.spec.ts` keeps a real Python child but splits its stdout reads into single-byte events. OS pipe coalescing cannot guarantee the 1024 fragments needed to seal a block: [run 34465259316](https://github.com/deepseek-harness/deepseek-harness/actions/runs/34465259316) passed all assertions but missed that branch. The controlled reads exercise repeated sealing and the final newline merge; exact output and bounded copy volume detect dropped bytes and repeated prefix copies. + The Linux coverage lane grants `DSH_COVERAGE_TEST_TIMEOUT_MS: '90000'`, matching the Windows coverage lane, because the disposal cases in `subprocess-local` and `bash-sandbox` exceed the 5000ms default when the lane's partitions, workers, and sibling gates share one host. The Windows folder-dialog smoke probes `CoCreateInstance(CLSID_FileOpenDialog)` through PowerShell instead of gating on `process.platform`. An image that answers `CLASS_E_CLASSNOTAVAILABLE` (0x80040111) runs the clean-rejection case and skips the real-dialog case, so `win32-dialog.ts` keeps its file coverage without a host that can open a dialog. Every exception from that activation reads as refusal, so a host failing the probe for another reason only loses the real-dialog case; a probe that cannot run at all keeps the win32 assumption. diff --git a/.agents/notes/implemented/testing/2026-09-10-hosted-image-test-assumptions.zh.md b/.agents/notes/implemented/testing/2026-09-10-hosted-image-test-assumptions.zh.md index e60910aebd..db82566c11 100644 --- a/.agents/notes/implemented/testing/2026-09-10-hosted-image-test-assumptions.zh.md +++ b/.agents/notes/implemented/testing/2026-09-10-hosted-image-test-assumptions.zh.md @@ -20,6 +20,8 @@ Status: implemented `packages/experimental/code-runtime-python/tests/runtime.spec.ts` 的两个非法 UTF-8 残余用例都用 `time.sleep(0.001)` 控制写入节奏:`os.sched_yield()` 会让被抢占的读端把多次写入合并成一个分块,而被包裹的 `Buffer.concat` 测量的正是该分块(在正确实现下,托管镜像测得 2563,超过了 2048 的界)。两个用例的载荷都保持在该界之上——`0xFF` 用例 3200 字节,CESU-8 用例 1100 个 `ED A0 80` 序列(3300 原始字节,超过按原始字节计费会触及的 3072 字节预算)——因此少计仍然会在 2048 之上触发 flush。两者各自带有 20s 的用例预算,容纳带节奏的写入与解释器启动。 +`packages/experimental/code-runtime-python/tests/stray-fragments.spec.ts` 的原生输出分块封存测试保留真实 Python 子进程,但把 stdout 读取拆成单字节事件。操作系统的管道合并无法保证达到封存一块所需的 1024 个片段:[run 34465259316](https://github.com/deepseek-harness/deepseek-harness/actions/runs/34465259316) 的全部断言通过,却未覆盖该分支。可控读取覆盖反复封存和末尾换行合并;精确输出与复制总量上限检测字节丢失和前缀反复复制。 + Linux coverage 通道授予 `DSH_COVERAGE_TEST_TIMEOUT_MS: '90000'`,与 Windows coverage 通道一致,因为当该通道的分区、worker 与同级门禁共用一个宿主时,`subprocess-local` 与 `bash-sandbox` 的处置用例会超过 5000ms 默认值。 Windows 文件夹对话框冒烟测试改为通过 PowerShell 探测 `CoCreateInstance(CLSID_FileOpenDialog)`,而不再按 `process.platform` 分流。回答 `CLASS_E_CLASSNOTAVAILABLE`(0x80040111)的镜像会跑干净的拒绝用例并跳过真实对话框用例,因此 `win32-dialog.ts` 在没有可开对话框的宿主上仍保有文件覆盖率。该激活过程抛出的任何异常都按拒绝解读,因此因其它原因探测失败的宿主只会失去真实对话框用例;完全无法运行的探测则保留 win32 假设。 diff --git a/packages/experimental/code-runtime-python/tests/runtime.spec.ts b/packages/experimental/code-runtime-python/tests/runtime.spec.ts index b89ec692b0..1f3fd6cb1e 100644 --- a/packages/experimental/code-runtime-python/tests/runtime.spec.ts +++ b/packages/experimental/code-runtime-python/tests/runtime.spec.ts @@ -4759,58 +4759,6 @@ describe('PythonCodeRuntime — hostile peer', () => { expect(copied).toBeLessThan(256 * 1024) }, 40_000) - it('seals trickled stray fragments into blocks without recopying the sealed prefix', async () => { - // The stray-capture buffer has the same object-overhead exposure as the fd-3 - // reader above: each newline-free `data` chunk is its own Buffer, so a - // program pacing single-byte `os.write(1, ...)` accumulates one object per - // write, which the serialized-cost counter cannot see. Past MAX_PENDING_CHUNKS - // the fragments seal into a finished block; re-merging the whole residual at - // each threshold instead would copy the sealed prefix again and again, making - // the cumulative copy volume quadratic. `Buffer.concat` is wrapped to measure - // that volume — both shapes admit the same final log entry, so the copy total - // is the discriminator. maxLogBytes is raised so the trickle is retained, - // not truncated, which is what forces the fragments to accumulate and seal. - const realConcat = Buffer.concat.bind(Buffer) - let copied = 0 - Buffer.concat = (list: readonly Uint8Array[], total?: number): Buffer => { - for (const part of list) copied += part.length - return realConcat(list, total) - } - let result: CodeRunResult - try { - const { runtime } = await setup({ maxLogBytes: 200_000, maxWallMs: 30_000 }) - result = await runtime.run({ - program: [ - 'import os', - 'for _ in range(60000):', - ' os.write(1, b"x")', - ' os.sched_yield()', - 'os.write(1, b"\\n")', - 'return "done"', - ].join('\n'), - bindings: [], - }) - } finally { - Buffer.concat = realConcat - } - expect(result.error).toBeUndefined() - expect(result.value).toBe('done') - // The trickle coalesces into one log line (no interior newlines). Its exact - // length depends on pipe coalescing, but it is one entry and non-empty. - expect(result.logs.length).toBe(1) - expect((result.logs[0] as string).length).toBeGreaterThan(0) - // Sealing appends a finished block rather than re-merging everything held, so - // each byte is copied a bounded number of times. Re-merging the whole - // residual at every seal threshold instead makes the cumulative copy volume - // quadratic. Measured like the fd-3 sibling above rather than reasoned about: - // this sealed shape copies about 120 KB for 60000 trickled bytes, the - // re-merging shape about 538 KB (the stray path adds one whole-residual - // concat at the terminating newline over the fd-3 sibling's 119/540, landing - // at the same order). 256 KiB sits between them with margin on both sides, so - // reverting the seal to a re-merge turns this assertion red. - expect(copied).toBeLessThan(256 * 1024) - }, 40_000) - it('caps a huge exception diagnostic child-side before it crosses the wire', async () => { // A program can raise with a multi-megabyte message; the child must cap // it at maxValueBytes before formatting/sending, not ship the whole diff --git a/packages/experimental/code-runtime-python/tests/stray-fragments.spec.ts b/packages/experimental/code-runtime-python/tests/stray-fragments.spec.ts new file mode 100644 index 0000000000..9ed70c558b --- /dev/null +++ b/packages/experimental/code-runtime-python/tests/stray-fragments.spec.ts @@ -0,0 +1,56 @@ +import { Context } from '@deepseek-ai/cordis' +import { expect, it, vi } from 'vitest' + +// Keep the interpreter and pipe lifecycle real; only OS-dependent read sizes +// change. Each byte reaches the runtime as its own data event. +vi.mock('node:child_process', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + spawn: vi.fn(actual.spawn).mockImplementation((...args) => { + const child = actual.spawn(...args) + const stdout = child.stdout! + const emit = stdout.emit.bind(stdout) + stdout.emit = (event: string | symbol, ...values: unknown[]) => { + if (event !== 'data') return emit(event, ...values) + const chunk = values[0] as Buffer + for (let offset = 0; offset < chunk.length; offset++) { + emit('data', chunk.subarray(offset, offset + 1)) + } + return true + } + return child + }), + } +}) + +const { PythonCodeRuntime } = await import('../src/index.ts') + +it('seals stray fragments without recopying the sealed prefix', async () => { + const ctx = new Context() + const fiber = await ctx.plugin(PythonCodeRuntime, { maxLogBytes: 200_000, maxWallMs: 30_000 }) + const realConcat = Buffer.concat.bind(Buffer) + let copied = 0 + let maxFragments = 0 + const concat = vi.spyOn(Buffer, 'concat').mockImplementation((list, total) => { + maxFragments = Math.max(maxFragments, list.length) + for (const part of list) copied += part.length + return realConcat(list, total) + }) + try { + const result = await ctx.codeRuntime.run({ + program: 'import os\nos.write(1, b"x" * 60000 + b"\\n")\nreturn "done"', + bindings: [], + }) + expect(result.error).toBeUndefined() + expect(result.value).toBe('done') + expect(result.logs).toEqual(['x'.repeat(60_000)]) + // Sealing copies each byte at most twice; merging every accumulated prefix + // instead copies over a megabyte for these 60,001 controlled fragments. + expect(maxFragments).toBeLessThanOrEqual(1024) + expect(copied).toBeLessThan(256 * 1024) + } finally { + concat.mockRestore() + await fiber.dispose() + } +}, 40_000)