mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-09-11 04:00:38 +00:00
fix(code-runtime-python): bind RuntimeError and _BindingRejection for dispatch's rejection path
dispatch's call_failure and its except clause resolved the module globals at call time, so a program rebinding __main__._BindingRejection = ValueError let the internal marker type leak into model code. Bind _RuntimeError_cls and _BindingRejection_cls into _run locals before the program runs (names distinct from the module globals so the assignment RHS resolves the global, not an unbound local); dispatch now uses the locals. A regression test rebinds _BindingRejection and asserts a host rejection still surfaces as RuntimeError. The sys.__stdout__ flush test now reconfigures the streams back to block buffering (write_through=False) so the settlement drain path is what the case pins — verified fail-before: binding the stream objects instead of their flush methods turns the test red.
This commit is contained in:
@@ -674,6 +674,35 @@ describe('PythonCodeRuntime — programs and bindings', () => {
|
||||
expect(calls).toEqual([{ n: 1 }])
|
||||
})
|
||||
|
||||
it('keeps the rejection contract when _BindingRejection is rebound', async () => {
|
||||
// `dispatch`'s except clause resolves `_BindingRejection` at call time; a
|
||||
// program that rebinds `__main__._BindingRejection = ValueError` would
|
||||
// otherwise let the internal marker type leak into model code (the program
|
||||
// would catch a `ValueError` for a host rejection). The class is now bound
|
||||
// into `_run` locals before the program runs, so a host rejection still
|
||||
// surfaces as the declared `RuntimeError`.
|
||||
const { runtime } = await setup()
|
||||
const result = await runtime.run({
|
||||
program: [
|
||||
'import __main__',
|
||||
'__main__._BindingRejection = ValueError',
|
||||
'caught = ""',
|
||||
'try:',
|
||||
' await tools.fail({})',
|
||||
'except RuntimeError as e:',
|
||||
' caught = str(e)',
|
||||
'except Exception as e:',
|
||||
' caught = "WRONG TYPE: " + type(e).__name__',
|
||||
'return caught',
|
||||
].join('\n'),
|
||||
bindings: tools({
|
||||
fail: async () => { throw new Error('nope') },
|
||||
}),
|
||||
})
|
||||
expect(result.error).toBeUndefined()
|
||||
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
|
||||
@@ -4317,6 +4346,12 @@ describe('PythonCodeRuntime — hostile peer', () => {
|
||||
const result = await runtime.run({
|
||||
program: [
|
||||
'import sys',
|
||||
// `-u` makes the streams write-through; re-enable block buffering so
|
||||
// the bytes sit in the wrapper until the SETTLEMENT drain flushes them
|
||||
// — the drain path, not the -u immediate write, is what this case pins.
|
||||
'if hasattr(sys.__stdout__, "reconfigure"):',
|
||||
' sys.__stdout__.reconfigure(write_through=False)',
|
||||
' sys.__stderr__.reconfigure(write_through=False)',
|
||||
'sys.__stdout__.write("orig stdout\\n")',
|
||||
'sys.__stderr__.write("orig stderr\\n")',
|
||||
'return "done"',
|
||||
|
||||
Reference in New Issue
Block a user