Ensure functions have docstrings for documentation
def sandbox(self) -> Iterator[SandboxBackendProtocol]:
1"""Integration tests for the deepagents sandbox backend abstraction.23Implementers should subclass this test suite and provide a fixture that returns a4clean `SandboxBackendProtocol` instance.56Example:7```python8from __future__ import annotations910from collections.abc import Iterator1112import pytest13from deepagents.backends.protocol import SandboxBackendProtocol14from langchain_tests.integration_tests import SandboxIntegrationTests1516from my_pkg import make_sandbox171819class TestMySandboxStandard(SandboxIntegrationTests):20 @pytest.fixture(scope="class")21 def sandbox(self) -> Iterator[SandboxBackendProtocol]:22 backend = make_sandbox()23 try:24 yield backend25 finally:26 backend.delete()27```2829"""3031# ruff: noqa: E402, S1083233from __future__ import annotations3435import asyncio36import base6437import shlex38import sys39from abc import abstractmethod40from typing import TYPE_CHECKING4142import pytest4344deepagents = pytest.importorskip("deepagents")4546from deepagents.backends.protocol import (47 ExecuteResponse,48 FileDownloadResponse,49 FileUploadResponse,50 ReadResult,51 SandboxBackendProtocol,52)5354from langchain_tests.base import BaseStandardTests5556if TYPE_CHECKING:57 from collections.abc import Iterator585960def _quote(path: str) -> str:61 return shlex.quote(path)626364class SandboxIntegrationTests(BaseStandardTests):65 """Standard integration tests for a `SandboxBackendProtocol` implementation."""6667 @property68 def sandbox_root_dir(self) -> str:69 """Base directory used by sandbox file-operation tests."""70 return "/tmp/test_sandbox_ops/"7172 def sandbox_path(self, relative_path: str, *, root_dir: str | None = None) -> str:73 """Build a path under the configured sandbox test directory."""74 root = root_dir or self.sandbox_root_dir75 return f"{root.rstrip('/')}/{relative_path.lstrip('/')}"7677 @pytest.fixture(scope="class")78 def sandbox_backend(79 self, sandbox: SandboxBackendProtocol80 ) -> SandboxBackendProtocol:81 """Provide the sandbox backend under test.8283 Resets the shared test directory before yielding.84 """85 return sandbox8687 @abstractmethod88 @pytest.fixture(scope="class")89 def sandbox(self) -> Iterator[SandboxBackendProtocol]:90 """Yield a clean sandbox backend and tear it down after the class."""9192 @property93 def has_sync(self) -> bool:94 """Whether the sandbox supports sync methods."""95 return True9697 @property98 def has_async(self) -> bool:99 """Whether the sandbox supports async methods."""100 return True101102 @pytest.fixture(autouse=True)103 def sandbox_test_root(self, request: pytest.FixtureRequest) -> str:104 """Create an isolated sandbox root directory for each test case."""105 if not self.has_sync:106 pytest.skip("Sync tests not supported.")107 node_name = request.node.name.replace("/", "_").replace(" ", "_")108 return self.sandbox_path(node_name)109110 def test_write_new_file(111 self, sandbox_backend: SandboxBackendProtocol, sandbox_test_root: str112 ) -> None:113 """Write a new file and verify it can be read back via command execution."""114 if not self.has_sync:115 pytest.skip("Sync tests not supported.")116 test_path = self.sandbox_path("new_file.txt", root_dir=sandbox_test_root)117 content = "Hello, sandbox!\nLine 2\nLine 3"118 result = sandbox_backend.write(test_path, content)119 assert result.error is None120 assert result.path == test_path121 exec_result = sandbox_backend.execute(f"cat {test_path}")122 assert exec_result.output.strip() == content123124 def test_read_basic_file(125 self, sandbox_backend: SandboxBackendProtocol, sandbox_test_root: str126 ) -> None:127 """Write a file and verify `read()` returns expected contents."""128 if not self.has_sync:129 pytest.skip("Sync tests not supported.")130 test_path = self.sandbox_path("read_test.txt", root_dir=sandbox_test_root)131 content = "Line 1\nLine 2\nLine 3"132 sandbox_backend.write(test_path, content)133 result = sandbox_backend.read(test_path)134 assert isinstance(result, ReadResult)135 assert result.error is None136 assert result.file_data is not None137 assert all(138 line in result.file_data["content"]139 for line in ("Line 1", "Line 2", "Line 3")140 )141142 def test_read_binary_file(143 self, sandbox_backend: SandboxBackendProtocol, sandbox_test_root: str144 ) -> None:145 """Upload a binary file and verify `read()` returns base64-encoded content."""146 if not self.has_sync:147 pytest.skip("Sync tests not supported.")148 test_path = self.sandbox_path("binary.png", root_dir=sandbox_test_root)149 raw_bytes = bytes(range(256))150 sandbox_backend.upload_files([(test_path, raw_bytes)])151 result = sandbox_backend.read(test_path)152 assert isinstance(result, ReadResult)153 assert result.error is None154 assert result.file_data is not None155 assert result.file_data["encoding"] == "base64"156 assert base64.b64decode(result.file_data["content"]) == raw_bytes157158 def test_read_binary_file_100_kib(159 self, sandbox_backend: SandboxBackendProtocol, sandbox_test_root: str160 ) -> None:161 """Read should return base64 content for a 100 KiB binary file."""162 if not self.has_sync:163 pytest.skip("Sync tests not supported.")164165 test_path = self.sandbox_path("binary_100kib.png", root_dir=sandbox_test_root)166 chunk = bytes(range(256))167 raw_bytes = chunk * 400168169 sandbox_backend.upload_files([(test_path, raw_bytes)])170 result = sandbox_backend.read(test_path)171172 assert isinstance(result, ReadResult)173 assert result.error is None174 assert result.file_data is not None175 assert result.file_data["encoding"] == "base64"176 assert base64.b64decode(result.file_data["content"]) == raw_bytes177178 def test_read_binary_file_1_mib_returns_error(179 self, sandbox_backend: SandboxBackendProtocol, sandbox_test_root: str180 ) -> None:181 """Read should error when a binary file exceeds the preview size limit."""182 if not self.has_sync:183 pytest.skip("Sync tests not supported.")184185 test_path = self.sandbox_path("binary_1mib.png", root_dir=sandbox_test_root)186 chunk = bytes(range(256))187 raw_bytes = chunk * 4096188189 sandbox_backend.upload_files([(test_path, raw_bytes)])190 result = sandbox_backend.read(test_path)191192 assert isinstance(result, ReadResult)193 assert result.file_data is None194 expected_error = (195 f"File '{test_path}': Binary file exceeds maximum preview size of "196 "512000 bytes"197 )198 assert result.error == expected_error199200 def test_execute_large_stdout_payload(201 self, sandbox_backend: SandboxBackendProtocol202 ) -> None:203 """Execute should handle a command that emits about 500 KiB of stdout."""204 if not self.has_sync:205 pytest.skip("Sync tests not supported.")206207 command = "python -c \"import sys; sys.stdout.write('x' * (500 * 1024))\""208 result = sandbox_backend.execute(command)209210 assert result.exit_code == 0211 assert result.truncated is False212 assert len(result.output) >= 500 * 1024213 assert result.output.startswith("x")214215 def test_edit_single_occurrence(216 self, sandbox_backend: SandboxBackendProtocol, sandbox_test_root: str217 ) -> None:218 """Edit a file and assert exactly one occurrence was replaced."""219 if not self.has_sync:220 pytest.skip("Sync tests not supported.")221 test_path = self.sandbox_path("edit_single.txt", root_dir=sandbox_test_root)222 content = "Hello world\nGoodbye world\nHello again"223 sandbox_backend.write(test_path, content)224 result = sandbox_backend.edit(test_path, "Goodbye", "Farewell")225 assert result.error is None226 assert result.occurrences == 1227 file_result = sandbox_backend.read(test_path)228 assert isinstance(file_result, ReadResult)229 assert file_result.error is None230 assert file_result.file_data is not None231 assert "Farewell world" in file_result.file_data["content"]232 assert "Goodbye" not in file_result.file_data["content"]233234 def test_ls_lists_files(235 self, sandbox_backend: SandboxBackendProtocol, sandbox_test_root: str236 ) -> None:237 """Create files and verify `ls()` lists them."""238 if not self.has_sync:239 pytest.skip("Sync tests not supported.")240 sandbox_backend.write(241 self.sandbox_path("a.txt", root_dir=sandbox_test_root), "a"242 )243 sandbox_backend.write(244 self.sandbox_path("b.txt", root_dir=sandbox_test_root), "b"245 )246 result = sandbox_backend.ls(sandbox_test_root)247 assert result.error is None248 assert result.entries is not None249 paths = sorted([i["path"] for i in result.entries])250 assert self.sandbox_path("a.txt", root_dir=sandbox_test_root) in paths251 assert self.sandbox_path("b.txt", root_dir=sandbox_test_root) in paths252253 def test_glob(254 self, sandbox_backend: SandboxBackendProtocol, sandbox_test_root: str255 ) -> None:256 """Create files and verify `glob()` returns expected matches."""257 if not self.has_sync:258 pytest.skip("Sync tests not supported.")259 sandbox_backend.write(260 self.sandbox_path("x.py", root_dir=sandbox_test_root), "print('x')"261 )262 sandbox_backend.write(263 self.sandbox_path("y.txt", root_dir=sandbox_test_root), "y"264 )265 result = sandbox_backend.glob("*.py", path=sandbox_test_root)266 assert result.error is None267 assert result.matches is not None268 assert [m["path"] for m in result.matches] == ["x.py"]269270 def test_grep_literal(271 self, sandbox_backend: SandboxBackendProtocol, sandbox_test_root: str272 ) -> None:273 """Verify `grep()` performs literal matching on special characters."""274 if not self.has_sync:275 pytest.skip("Sync tests not supported.")276 sandbox_backend.write(277 self.sandbox_path("grep.txt", root_dir=sandbox_test_root),278 "a (b)\nstr | int\n",279 )280 result = sandbox_backend.grep("str | int", path=sandbox_test_root)281 assert result.error is None282 assert result.matches is not None283 assert len(result.matches) > 0284 assert result.matches[0]["path"].endswith("/grep.txt")285 assert result.matches[0]["text"].strip() == "str | int"286287 def test_upload_single_file(288 self, sandbox_backend: SandboxBackendProtocol, sandbox_test_root: str289 ) -> None:290 """Upload one file and verify its contents on the sandbox."""291 if not self.has_sync:292 pytest.skip("Sync tests not supported.")293294 test_path = self.sandbox_path(295 "test_upload_single.txt", root_dir=sandbox_test_root296 )297 test_content = b"Hello, Sandbox!"298299 upload_responses = sandbox_backend.upload_files([(test_path, test_content)])300301 assert len(upload_responses) == 1302 assert upload_responses[0].path == test_path303 assert upload_responses[0].error is None304305 result = sandbox_backend.execute(f"cat {test_path}")306 assert result.output.strip() == test_content.decode()307308 def test_download_single_file(309 self, sandbox_backend: SandboxBackendProtocol, sandbox_test_root: str310 ) -> None:311 """Upload then download a file and verify bytes match."""312 if not self.has_sync:313 pytest.skip("Sync tests not supported.")314315 test_path = self.sandbox_path(316 "test_download_single.txt", root_dir=sandbox_test_root317 )318 test_content = b"Download test content"319320 sandbox_backend.upload_files([(test_path, test_content)])321322 download_responses = sandbox_backend.download_files([test_path])323324 assert len(download_responses) == 1325 assert download_responses[0].path == test_path326 assert download_responses[0].content == test_content327 assert download_responses[0].error is None328329 def test_upload_download_roundtrip(330 self, sandbox_backend: SandboxBackendProtocol, sandbox_test_root: str331 ) -> None:332 """Upload then download and verify bytes survive a roundtrip."""333 if not self.has_sync:334 pytest.skip("Sync tests not supported.")335336 test_path = self.sandbox_path("test_roundtrip.txt", root_dir=sandbox_test_root)337 test_content = b"Roundtrip test: special chars \n\t\r\x00"338339 upload_responses = sandbox_backend.upload_files([(test_path, test_content)])340 assert upload_responses == [FileUploadResponse(path=test_path, error=None)]341342 download_responses = sandbox_backend.download_files([test_path])343 assert download_responses == [344 FileDownloadResponse(path=test_path, content=test_content, error=None)345 ]346347 def test_upload_multiple_files_order_preserved(348 self,349 sandbox_backend: SandboxBackendProtocol,350 sandbox_test_root: str,351 ) -> None:352 """Uploading multiple files should preserve input order in responses."""353 if not self.has_sync:354 pytest.skip("Sync tests not supported.")355356 files = [357 (358 self.sandbox_path("test_multi_1.txt", root_dir=sandbox_test_root),359 b"Content 1",360 ),361 (362 self.sandbox_path("test_multi_2.txt", root_dir=sandbox_test_root),363 b"Content 2",364 ),365 (366 self.sandbox_path("test_multi_3.txt", root_dir=sandbox_test_root),367 b"Content 3",368 ),369 ]370371 upload_responses = sandbox_backend.upload_files(files)372373 assert upload_responses == [374 FileUploadResponse(path=files[0][0], error=None),375 FileUploadResponse(path=files[1][0], error=None),376 FileUploadResponse(path=files[2][0], error=None),377 ]378379 def test_download_multiple_files_order_preserved(380 self,381 sandbox_backend: SandboxBackendProtocol,382 sandbox_test_root: str,383 ) -> None:384 """Downloading multiple files should preserve input order in responses."""385 if not self.has_sync:386 pytest.skip("Sync tests not supported.")387388 files = [389 (390 self.sandbox_path("test_batch_1.txt", root_dir=sandbox_test_root),391 b"Batch 1",392 ),393 (394 self.sandbox_path("test_batch_2.txt", root_dir=sandbox_test_root),395 b"Batch 2",396 ),397 (398 self.sandbox_path("test_batch_3.txt", root_dir=sandbox_test_root),399 b"Batch 3",400 ),401 ]402 sandbox_backend.upload_files(files)403404 paths = [p for p, _ in files]405 download_responses = sandbox_backend.download_files(paths)406407 assert download_responses == [408 FileDownloadResponse(path=files[0][0], content=files[0][1], error=None),409 FileDownloadResponse(path=files[1][0], content=files[1][1], error=None),410 FileDownloadResponse(path=files[2][0], content=files[2][1], error=None),411 ]412413 def test_upload_binary_content_roundtrip(414 self, sandbox_backend: SandboxBackendProtocol, sandbox_test_root: str415 ) -> None:416 """Upload and download binary bytes (0..255) without corruption."""417 if not self.has_sync:418 pytest.skip("Sync tests not supported.")419420 test_path = self.sandbox_path("binary_file.bin", root_dir=sandbox_test_root)421 test_content = bytes(range(256))422423 upload_responses = sandbox_backend.upload_files([(test_path, test_content)])424 assert upload_responses == [FileUploadResponse(path=test_path, error=None)]425426 download_responses = sandbox_backend.download_files([test_path])427 assert download_responses == [428 FileDownloadResponse(path=test_path, content=test_content, error=None)429 ]430431 def test_upload_large_file_reports_expected_size(432 self, sandbox_backend: SandboxBackendProtocol, sandbox_test_root: str433 ) -> None:434 """Upload a ~10 MiB file, verify its size, then download it again."""435 if not self.has_sync:436 pytest.skip("Sync tests not supported.")437438 test_path = self.sandbox_path("large_upload.txt", root_dir=sandbox_test_root)439 chunk = b"0123456789abcdef" * 1024440 repeat_count = 640441 test_content = chunk * repeat_count442443 assert len(test_content) == 10 * 1024 * 1024444445 upload_responses = sandbox_backend.upload_files([(test_path, test_content)])446 assert upload_responses == [FileUploadResponse(path=test_path, error=None)]447448 exec_result = sandbox_backend.execute(f"wc -c {_quote(test_path)}")449 assert exec_result.exit_code == 0450 assert str(len(test_content)) in exec_result.output451452 download_responses = sandbox_backend.download_files([test_path])453 assert download_responses == [454 FileDownloadResponse(path=test_path, content=test_content, error=None)455 ]456457 def test_download_error_file_not_found(458 self, sandbox_backend: SandboxBackendProtocol, sandbox_test_root: str459 ) -> None:460 """Downloading a missing file should return `error="file_not_found"`."""461 if not self.has_sync:462 pytest.skip("Sync tests not supported.")463464 missing_path = self.sandbox_path(465 "nonexistent_test_file.txt", root_dir=sandbox_test_root466 )467468 responses = sandbox_backend.download_files([missing_path])469470 assert responses == [471 FileDownloadResponse(472 path=missing_path, content=None, error="file_not_found"473 )474 ]475476 def test_download_error_is_directory(477 self, sandbox_backend: SandboxBackendProtocol, sandbox_test_root: str478 ) -> None:479 """Downloading a directory should fail with a reasonable error code."""480 if not self.has_sync:481 pytest.skip("Sync tests not supported.")482483 dir_path = self.sandbox_path("test_directory", root_dir=sandbox_test_root)484 sandbox_backend.execute(f"rm -rf {dir_path} && mkdir -p {dir_path}")485486 responses = sandbox_backend.download_files([dir_path])487488 assert len(responses) == 1489 assert responses[0].path == dir_path490 assert responses[0].content is None491 assert responses[0].error in {"is_directory", "file_not_found", "invalid_path"}492493 def test_download_error_permission_denied(494 self, sandbox_backend: SandboxBackendProtocol, sandbox_test_root: str495 ) -> None:496 """Downloading a chmod 000 file should fail with a reasonable error code."""497 if not self.has_sync:498 pytest.skip("Sync tests not supported.")499500 test_path = self.sandbox_path("test_no_read.txt", root_dir=sandbox_test_root)501 sandbox_backend.execute(502 f"rm -f {test_path} && echo secret > {test_path} && chmod 000 {test_path}"503 )504505 try:506 responses = sandbox_backend.download_files([test_path])507 finally:508 sandbox_backend.execute(f"chmod 644 {test_path} || true")509510 assert len(responses) == 1511 assert responses[0].path == test_path512 assert responses[0].content is None513 assert responses[0].error in {514 "permission_denied",515 "file_not_found",516 "invalid_path",517 }518519 def test_download_error_invalid_path_relative(520 self,521 sandbox_backend: SandboxBackendProtocol,522 ) -> None:523 """Downloading a relative path should fail with `error="invalid_path"`."""524 if not self.has_sync:525 pytest.skip("Sync tests not supported.")526527 responses = sandbox_backend.download_files(["relative/path.txt"])528529 assert responses == [530 FileDownloadResponse(531 path="relative/path.txt",532 content=None,533 error="invalid_path",534 )535 ]536537 def test_upload_missing_parent_dir_or_roundtrip(538 self,539 sandbox_backend: SandboxBackendProtocol,540 sandbox_test_root: str,541 ) -> None:542 """Uploading into a missing parent dir should error or roundtrip.543544 Some sandboxes auto-create parent directories; others return an error.545 """546 if not self.has_sync:547 pytest.skip("Sync tests not supported.")548549 dir_path = self.sandbox_path(550 "test_upload_missing_parent_dir", root_dir=sandbox_test_root551 )552 path = f"{dir_path}/deepagents_test_upload.txt"553 content = b"nope"554 sandbox_backend.execute(f"rm -rf {dir_path}")555556 responses = sandbox_backend.upload_files([(path, content)])557 assert len(responses) == 1558 assert responses[0].path == path559560 if responses[0].error is not None:561 assert responses[0].error in {562 "invalid_path",563 "permission_denied",564 "file_not_found",565 }566 return567568 download = sandbox_backend.download_files([path])569 assert download == [570 FileDownloadResponse(path=path, content=content, error=None)571 ]572573 def test_upload_relative_path_returns_invalid_path(574 self,575 sandbox_backend: SandboxBackendProtocol,576 ) -> None:577 """Uploading to a relative path should fail with `error="invalid_path"`."""578 if not self.has_sync:579 pytest.skip("Sync tests not supported.")580581 path = "relative_upload.txt"582 content = b"nope"583 responses = sandbox_backend.upload_files([(path, content)])584585 assert responses == [FileUploadResponse(path=path, error="invalid_path")]586587 def test_write_creates_parent_dirs(588 self, sandbox_backend: SandboxBackendProtocol, sandbox_test_root: str589 ) -> None:590 """Writing into a missing nested directory should succeed."""591 if not self.has_sync:592 pytest.skip("Sync tests not supported.")593594 test_path = self.sandbox_path(595 "deep/nested/dir/file.txt", root_dir=sandbox_test_root596 )597 content = "Nested file content"598599 result = sandbox_backend.write(test_path, content)600601 assert result.error is None602 assert result.path == test_path603 exec_result = sandbox_backend.execute(f"cat {_quote(test_path)}")604 assert exec_result.output.strip() == content605606 def test_write_existing_file_fails(607 self, sandbox_backend: SandboxBackendProtocol, sandbox_test_root: str608 ) -> None:609 """Writing to an existing file should return an error without overwriting."""610 if not self.has_sync:611 pytest.skip("Sync tests not supported.")612613 test_path = self.sandbox_path("existing.txt", root_dir=sandbox_test_root)614 sandbox_backend.write(test_path, "First content")615616 result = sandbox_backend.write(test_path, "Second content")617618 assert result.error is not None619 assert "already exists" in result.error.lower()620 exec_result = sandbox_backend.execute(f"cat {_quote(test_path)}")621 assert exec_result.output.strip() == "First content"622623 def test_write_special_characters(624 self, sandbox_backend: SandboxBackendProtocol, sandbox_test_root: str625 ) -> None:626 """Writing should preserve shell-sensitive characters exactly."""627 if not self.has_sync:628 pytest.skip("Sync tests not supported.")629630 test_path = self.sandbox_path("special.txt", root_dir=sandbox_test_root)631 content = (632 "Special chars: $VAR, `command`, $(subshell), 'quotes', \"quotes\"\n"633 "Tab\there\n"634 "Backslash: \\\\"635 )636637 result = sandbox_backend.write(test_path, content)638639 assert result.error is None640 exec_result = sandbox_backend.execute(f"cat {_quote(test_path)}")641 assert exec_result.output.strip() == content642643 def test_write_empty_file(644 self, sandbox_backend: SandboxBackendProtocol, sandbox_test_root: str645 ) -> None:646 """Writing empty content should still create the file."""647 if not self.has_sync:648 pytest.skip("Sync tests not supported.")649650 test_path = self.sandbox_path("empty.txt", root_dir=sandbox_test_root)651652 result = sandbox_backend.write(test_path, "")653654 assert result.error is None655 exec_result = sandbox_backend.execute(656 f"[ -f {_quote(test_path)} ] && echo exists || echo missing"657 )658 assert "exists" in exec_result.output659660 def test_write_path_with_spaces(661 self, sandbox_backend: SandboxBackendProtocol, sandbox_test_root: str662 ) -> None:663 """Writing should support file paths containing spaces."""664 if not self.has_sync:665 pytest.skip("Sync tests not supported.")666667 test_path = self.sandbox_path(668 "dir with spaces/file name.txt", root_dir=sandbox_test_root669 )670 content = "Content in file with spaces"671672 result = sandbox_backend.write(test_path, content)673674 assert result.error is None675 exec_result = sandbox_backend.execute(f"cat {_quote(test_path)}")676 assert exec_result.output.strip() == content677678 def test_write_unicode_content(679 self, sandbox_backend: SandboxBackendProtocol, sandbox_test_root: str680 ) -> None:681 """Writing should preserve unicode content."""682 if not self.has_sync:683 pytest.skip("Sync tests not supported.")684685 test_path = self.sandbox_path("unicode.txt", root_dir=sandbox_test_root)686 content = "Hello 👋 世界 مرحبا Привет 🌍\nLine with émojis 🎉"687688 result = sandbox_backend.write(test_path, content)689690 assert result.error is None691 exec_result = sandbox_backend.execute(f"cat {_quote(test_path)}")692 assert exec_result.output.strip() == content693694 def test_write_consecutive_slashes_in_path(695 self, sandbox_backend: SandboxBackendProtocol, sandbox_test_root: str696 ) -> None:697 """Writing should tolerate normalized paths with consecutive slashes."""698 if not self.has_sync:699 pytest.skip("Sync tests not supported.")700701 test_path = self.sandbox_path("file.txt", root_dir=sandbox_test_root)702 content = "Content"703704 result = sandbox_backend.write(test_path, content)705706 assert result.error is None707 exec_result = sandbox_backend.execute(f"cat {_quote(test_path)}")708 assert exec_result.output.strip() == content709710 def test_write_very_long_content(711 self, sandbox_backend: SandboxBackendProtocol, sandbox_test_root: str712 ) -> None:713 """Writing moderately long multi-line content should succeed."""714 if not self.has_sync:715 pytest.skip("Sync tests not supported.")716717 test_path = self.sandbox_path("very_long.txt", root_dir=sandbox_test_root)718 content = "\n".join([f"Line {i} with some content here" for i in range(1000)])719720 result = sandbox_backend.write(test_path, content)721722 assert result.error is None723 read_result = sandbox_backend.read(test_path)724 assert read_result.error is None725 assert read_result.file_data is not None726 assert "Line 0 with some content here" in read_result.file_data["content"]727728 def test_write_content_with_only_newlines(729 self, sandbox_backend: SandboxBackendProtocol, sandbox_test_root: str730 ) -> None:731 """Writing newline-only content should preserve the newline count."""732 if not self.has_sync:733 pytest.skip("Sync tests not supported.")734735 test_path = self.sandbox_path("only_newlines.txt", root_dir=sandbox_test_root)736 content = "\n\n\n\n\n"737738 result = sandbox_backend.write(test_path, content)739740 assert result.error is None741 exec_result = sandbox_backend.execute(f"wc -l {_quote(test_path)}")742 assert "5" in exec_result.output743744 def test_read_nonexistent_file(745 self, sandbox_backend: SandboxBackendProtocol, sandbox_test_root: str746 ) -> None:747 """Reading a missing file should return a file-not-found style error."""748 if not self.has_sync:749 pytest.skip("Sync tests not supported.")750751 result = sandbox_backend.read(752 self.sandbox_path("nonexistent.txt", root_dir=sandbox_test_root)753 )754755 assert result.error is not None756 assert (757 "not_found" in result.error.lower() or "not found" in result.error.lower()758 )759760 def test_read_empty_file(761 self, sandbox_backend: SandboxBackendProtocol, sandbox_test_root: str762 ) -> None:763 """Reading an empty file should succeed with empty-or-empty-notice content."""764 if not self.has_sync:765 pytest.skip("Sync tests not supported.")766767 test_path = self.sandbox_path("empty_read.txt", root_dir=sandbox_test_root)768 sandbox_backend.write(test_path, "")769770 result = sandbox_backend.read(test_path)771772 assert result.error is None773 assert result.file_data is not None774 content = result.file_data["content"]775 assert "empty" in content.lower() or content.strip() == ""776777 def test_read_with_offset(778 self, sandbox_backend: SandboxBackendProtocol, sandbox_test_root: str779 ) -> None:780 """Reading with offset should skip the requested number of lines."""781 if not self.has_sync:782 pytest.skip("Sync tests not supported.")783784 test_path = self.sandbox_path("offset_test.txt", root_dir=sandbox_test_root)785 content = "\n".join([f"Row_{i}_content" for i in range(1, 11)])786 sandbox_backend.write(test_path, content)787788 result = sandbox_backend.read(test_path, offset=5)789790 assert result.error is None791 assert result.file_data is not None792 assert "Row_6_content" in result.file_data["content"]793 assert "Row_1_content" not in result.file_data["content"]794795 def test_read_with_limit(796 self, sandbox_backend: SandboxBackendProtocol, sandbox_test_root: str797 ) -> None:798 """Reading with limit should cap the number of returned lines."""799 if not self.has_sync:800 pytest.skip("Sync tests not supported.")801802 test_path = self.sandbox_path("limit_test.txt", root_dir=sandbox_test_root)803 content = "\n".join([f"Row_{i}_content" for i in range(1, 101)])804 sandbox_backend.write(test_path, content)805806 result = sandbox_backend.read(test_path, offset=0, limit=5)807808 assert result.error is None809 assert result.file_data is not None810 assert "Row_1_content" in result.file_data["content"]811 assert "Row_5_content" in result.file_data["content"]812 assert "Row_6_content" not in result.file_data["content"]813814 def test_read_with_offset_and_limit(815 self, sandbox_backend: SandboxBackendProtocol, sandbox_test_root: str816 ) -> None:817 """Reading with offset and limit should return the expected slice."""818 if not self.has_sync:819 pytest.skip("Sync tests not supported.")820821 test_path = self.sandbox_path(822 "offset_limit_test.txt", root_dir=sandbox_test_root823 )824 content = "\n".join([f"Row_{i}_content" for i in range(1, 21)])825 sandbox_backend.write(test_path, content)826827 result = sandbox_backend.read(test_path, offset=10, limit=5)828829 assert result.error is None830 assert result.file_data is not None831 assert "Row_11_content" in result.file_data["content"]832 assert "Row_15_content" in result.file_data["content"]833 assert "Row_10_content" not in result.file_data["content"]834 assert "Row_16_content" not in result.file_data["content"]835836 def test_read_unicode_content(837 self, sandbox_backend: SandboxBackendProtocol, sandbox_test_root: str838 ) -> None:839 """Reading unicode content should preserve non-ASCII text."""840 if not self.has_sync:841 pytest.skip("Sync tests not supported.")842843 test_path = self.sandbox_path("unicode_read.txt", root_dir=sandbox_test_root)844 content = "Hello 👋 世界\nПривет мир\nمرحبا العالم" # noqa: RUF001845 sandbox_backend.write(test_path, content)846847 result = sandbox_backend.read(test_path)848849 assert result.error is None850 assert result.file_data is not None851 assert "👋" in result.file_data["content"]852 assert "世界" in result.file_data["content"]853 assert "Привет" in result.file_data["content"]854855 def test_read_file_with_very_long_lines(856 self, sandbox_backend: SandboxBackendProtocol, sandbox_test_root: str857 ) -> None:858 """Reading files with long lines should still succeed."""859 if not self.has_sync:860 pytest.skip("Sync tests not supported.")861862 test_path = self.sandbox_path("long_lines.txt", root_dir=sandbox_test_root)863 long_line = "x" * 3000864 content = f"Short line\n{long_line}\nAnother short line"865 sandbox_backend.write(test_path, content)866867 result = sandbox_backend.read(test_path)868869 assert result.error is None870 assert result.file_data is not None871 assert "Short line" in result.file_data["content"]872873 def test_read_with_zero_limit(874 self, sandbox_backend: SandboxBackendProtocol, sandbox_test_root: str875 ) -> None:876 """Reading with `limit=0` should not include file content."""877 if not self.has_sync:878 pytest.skip("Sync tests not supported.")879880 test_path = self.sandbox_path("zero_limit.txt", root_dir=sandbox_test_root)881 sandbox_backend.write(test_path, "Line 1\nLine 2\nLine 3")882883 result = sandbox_backend.read(test_path, offset=0, limit=0)884885 content = result.file_data["content"] if result.file_data else ""886 assert "Line 1" not in content or content.strip() == ""887888 def test_read_offset_beyond_file_length(889 self, sandbox_backend: SandboxBackendProtocol, sandbox_test_root: str890 ) -> None:891 """Reading beyond EOF should return no file lines."""892 if not self.has_sync:893 pytest.skip("Sync tests not supported.")894895 test_path = self.sandbox_path("offset_beyond.txt", root_dir=sandbox_test_root)896 sandbox_backend.write(test_path, "Line 1\nLine 2\nLine 3")897898 result = sandbox_backend.read(test_path, offset=100, limit=10)899900 content = result.file_data["content"] if result.file_data else ""901 error = result.error or ""902 assert "Line 1" not in content903 assert "Line 1" not in error904 assert "Line 2" not in content905 assert "Line 2" not in error906 assert "Line 3" not in content907 assert "Line 3" not in error908909 def test_read_offset_at_exact_file_length(910 self, sandbox_backend: SandboxBackendProtocol, sandbox_test_root: str911 ) -> None:912 """Reading exactly at EOF should return no file lines."""913 if not self.has_sync:914 pytest.skip("Sync tests not supported.")915916 test_path = self.sandbox_path("offset_exact.txt", root_dir=sandbox_test_root)917 content = "\n".join([f"Line {i}" for i in range(1, 6)])918 sandbox_backend.write(test_path, content)919920 result = sandbox_backend.read(test_path, offset=5, limit=10)921922 text = result.file_data["content"] if result.file_data else ""923 error = result.error or ""924 assert "Line 1" not in text925 assert "Line 1" not in error926 assert "Line 5" not in text927 assert "Line 5" not in error928929 def test_read_very_large_file_in_chunks(930 self, sandbox_backend: SandboxBackendProtocol, sandbox_test_root: str931 ) -> None:932 """Repeated offset+limit reads should cover different slices of a large file."""933 if not self.has_sync:934 pytest.skip("Sync tests not supported.")935936 test_path = self.sandbox_path("large_chunked.txt", root_dir=sandbox_test_root)937 content = "\n".join([f"Line_{i:04d}_content" for i in range(1000)])938 sandbox_backend.write(test_path, content)939940 first = sandbox_backend.read(test_path, offset=0, limit=100)941 middle = sandbox_backend.read(test_path, offset=500, limit=100)942 last = sandbox_backend.read(test_path, offset=900, limit=100)943944 assert first.error is None945 assert first.file_data is not None946 assert "Line_0000_content" in first.file_data["content"]947 assert "Line_0099_content" in first.file_data["content"]948 assert "Line_0100_content" not in first.file_data["content"]949950 assert middle.error is None951 assert middle.file_data is not None952 assert "Line_0500_content" in middle.file_data["content"]953 assert "Line_0599_content" in middle.file_data["content"]954 assert "Line_0499_content" not in middle.file_data["content"]955956 assert last.error is None957 assert last.file_data is not None958 assert "Line_0900_content" in last.file_data["content"]959 assert "Line_0999_content" in last.file_data["content"]960961 def test_edit_multiple_occurrences_without_replace_all(962 self, sandbox_backend: SandboxBackendProtocol, sandbox_test_root: str963 ) -> None:964 """Editing multiple matches without `replace_all` should fail."""965 if not self.has_sync:966 pytest.skip("Sync tests not supported.")967968 test_path = self.sandbox_path("edit_multi.txt", root_dir=sandbox_test_root)969 content = "apple\nbanana\napple\norange\napple"970 sandbox_backend.write(test_path, content)971972 result = sandbox_backend.edit(test_path, "apple", "pear", replace_all=False)973974 assert result.error is not None975 assert "multiple" in result.error.lower()976 read_result = sandbox_backend.read(test_path)977 assert read_result.error is None978 assert read_result.file_data is not None979 assert "apple" in read_result.file_data["content"]980 assert "pear" not in read_result.file_data["content"]981982 def test_edit_multiple_occurrences_with_replace_all(983 self, sandbox_backend: SandboxBackendProtocol, sandbox_test_root: str984 ) -> None:985 """Editing multiple matches with `replace_all` should replace each match."""986 if not self.has_sync:987 pytest.skip("Sync tests not supported.")988989 test_path = self.sandbox_path(990 "edit_replace_all.txt", root_dir=sandbox_test_root991 )992 content = "apple\nbanana\napple\norange\napple"993 sandbox_backend.write(test_path, content)994995 result = sandbox_backend.edit(test_path, "apple", "pear", replace_all=True)996997 assert result.error is None998 assert result.occurrences == 3999 read_result = sandbox_backend.read(test_path)1000 assert read_result.error is None1001 assert read_result.file_data is not None1002 assert "apple" not in read_result.file_data["content"]1003 assert read_result.file_data["content"].count("pear") == 310041005 def test_edit_string_not_found(1006 self, sandbox_backend: SandboxBackendProtocol, sandbox_test_root: str1007 ) -> None:1008 """Editing a missing string should return a not-found style error."""1009 if not self.has_sync:1010 pytest.skip("Sync tests not supported.")10111012 test_path = self.sandbox_path("edit_not_found.txt", root_dir=sandbox_test_root)1013 sandbox_backend.write(test_path, "Hello world")10141015 result = sandbox_backend.edit(test_path, "nonexistent", "replacement")10161017 assert result.error is not None1018 assert "not found" in result.error.lower()10191020 def test_edit_nonexistent_file(1021 self, sandbox_backend: SandboxBackendProtocol, sandbox_test_root: str1022 ) -> None:1023 """Editing a missing file should return a file-not-found style error."""1024 if not self.has_sync:1025 pytest.skip("Sync tests not supported.")10261027 result = sandbox_backend.edit(1028 self.sandbox_path("nonexistent_edit.txt", root_dir=sandbox_test_root),1029 "old",1030 "new",1031 )10321033 assert result.error is not None1034 assert (1035 "not_found" in result.error.lower() or "not found" in result.error.lower()1036 )10371038 def test_edit_special_characters(1039 self, sandbox_backend: SandboxBackendProtocol, sandbox_test_root: str1040 ) -> None:1041 """Editing should treat special characters as literal strings."""1042 if not self.has_sync:1043 pytest.skip("Sync tests not supported.")10441045 test_path = self.sandbox_path("edit_special.txt", root_dir=sandbox_test_root)1046 content = "Price: $100.00\nPattern: [a-z]*\nPath: /usr/bin"1047 sandbox_backend.write(test_path, content)10481049 first = sandbox_backend.edit(test_path, "$100.00", "$200.00")1050 second = sandbox_backend.edit(test_path, "[a-z]*", "[0-9]+")10511052 assert first.error is None1053 assert second.error is None1054 read_result = sandbox_backend.read(test_path)1055 assert read_result.error is None1056 assert read_result.file_data is not None1057 assert "$200.00" in read_result.file_data["content"]1058 assert "[0-9]+" in read_result.file_data["content"]10591060 def test_edit_multiline_support(1061 self, sandbox_backend: SandboxBackendProtocol, sandbox_test_root: str1062 ) -> None:1063 """Editing should support replacing multi-line strings."""1064 if not self.has_sync:1065 pytest.skip("Sync tests not supported.")10661067 test_path = self.sandbox_path("edit_multiline.txt", root_dir=sandbox_test_root)1068 sandbox_backend.write(test_path, "Line 1\nLine 2\nLine 3")10691070 result = sandbox_backend.edit(test_path, "Line 1\nLine 2", "Combined")10711072 assert result.error is None1073 assert result.occurrences == 11074 read_result = sandbox_backend.read(test_path)1075 assert read_result.error is None1076 assert read_result.file_data is not None1077 assert "Combined" in read_result.file_data["content"]10781079 def test_ls_lists_nested_directories(1080 self, sandbox_backend: SandboxBackendProtocol, sandbox_test_root: str1081 ) -> None:1082 """Listing should include nested directories and immediate child files."""1083 if not self.has_sync:1084 pytest.skip("Sync tests not supported.")10851086 base_dir = self.sandbox_path("ls_nested", root_dir=sandbox_test_root)1087 sandbox_backend.execute(1088 f"mkdir -p {_quote(base_dir)}/subdir && touch {_quote(base_dir)}/root.txt"1089 )10901091 result = sandbox_backend.ls(base_dir)10921093 assert result.error is None1094 assert result.entries is not None1095 paths = [entry["path"] for entry in result.entries]1096 assert f"{base_dir}/subdir" in paths1097 assert f"{base_dir}/root.txt" in paths10981099 def test_ls_unicode_filenames(1100 self, sandbox_backend: SandboxBackendProtocol, sandbox_test_root: str1101 ) -> None:1102 """Listing should preserve unicode filenames."""1103 if not self.has_sync:1104 pytest.skip("Sync tests not supported.")11051106 base_dir = self.sandbox_path("ls_unicode", root_dir=sandbox_test_root)1107 sandbox_backend.execute(f"mkdir -p {_quote(base_dir)}")1108 sandbox_backend.write(f"{base_dir}/测试文件.txt", "content")1109 sandbox_backend.write(f"{base_dir}/файл.txt", "content")11101111 result = sandbox_backend.ls(base_dir)11121113 assert result.error is None1114 assert result.entries is not None1115 paths = [entry["path"] for entry in result.entries]1116 assert f"{base_dir}/测试文件.txt" in paths1117 assert f"{base_dir}/файл.txt" in paths11181119 def test_ls_large_directory(1120 self, sandbox_backend: SandboxBackendProtocol, sandbox_test_root: str1121 ) -> None:1122 """Listing a larger directory should include all created entries."""1123 if not self.has_sync:1124 pytest.skip("Sync tests not supported.")11251126 base_dir = self.sandbox_path("ls_large", root_dir=sandbox_test_root)1127 sandbox_backend.execute(1128 f"mkdir -p {_quote(base_dir)} && "1129 f"cd {_quote(base_dir)} && "1130 "for i in $(seq 0 49); do "1131 "echo content > file_$(printf '%03d' $i).txt; "1132 "done"1133 )11341135 result = sandbox_backend.ls(base_dir)11361137 assert result.error is None1138 assert result.entries is not None1139 assert len(result.entries) == 501140 paths = [entry["path"] for entry in result.entries]1141 assert f"{base_dir}/file_000.txt" in paths1142 assert f"{base_dir}/file_049.txt" in paths11431144 def test_ls_path_with_trailing_slash(1145 self, sandbox_backend: SandboxBackendProtocol, sandbox_test_root: str1146 ) -> None:1147 """Listing a path with a trailing slash should match the normalized path."""1148 if not self.has_sync:1149 pytest.skip("Sync tests not supported.")11501151 base_dir = self.sandbox_path("ls_trailing", root_dir=sandbox_test_root)1152 sandbox_backend.execute(f"mkdir -p {_quote(base_dir)}")1153 sandbox_backend.write(f"{base_dir}/file.txt", "content")11541155 result = sandbox_backend.ls(f"{base_dir}/")11561157 assert result.error is None1158 assert result.entries is not None1159 paths = [entry["path"] for entry in result.entries]1160 assert f"{base_dir}/file.txt" in paths11611162 def test_ls_special_characters_in_filenames(1163 self, sandbox_backend: SandboxBackendProtocol, sandbox_test_root: str1164 ) -> None:1165 """Listing should preserve filenames with shell metacharacters."""1166 if not self.has_sync:1167 pytest.skip("Sync tests not supported.")11681169 base_dir = self.sandbox_path("ls_special", root_dir=sandbox_test_root)1170 sandbox_backend.execute(f"mkdir -p {_quote(base_dir)}")1171 sandbox_backend.write(f"{base_dir}/file(1).txt", "content")1172 sandbox_backend.write(f"{base_dir}/file[2].txt", "content")1173 sandbox_backend.write(f"{base_dir}/file-3.txt", "content")11741175 result = sandbox_backend.ls(base_dir)11761177 assert result.error is None1178 assert result.entries is not None1179 paths = [entry["path"] for entry in result.entries]1180 assert f"{base_dir}/file(1).txt" in paths1181 assert f"{base_dir}/file[2].txt" in paths1182 assert f"{base_dir}/file-3.txt" in paths11831184 def test_ls_path_is_sanitized(1185 self, sandbox_backend: SandboxBackendProtocol1186 ) -> None:1187 """Listing an injected path should not execute attacker-controlled code."""1188 if not self.has_sync:1189 pytest.skip("Sync tests not supported.")11901191 malicious_path = "'; import os; os.system('echo INJECTED'); #"1192 result = sandbox_backend.ls(malicious_path)11931194 assert result.error is not None or result.entries == []11951196 def test_read_path_is_sanitized(1197 self, sandbox_backend: SandboxBackendProtocol1198 ) -> None:1199 """Reading an injected path should return an error without executing it."""1200 if not self.has_sync:1201 pytest.skip("Sync tests not supported.")12021203 malicious_path = "'; import os; os.system('echo INJECTED'); #"1204 result = sandbox_backend.read(malicious_path)12051206 assert result.error is not None1207 assert result.file_data is None12081209 def test_grep_basic_search(1210 self, sandbox_backend: SandboxBackendProtocol, sandbox_test_root: str1211 ) -> None:1212 """Grep should return matches across multiple files."""1213 if not self.has_sync:1214 pytest.skip("Sync tests not supported.")12151216 base_dir = self.sandbox_path("grep_test", root_dir=sandbox_test_root)1217 sandbox_backend.execute(f"mkdir -p {_quote(base_dir)}")1218 sandbox_backend.write(f"{base_dir}/file1.txt", "Hello world\nGoodbye world")1219 sandbox_backend.write(f"{base_dir}/file2.txt", "Hello there\nGoodbye friend")12201221 result = sandbox_backend.grep("Hello", path=base_dir)12221223 assert result.error is None1224 assert result.matches is not None1225 assert len(result.matches) == 21226 paths = [match["path"] for match in result.matches]1227 assert any(path.endswith("file1.txt") for path in paths)1228 assert any(path.endswith("file2.txt") for path in paths)1229 assert all(match["line"] == 1 for match in result.matches)12301231 def test_grep_with_glob_pattern(1232 self, sandbox_backend: SandboxBackendProtocol, sandbox_test_root: str1233 ) -> None:1234 """Grep should honor the file glob filter."""1235 if not self.has_sync:1236 pytest.skip("Sync tests not supported.")12371238 base_dir = self.sandbox_path("grep_glob", root_dir=sandbox_test_root)1239 sandbox_backend.execute(f"mkdir -p {_quote(base_dir)}")1240 sandbox_backend.write(f"{base_dir}/test.txt", "pattern")1241 sandbox_backend.write(f"{base_dir}/test.py", "pattern")1242 sandbox_backend.write(f"{base_dir}/test.md", "pattern")12431244 result = sandbox_backend.grep("pattern", path=base_dir, glob="*.py")12451246 assert result.error is None1247 assert result.matches == [1248 {"path": f"{base_dir}/test.py", "line": 1, "text": "pattern"}1249 ]12501251 def test_grep_no_matches(1252 self, sandbox_backend: SandboxBackendProtocol, sandbox_test_root: str1253 ) -> None:1254 """Grep with no matches should return an empty match list."""1255 if not self.has_sync:1256 pytest.skip("Sync tests not supported.")12571258 base_dir = self.sandbox_path("grep_empty", root_dir=sandbox_test_root)1259 sandbox_backend.execute(f"mkdir -p {_quote(base_dir)}")1260 sandbox_backend.write(f"{base_dir}/file.txt", "Hello world")12611262 result = sandbox_backend.grep("nonexistent", path=base_dir)12631264 assert result.error is None1265 assert result.matches == []12661267 def test_grep_multiple_matches_per_file(1268 self, sandbox_backend: SandboxBackendProtocol, sandbox_test_root: str1269 ) -> None:1270 """Grep should report multiple matches from a single file with line numbers."""1271 if not self.has_sync:1272 pytest.skip("Sync tests not supported.")12731274 base_dir = self.sandbox_path("grep_multi", root_dir=sandbox_test_root)1275 sandbox_backend.execute(f"mkdir -p {_quote(base_dir)}")1276 sandbox_backend.write(1277 f"{base_dir}/fruits.txt", "apple\nbanana\napple\norange\napple"1278 )12791280 result = sandbox_backend.grep("apple", path=base_dir)12811282 assert result.error is None1283 assert result.matches is not None1284 assert len(result.matches) == 31285 assert [match["line"] for match in result.matches] == [1, 3, 5]12861287 def test_grep_literal_string_matching(1288 self, sandbox_backend: SandboxBackendProtocol, sandbox_test_root: str1289 ) -> None:1290 """Grep should treat the search pattern literally rather than as regex."""1291 if not self.has_sync:1292 pytest.skip("Sync tests not supported.")12931294 base_dir = self.sandbox_path("grep_literal", root_dir=sandbox_test_root)1295 sandbox_backend.execute(f"mkdir -p {_quote(base_dir)}")1296 sandbox_backend.write(f"{base_dir}/numbers.txt", "test123\ntest456\nabcdef")12971298 result = sandbox_backend.grep("test123", path=base_dir)12991300 assert result.error is None1301 assert result.matches is not None1302 assert len(result.matches) == 11303 assert "test123" in result.matches[0]["text"]13041305 def test_grep_unicode_pattern(1306 self, sandbox_backend: SandboxBackendProtocol, sandbox_test_root: str1307 ) -> None:1308 """Grep should match unicode patterns in unicode content."""1309 if not self.has_sync:1310 pytest.skip("Sync tests not supported.")13111312 base_dir = self.sandbox_path("grep_unicode", root_dir=sandbox_test_root)1313 sandbox_backend.execute(f"mkdir -p {_quote(base_dir)}")1314 sandbox_backend.write(1315 f"{base_dir}/unicode.txt",1316 "Hello 世界\nПривет мир\n测试 pattern", # noqa: RUF0011317 )13181319 result = sandbox_backend.grep("世界", path=base_dir)13201321 assert result.error is None1322 assert result.matches is not None1323 assert len(result.matches) == 11324 assert "世界" in result.matches[0]["text"]13251326 def test_grep_case_sensitivity(1327 self, sandbox_backend: SandboxBackendProtocol, sandbox_test_root: str1328 ) -> None:1329 """Grep should be case-sensitive by default."""1330 if not self.has_sync:1331 pytest.skip("Sync tests not supported.")13321333 base_dir = self.sandbox_path("grep_case", root_dir=sandbox_test_root)1334 sandbox_backend.execute(f"mkdir -p {_quote(base_dir)}")1335 sandbox_backend.write(f"{base_dir}/case.txt", "Hello\nhello\nHELLO")13361337 result = sandbox_backend.grep("Hello", path=base_dir)13381339 assert result.error is None1340 assert result.matches is not None1341 assert len(result.matches) == 11342 assert result.matches[0]["text"] == "Hello"13431344 def test_grep_with_special_characters(1345 self, sandbox_backend: SandboxBackendProtocol, sandbox_test_root: str1346 ) -> None:1347 """Grep should treat special characters in the pattern literally."""1348 if not self.has_sync:1349 pytest.skip("Sync tests not supported.")13501351 base_dir = self.sandbox_path("grep_special", root_dir=sandbox_test_root)1352 sandbox_backend.execute(f"mkdir -p {_quote(base_dir)}")1353 sandbox_backend.write(1354 f"{base_dir}/special.txt", "Price: $100\nPath: /usr/bin\nPattern: [a-z]*"1355 )13561357 dollar = sandbox_backend.grep("$100", path=base_dir)1358 brackets = sandbox_backend.grep("[a-z]*", path=base_dir)13591360 assert dollar.error is None1361 assert dollar.matches is not None1362 assert len(dollar.matches) == 11363 assert "$100" in dollar.matches[0]["text"]13641365 assert brackets.error is None1366 assert brackets.matches is not None1367 assert len(brackets.matches) == 11368 assert "[a-z]*" in brackets.matches[0]["text"]13691370 def test_grep_empty_directory(1371 self, sandbox_backend: SandboxBackendProtocol, sandbox_test_root: str1372 ) -> None:1373 """Grep in an empty directory should return no matches."""1374 if not self.has_sync:1375 pytest.skip("Sync tests not supported.")13761377 base_dir = self.sandbox_path("grep_empty_dir", root_dir=sandbox_test_root)1378 sandbox_backend.execute(f"mkdir -p {_quote(base_dir)}")13791380 result = sandbox_backend.grep("anything", path=base_dir)13811382 assert result.error is None1383 assert result.matches == []13841385 def test_grep_across_nested_directories(1386 self, sandbox_backend: SandboxBackendProtocol, sandbox_test_root: str1387 ) -> None:1388 """Grep should recurse into nested directories."""1389 if not self.has_sync:1390 pytest.skip("Sync tests not supported.")13911392 base_dir = self.sandbox_path("grep_nested", root_dir=sandbox_test_root)1393 sandbox_backend.execute(f"mkdir -p {_quote(base_dir)}/sub1/sub2")1394 sandbox_backend.write(f"{base_dir}/root.txt", "target here")1395 sandbox_backend.write(f"{base_dir}/sub1/level1.txt", "target here")1396 sandbox_backend.write(f"{base_dir}/sub1/sub2/level2.txt", "target here")13971398 result = sandbox_backend.grep("target", path=base_dir)13991400 assert result.error is None1401 assert result.matches is not None1402 assert len(result.matches) == 314031404 def test_grep_with_globstar_include_pattern(1405 self, sandbox_backend: SandboxBackendProtocol, sandbox_test_root: str1406 ) -> None:1407 """Grep with a glob filter should still find nested matching files."""1408 if not self.has_sync:1409 pytest.skip("Sync tests not supported.")14101411 base_dir = self.sandbox_path("grep_globstar", root_dir=sandbox_test_root)1412 sandbox_backend.execute(f"mkdir -p {_quote(base_dir)}/a/b")1413 sandbox_backend.write(f"{base_dir}/a/b/target.py", "needle")1414 sandbox_backend.write(f"{base_dir}/a/ignore.txt", "needle")14151416 result = sandbox_backend.grep("needle", path=base_dir, glob="*.py")14171418 assert result.error is None1419 assert result.matches == [1420 {"path": f"{base_dir}/a/b/target.py", "line": 1, "text": "needle"}1421 ]14221423 def test_grep_reports_correct_line_numbers(1424 self, sandbox_backend: SandboxBackendProtocol, sandbox_test_root: str1425 ) -> None:1426 """Grep should report the original file line number for a match."""1427 if not self.has_sync:1428 pytest.skip("Sync tests not supported.")14291430 base_dir = self.sandbox_path("grep_multiline", root_dir=sandbox_test_root)1431 sandbox_backend.execute(f"mkdir -p {_quote(base_dir)}")1432 content = "\n".join([f"Line {i}" for i in range(1, 101)])1433 sandbox_backend.write(f"{base_dir}/long.txt", content)14341435 result = sandbox_backend.grep("Line 50", path=base_dir)14361437 assert result.error is None1438 assert result.matches == [1439 {"path": f"{base_dir}/long.txt", "line": 50, "text": "Line 50"}1440 ]14411442 def test_glob_basic_pattern(1443 self, sandbox_backend: SandboxBackendProtocol, sandbox_test_root: str1444 ) -> None:1445 """Glob should match basic wildcard patterns."""1446 if not self.has_sync:1447 pytest.skip("Sync tests not supported.")14481449 base_dir = self.sandbox_path("glob_test", root_dir=sandbox_test_root)1450 sandbox_backend.execute(f"mkdir -p {_quote(base_dir)}")1451 sandbox_backend.write(f"{base_dir}/file1.txt", "content")1452 sandbox_backend.write(f"{base_dir}/file2.txt", "content")1453 sandbox_backend.write(f"{base_dir}/file3.py", "content")14541455 result = sandbox_backend.glob("*.txt", path=base_dir)14561457 assert result.error is None1458 assert result.matches is not None1459 paths = [info["path"] for info in result.matches]1460 assert len(paths) == 21461 assert "file1.txt" in paths1462 assert "file2.txt" in paths1463 assert not any(path.endswith(".py") for path in paths)14641465 def test_glob_recursive_pattern(1466 self, sandbox_backend: SandboxBackendProtocol, sandbox_test_root: str1467 ) -> None:1468 """Glob should support recursive patterns with `**`."""1469 if not self.has_sync:1470 pytest.skip("Sync tests not supported.")14711472 base_dir = self.sandbox_path("glob_recursive", root_dir=sandbox_test_root)1473 sandbox_backend.execute(1474 f"mkdir -p {_quote(base_dir)}/subdir1 {_quote(base_dir)}/subdir2"1475 )1476 sandbox_backend.write(f"{base_dir}/root.txt", "content")1477 sandbox_backend.write(f"{base_dir}/subdir1/nested1.txt", "content")1478 sandbox_backend.write(f"{base_dir}/subdir2/nested2.txt", "content")14791480 result = sandbox_backend.glob("**/*.txt", path=base_dir)14811482 assert result.error is None1483 assert result.matches is not None1484 paths = [info["path"] for info in result.matches]1485 assert any(path.endswith("nested1.txt") for path in paths)1486 assert any(path.endswith("nested2.txt") for path in paths)14871488 def test_glob_no_matches(1489 self, sandbox_backend: SandboxBackendProtocol, sandbox_test_root: str1490 ) -> None:1491 """Glob with no matches should return an empty match list."""1492 if not self.has_sync:1493 pytest.skip("Sync tests not supported.")14941495 base_dir = self.sandbox_path("glob_empty", root_dir=sandbox_test_root)1496 sandbox_backend.execute(f"mkdir -p {_quote(base_dir)}")1497 sandbox_backend.write(f"{base_dir}/file.txt", "content")14981499 result = sandbox_backend.glob("*.py", path=base_dir)15001501 assert result.error is None1502 assert result.matches == []15031504 def test_glob_with_directories(1505 self, sandbox_backend: SandboxBackendProtocol, sandbox_test_root: str1506 ) -> None:1507 """Glob should include directories and mark them with `is_dir`."""1508 if not self.has_sync:1509 pytest.skip("Sync tests not supported.")15101511 base_dir = self.sandbox_path("glob_dirs", root_dir=sandbox_test_root)1512 sandbox_backend.execute(1513 f"mkdir -p {_quote(base_dir)}/dir1 {_quote(base_dir)}/dir2"1514 )1515 sandbox_backend.write(f"{base_dir}/file.txt", "content")15161517 result = sandbox_backend.glob("*", path=base_dir)15181519 assert result.error is None1520 assert result.matches is not None1521 assert len(result.matches) == 31522 dir_count = sum(1 for info in result.matches if info["is_dir"])1523 file_count = sum(1 for info in result.matches if not info["is_dir"])1524 assert dir_count == 21525 assert file_count == 115261527 def test_glob_hidden_files_explicitly(1528 self, sandbox_backend: SandboxBackendProtocol, sandbox_test_root: str1529 ) -> None:1530 """Glob should match hidden files when the pattern explicitly requests them."""1531 if not self.has_sync:1532 pytest.skip("Sync tests not supported.")15331534 base_dir = self.sandbox_path("glob_hidden", root_dir=sandbox_test_root)1535 sandbox_backend.execute(f"mkdir -p {_quote(base_dir)}")1536 sandbox_backend.write(f"{base_dir}/.hidden1", "content")1537 sandbox_backend.write(f"{base_dir}/.hidden2", "content")1538 sandbox_backend.write(f"{base_dir}/visible.txt", "content")15391540 result = sandbox_backend.glob(".*", path=base_dir)15411542 assert result.error is None1543 assert result.matches is not None1544 paths = [info["path"] for info in result.matches]1545 assert ".hidden1" in paths or ".hidden2" in paths1546 assert not any(path == "visible.txt" for path in paths)15471548 def test_glob_with_character_class(1549 self, sandbox_backend: SandboxBackendProtocol, sandbox_test_root: str1550 ) -> None:1551 """Glob should support character classes in patterns."""1552 if not self.has_sync:1553 pytest.skip("Sync tests not supported.")15541555 base_dir = self.sandbox_path("glob_charclass", root_dir=sandbox_test_root)1556 sandbox_backend.execute(f"mkdir -p {_quote(base_dir)}")1557 sandbox_backend.write(f"{base_dir}/file1.txt", "content")1558 sandbox_backend.write(f"{base_dir}/file2.txt", "content")1559 sandbox_backend.write(f"{base_dir}/file3.txt", "content")1560 sandbox_backend.write(f"{base_dir}/fileA.txt", "content")15611562 result = sandbox_backend.glob("file[1-2].txt", path=base_dir)15631564 assert result.error is None1565 assert result.matches is not None1566 paths = [info["path"] for info in result.matches]1567 assert len(paths) == 21568 assert "file1.txt" in paths1569 assert "file2.txt" in paths1570 assert "file3.txt" not in paths1571 assert "fileA.txt" not in paths15721573 def test_glob_with_question_mark(1574 self, sandbox_backend: SandboxBackendProtocol, sandbox_test_root: str1575 ) -> None:1576 """Glob should support single-character wildcards."""1577 if not self.has_sync:1578 pytest.skip("Sync tests not supported.")15791580 base_dir = self.sandbox_path("glob_question", root_dir=sandbox_test_root)1581 sandbox_backend.execute(f"mkdir -p {_quote(base_dir)}")1582 sandbox_backend.write(f"{base_dir}/file1.txt", "content")1583 sandbox_backend.write(f"{base_dir}/file2.txt", "content")1584 sandbox_backend.write(f"{base_dir}/file10.txt", "content")15851586 result = sandbox_backend.glob("file?.txt", path=base_dir)15871588 assert result.error is None1589 assert result.matches is not None1590 paths = [info["path"] for info in result.matches]1591 assert len(paths) == 21592 assert "file1.txt" in paths1593 assert "file2.txt" in paths1594 assert "file10.txt" not in paths15951596 async def test_awrite_aread_large_text_payload(1597 self, sandbox_backend: SandboxBackendProtocol, sandbox_test_root: str1598 ) -> None:1599 """Async write should allow a large text file to be read back non-empty."""1600 if not self.has_async:1601 pytest.skip("Async tests not supported.")16021603 test_path = self.sandbox_path(1604 "large_async_text.txt", root_dir=sandbox_test_root1605 )1606 line = "0123456789abcdef" * 2561607 lines = [line for _ in range(2560)]1608 test_content = "\n".join(lines)16091610 write_result = await sandbox_backend.awrite(test_path, test_content)1611 assert write_result.error is None1612 assert write_result.path == test_path16131614 exec_result = await sandbox_backend.aexecute(f"wc -c {_quote(test_path)}")1615 assert exec_result.exit_code == 01616 assert str(len(test_content.encode("utf-8"))) in exec_result.output16171618 read_result = await sandbox_backend.aread(test_path)1619 assert isinstance(read_result, ReadResult)1620 assert read_result.error is None1621 assert read_result.file_data is not None1622 assert read_result.file_data["encoding"] == "utf-8"1623 assert read_result.file_data["content"].startswith(lines[0])16241625 async def test_aread_large_text_payload_paginated_roundtrip(1626 self, sandbox_backend: SandboxBackendProtocol, sandbox_test_root: str1627 ) -> None:1628 """Async paginated reads should reconstruct the full large text payload."""1629 if not self.has_async:1630 pytest.skip("Async tests not supported.")16311632 test_path = self.sandbox_path(1633 "large_async_chunked.txt", root_dir=sandbox_test_root1634 )1635 lines = [f"Line_{i:04d}_content" for i in range(2500)]1636 test_content = "\n".join(lines)16371638 write_result = await sandbox_backend.awrite(test_path, test_content)1639 assert write_result.error is None16401641 parts: list[str] = []1642 for offset in range(0, len(lines), 100):1643 page = await sandbox_backend.aread(test_path, offset=offset, limit=100)1644 assert page.error is None1645 assert page.file_data is not None1646 assert page.file_data["content"] == "\n".join(lines[offset : offset + 100])1647 parts.append(page.file_data["content"])16481649 assert "\n".join(parts) == test_content16501651 async def test_adownload_large_text_payload_roundtrip(1652 self, sandbox_backend: SandboxBackendProtocol, sandbox_test_root: str1653 ) -> None:1654 """Async download should preserve the full large text payload exactly."""1655 if not self.has_async:1656 pytest.skip("Async tests not supported.")16571658 test_path = self.sandbox_path(1659 "large_async_download.txt", root_dir=sandbox_test_root1660 )1661 line = "0123456789abcdef" * 2561662 lines = [line for _ in range(2560)]1663 test_content = "\n".join(lines)16641665 write_result = await sandbox_backend.awrite(test_path, test_content)1666 assert write_result.error is None16671668 download_responses = await sandbox_backend.adownload_files([test_path])1669 assert download_responses == [1670 FileDownloadResponse(1671 path=test_path,1672 content=test_content.encode("utf-8"),1673 error=None,1674 )1675 ]16761677 def test_write_read_download_large_text_with_escaped_content(1678 self, sandbox_backend: SandboxBackendProtocol, sandbox_test_root: str1679 ) -> None:1680 """Sync large-text roundtrips should preserve escaped and unicode content."""1681 if not self.has_sync:1682 pytest.skip("Sync tests not supported.")16831684 test_path = self.sandbox_path(1685 "large_sync_escaped.txt", root_dir=sandbox_test_root1686 )1687 line = (1688 "prefix\t\u2603\u4e16\u754c\u03c0\u22483.14159"1689 " | spaces preserved"1690 " | quotes ' \""1691 " | brackets [] {{}}"1692 " | shell $VAR `cmd` $(subshell)"1693 " | slash /tmp/path and backslash \\\\"1694 " | control-ish \\r \\n"1695 " | suffix"1696 )1697 lines = [f"{i:04d}:{line}" for i in range(2500)]1698 test_content = "\n".join(lines)16991700 write_result = sandbox_backend.write(test_path, test_content)1701 assert write_result.error is None17021703 pages: list[str] = []1704 for offset in range(0, len(lines), 100):1705 page = sandbox_backend.read(test_path, offset=offset, limit=100)1706 assert page.error is None1707 assert page.file_data is not None1708 assert page.file_data["content"] == "\n".join(lines[offset : offset + 100])1709 pages.append(page.file_data["content"])17101711 assert "\n".join(pages) == test_content17121713 download_responses = sandbox_backend.download_files([test_path])1714 assert download_responses == [1715 FileDownloadResponse(1716 path=test_path,1717 content=test_content.encode("utf-8"),1718 error=None,1719 )1720 ]17211722 async def test_awrite_aread_adownload_large_text_with_escaped_content(1723 self, sandbox_backend: SandboxBackendProtocol, sandbox_test_root: str1724 ) -> None:1725 """Async large-text roundtrips should preserve escaped and unicode content."""1726 if not self.has_async:1727 pytest.skip("Async tests not supported.")17281729 test_path = self.sandbox_path(1730 "large_async_escaped.txt", root_dir=sandbox_test_root1731 )1732 line = (1733 "prefix\t\u2603\u4e16\u754c\u03c0\u22483.14159"1734 " | spaces preserved"1735 " | quotes ' \""1736 " | brackets [] {{}}"1737 " | shell $VAR `cmd` $(subshell)"1738 " | slash /tmp/path and backslash \\\\"1739 " | control-ish \\r \\n"1740 " | suffix"1741 )1742 lines = [f"{i:04d}:{line}" for i in range(2500)]1743 test_content = "\n".join(lines)17441745 write_result = await sandbox_backend.awrite(test_path, test_content)1746 assert write_result.error is None17471748 pages: list[str] = []1749 for offset in range(0, len(lines), 100):1750 page = await sandbox_backend.aread(test_path, offset=offset, limit=100)1751 assert page.error is None1752 assert page.file_data is not None1753 assert page.file_data["content"] == "\n".join(lines[offset : offset + 100])1754 pages.append(page.file_data["content"])17551756 assert "\n".join(pages) == test_content17571758 download_responses = await sandbox_backend.adownload_files([test_path])1759 assert download_responses == [1760 FileDownloadResponse(1761 path=test_path,1762 content=test_content.encode("utf-8"),1763 error=None,1764 )1765 ]17661767 async def test_aread_binary_image_file(1768 self, sandbox_backend: SandboxBackendProtocol, sandbox_test_root: str1769 ) -> None:1770 """Async read should return base64-encoded content for a binary image file."""1771 if not self.has_async:1772 pytest.skip("Async tests not supported.")17731774 test_path = self.sandbox_path("async_binary.png", root_dir=sandbox_test_root)1775 raw_bytes = bytes(range(256))17761777 upload_responses = await sandbox_backend.aupload_files([(test_path, raw_bytes)])1778 assert upload_responses == [FileUploadResponse(path=test_path, error=None)]17791780 result = await sandbox_backend.aread(test_path)1781 assert isinstance(result, ReadResult)1782 assert result.error is None1783 assert result.file_data is not None1784 assert result.file_data["encoding"] == "base64"1785 assert base64.b64decode(result.file_data["content"]) == raw_bytes17861787 async def test_aread_binary_file_100_kib(1788 self, sandbox_backend: SandboxBackendProtocol, sandbox_test_root: str1789 ) -> None:1790 """Async read should return base64 content for a 100 KiB binary file."""1791 if not self.has_async:1792 pytest.skip("Async tests not supported.")17931794 test_path = self.sandbox_path(1795 "async_binary_100kib.png", root_dir=sandbox_test_root1796 )1797 chunk = bytes(range(256))1798 raw_bytes = chunk * 40017991800 upload_responses = await sandbox_backend.aupload_files([(test_path, raw_bytes)])1801 assert upload_responses == [FileUploadResponse(path=test_path, error=None)]18021803 result = await sandbox_backend.aread(test_path)1804 assert isinstance(result, ReadResult)1805 assert result.error is None1806 assert result.file_data is not None1807 assert result.file_data["encoding"] == "base64"1808 assert base64.b64decode(result.file_data["content"]) == raw_bytes18091810 async def test_aread_binary_file_1_mib_returns_error(1811 self, sandbox_backend: SandboxBackendProtocol, sandbox_test_root: str1812 ) -> None:1813 """Async read should error when a binary file exceeds the preview size limit."""1814 if not self.has_async:1815 pytest.skip("Async tests not supported.")18161817 test_path = self.sandbox_path(1818 "async_binary_1mib.png", root_dir=sandbox_test_root1819 )1820 chunk = bytes(range(256))1821 raw_bytes = chunk * 409618221823 upload_responses = await sandbox_backend.aupload_files([(test_path, raw_bytes)])1824 assert upload_responses == [FileUploadResponse(path=test_path, error=None)]18251826 result = await sandbox_backend.aread(test_path)1827 assert isinstance(result, ReadResult)1828 assert result.file_data is None1829 expected_error = (1830 f"File '{test_path}': Binary file exceeds maximum preview size of "1831 "512000 bytes"1832 )1833 assert result.error == expected_error18341835 async def test_aexecute_large_stdout_payload(1836 self, sandbox_backend: SandboxBackendProtocol1837 ) -> None:1838 """Async execute should handle five parallel 500 KiB stdout commands."""1839 if not self.has_async:1840 pytest.skip("Async tests not supported.")18411842 command = "python -c \"import sys; sys.stdout.write('x' * (500 * 1024))\""1843 if sys.version_info >= (3, 11):1844 tasks: list[asyncio.Task[ExecuteResponse]] = []1845 async with asyncio.TaskGroup() as tg:1846 tasks.extend(1847 tg.create_task(sandbox_backend.aexecute(command)) for _ in range(5)1848 )18491850 for task in tasks:1851 result = task.result()1852 assert result.exit_code == 01853 assert result.truncated is False1854 assert len(result.output) >= 500 * 10241855 assert result.output.startswith("x")1856 else:1857 pytest.skip("asyncio.TaskGroup requires Python 3.11+")18581859 async def test_aupload_adownload_large_file_roundtrip(1860 self, sandbox_backend: SandboxBackendProtocol, sandbox_test_root: str1861 ) -> None:1862 """Async upload/download should preserve a ~10 MiB payload exactly."""1863 if not self.has_async:1864 pytest.skip("Async tests not supported.")18651866 test_path = self.sandbox_path(1867 "large_async_upload.bin", root_dir=sandbox_test_root1868 )1869 chunk = b"0123456789abcdef" * 10241870 repeat_count = 6401871 test_content = chunk * repeat_count18721873 assert len(test_content) == 10 * 1024 * 102418741875 upload_responses = await sandbox_backend.aupload_files(1876 [(test_path, test_content)]1877 )1878 assert upload_responses == [FileUploadResponse(path=test_path, error=None)]18791880 exec_result = await sandbox_backend.aexecute(f"wc -c {_quote(test_path)}")1881 assert exec_result.exit_code == 01882 assert str(len(test_content)) in exec_result.output18831884 download_responses = await sandbox_backend.adownload_files([test_path])1885 assert download_responses == [1886 FileDownloadResponse(path=test_path, content=test_content, error=None)1887 ]
Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.