langchain-agentcore-codeinterpreter 0.0.4__tar.gz → 0.0.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {langchain_agentcore_codeinterpreter-0.0.4 → langchain_agentcore_codeinterpreter-0.0.5}/Makefile +2 -2
- {langchain_agentcore_codeinterpreter-0.0.4 → langchain_agentcore_codeinterpreter-0.0.5}/PKG-INFO +1 -1
- {langchain_agentcore_codeinterpreter-0.0.4 → langchain_agentcore_codeinterpreter-0.0.5}/langchain_agentcore_codeinterpreter/sandbox.py +115 -6
- {langchain_agentcore_codeinterpreter-0.0.4 → langchain_agentcore_codeinterpreter-0.0.5}/pyproject.toml +7 -1
- {langchain_agentcore_codeinterpreter-0.0.4 → langchain_agentcore_codeinterpreter-0.0.5}/tests/integration_tests/sandbox/test_sandbox.py +60 -5
- {langchain_agentcore_codeinterpreter-0.0.4 → langchain_agentcore_codeinterpreter-0.0.5}/tests/unit_tests/test_sandbox.py +181 -3
- {langchain_agentcore_codeinterpreter-0.0.4 → langchain_agentcore_codeinterpreter-0.0.5}/uv.lock +452 -423
- {langchain_agentcore_codeinterpreter-0.0.4 → langchain_agentcore_codeinterpreter-0.0.5}/.gitignore +0 -0
- {langchain_agentcore_codeinterpreter-0.0.4 → langchain_agentcore_codeinterpreter-0.0.5}/LICENSE +0 -0
- {langchain_agentcore_codeinterpreter-0.0.4 → langchain_agentcore_codeinterpreter-0.0.5}/README.md +0 -0
- {langchain_agentcore_codeinterpreter-0.0.4 → langchain_agentcore_codeinterpreter-0.0.5}/langchain_agentcore_codeinterpreter/__init__.py +0 -0
- {langchain_agentcore_codeinterpreter-0.0.4 → langchain_agentcore_codeinterpreter-0.0.5}/scripts/check_imports.py +0 -0
- {langchain_agentcore_codeinterpreter-0.0.4 → langchain_agentcore_codeinterpreter-0.0.5}/tests/__init__.py +0 -0
- {langchain_agentcore_codeinterpreter-0.0.4 → langchain_agentcore_codeinterpreter-0.0.5}/tests/integration_tests/__init__.py +0 -0
- {langchain_agentcore_codeinterpreter-0.0.4 → langchain_agentcore_codeinterpreter-0.0.5}/tests/integration_tests/sandbox/__init__.py +0 -0
- {langchain_agentcore_codeinterpreter-0.0.4 → langchain_agentcore_codeinterpreter-0.0.5}/tests/integration_tests/test_compile.py +0 -0
- {langchain_agentcore_codeinterpreter-0.0.4 → langchain_agentcore_codeinterpreter-0.0.5}/tests/unit_tests/__init__.py +0 -0
- {langchain_agentcore_codeinterpreter-0.0.4 → langchain_agentcore_codeinterpreter-0.0.5}/tests/unit_tests/test_imports.py +0 -0
- {langchain_agentcore_codeinterpreter-0.0.4 → langchain_agentcore_codeinterpreter-0.0.5}/tests/unit_tests/test_stream_parsing.py +0 -0
{langchain_agentcore_codeinterpreter-0.0.4 → langchain_agentcore_codeinterpreter-0.0.5}/Makefile
RENAMED
|
@@ -30,10 +30,10 @@ test: ## Run individual unit test: make test TEST_FILE=tests/unit_tests/test.py
|
|
|
30
30
|
env -u AWS_ACCESS_KEY_ID -u AWS_SECRET_ACCESS_KEY -u AWS_SESSION_TOKEN uv run --group test pytest --disable-socket --allow-unix-socket $(TEST_FILE)
|
|
31
31
|
|
|
32
32
|
integration_tests: ## Run all integration tests
|
|
33
|
-
uv run --group test --group test_integration pytest -n auto $(TEST_FILE)
|
|
33
|
+
uv run --group test --group test_integration pytest -n auto --benchmark-disable $(TEST_FILE)
|
|
34
34
|
|
|
35
35
|
integration_test: ## Run individual integration test: make integration_test TEST_FILE=tests/integration_tests/integ_test.py
|
|
36
|
-
uv run --group test --group test_integration pytest -n auto $(TEST_FILE)
|
|
36
|
+
uv run --group test --group test_integration pytest -n auto --benchmark-disable $(TEST_FILE)
|
|
37
37
|
|
|
38
38
|
coverage_tests: ## Run unit tests with coverage
|
|
39
39
|
env -u AWS_ACCESS_KEY_ID -u AWS_SECRET_ACCESS_KEY -u AWS_SESSION_TOKEN uv run --group test pytest --cov=langchain_agentcore_codeinterpreter --cov-report=html --cov-report=term-missing --cov-branch tests/unit_tests/
|
{langchain_agentcore_codeinterpreter-0.0.4 → langchain_agentcore_codeinterpreter-0.0.5}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: langchain-agentcore-codeinterpreter
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.5
|
|
4
4
|
Summary: Amazon Bedrock AgentCore Code Interpreter sandbox integration for Deep Agents
|
|
5
5
|
Project-URL: Source Code, https://github.com/langchain-ai/langchain-aws/tree/main/libs/agentcore-codeinterpreter
|
|
6
6
|
Project-URL: Repository, https://github.com/langchain-ai/langchain-aws
|
|
@@ -14,6 +14,9 @@ from deepagents.backends.protocol import (
|
|
|
14
14
|
ExecuteResponse,
|
|
15
15
|
FileDownloadResponse,
|
|
16
16
|
FileUploadResponse,
|
|
17
|
+
GlobResult,
|
|
18
|
+
GrepResult,
|
|
19
|
+
LsResult,
|
|
17
20
|
ReadResult,
|
|
18
21
|
WriteResult,
|
|
19
22
|
)
|
|
@@ -174,7 +177,7 @@ class AgentCoreSandbox(BaseSandbox):
|
|
|
174
177
|
against the real cwd before shell preflight commands and stripped of
|
|
175
178
|
the cwd prefix before the AgentCore ``writeFiles``/``readFiles`` APIs.
|
|
176
179
|
Pass the known cwd via the ``cwd`` constructor argument, or let the
|
|
177
|
-
sandbox detect it automatically on the first ``
|
|
180
|
+
sandbox detect it automatically on the first path operation via ``pwd``.
|
|
178
181
|
|
|
179
182
|
Example:
|
|
180
183
|
.. code-block:: python
|
|
@@ -209,7 +212,7 @@ class AgentCoreSandbox(BaseSandbox):
|
|
|
209
212
|
``write()`` uses it to resolve virtual paths to real absolute
|
|
210
213
|
paths and to strip the prefix before the AgentCore
|
|
211
214
|
``writeFiles`` API. When omitted, the cwd is detected
|
|
212
|
-
automatically on the first
|
|
215
|
+
automatically on the first path operation via ``pwd``.
|
|
213
216
|
"""
|
|
214
217
|
self._interpreter = interpreter
|
|
215
218
|
self._cwd: str | None = cwd.rstrip("/") if cwd is not None else None
|
|
@@ -439,7 +442,7 @@ class AgentCoreSandbox(BaseSandbox):
|
|
|
439
442
|
def upload_files(self, files: list[tuple[str, bytes]]) -> list[FileUploadResponse]:
|
|
440
443
|
"""Upload files to the AgentCore sandbox.
|
|
441
444
|
|
|
442
|
-
Text files are sent directly; binary files are
|
|
445
|
+
Text files are sent directly; binary files are sent as raw blobs.
|
|
443
446
|
|
|
444
447
|
Args:
|
|
445
448
|
files: List of ``(path, content)`` tuples to upload.
|
|
@@ -448,7 +451,7 @@ class AgentCoreSandbox(BaseSandbox):
|
|
|
448
451
|
List of :class:`FileUploadResponse` objects in the same order
|
|
449
452
|
as the input files.
|
|
450
453
|
"""
|
|
451
|
-
file_list: list[dict[str, str]] = []
|
|
454
|
+
file_list: list[dict[str, str | bytes]] = []
|
|
452
455
|
|
|
453
456
|
for path, content in files:
|
|
454
457
|
rel_path = self._to_relative_path(path)
|
|
@@ -456,8 +459,7 @@ class AgentCoreSandbox(BaseSandbox):
|
|
|
456
459
|
text_content = content.decode("utf-8")
|
|
457
460
|
file_list.append({"path": rel_path, "text": text_content})
|
|
458
461
|
except UnicodeDecodeError:
|
|
459
|
-
|
|
460
|
-
file_list.append({"path": rel_path, "blob": encoded})
|
|
462
|
+
file_list.append({"path": rel_path, "blob": content})
|
|
461
463
|
|
|
462
464
|
try:
|
|
463
465
|
if file_list:
|
|
@@ -479,6 +481,53 @@ class AgentCoreSandbox(BaseSandbox):
|
|
|
479
481
|
for path, _ in files
|
|
480
482
|
]
|
|
481
483
|
|
|
484
|
+
def ls(self, path: str) -> LsResult:
|
|
485
|
+
"""List a directory, resolving ``path`` against the sandbox cwd."""
|
|
486
|
+
return super().ls(self._to_absolute_path(path))
|
|
487
|
+
|
|
488
|
+
def read(
|
|
489
|
+
self,
|
|
490
|
+
file_path: str,
|
|
491
|
+
offset: int = 0,
|
|
492
|
+
limit: int = 2000,
|
|
493
|
+
) -> ReadResult:
|
|
494
|
+
"""Read a file, resolving ``file_path`` against the sandbox cwd."""
|
|
495
|
+
return super().read(self._to_absolute_path(file_path), offset, limit)
|
|
496
|
+
|
|
497
|
+
def grep(
|
|
498
|
+
self,
|
|
499
|
+
pattern: str,
|
|
500
|
+
path: str | None = None,
|
|
501
|
+
glob: str | None = None,
|
|
502
|
+
) -> GrepResult:
|
|
503
|
+
"""Search file contents, resolving ``path`` against the sandbox cwd."""
|
|
504
|
+
resolved = self._to_absolute_path(path) if path else path
|
|
505
|
+
return super().grep(pattern, resolved, glob)
|
|
506
|
+
|
|
507
|
+
def glob(
|
|
508
|
+
self,
|
|
509
|
+
pattern: str,
|
|
510
|
+
path: str | None = None,
|
|
511
|
+
) -> GlobResult:
|
|
512
|
+
"""Match paths by glob, resolving ``path`` against the sandbox cwd."""
|
|
513
|
+
resolved = self._to_absolute_path(path) if path else path
|
|
514
|
+
return super().glob(pattern, resolved)
|
|
515
|
+
|
|
516
|
+
def edit(
|
|
517
|
+
self,
|
|
518
|
+
file_path: str,
|
|
519
|
+
old_string: str,
|
|
520
|
+
new_string: str,
|
|
521
|
+
replace_all: bool = False, # noqa: FBT001, FBT002
|
|
522
|
+
) -> EditResult:
|
|
523
|
+
"""Edit a file, resolving ``file_path`` against the sandbox cwd."""
|
|
524
|
+
return super().edit(
|
|
525
|
+
self._to_absolute_path(file_path),
|
|
526
|
+
old_string,
|
|
527
|
+
new_string,
|
|
528
|
+
replace_all,
|
|
529
|
+
)
|
|
530
|
+
|
|
482
531
|
# ------------------------------------------------------------------
|
|
483
532
|
# Async overrides — use a dedicated executor to avoid starving the
|
|
484
533
|
# default asyncio thread pool with long-running boto3 stream reads.
|
|
@@ -618,3 +667,63 @@ class AgentCoreSandbox(BaseSandbox):
|
|
|
618
667
|
_AGENTCORE_EXECUTOR,
|
|
619
668
|
lambda: self.download_files(paths),
|
|
620
669
|
)
|
|
670
|
+
|
|
671
|
+
async def als(self, path: str) -> LsResult:
|
|
672
|
+
"""Async version of :meth:`ls`.
|
|
673
|
+
|
|
674
|
+
Args:
|
|
675
|
+
path: Directory path to list, resolved against the sandbox cwd.
|
|
676
|
+
|
|
677
|
+
Returns:
|
|
678
|
+
``LsResult`` with directory entries on success or ``error`` on
|
|
679
|
+
failure.
|
|
680
|
+
"""
|
|
681
|
+
loop = asyncio.get_running_loop()
|
|
682
|
+
return await loop.run_in_executor(
|
|
683
|
+
_AGENTCORE_EXECUTOR,
|
|
684
|
+
lambda: self.ls(path),
|
|
685
|
+
)
|
|
686
|
+
|
|
687
|
+
async def agrep(
|
|
688
|
+
self,
|
|
689
|
+
pattern: str,
|
|
690
|
+
path: str | None = None,
|
|
691
|
+
glob: str | None = None,
|
|
692
|
+
) -> GrepResult:
|
|
693
|
+
"""Async version of :meth:`grep`.
|
|
694
|
+
|
|
695
|
+
Args:
|
|
696
|
+
pattern: Literal string to search for.
|
|
697
|
+
path: Directory or file to search in, resolved against the sandbox
|
|
698
|
+
cwd. When ``None``, the ``BaseSandbox`` default is used.
|
|
699
|
+
glob: Optional file-name glob to restrict the search.
|
|
700
|
+
|
|
701
|
+
Returns:
|
|
702
|
+
``GrepResult`` with a list of matches or ``error`` on failure.
|
|
703
|
+
"""
|
|
704
|
+
loop = asyncio.get_running_loop()
|
|
705
|
+
return await loop.run_in_executor(
|
|
706
|
+
_AGENTCORE_EXECUTOR,
|
|
707
|
+
lambda: self.grep(pattern, path, glob),
|
|
708
|
+
)
|
|
709
|
+
|
|
710
|
+
async def aglob(
|
|
711
|
+
self,
|
|
712
|
+
pattern: str,
|
|
713
|
+
path: str | None = None,
|
|
714
|
+
) -> GlobResult:
|
|
715
|
+
"""Async version of :meth:`glob`.
|
|
716
|
+
|
|
717
|
+
Args:
|
|
718
|
+
pattern: Glob pattern to match.
|
|
719
|
+
path: Directory to search in, resolved against the sandbox cwd.
|
|
720
|
+
When ``None``, the ``BaseSandbox`` default is used.
|
|
721
|
+
|
|
722
|
+
Returns:
|
|
723
|
+
``GlobResult`` with a list of matches or ``error`` on failure.
|
|
724
|
+
"""
|
|
725
|
+
loop = asyncio.get_running_loop()
|
|
726
|
+
return await loop.run_in_executor(
|
|
727
|
+
_AGENTCORE_EXECUTOR,
|
|
728
|
+
lambda: self.glob(pattern, path),
|
|
729
|
+
)
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "langchain-agentcore-codeinterpreter"
|
|
7
|
-
version = "0.0.
|
|
7
|
+
version = "0.0.5"
|
|
8
8
|
description = "Amazon Bedrock AgentCore Code Interpreter sandbox integration for Deep Agents"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -62,6 +62,12 @@ disallow_untyped_defs = "True"
|
|
|
62
62
|
|
|
63
63
|
[tool.pytest.ini_options]
|
|
64
64
|
addopts = "--strict-markers --strict-config --durations=5"
|
|
65
|
+
filterwarnings = [
|
|
66
|
+
# `bedrock-agentcore` (third-party) still uses class-based pydantic `Config`
|
|
67
|
+
# in `bedrock_agentcore.runtime.context.RequestContext`. Remove once upstream
|
|
68
|
+
# migrates to `ConfigDict`.
|
|
69
|
+
"ignore:Support for class-based .config. is deprecated:DeprecationWarning",
|
|
70
|
+
]
|
|
65
71
|
markers = [
|
|
66
72
|
"requires: mark tests as requiring a specific library",
|
|
67
73
|
"asyncio: mark tests as requiring asyncio",
|
|
@@ -82,20 +82,50 @@ class TestAgentCoreSandboxIntegration:
|
|
|
82
82
|
assert results[0].error is not None
|
|
83
83
|
|
|
84
84
|
def test_binary_roundtrip(self, sandbox: AgentCoreSandbox) -> None:
|
|
85
|
-
"""Binary content uploaded
|
|
86
|
-
import base64
|
|
87
|
-
|
|
85
|
+
"""Binary content uploaded as a blob should round-trip unchanged."""
|
|
88
86
|
content = b"\x00\x01\x02\xff\xfe\xfd"
|
|
89
87
|
sandbox.upload_files([("binary_test.bin", content)])
|
|
90
88
|
results = sandbox.download_files(["binary_test.bin"])
|
|
91
89
|
assert results[0].error is None
|
|
92
|
-
|
|
93
|
-
assert results[0].content in (content, base64.b64encode(content))
|
|
90
|
+
assert results[0].content == content
|
|
94
91
|
|
|
95
92
|
def test_session_id(self, sandbox: AgentCoreSandbox) -> None:
|
|
96
93
|
"""The sandbox should have a non-empty session ID."""
|
|
97
94
|
assert sandbox.id != ""
|
|
98
95
|
|
|
96
|
+
def test_read_plane_roundtrip_virtual_path(self, sandbox: AgentCoreSandbox) -> None:
|
|
97
|
+
"""Read plane round-trips a virtual path against a non-root cwd."""
|
|
98
|
+
vpath = "/workspace/readplane_roundtrip.txt"
|
|
99
|
+
vdir = "/workspace"
|
|
100
|
+
content = "needle in haystack\nsecond line\n"
|
|
101
|
+
|
|
102
|
+
write_result = sandbox.write(vpath, content)
|
|
103
|
+
assert write_result.error is None, write_result.error
|
|
104
|
+
|
|
105
|
+
read_result = sandbox.read(vpath)
|
|
106
|
+
assert read_result.error is None, read_result.error
|
|
107
|
+
assert read_result.file_data is not None
|
|
108
|
+
assert read_result.file_data["content"].strip() == content.strip()
|
|
109
|
+
|
|
110
|
+
ls_result = sandbox.ls(vdir)
|
|
111
|
+
assert ls_result.error is None, ls_result.error
|
|
112
|
+
assert ls_result.entries is not None
|
|
113
|
+
assert any("readplane_roundtrip.txt" in e["path"] for e in ls_result.entries)
|
|
114
|
+
|
|
115
|
+
grep_result = sandbox.grep("needle", vdir)
|
|
116
|
+
assert grep_result.error is None, grep_result.error
|
|
117
|
+
assert grep_result.matches
|
|
118
|
+
|
|
119
|
+
glob_result = sandbox.glob("*.txt", vdir)
|
|
120
|
+
assert glob_result.error is None, glob_result.error
|
|
121
|
+
assert glob_result.matches
|
|
122
|
+
|
|
123
|
+
edit_result = sandbox.edit(vpath, "needle", "pin")
|
|
124
|
+
assert edit_result.error is None, edit_result.error
|
|
125
|
+
after = sandbox.read(vpath)
|
|
126
|
+
assert after.file_data is not None
|
|
127
|
+
assert "pin in haystack" in after.file_data["content"]
|
|
128
|
+
|
|
99
129
|
# ------------------------------------------------------------------
|
|
100
130
|
# Async integration tests
|
|
101
131
|
# ------------------------------------------------------------------
|
|
@@ -135,3 +165,28 @@ class TestAgentCoreSandboxIntegration:
|
|
|
135
165
|
for i, result in enumerate(results):
|
|
136
166
|
assert result.exit_code == 0
|
|
137
167
|
assert f"concurrent-{i}" in result.output
|
|
168
|
+
|
|
169
|
+
def test_aread_plane_roundtrip_virtual_path(
|
|
170
|
+
self, sandbox: AgentCoreSandbox
|
|
171
|
+
) -> None:
|
|
172
|
+
"""Async read plane round-trips a virtual path against a non-root cwd."""
|
|
173
|
+
vpath = "/workspace/areadplane_roundtrip.txt"
|
|
174
|
+
vdir = "/workspace"
|
|
175
|
+
|
|
176
|
+
async def run() -> None:
|
|
177
|
+
write_result = await sandbox.awrite(vpath, "async needle\n")
|
|
178
|
+
assert write_result.error is None, write_result.error
|
|
179
|
+
|
|
180
|
+
read_result = await sandbox.aread(vpath)
|
|
181
|
+
assert read_result.error is None, read_result.error
|
|
182
|
+
|
|
183
|
+
ls_result = await sandbox.als(vdir)
|
|
184
|
+
assert ls_result.error is None, ls_result.error
|
|
185
|
+
|
|
186
|
+
grep_result = await sandbox.agrep("needle", vdir)
|
|
187
|
+
assert grep_result.error is None, grep_result.error
|
|
188
|
+
|
|
189
|
+
glob_result = await sandbox.aglob("*.txt", vdir)
|
|
190
|
+
assert glob_result.error is None, glob_result.error
|
|
191
|
+
|
|
192
|
+
asyncio.run(run())
|
|
@@ -9,10 +9,19 @@ from __future__ import annotations
|
|
|
9
9
|
import asyncio
|
|
10
10
|
from collections.abc import Iterator
|
|
11
11
|
from typing import Any
|
|
12
|
-
from unittest.mock import MagicMock
|
|
12
|
+
from unittest.mock import MagicMock, patch
|
|
13
13
|
|
|
14
14
|
import pytest
|
|
15
15
|
from botocore.exceptions import ClientError
|
|
16
|
+
from deepagents.backends.protocol import (
|
|
17
|
+
EditResult,
|
|
18
|
+
FileData,
|
|
19
|
+
GlobResult,
|
|
20
|
+
GrepResult,
|
|
21
|
+
LsResult,
|
|
22
|
+
ReadResult,
|
|
23
|
+
)
|
|
24
|
+
from deepagents.backends.sandbox import BaseSandbox
|
|
16
25
|
|
|
17
26
|
from langchain_agentcore_codeinterpreter import AgentCoreSandbox
|
|
18
27
|
from langchain_agentcore_codeinterpreter.sandbox import (
|
|
@@ -141,12 +150,37 @@ def test_upload_files_text() -> None:
|
|
|
141
150
|
|
|
142
151
|
|
|
143
152
|
def test_upload_files_binary_uses_blob() -> None:
|
|
144
|
-
"""Non-UTF-8 content should be
|
|
153
|
+
"""Non-UTF-8 content should be uploaded as raw blob bytes."""
|
|
145
154
|
sandbox, mock = _make_sandbox()
|
|
146
|
-
|
|
155
|
+
binary_content = b"\x80\x81\x82"
|
|
156
|
+
|
|
157
|
+
sandbox.upload_files([("/data.bin", binary_content)])
|
|
158
|
+
|
|
147
159
|
content = mock.invoke.call_args.kwargs["params"]["content"][0]
|
|
148
160
|
assert "blob" in content
|
|
149
161
|
assert "text" not in content
|
|
162
|
+
assert content["blob"] == binary_content
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def test_upload_files_mixed_text_and_binary() -> None:
|
|
166
|
+
"""Text and binary files should use the correct writeFiles fields."""
|
|
167
|
+
sandbox, mock = _make_sandbox()
|
|
168
|
+
binary_content = b"\x00\xff\xfe"
|
|
169
|
+
|
|
170
|
+
result = sandbox.upload_files(
|
|
171
|
+
[
|
|
172
|
+
("/hello.py", b"print('hi')"),
|
|
173
|
+
("/data.bin", binary_content),
|
|
174
|
+
]
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
uploaded = mock.invoke.call_args.kwargs["params"]["content"]
|
|
178
|
+
assert uploaded == [
|
|
179
|
+
{"path": "hello.py", "text": "print('hi')"},
|
|
180
|
+
{"path": "data.bin", "blob": binary_content},
|
|
181
|
+
]
|
|
182
|
+
assert [response.path for response in result] == ["/hello.py", "/data.bin"]
|
|
183
|
+
assert [response.error for response in result] == [None, None]
|
|
150
184
|
|
|
151
185
|
|
|
152
186
|
def test_upload_files_empty_list() -> None:
|
|
@@ -570,6 +604,119 @@ def test_write_root_cwd_preserves_existing_behavior() -> None:
|
|
|
570
604
|
assert uploaded_path == "hello.py"
|
|
571
605
|
|
|
572
606
|
|
|
607
|
+
# ------------------------------------------------------------------
|
|
608
|
+
# Read-plane path normalization (ls/read/grep/glob/edit)
|
|
609
|
+
# ------------------------------------------------------------------
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
@pytest.mark.parametrize(
|
|
613
|
+
("method_name", "virtual_path", "expected_path", "extra_args", "return_value"),
|
|
614
|
+
[
|
|
615
|
+
(
|
|
616
|
+
"ls",
|
|
617
|
+
"/workspace",
|
|
618
|
+
"/opt/sandbox/workspace",
|
|
619
|
+
(),
|
|
620
|
+
LsResult(entries=[]),
|
|
621
|
+
),
|
|
622
|
+
(
|
|
623
|
+
"read",
|
|
624
|
+
"/workspace/hello.py",
|
|
625
|
+
"/opt/sandbox/workspace/hello.py",
|
|
626
|
+
(0, 2000),
|
|
627
|
+
ReadResult(file_data=FileData(content="hello", encoding="utf-8")),
|
|
628
|
+
),
|
|
629
|
+
(
|
|
630
|
+
"grep",
|
|
631
|
+
"/workspace",
|
|
632
|
+
"/opt/sandbox/workspace",
|
|
633
|
+
("needle", None),
|
|
634
|
+
GrepResult(matches=[]),
|
|
635
|
+
),
|
|
636
|
+
(
|
|
637
|
+
"glob",
|
|
638
|
+
"/workspace",
|
|
639
|
+
"/opt/sandbox/workspace",
|
|
640
|
+
("*.py",),
|
|
641
|
+
GlobResult(matches=[]),
|
|
642
|
+
),
|
|
643
|
+
(
|
|
644
|
+
"edit",
|
|
645
|
+
"/workspace/hello.py",
|
|
646
|
+
"/opt/sandbox/workspace/hello.py",
|
|
647
|
+
("old", "new", False),
|
|
648
|
+
EditResult(path="/opt/sandbox/workspace/hello.py", occurrences=1),
|
|
649
|
+
),
|
|
650
|
+
],
|
|
651
|
+
)
|
|
652
|
+
def test_read_plane_resolves_virtual_paths(
|
|
653
|
+
method_name: str,
|
|
654
|
+
virtual_path: str,
|
|
655
|
+
expected_path: str,
|
|
656
|
+
extra_args: tuple[Any, ...],
|
|
657
|
+
return_value: Any,
|
|
658
|
+
) -> None:
|
|
659
|
+
"""Read-plane methods should resolve virtual paths against the sandbox cwd."""
|
|
660
|
+
sandbox, _ = _make_sandbox(cwd="/opt/sandbox")
|
|
661
|
+
result: Any
|
|
662
|
+
|
|
663
|
+
with patch.object(
|
|
664
|
+
BaseSandbox, method_name, return_value=return_value
|
|
665
|
+
) as mock_method:
|
|
666
|
+
if method_name == "grep":
|
|
667
|
+
result = sandbox.grep(extra_args[0], virtual_path)
|
|
668
|
+
mock_method.assert_called_once_with(extra_args[0], expected_path, None)
|
|
669
|
+
elif method_name == "glob":
|
|
670
|
+
result = sandbox.glob(extra_args[0], virtual_path)
|
|
671
|
+
mock_method.assert_called_once_with(extra_args[0], expected_path)
|
|
672
|
+
elif method_name == "read":
|
|
673
|
+
result = sandbox.read(virtual_path, *extra_args)
|
|
674
|
+
mock_method.assert_called_once_with(expected_path, *extra_args)
|
|
675
|
+
elif method_name == "edit":
|
|
676
|
+
result = sandbox.edit(virtual_path, *extra_args)
|
|
677
|
+
mock_method.assert_called_once_with(expected_path, *extra_args)
|
|
678
|
+
else:
|
|
679
|
+
result = sandbox.ls(virtual_path)
|
|
680
|
+
mock_method.assert_called_once_with(expected_path)
|
|
681
|
+
|
|
682
|
+
assert result == return_value
|
|
683
|
+
|
|
684
|
+
|
|
685
|
+
def test_grep_with_none_path_passes_through() -> None:
|
|
686
|
+
"""grep() should not resolve when path is None (BaseSandbox defaults to '.')."""
|
|
687
|
+
sandbox, _ = _make_sandbox(cwd="/opt/sandbox")
|
|
688
|
+
|
|
689
|
+
with patch.object(
|
|
690
|
+
BaseSandbox, "grep", return_value=GrepResult(matches=[])
|
|
691
|
+
) as mock_grep:
|
|
692
|
+
sandbox.grep("needle")
|
|
693
|
+
mock_grep.assert_called_once_with("needle", None, None)
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
def test_glob_with_none_path_passes_through() -> None:
|
|
697
|
+
"""glob() should not resolve when path is None (BaseSandbox defaults to '/')."""
|
|
698
|
+
sandbox, _ = _make_sandbox(cwd="/opt/sandbox")
|
|
699
|
+
|
|
700
|
+
with patch.object(
|
|
701
|
+
BaseSandbox, "glob", return_value=GlobResult(matches=[])
|
|
702
|
+
) as mock_glob:
|
|
703
|
+
sandbox.glob("*.py")
|
|
704
|
+
mock_glob.assert_called_once_with("*.py", None)
|
|
705
|
+
|
|
706
|
+
|
|
707
|
+
def test_read_root_cwd_preserves_virtual_path() -> None:
|
|
708
|
+
"""When cwd is /, read() should pass virtual paths through unchanged."""
|
|
709
|
+
sandbox, _ = _make_sandbox(cwd="/")
|
|
710
|
+
|
|
711
|
+
with patch.object(
|
|
712
|
+
BaseSandbox,
|
|
713
|
+
"read",
|
|
714
|
+
return_value=ReadResult(file_data=FileData(content="ok", encoding="utf-8")),
|
|
715
|
+
) as mock_read:
|
|
716
|
+
sandbox.read("/workspace/hello.py")
|
|
717
|
+
mock_read.assert_called_once_with("/workspace/hello.py", 0, 2000)
|
|
718
|
+
|
|
719
|
+
|
|
573
720
|
# ------------------------------------------------------------------
|
|
574
721
|
# _invoke() — eager stream consumption
|
|
575
722
|
# ------------------------------------------------------------------
|
|
@@ -778,6 +925,37 @@ def test_aexecute_handles_session_expiry() -> None:
|
|
|
778
925
|
assert "expired" in result.output.lower()
|
|
779
926
|
|
|
780
927
|
|
|
928
|
+
def test_als_routes_through_cwd_aware_sync_ls() -> None:
|
|
929
|
+
"""als() must resolve the virtual path against the cwd."""
|
|
930
|
+
sandbox, _ = _make_sandbox(cwd="/opt/sandbox")
|
|
931
|
+
|
|
932
|
+
with patch.object(BaseSandbox, "ls", return_value=LsResult(entries=[])) as mock_ls:
|
|
933
|
+
asyncio.run(sandbox.als("/workspace"))
|
|
934
|
+
mock_ls.assert_called_once_with("/opt/sandbox/workspace")
|
|
935
|
+
|
|
936
|
+
|
|
937
|
+
def test_agrep_routes_through_cwd_aware_sync_grep() -> None:
|
|
938
|
+
"""agrep() must resolve the virtual path against the cwd (not literal)."""
|
|
939
|
+
sandbox, _ = _make_sandbox(cwd="/opt/sandbox")
|
|
940
|
+
|
|
941
|
+
with patch.object(
|
|
942
|
+
BaseSandbox, "grep", return_value=GrepResult(matches=[])
|
|
943
|
+
) as mock_grep:
|
|
944
|
+
asyncio.run(sandbox.agrep("needle", "/workspace"))
|
|
945
|
+
mock_grep.assert_called_once_with("needle", "/opt/sandbox/workspace", None)
|
|
946
|
+
|
|
947
|
+
|
|
948
|
+
def test_aglob_routes_through_cwd_aware_sync_glob() -> None:
|
|
949
|
+
"""aglob() must resolve the virtual path against the cwd (not literal)."""
|
|
950
|
+
sandbox, _ = _make_sandbox(cwd="/opt/sandbox")
|
|
951
|
+
|
|
952
|
+
with patch.object(
|
|
953
|
+
BaseSandbox, "glob", return_value=GlobResult(matches=[])
|
|
954
|
+
) as mock_glob:
|
|
955
|
+
asyncio.run(sandbox.aglob("*.py", "/workspace"))
|
|
956
|
+
mock_glob.assert_called_once_with("*.py", "/opt/sandbox/workspace")
|
|
957
|
+
|
|
958
|
+
|
|
781
959
|
# ------------------------------------------------------------------
|
|
782
960
|
# Executor configuration
|
|
783
961
|
# ------------------------------------------------------------------
|