Something went wrong. Try again.
A 5e storytelling engine with an LLM DM
Something went wrong. Try again.
Python
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178"""Tests for sandboxed code execution via Monty."""
from storied.sandbox import build_tool_signatures, executefrom storied.tools import ToolContext
class TestBasicExecution: """Core execution: expressions, statements, print output."""
def test_simple_expression(self): result = execute("1 + 2")
assert "3" in result
def test_multi_line_script(self): result = execute("x = 10\ny = 20\nx + y")
assert "30" in result
def test_print_captured(self): result = execute('print("hello")\nprint("world")')
assert "hello" in result assert "world" in result
def test_print_and_expression(self): result = execute('print("debug")\n42')
assert "debug" in result assert "42" in result
def test_no_return_value(self): result = execute("x = 5")
assert isinstance(result, str)
def test_empty_code(self): result = execute("")
assert isinstance(result, str)
class TestHostFunctions: """Host functions exposed to sandboxed code."""
def test_roll_available(self): result = execute('r = roll("1d6")\nr["total"]')
assert result # should contain a number 1-6
def test_roll_returns_dict(self): result = execute("r = roll('2d6+3')\nprint('total=' + str(r['total']))")
assert "total=" in result
def test_roll_with_reason_positional(self): result = execute('r = roll("1d6", "picking a name")\nr["total"]')
assert result
def test_roll_with_reason_keyword(self): result = execute('r = roll(notation="1d6", reason="picking")\nr["total"]')
assert result
def test_custom_host_function(self): result = execute( "double(5)", host_functions={"double": lambda x: x * 2}, )
assert "10" in result
class TestErrorHandling: """Errors returned as strings, not raised."""
def test_division_by_zero(self): result = execute("1 / 0")
assert "ZeroDivisionError" in result
def test_name_error(self): result = execute("undefined_variable")
assert "NameError" in result
def test_syntax_error(self): result = execute("if True")
assert result # Monty returns a parse error message
def test_type_error(self): result = execute('"hello" + 5')
assert "TypeError" in result
class TestResourceLimits: """Sandbox enforces time and memory limits."""
def test_infinite_loop_times_out(self): result = execute("while True: pass")
assert "time" in result.lower() or "timeout" in result.lower()
class TestSandboxSecurity: """Dangerous operations are blocked."""
def test_no_file_access(self): result = execute('open("/etc/passwd").read()')
assert "Error" in result
def test_no_os_access(self): result = execute("import os\nos.environ")
assert "NotImplementedError" in result
class TestToolBridge: """DM tools exposed as direct host functions.
The sandbox resolves Dependency parameters from the process-global ToolContext (set up by the `ctx` fixture), so callers don't have to pass `ctx` explicitly anymore. """
def test_recall(self, ctx: ToolContext): result = execute("recall(query='nonexistent')")
assert "Nothing found" in result
def test_establish(self, ctx: ToolContext): result = execute( "establish(entity_type='npc', name='Bob the Baker', " "description='A friendly baker')" )
assert "Bob" in result
class TestToolSignatures: """Dynamic signature builder for the run_code tool description."""
def test_includes_all_tools(self): sigs = build_tool_signatures()
for name in [ "roll", "recall", "establish", "mark", "damage", "heal", "enter_initiative", "end_initiative", "next_turn", ]: assert f"{name}(" in sigs
def test_excludes_dependency_params(self): """Dependency-default params are wrapper bookkeeping; the LLM-facing signature should drop them all.""" sigs = build_tool_signatures()
# None of the Dependency-class instances should appear as defaults for marker in ( "Combat()", "Lore()", "Player()", "Timekeeper()", "Entities()", "World()", ): assert marker not in sigs