-
Notifications
You must be signed in to change notification settings - Fork 245
feat: add --import-modules CLI flag for preloading custom modules #2771
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
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
9227144
feat: add --import-modules CLI flag for preloading custom modules
09473ZH 62a7c30
style: move importlib to top-level imports
09473ZH d026f11
test: add tests for --import-modules preload
09473ZH ed8c515
Merge branch 'main' into feat/import-modules-cli
xingyaoww 1580629
fix: add error handling and move preload after check-browser
openhands-agent 64421a1
test: add check-browser ordering and error-logging tests
openhands-agent b72c7c6
test: cover import module preload in live server
xingyaoww 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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,143 @@ | ||
| """Tests for the --import-modules preloading helper.""" | ||
|
|
||
| import logging | ||
| import sys | ||
| import textwrap | ||
| from unittest.mock import MagicMock, patch | ||
|
|
||
| import pytest | ||
|
|
||
| from openhands.agent_server.__main__ import preload_modules | ||
|
|
||
|
|
||
| class TestPreloadModules: | ||
| def test_none_is_noop(self): | ||
| with patch( | ||
| "openhands.agent_server.__main__.importlib.import_module" | ||
| ) as mock_import: | ||
| preload_modules(None) | ||
| mock_import.assert_not_called() | ||
|
|
||
| def test_empty_string_is_noop(self): | ||
| with patch( | ||
| "openhands.agent_server.__main__.importlib.import_module" | ||
| ) as mock_import: | ||
| preload_modules("") | ||
| mock_import.assert_not_called() | ||
|
|
||
| def test_single_module(self): | ||
| with patch( | ||
| "openhands.agent_server.__main__.importlib.import_module" | ||
| ) as mock_import: | ||
| preload_modules("myapp.tools") | ||
| mock_import.assert_called_once_with("myapp.tools") | ||
|
|
||
| def test_comma_separated_strips_whitespace(self): | ||
| with patch( | ||
| "openhands.agent_server.__main__.importlib.import_module" | ||
| ) as mock_import: | ||
| preload_modules(" myapp.tools , myapp.plugins ") | ||
| assert [c.args[0] for c in mock_import.call_args_list] == [ | ||
| "myapp.tools", | ||
| "myapp.plugins", | ||
| ] | ||
|
|
||
| def test_empty_segments_skipped(self): | ||
| with patch( | ||
| "openhands.agent_server.__main__.importlib.import_module" | ||
| ) as mock_import: | ||
| preload_modules("myapp.tools,,myapp.plugins, ") | ||
| assert [c.args[0] for c in mock_import.call_args_list] == [ | ||
| "myapp.tools", | ||
| "myapp.plugins", | ||
| ] | ||
|
|
||
| def test_missing_module_raises(self): | ||
| # Follow project convention: don't swallow import errors. | ||
| with pytest.raises(ModuleNotFoundError): | ||
| preload_modules("definitely_not_a_real_module_xyz_2771") | ||
|
|
||
| @pytest.fixture | ||
| def fake_tool_module(self, tmp_path, monkeypatch): | ||
| """Create an on-disk module whose top-level body has an observable | ||
| side effect (analogous to a `register_tool(...)` call).""" | ||
| pkg_name = "preload_modules_test_pkg" | ||
| pkg = tmp_path / pkg_name | ||
| pkg.mkdir() | ||
| (pkg / "__init__.py").write_text("") | ||
| (pkg / "my_tool.py").write_text( | ||
| textwrap.dedent( | ||
| """\ | ||
| REGISTRY = [] | ||
| REGISTRY.append("MyCustomTool") | ||
| """ | ||
| ) | ||
| ) | ||
| monkeypatch.syspath_prepend(str(tmp_path)) | ||
| qualname = f"{pkg_name}.my_tool" | ||
| sys.modules.pop(pkg_name, None) | ||
| sys.modules.pop(qualname, None) | ||
| yield qualname | ||
| sys.modules.pop(pkg_name, None) | ||
| sys.modules.pop(qualname, None) | ||
|
|
||
| def test_module_side_effects_execute(self, fake_tool_module): | ||
| """With the flag: side effects land before conversations are served — | ||
| the race this flag exists to fix.""" | ||
| preload_modules(fake_tool_module) | ||
|
|
||
| imported = sys.modules[fake_tool_module] | ||
| assert imported.REGISTRY == ["MyCustomTool"] | ||
|
|
||
| def test_module_not_imported_without_flag(self, fake_tool_module): | ||
| """Contract companion: if `preload_modules` is not called (i.e. the | ||
| operator forgot `--import-modules`), the module stays unimported and | ||
| its `register_tool`-style side effects never run. This is exactly | ||
| the broken state the CLI flag exists to prevent.""" | ||
| preload_modules(None) | ||
|
|
||
| assert fake_tool_module not in sys.modules | ||
|
|
||
| def test_import_error_is_logged_before_raising(self, caplog): | ||
| """Import failures should log the module name and error for | ||
| operator diagnostics before re-raising.""" | ||
| with caplog.at_level(logging.ERROR): | ||
| with pytest.raises(ModuleNotFoundError): | ||
| preload_modules("no_such_module_xyz_2771") | ||
|
|
||
| assert any( | ||
| "no_such_module_xyz_2771" in r.message and "--import-modules" in r.message | ||
| for r in caplog.records | ||
| ) | ||
|
|
||
|
|
||
| class TestMainCheckBrowserOrdering: | ||
| """Verify --check-browser runs independently of --import-modules.""" | ||
|
|
||
| def test_check_browser_exits_before_preload(self): | ||
| """--check-browser should short-circuit before preload_modules | ||
| runs, so a broken user module cannot mask the browser check.""" | ||
| mock_result = MagicMock() | ||
| mock_result.is_error = False | ||
|
|
||
| mock_executor = MagicMock() | ||
| mock_executor.return_value = mock_result | ||
|
|
||
| with ( | ||
| patch("sys.argv", ["prog", "--check-browser", "--import-modules", "boom"]), | ||
| patch("openhands.tools.preset.default.register_default_tools"), | ||
| patch( | ||
| "openhands.tools.browser_use.impl.BrowserToolExecutor", | ||
| return_value=mock_executor, | ||
| ), | ||
| patch("openhands.agent_server.__main__.preload_modules") as mock_preload, | ||
| ): | ||
| from openhands.agent_server.__main__ import main | ||
|
|
||
| with pytest.raises(SystemExit) as exc_info: | ||
| main() | ||
|
|
||
| # Browser check succeeded → exit 0 | ||
| assert exc_info.value.code == 0 | ||
| # preload_modules must NOT have been called | ||
| mock_preload.assert_not_called() |
Oops, something went wrong.
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.