A 5e storytelling engine with an LLM DM
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 177 lines 4.6 kB view raw
1"""Tests for sandboxed code execution via Monty.""" 2 3from storied.sandbox import build_tool_signatures, execute 4from storied.tools import ToolContext 5 6 7class TestBasicExecution: 8 """Core execution: expressions, statements, print output.""" 9 10 def test_simple_expression(self): 11 result = execute("1 + 2") 12 13 assert "3" in result 14 15 def test_multi_line_script(self): 16 result = execute("x = 10\ny = 20\nx + y") 17 18 assert "30" in result 19 20 def test_print_captured(self): 21 result = execute('print("hello")\nprint("world")') 22 23 assert "hello" in result 24 assert "world" in result 25 26 def test_print_and_expression(self): 27 result = execute('print("debug")\n42') 28 29 assert "debug" in result 30 assert "42" in result 31 32 def test_no_return_value(self): 33 result = execute("x = 5") 34 35 assert isinstance(result, str) 36 37 def test_empty_code(self): 38 result = execute("") 39 40 assert isinstance(result, str) 41 42 43class TestHostFunctions: 44 """Host functions exposed to sandboxed code.""" 45 46 def test_roll_available(self): 47 result = execute('r = roll("1d6")\nr["total"]') 48 49 assert result # should contain a number 1-6 50 51 def test_roll_returns_dict(self): 52 result = execute("r = roll('2d6+3')\nprint('total=' + str(r['total']))") 53 54 assert "total=" in result 55 56 def test_roll_with_reason_positional(self): 57 result = execute('r = roll("1d6", "picking a name")\nr["total"]') 58 59 assert result 60 61 def test_roll_with_reason_keyword(self): 62 result = execute('r = roll(notation="1d6", reason="picking")\nr["total"]') 63 64 assert result 65 66 def test_custom_host_function(self): 67 result = execute( 68 "double(5)", 69 host_functions={"double": lambda x: x * 2}, 70 ) 71 72 assert "10" in result 73 74 75class TestErrorHandling: 76 """Errors returned as strings, not raised.""" 77 78 def test_division_by_zero(self): 79 result = execute("1 / 0") 80 81 assert "ZeroDivisionError" in result 82 83 def test_name_error(self): 84 result = execute("undefined_variable") 85 86 assert "NameError" in result 87 88 def test_syntax_error(self): 89 result = execute("if True") 90 91 assert result # Monty returns a parse error message 92 93 def test_type_error(self): 94 result = execute('"hello" + 5') 95 96 assert "TypeError" in result 97 98 99class TestResourceLimits: 100 """Sandbox enforces time and memory limits.""" 101 102 def test_infinite_loop_times_out(self): 103 result = execute("while True: pass") 104 105 assert "time" in result.lower() or "timeout" in result.lower() 106 107 108class TestSandboxSecurity: 109 """Dangerous operations are blocked.""" 110 111 def test_no_file_access(self): 112 result = execute('open("/etc/passwd").read()') 113 114 assert "Error" in result 115 116 def test_no_os_access(self): 117 result = execute("import os\nos.environ") 118 119 assert "NotImplementedError" in result 120 121 122class TestToolBridge: 123 """DM tools exposed as direct host functions. 124 125 The sandbox resolves Dependency parameters from the process-global 126 ToolContext (set up by the `ctx` fixture), so callers don't have to 127 pass `ctx` explicitly anymore. 128 """ 129 130 def test_recall(self, ctx: ToolContext): 131 result = execute("recall(query='nonexistent')") 132 133 assert "Nothing found" in result 134 135 def test_establish(self, ctx: ToolContext): 136 result = execute( 137 "establish(entity_type='npc', name='Bob the Baker', " 138 "description='A friendly baker')" 139 ) 140 141 assert "Bob" in result 142 143 144class TestToolSignatures: 145 """Dynamic signature builder for the run_code tool description.""" 146 147 def test_includes_all_tools(self): 148 sigs = build_tool_signatures() 149 150 for name in [ 151 "roll", 152 "recall", 153 "establish", 154 "mark", 155 "damage", 156 "heal", 157 "enter_initiative", 158 "end_initiative", 159 "next_turn", 160 ]: 161 assert f"{name}(" in sigs 162 163 def test_excludes_dependency_params(self): 164 """Dependency-default params are wrapper bookkeeping; the LLM-facing 165 signature should drop them all.""" 166 sigs = build_tool_signatures() 167 168 # None of the Dependency-class instances should appear as defaults 169 for marker in ( 170 "Combat()", 171 "Lore()", 172 "Player()", 173 "Timekeeper()", 174 "Entities()", 175 "World()", 176 ): 177 assert marker not in sigs