From aa685028a732c1541f2519e69b58fd00d3cd9d9b Mon Sep 17 00:00:00 2001 From: Chinesezjc Date: Tue, 25 Aug 2026 15:10:09 +0800 Subject: [PATCH] test(code-runtime-python): pin the stdin-close behavior with an EOF-observing case MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The stdin destroy (child.stdin?.destroy() right after spawn) previously had no in-tree coverage. A program that reads fd 0 now sees EOF immediately; without the destroy it blocks and the run would hang to maxWallMs as a timeout — verified fail-before by disabling the destroy (the test turns red at the wall ceiling) and restoring it (green). The _str rebind regression was attempted but is not viable: the success path's done-frame serialization reaches str transitively through _encode_json_plain, which the README Known Limitations already records as the accepted success-to-exception residual, so any rebind test trips that documented residual before send_done's bound _str. --- .../code-runtime-python/tests/runtime.spec.ts | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/packages/code-runtime/code-runtime-python/tests/runtime.spec.ts b/packages/code-runtime/code-runtime-python/tests/runtime.spec.ts index 218b7d908f..e8618b5e07 100644 --- a/packages/code-runtime/code-runtime-python/tests/runtime.spec.ts +++ b/packages/code-runtime/code-runtime-python/tests/runtime.spec.ts @@ -725,6 +725,7 @@ describe('PythonCodeRuntime — programs and bindings', () => { expect(result.value).toBe('nope') }, 15_000) + it('still answers the call when the rejection value cannot be converted to a string', async () => { // `messageOf` calls `String(error)`, which runs the value's own conversion, // and this call site is a DETACHED async reply callback. A rejection whose @@ -2644,6 +2645,26 @@ describe('PythonCodeRuntime — budgets, termination, disposal', () => { expect(result.logs).toContain('leader-diagnostic-no-newline') }, 8000) + it('closes the child stdin so a program read sees EOF instead of blocking', async () => { + // The host closes the child's stdin write handle immediately after spawn + // (the program is an async body that reads nothing from fd 0; a live pipe + // would hold a host-side handle open past the run). A program that DOES + // read fd 0 therefore sees EOF at once. Fail-before: with the handle left + // open and no data written, `sys.stdin.read()` blocks and the run would + // hang to maxWallMs as a timeout. + const { runtime } = await setup({ maxWallMs: 8_000 }) + const result = await runtime.run({ + program: [ + 'import sys', + 'data = sys.stdin.read()', + 'return "read: " + repr(data)', + ].join('\n'), + bindings: [], + }) + expect(result.error).toBeUndefined() + expect(result.value).toBe("read: ''") + }, 15_000) + it('reaps a same-group child that ignores SIGTERM and releases the pipes before close', async () => { // The same-group counterpart to the setsid-orphan case above. A descendant // left in the child's OWN process group (no setsid, so `kill(-pid)` reaches