char-fork/llama.cpp
git clone https://git.t4t.associates/char-fork/llama.cpp
42f0225fe
char/aliceai-t5-moe
1import os 2import shutil 3import subprocess 4 5import pytest 6from utils import * 7 8server :ServerProcess 9 10# project root, used as the search directory for grep_search/file_glob_search 11PROJECT_ROOT = os .path .abspath (os .path .join (os .path .dirname (__file__ ),".." ,".." ,".." ,".." )) 12 13# marker for the grep_search test to find in this file 14GREP_MARKER = "llama_cpp_test_tools_builtin_marker_grep_search" 15 16# image the container runtime tests run their shell in 17CONTAINER_IMAGE = "busybox" 18 19 20@ pytest . fixture ( autouse = True ) 21def create_server (): 22global server 23server = ServerPreset .router () 24server .server_tools = "all" 25 26 27def call_tool (name :str ,params :dict ,headers :dict | None = None )-> dict : 28res = server .make_request ("POST" ,"/tools" ,data = {"tool" :name ,"params" :params },headers = headers ) 29assert res .status_code == 200 ,res .body 30assert "error" not in res .body ,res .body 31return res .body 32 33 34def call_tool_expect_error (name :str ,params :dict )-> str : 35res = server .make_request ("POST" ,"/tools" ,data = {"tool" :name ,"params" :params }) 36assert res .status_code == 200 ,res .body 37assert "error" in res .body ,res .body 38return res .body ["error" ] 39 40 41def test_tools_builtin_grep_search (): 42global server 43server .start () 44 45res = call_tool ("grep_search" , { 46"path" :PROJECT_ROOT , 47"pattern" :GREP_MARKER , 48"include" :"test_tools_builtin.py" ,# bare pattern -> matches basename at any depth 49 }) 50text = res ["plain_text_response" ] 51assert "test_tools_builtin.py" in text 52assert GREP_MARKER in text 53assert "Total matches: 1" in text 54 55 56def test_tools_builtin_read_file (): 57global server 58server .start () 59 60this_file = os .path .join (PROJECT_ROOT ,"tools" ,"server" ,"tests" ,"unit" ,"test_tools_builtin.py" ) 61res = call_tool ("read_file" , {"path" :this_file }) 62text = res ["plain_text_response" ] 63assert GREP_MARKER in text 64assert "def test_tools_builtin_read_file" in text 65 66 67def test_tools_builtin_write_then_edit_file (tmp_path ): 68global server 69server .start () 70 71log_path = str (tmp_path / "test.log" ) 72try : 73write_res = call_tool ("write_file" , {"path" :log_path ,"content" :"line1\nline2\nline3\n" }) 74assert write_res ["result" ]== "file written successfully" 75 76read_before = call_tool ("read_file" , {"path" :log_path }) 77assert read_before ["plain_text_response" ]== "line1\nline2\nline3\n" 78 79edit_res = call_tool ("edit_file" , { 80"path" :log_path , 81"edits" : [ 82 {"old_text" :"line2" ,"new_text" :"line2-edited" }, 83 {"old_text" :"line3\n" ,"new_text" :"line3\nline4\n" }, 84 ], 85 }) 86assert edit_res ["result" ]== "file edited successfully" 87assert edit_res ["edits_applied" ]== 2 88 89read_after = call_tool ("read_file" , {"path" :log_path }) 90assert read_after ["plain_text_response" ]== "line1\nline2-edited\nline3\nline4\n" 91finally : 92if os .path .exists (log_path ): 93os .remove (log_path ) 94 95 96def test_tools_builtin_edit_file_rejects_non_unique_old_text (tmp_path ): 97global server 98server .start () 99 100log_path = str (tmp_path / "test.log" ) 101try : 102call_tool ("write_file" , {"path" :log_path ,"content" :"dup\ndup\n" }) 103err = call_tool_expect_error ("edit_file" , { 104"path" :log_path , 105"edits" : [{"old_text" :"dup" ,"new_text" :"changed" }], 106 }) 107assert "unique" in err 108finally : 109if os .path .exists (log_path ): 110os .remove (log_path ) 111 112 113def test_tools_builtin_exec_shell_command_stream (): 114global server 115server .start () 116 117events = list (server .make_stream_request ("POST" ,"/tools" ,data = { 118"tool" :"exec_shell_command" , 119"params" : {"command" :"echo hello" }, 120"stream" :True , 121 })) 122 123assert len (events )>= 2 124assert events [- 1 ]["done" ]is True 125assert not events [- 1 ].get ("error" ) 126chunks = "" .join (e ["chunk" ]for e in events [:- 1 ]) 127assert "hello" in chunks 128assert "[exit code: 0]" in chunks 129 130 131def test_tools_builtin_cwd_header (): 132global server 133server .start () 134 135cwd_dir = os .path .join (PROJECT_ROOT ,"tools" ,"server" ,"tests" ,"unit" ) 136headers = {"x-tool-cwd" :cwd_dir } 137 138res = call_tool ("read_file" , {"path" :"test_tools_builtin.py" },headers = headers ) 139assert GREP_MARKER in res ["plain_text_response" ] 140 141# exec_shell_command should also run with that directory as its working directory: 142# writing to a relative filename must land inside cwd_dir 143marker_name = "llama_cpp_test_tools_builtin_cwd_marker.txt" 144marker_path = os .path .join (cwd_dir ,marker_name ) 145try : 146command = f"echo hello > { marker_name } " 147call_tool ("exec_shell_command" , {"command" :command },headers = headers ) 148assert os .path .exists (marker_path ) 149finally : 150if os .path .exists (marker_path ): 151os .remove (marker_path ) 152 153 154def _container_engine_unavailable_reason (engine :str )-> str | None : 155"""None if `engine` can run the image these tests use, otherwise the reason it can't.""" 156engine_bin = shutil .which (engine ) 157if engine_bin is None : 158return f" { engine } is not installed" 159try : 160# a daemon that answers `info` still cannot run a linux image when it serves windows 161# containers, so probe the image itself, which also pulls it before the tests 162subprocess .run ([engine_bin ,"run" ,"--rm" ,CONTAINER_IMAGE ,"true" ],capture_output = True ,timeout = 60 ,check = True ) 163except Exception as e : 164return f" { engine } cannot run { CONTAINER_IMAGE } : { e } " 165return None 166 167 168@ pytest . fixture ( params = [ "docker" , "podman" ]) 169def container_engine (request ): 170engine = request .param 171reason = _container_engine_unavailable_reason (engine ) 172if reason is not None : 173pytest .skip (reason )# ty: ignore[too-many-positional-arguments, invalid-argument-type] 174return engine 175 176 177@ pytest . fixture 178def container_id (container_engine :str ): 179proc = subprocess .run ( 180 [container_engine ,"run" ,"-d" ,"--rm" ,CONTAINER_IMAGE ,"sleep" ,"300" ], 181capture_output = True ,text = True , 182 ) 183if proc .returncode != 0 : 184pytest .skip (f"failed to start { container_engine } container: { proc . stderr . strip () } " )# ty: ignore[too-many-positional-arguments, invalid-argument-type] 185 186cid = proc .stdout .strip () 187try : 188yield cid 189finally : 190subprocess .run ([container_engine ,"rm" ,"-f" ,cid ],capture_output = True ) 191 192 193def test_tools_builtin_runtime_header (container_engine :str ,container_id :str ): 194global server 195server .start () 196 197headers = {"x-tool-runtime" :f" { container_engine } -container: { container_id } " ,"x-tool-cwd" :"/tmp" } 198 199write_res = call_tool ("write_file" , {"path" :"test.log" ,"content" :"hello container\n" },headers = headers ) 200assert write_res ["result" ]== "file written successfully" 201 202read_res = call_tool ("read_file" , {"path" :"test.log" },headers = headers ) 203assert read_res ["plain_text_response" ]== "hello container\n" 204 205exec_res = call_tool ("exec_shell_command" , {"command" :"cat test.log" },headers = headers ) 206assert "hello container" in exec_res ["plain_text_response" ] 207 208 209def test_tools_builtin_runtime_header_unknown_scheme (): 210global server 211server .start () 212 213# an unknown runtime must fail, never silently fall back to running on the host 214res = server .make_request ("POST" ,"/tools" , 215data = {"tool" :"exec_shell_command" ,"params" : {"command" :"echo hi" }}, 216headers = {"x-tool-runtime" :"fake:does-not-exist" }) 217assert res .status_code == 500 ,res .body 218assert "unknown tool runtime" in str (res .body ) 219 220 221def test_tools_builtin_runtime_header_rejects_ssh_option_injection (): 222global server 223server .start () 224 225# ssh reads options from its argv, so a target starting with '-' must be rejected 226res = server .make_request ("POST" ,"/tools" , 227data = {"tool" :"exec_shell_command" ,"params" : {"command" :"echo hi" }}, 228headers = {"x-tool-runtime" :"ssh:-oProxyCommand=touch /tmp/pwned" }) 229assert res .status_code == 500 ,res .body 230assert "invalid ssh target" in str (res .body ) 231 232 233@ pytest . mark . parametrize ( "engine" , [ "docker" , "podman" ]) 234def test_tools_builtin_runtime_header_rejects_container_option_injection (engine :str ): 235global server 236server .start () 237 238# the container id lands on the `<engine> exec` command line, so an id that looks 239# like an option must be rejected 240res = server .make_request ("POST" ,"/tools" , 241data = {"tool" :"exec_shell_command" ,"params" : {"command" :"echo hi" }}, 242headers = {"x-tool-runtime" :f" { engine } -container:--privileged" }) 243assert res .status_code == 500 ,res .body 244assert "invalid container id" in str (res .body ) 245 246 247def test_tools_builtin_docker_runtime_cleans_up_spawned_container (): 248# docker-only: this reads the container hostname to get the spawned id, which only docker 249# sets to the short id. podman is covered by the attach path above 250reason = _container_engine_unavailable_reason ("docker" ) 251if reason is not None : 252pytest .skip (reason )# ty: ignore[too-many-positional-arguments, invalid-argument-type] 253 254global server 255server .server_tools_runtime = f"docker: { CONTAINER_IMAGE } " 256server .start () 257 258# exec_shell_command runs inside the container spawned for --tools-runtime; docker sets 259# the container's hostname to its own short id, so this also tells us which one to check 260res = call_tool ("exec_shell_command" , {"command" :"hostname" }) 261container_id = res ["plain_text_response" ].splitlines ()[0 ].strip () 262assert len (container_id )>= 8 ,res 263 264running = subprocess .run ( 265 ["docker" ,"inspect" ,"-f" ,"{{.State.Running}}" ,container_id ], 266capture_output = True ,text = True , 267 ) 268assert running .returncode == 0 and running .stdout .strip ()== "true" ,running .stderr 269 270server .stop () 271 272# a clean server shutdown must stop and remove the container it spawned (it runs with --rm), 273# not leave it behind as an abandoned child 274leftover = subprocess .run (["docker" ,"inspect" ,container_id ],capture_output = True ,text = True ) 275assert leftover .returncode != 0 ,f"container { container_id } was not cleaned up after server exit" 276 277 278def test_tools_builtin_edit_file_rejects_overlapping_edits (tmp_path ): 279global server 280server .start () 281 282log_path = str (tmp_path / "test.log" ) 283try : 284call_tool ("write_file" , {"path" :log_path ,"content" :"line1\nline2\n" }) 285err = call_tool_expect_error ("edit_file" , { 286"path" :log_path , 287"edits" : [ 288 {"old_text" :"line1\nline2" ,"new_text" :"a" }, 289 {"old_text" :"line2" ,"new_text" :"b" }, 290 ], 291 }) 292assert "overlap" in err 293finally : 294if os .path .exists (log_path ): 295os .remove (log_path ) 296 297 298def test_tools_builtin_file_glob_search_type_dir (tmp_path ): 299global server 300server .start () 301 302 (tmp_path / "project-alpha" / "src" ).mkdir (parents = True ) 303 (tmp_path / "project-alpha" / "README.md" ).write_text ("alpha" ) 304 (tmp_path / "project-alpha" / "src" / "main.cpp" ).write_text ("int main() {}" ) 305 (tmp_path / "project-beta" ).mkdir () 306 (tmp_path / "project-beta" / "notes.txt" ).write_text ("beta" ) 307 308res = call_tool ("file_glob_search" , {"path" :str (tmp_path ),"type" :"dir" }) 309text = res ["plain_text_response" ] 310assert "project-alpha/" in text 311assert "project-beta/" in text 312assert "project-alpha/src/" in text 313assert "README.md" not in text 314types = {e ["path" ]:e ["type" ]for e in res ["entries" ]} 315assert types ["project-alpha" ]== "dir" 316assert types ["project-alpha/src" ]== "dir" 317 318res_all = call_tool ("file_glob_search" , {"path" :str (tmp_path ),"type" :"all" ,"include" :"*proj*" }) 319paths = [e ["path" ]for e in res_all ["entries" ]] 320assert "project-alpha" in paths 321assert "project-beta" in paths 322 323 324def test_tools_builtin_file_glob_search_max_depth_and_limit (tmp_path ): 325global server 326server .start () 327 328 (tmp_path / "a" / "b" / "c" ).mkdir (parents = True ) 329 (tmp_path / "top.txt" ).write_text ("top" ) 330 (tmp_path / "a" / "mid.txt" ).write_text ("mid" ) 331 (tmp_path / "a" / "b" / "deep.txt" ).write_text ("deep" ) 332 333res = call_tool ("file_glob_search" , {"path" :str (tmp_path ),"max_depth" :1 }) 334assert "top.txt" in res ["plain_text_response" ] 335assert "mid.txt" not in res ["plain_text_response" ] 336 337res = call_tool ("file_glob_search" , {"path" :str (tmp_path ),"max_depth" :2 }) 338assert "mid.txt" in res ["plain_text_response" ] 339assert "deep.txt" not in res ["plain_text_response" ] 340 341res = call_tool ("file_glob_search" , {"path" :str (tmp_path ),"limit" :1 }) 342assert len (res ["entries" ])== 1 343assert "Total matches: 3" in res ["plain_text_response" ] 344 345 346def test_tools_builtin_file_glob_search_junk_dirs (tmp_path ): 347global server 348server .start () 349 350 (tmp_path / "build" / "nested" ).mkdir (parents = True ) 351 (tmp_path / "build" / "artifact.txt" ).write_text ("built" ) 352 (tmp_path / "src" ).mkdir () 353 (tmp_path / "src" / "main.cpp" ).write_text ("int main() {}" ) 354 355# a junk directory stays selectable as a working directory 356res = call_tool ("file_glob_search" , {"path" :str (tmp_path ),"type" :"dir" ,"max_depth" :1 }) 357assert "build" in [e ["path" ]for e in res ["entries" ]] 358 359# but it is never walked, so nothing inside it shows up 360res = call_tool ("file_glob_search" , {"path" :str (tmp_path ),"type" :"all" }) 361paths = [e ["path" ]for e in res ["entries" ]] 362assert "src/main.cpp" in paths 363assert "build/artifact.txt" not in paths 364assert "build/nested" not in paths 365 366 367def test_tools_builtin_file_glob_search_rejects_invalid_type (tmp_path ): 368global server 369server .start () 370 371err = call_tool_expect_error ("file_glob_search" , {"path" :str (tmp_path ),"type" :"bogus" }) 372assert "invalid type" in err 373 374 375def test_tools_builtin_cwd_header_overrides_model_param (tmp_path ): 376global server 377server .start () 378 379workdir = tmp_path / "workdir" 380workdir .mkdir () 381 (workdir / "marker.txt" ).write_text ("marker" ) 382 383# a model-provided "cwd" in the params is overridden by the x-tool-cwd header 384res = call_tool ("read_file" , {"path" :"marker.txt" ,"cwd" :"/definitely/not/a/real/path" }, 385headers = {"x-tool-cwd" :str (workdir )}) 386assert "marker" in res ["plain_text_response" ] 387 388 389def test_tools_builtin_cwd_relative_paths (tmp_path ): 390global server 391server .start () 392 393workdir = tmp_path / "workdir" 394workdir .mkdir () 395 (workdir / "rel.txt" ).write_text ("relative-content" ) 396 397headers = {"x-tool-cwd" :str (workdir )} 398 399# relative paths in file tools resolve against the header cwd 400res = call_tool ("read_file" , {"path" :"rel.txt" },headers = headers ) 401assert "relative-content" in res ["plain_text_response" ] 402 403res = call_tool ("write_file" , {"path" :"sub/out.txt" ,"content" :"written" },headers = headers ) 404assert (workdir / "sub" / "out.txt" ).read_text ()== "written" 405 406res = call_tool ("file_glob_search" , {"path" :"." ,"include" :"*.txt" },headers = headers ) 407assert "rel.txt" in res ["plain_text_response" ] 408 409# absolute paths are unaffected by the cwd 410other = tmp_path / "other" 411other .mkdir () 412 (other / "abs.txt" ).write_text ("absolute-content" ) 413res = call_tool ("read_file" , {"path" :str (other / "abs.txt" )},headers = headers ) 414assert "absolute-content" in res ["plain_text_response" ]