-
Notifications
You must be signed in to change notification settings - Fork 2.2k
fix(compile): salvage partial output on timeout and iteration limits #3948
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+2,620
−152
Merged
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
9622f4f
fix(service): break startup circular imports with lazy exports
fujiajie666 0887ea9
fix(fs): avoid root semantic refresh when removing resource scope
fujiajie666 7aca501
fix(compile): preserve existing wiki links
fujiajie666 85547ca
fix(sdk): extend HTTP timeout for blocking batch writes
fujiajie666 6116c07
fix(compile): salvage workspace output on runtime timeout
fujiajie666 8412b3f
feat(compile): support runtime timeout and salvage partial output
fujiajie666 1bbc2ce
Merge branch 'main' into gbrains-test
fujiajie666 278504b
Merge branch 'main' into fix/compile-output-salvage
fujiajie666 b4e07d5
Merge branch 'main' into fix/compile-output-salvage
fujiajie666 2190416
fix: expand compile task and output limits
fujiajie666 4265f1a
revert file
fujiajie666 d8742be
fix(compile): harden salvage and deadline handling
fujiajie666 0b048aa
Merge branch 'main' into fix/compile-output-salvage
fujiajie666 02531ff
update
fujiajie666 7bce13e
fix(compile): address salvage review feedback
fujiajie666 e54d12d
fix(compile): normalize escaped salvage links
fujiajie666 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,213 @@ | ||
| # Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd. | ||
| # SPDX-License-Identifier: AGPL-3.0 | ||
| """Regression tests for bounded local and remote sandbox file access.""" | ||
|
|
||
| import json | ||
| from pathlib import Path | ||
| from types import SimpleNamespace | ||
|
|
||
| import pytest | ||
| from vikingbot.sandbox.backends.aiosandbox import AioSandboxBackend | ||
| from vikingbot.sandbox.backends.direct import DirectBackend | ||
| from vikingbot.sandbox.backends.opensandbox import OpenSandboxBackend | ||
| from vikingbot.sandbox.base import SandboxFileInfo | ||
|
|
||
|
|
||
| def _direct_backend(workspace: Path) -> DirectBackend: | ||
| return DirectBackend(SimpleNamespace(restrict_workspaces={}), "test", workspace) | ||
|
|
||
|
|
||
| @pytest.mark.asyncio | ||
| async def test_local_workspace_listing_and_reads_are_bounded(tmp_path: Path): | ||
| backend = _direct_backend(tmp_path) | ||
| await backend.start() | ||
| (tmp_path / ".hidden").write_bytes(b"h") | ||
| (tmp_path / "nested").mkdir() | ||
| (tmp_path / "nested" / "artifact.bin").write_bytes(b"artifact") | ||
|
|
||
| assert await backend.list_files(max_entries=3) == [ | ||
| SandboxFileInfo(path=".hidden", size=1), | ||
| SandboxFileInfo(path="nested/artifact.bin", size=8), | ||
| ] | ||
| assert await backend.read_file_bytes("nested/artifact.bin", max_bytes=8) == b"artifact" | ||
| with pytest.raises(ValueError, match="7-byte read limit"): | ||
| await backend.read_file_bytes("nested/artifact.bin", max_bytes=7) | ||
| with pytest.raises(ValueError, match="inventory exceeds 2 entries"): | ||
| await backend.list_files(max_entries=2) | ||
|
|
||
|
|
||
| class _AioFileClient: | ||
| def __init__(self, *, truncated=False): | ||
| self.list_calls = [] | ||
| self.glob_calls = [] | ||
| self.download_calls = [] | ||
| self.truncated = truncated | ||
| self.entries = { | ||
| "/home/gem": [ | ||
| SimpleNamespace(name="artifact.bin", is_directory=False, size=6), | ||
| SimpleNamespace(name="nested", is_directory=True, size=0), | ||
| ], | ||
| "/home/gem/nested": [ | ||
| SimpleNamespace(name="page.md", is_directory=False, size=4), | ||
| ], | ||
| } | ||
|
|
||
| async def list_path(self, **kwargs): | ||
| self.list_calls.append(kwargs) | ||
| return SimpleNamespace(data=SimpleNamespace(files=self.entries[kwargs["path"]])) | ||
|
|
||
| async def glob_files(self, **kwargs): | ||
| self.glob_calls.append(kwargs) | ||
| return SimpleNamespace( | ||
| data=SimpleNamespace( | ||
| files=[ | ||
| SimpleNamespace( | ||
| path="/home/gem", | ||
| is_directory=True, | ||
| size=0, | ||
| ), | ||
| SimpleNamespace( | ||
| path="/home/gem/artifact.bin", | ||
| is_directory=False, | ||
| size=6, | ||
| ), | ||
| SimpleNamespace( | ||
| path="/home/gem/nested", | ||
| is_directory=True, | ||
| size=0, | ||
| ), | ||
| SimpleNamespace( | ||
| path="/home/gem/nested/page.md", | ||
| is_directory=False, | ||
| size=4, | ||
| ), | ||
| ], | ||
| total_count=5 if self.truncated else 4, | ||
| truncated=self.truncated, | ||
| ) | ||
| ) | ||
|
|
||
| async def download_file(self, *, path): | ||
| self.download_calls.append(path) | ||
| yield b"abc" | ||
| yield b"def" | ||
|
|
||
|
|
||
| def _aio_backend(tmp_path: Path, file_client: _AioFileClient) -> AioSandboxBackend: | ||
| config = SimpleNamespace( | ||
| backends=SimpleNamespace(aiosandbox=SimpleNamespace(base_url="http://sandbox")) | ||
| ) | ||
| backend = AioSandboxBackend(config, "test", tmp_path) | ||
| backend._client = SimpleNamespace(file=file_client) | ||
| return backend | ||
|
|
||
|
|
||
| @pytest.mark.asyncio | ||
| async def test_aiosandbox_lists_and_streams_from_remote_workspace(tmp_path: Path): | ||
| files = _AioFileClient() | ||
| backend = _aio_backend(tmp_path, files) | ||
|
|
||
| assert await backend.list_files(max_entries=3) == [ | ||
| SandboxFileInfo(path="artifact.bin", size=6), | ||
| SandboxFileInfo(path="nested/page.md", size=4), | ||
| ] | ||
| assert files.glob_calls == [ | ||
| { | ||
| "path": "/home/gem", | ||
| "pattern": "**", | ||
| "include_hidden": True, | ||
| "files_only": False, | ||
| "include_metadata": True, | ||
| "max_results": 4, | ||
| "sort_by": "path", | ||
| }, | ||
| ] | ||
| assert await backend.read_file_bytes("artifact.bin", max_bytes=6) == b"abcdef" | ||
| with pytest.raises(ValueError, match="5-byte read limit"): | ||
| await backend.read_file_bytes("artifact.bin", max_bytes=5) | ||
| assert files.download_calls == ["/home/gem/artifact.bin", "/home/gem/artifact.bin"] | ||
|
|
||
|
|
||
| @pytest.mark.asyncio | ||
| async def test_aiosandbox_inventory_uses_the_remote_result_limit(tmp_path: Path): | ||
| files = _AioFileClient(truncated=True) | ||
| backend = _aio_backend(tmp_path, files) | ||
|
|
||
| with pytest.raises(ValueError, match="inventory exceeds 3 entries"): | ||
| await backend.list_files(max_entries=3) | ||
| assert files.glob_calls[0]["max_results"] == 4 | ||
|
|
||
|
|
||
| class _OpenSandboxFiles: | ||
| def __init__(self): | ||
| self.read_calls = [] | ||
|
|
||
| async def read_bytes_stream(self, path, *, range_header): | ||
| self.read_calls.append((path, range_header)) | ||
| payload = b"abcdef" | ||
|
|
||
| async def chunks(): | ||
| yield payload[:3] | ||
| yield payload[3:] | ||
|
|
||
| return chunks() | ||
|
|
||
|
|
||
| class _OpenSandboxCommands: | ||
| def __init__(self, *, overflow=False): | ||
| self.overflow = overflow | ||
| self.calls = [] | ||
|
|
||
| async def run(self, command, *, opts): | ||
| self.calls.append((command, opts)) | ||
| payload = { | ||
| "overflow": self.overflow, | ||
| "files": [["artifact.bin", 6], ["nested/page.md", 4]], | ||
| } | ||
| return SimpleNamespace( | ||
| error=None, | ||
| logs=SimpleNamespace(stdout=[SimpleNamespace(text=json.dumps(payload))]), | ||
| ) | ||
|
|
||
|
|
||
| def _opensandbox_vke_backend( | ||
| tmp_path: Path, | ||
| file_client: _OpenSandboxFiles, | ||
| commands: _OpenSandboxCommands, | ||
| ) -> OpenSandboxBackend: | ||
| backend = object.__new__(OpenSandboxBackend) | ||
| backend._workspace = tmp_path | ||
| backend._is_vke = True | ||
| backend._sandbox = SimpleNamespace(files=file_client, commands=commands) | ||
| return backend | ||
|
|
||
|
|
||
| @pytest.mark.asyncio | ||
| async def test_opensandbox_vke_uses_bounded_remote_inventory_and_range_reads(tmp_path: Path): | ||
| files = _OpenSandboxFiles() | ||
| commands = _OpenSandboxCommands() | ||
| backend = _opensandbox_vke_backend(tmp_path, files, commands) | ||
|
|
||
| assert await backend.list_files(max_entries=2) == [ | ||
| SandboxFileInfo(path="artifact.bin", size=6), | ||
| SandboxFileInfo(path="nested/page.md", size=4), | ||
| ] | ||
| assert "python3 -c" in commands.calls[0][0] | ||
| assert "limit = 2" in commands.calls[0][0] | ||
| assert await backend.read_file_bytes("artifact.bin", max_bytes=6) == b"abcdef" | ||
| with pytest.raises(ValueError, match="5-byte read limit"): | ||
| await backend.read_file_bytes("artifact.bin", max_bytes=5) | ||
| assert files.read_calls == [ | ||
| ("/workspace/artifact.bin", "bytes=0-6"), | ||
| ("/workspace/artifact.bin", "bytes=0-5"), | ||
| ] | ||
|
|
||
|
|
||
| @pytest.mark.asyncio | ||
| async def test_opensandbox_vke_inventory_stops_at_the_remote_limit(tmp_path: Path): | ||
| commands = _OpenSandboxCommands(overflow=True) | ||
| backend = _opensandbox_vke_backend(tmp_path, _OpenSandboxFiles(), commands) | ||
|
|
||
| with pytest.raises(ValueError, match="inventory exceeds 1 entries"): | ||
| await backend.list_files(max_entries=1) | ||
| assert "limit = 1" in commands.calls[0][0] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.