fix(code-runtime-python): make the float encoder context-independent; correct the binding-reply README entry

The review's critical: Decimal(repr(value)).normalize() read the process-global
decimal context, so a legitimate program setting getcontext().prec = 2 silently
rounded the completion value's digits and traps[Inexact] = True made the encode
raise, misclassifying a successful run as an exception. A fixed module-level
Context(prec=28) makes the spelling decision context-independent; a regression
case mutates both context knobs and asserts the float round-trips exactly.

The binding-reply README entry now states the fact (no seam-level cap;
maxValueBytes meters only the done frame; a wide reply is rebuilt and encoded
whole, bounded by process memory), matching the earlier reviewer wording.
This commit is contained in:
Chinesezjc
2026-08-31 15:02:38 +08:00
committed by Tianyi Cui
parent 666ff2855e
commit 35de0682c7
5 changed files with 35 additions and 7 deletions
@@ -1843,6 +1843,27 @@ describe('PythonCodeRuntime — programs and bindings', () => {
expect(result.error?.kind).not.toBe('worker-exit')
}, 90_000)
it('keeps a float completion exact when the program mutates the decimal context', async () => {
// The float encoder's Decimal(repr(value)).normalize() used the process
// GLOBAL decimal context: a legitimate program setting
// `getcontext().prec = 2` silently rounded the completion value's digits,
// and `traps[Inexact] = True` made the encode raise, misclassifying a
// successful run as an exception. A fixed module-level Context(prec=28)
// makes the spelling decision context-independent.
const { runtime } = await setup()
const result = await runtime.run({
program: [
'from decimal import getcontext',
'getcontext().prec = 2',
'getcontext().traps[__import__("decimal").Inexact] = True',
'return 1.2345678901234567',
].join('\n'),
bindings: [],
})
expect(result.error).toBeUndefined()
expect(result.value).toBe(1.2345678901234567)
}, 15_000)
it('bounds an over-cap exception-group nesting on the copy', async () => {
// Exception groups link through `exceptions`, not the cause/context
// dunders, so the cap has to count that edge too — otherwise a deeply
@@ -2952,7 +2973,6 @@ describe('PythonCodeRuntime — budgets, termination, disposal', () => {
expect(still).toBe(true)
}, 20_000)
it('dispose awaits reaping of a same-group survivor from a completed run', async () => {
// The quiescence contract also holds for a run that ALREADY resolved: the run
// stays tracked in `live` until its process group is reaped, so a `dispose()`