openhands 1.0.0__tar.gz → 1.0.2__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.
Potentially problematic release.
This version of openhands might be problematic. Click here for more details.
- {openhands-1.0.0 → openhands-1.0.2}/PKG-INFO +3 -3
- {openhands-1.0.0 → openhands-1.0.2}/build.py +1 -1
- {openhands-1.0.0 → openhands-1.0.2}/openhands.spec +2 -2
- openhands-1.0.2/openhands_cli/__init__.py +8 -0
- {openhands-1.0.0 → openhands-1.0.2}/openhands_cli/agent_chat.py +1 -0
- {openhands-1.0.0 → openhands-1.0.2}/openhands_cli/gui_launcher.py +1 -10
- {openhands-1.0.0 → openhands-1.0.2}/openhands_cli/tui/tui.py +0 -2
- {openhands-1.0.0 → openhands-1.0.2}/openhands_cli/user_actions/agent_action.py +8 -22
- {openhands-1.0.0 → openhands-1.0.2}/openhands_cli/user_actions/settings_action.py +7 -1
- {openhands-1.0.0 → openhands-1.0.2}/pyproject.toml +14 -8
- openhands-1.0.2/tests/settings/test_api_key_preservation.py +56 -0
- {openhands-1.0.0 → openhands-1.0.2}/tests/test_confirmation_mode.py +44 -18
- {openhands-1.0.0 → openhands-1.0.2}/tests/test_gui_launcher.py +0 -2
- {openhands-1.0.0 → openhands-1.0.2}/uv.lock +1342 -942
- openhands-1.0.0/openhands_cli/__init__.py +0 -3
- {openhands-1.0.0 → openhands-1.0.2}/.gitignore +0 -0
- {openhands-1.0.0 → openhands-1.0.2}/Makefile +0 -0
- {openhands-1.0.0 → openhands-1.0.2}/README.md +0 -0
- {openhands-1.0.0 → openhands-1.0.2}/build.sh +0 -0
- {openhands-1.0.0 → openhands-1.0.2}/hooks/rthook_profile_imports.py +0 -0
- {openhands-1.0.0 → openhands-1.0.2}/openhands_cli/argparsers/main_parser.py +0 -0
- {openhands-1.0.0 → openhands-1.0.2}/openhands_cli/argparsers/serve_parser.py +0 -0
- {openhands-1.0.0 → openhands-1.0.2}/openhands_cli/listeners/__init__.py +0 -0
- {openhands-1.0.0 → openhands-1.0.2}/openhands_cli/listeners/loading_listener.py +0 -0
- {openhands-1.0.0 → openhands-1.0.2}/openhands_cli/listeners/pause_listener.py +0 -0
- {openhands-1.0.0 → openhands-1.0.2}/openhands_cli/llm_utils.py +0 -0
- {openhands-1.0.0 → openhands-1.0.2}/openhands_cli/locations.py +0 -0
- {openhands-1.0.0 → openhands-1.0.2}/openhands_cli/pt_style.py +0 -0
- {openhands-1.0.0 → openhands-1.0.2}/openhands_cli/runner.py +0 -0
- {openhands-1.0.0 → openhands-1.0.2}/openhands_cli/setup.py +0 -0
- {openhands-1.0.0 → openhands-1.0.2}/openhands_cli/simple_main.py +0 -0
- {openhands-1.0.0 → openhands-1.0.2}/openhands_cli/tui/__init__.py +0 -0
- {openhands-1.0.0 → openhands-1.0.2}/openhands_cli/tui/settings/mcp_screen.py +0 -0
- {openhands-1.0.0 → openhands-1.0.2}/openhands_cli/tui/settings/settings_screen.py +0 -0
- {openhands-1.0.0 → openhands-1.0.2}/openhands_cli/tui/settings/store.py +0 -0
- {openhands-1.0.0 → openhands-1.0.2}/openhands_cli/tui/status.py +0 -0
- {openhands-1.0.0 → openhands-1.0.2}/openhands_cli/tui/utils.py +0 -0
- {openhands-1.0.0 → openhands-1.0.2}/openhands_cli/user_actions/__init__.py +0 -0
- {openhands-1.0.0 → openhands-1.0.2}/openhands_cli/user_actions/exit_session.py +0 -0
- {openhands-1.0.0 → openhands-1.0.2}/openhands_cli/user_actions/types.py +0 -0
- {openhands-1.0.0 → openhands-1.0.2}/openhands_cli/user_actions/utils.py +0 -0
- {openhands-1.0.0 → openhands-1.0.2}/tests/__init__.py +0 -0
- {openhands-1.0.0 → openhands-1.0.2}/tests/commands/test_confirm_command.py +0 -0
- {openhands-1.0.0 → openhands-1.0.2}/tests/commands/test_new_command.py +0 -0
- {openhands-1.0.0 → openhands-1.0.2}/tests/commands/test_status_command.py +0 -0
- {openhands-1.0.0 → openhands-1.0.2}/tests/conftest.py +0 -0
- {openhands-1.0.0 → openhands-1.0.2}/tests/settings/test_first_time_user_settings.py +0 -0
- {openhands-1.0.0 → openhands-1.0.2}/tests/settings/test_settings_input.py +0 -0
- {openhands-1.0.0 → openhands-1.0.2}/tests/settings/test_settings_workflow.py +0 -0
- {openhands-1.0.0 → openhands-1.0.2}/tests/test_conversation_runner.py +0 -0
- {openhands-1.0.0 → openhands-1.0.2}/tests/test_directory_separation.py +0 -0
- {openhands-1.0.0 → openhands-1.0.2}/tests/test_exit_session_confirmation.py +0 -0
- {openhands-1.0.0 → openhands-1.0.2}/tests/test_loading.py +0 -0
- {openhands-1.0.0 → openhands-1.0.2}/tests/test_main.py +0 -0
- {openhands-1.0.0 → openhands-1.0.2}/tests/test_mcp_config_validation.py +0 -0
- {openhands-1.0.0 → openhands-1.0.2}/tests/test_pause_listener.py +0 -0
- {openhands-1.0.0 → openhands-1.0.2}/tests/test_session_prompter.py +0 -0
- {openhands-1.0.0 → openhands-1.0.2}/tests/test_tui.py +0 -0
- {openhands-1.0.0 → openhands-1.0.2}/tests/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: openhands
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.2
|
|
4
4
|
Summary: OpenHands CLI - Terminal User Interface for OpenHands AI Agent
|
|
5
5
|
Author-email: OpenHands Team <contact@all-hands.dev>
|
|
6
6
|
License: MIT
|
|
@@ -8,8 +8,8 @@ Classifier: Programming Language :: Python :: 3 :: Only
|
|
|
8
8
|
Classifier: Programming Language :: Python :: 3.12
|
|
9
9
|
Classifier: Programming Language :: Python :: 3.13
|
|
10
10
|
Requires-Python: >=3.12
|
|
11
|
-
Requires-Dist: openhands-sdk
|
|
12
|
-
Requires-Dist: openhands-tools
|
|
11
|
+
Requires-Dist: openhands-sdk==1.0.0a3
|
|
12
|
+
Requires-Dist: openhands-tools==1.0.0a3
|
|
13
13
|
Requires-Dist: prompt-toolkit>=3
|
|
14
14
|
Requires-Dist: typer>=0.17.4
|
|
15
15
|
Description-Content-Type: text/markdown
|
|
@@ -32,8 +32,8 @@ a = Analysis(
|
|
|
32
32
|
*collect_data_files('litellm'),
|
|
33
33
|
*collect_data_files('fastmcp'),
|
|
34
34
|
*collect_data_files('mcp'),
|
|
35
|
-
# Include
|
|
36
|
-
*collect_data_files('openhands.sdk
|
|
35
|
+
# Include all data files from openhands.sdk (templates, configs, etc.)
|
|
36
|
+
*collect_data_files('openhands.sdk'),
|
|
37
37
|
# Include package metadata for importlib.metadata
|
|
38
38
|
*copy_metadata('fastmcp'),
|
|
39
39
|
],
|
|
@@ -113,21 +113,12 @@ def launch_gui_server(mount_cwd: bool = False, gpu: bool = False) -> None:
|
|
|
113
113
|
pull_cmd = ['docker', 'pull', runtime_image]
|
|
114
114
|
print_formatted_text(HTML(_format_docker_command_for_logging(pull_cmd)))
|
|
115
115
|
try:
|
|
116
|
-
subprocess.run(
|
|
117
|
-
pull_cmd,
|
|
118
|
-
check=True,
|
|
119
|
-
timeout=300, # 5 minutes timeout
|
|
120
|
-
)
|
|
116
|
+
subprocess.run(pull_cmd, check=True)
|
|
121
117
|
except subprocess.CalledProcessError:
|
|
122
118
|
print_formatted_text(
|
|
123
119
|
HTML('<ansired>❌ Failed to pull runtime image.</ansired>')
|
|
124
120
|
)
|
|
125
121
|
sys.exit(1)
|
|
126
|
-
except subprocess.TimeoutExpired:
|
|
127
|
-
print_formatted_text(
|
|
128
|
-
HTML('<ansired>❌ Timeout while pulling runtime image.</ansired>')
|
|
129
|
-
)
|
|
130
|
-
sys.exit(1)
|
|
131
122
|
|
|
132
123
|
print_formatted_text('')
|
|
133
124
|
print_formatted_text(
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import html
|
|
1
2
|
from prompt_toolkit import HTML, print_formatted_text
|
|
2
3
|
|
|
3
4
|
from openhands.sdk.security.confirmation_policy import (
|
|
@@ -37,14 +38,13 @@ def ask_user_confirmation(
|
|
|
37
38
|
or '[unknown action]'
|
|
38
39
|
)
|
|
39
40
|
print_formatted_text(
|
|
40
|
-
HTML(f'<grey> {i}. {tool_name}: {action_content}...</grey>')
|
|
41
|
+
HTML(f'<grey> {i}. {tool_name}: {html.escape(action_content)}...</grey>')
|
|
41
42
|
)
|
|
42
43
|
|
|
43
44
|
question = 'Choose an option:'
|
|
44
45
|
options = [
|
|
45
46
|
'Yes, proceed',
|
|
46
|
-
'
|
|
47
|
-
'No, reject with reason',
|
|
47
|
+
'Reject',
|
|
48
48
|
"Always proceed (don't ask again)",
|
|
49
49
|
]
|
|
50
50
|
|
|
@@ -60,32 +60,18 @@ def ask_user_confirmation(
|
|
|
60
60
|
if index == 0:
|
|
61
61
|
return ConfirmationResult(decision=UserConfirmation.ACCEPT)
|
|
62
62
|
elif index == 1:
|
|
63
|
-
|
|
64
|
-
elif index == 2:
|
|
63
|
+
# Handle "Reject" option with optional reason
|
|
65
64
|
try:
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
)
|
|
69
|
-
except Exception:
|
|
70
|
-
return ConfirmationResult(decision=UserConfirmation.DEFER)
|
|
71
|
-
|
|
72
|
-
# Support both string return and (reason, cancelled) tuple for tests
|
|
73
|
-
cancelled = False
|
|
74
|
-
if isinstance(reason_result, tuple) and len(reason_result) >= 1:
|
|
75
|
-
reason = reason_result[0] or ''
|
|
76
|
-
cancelled = bool(reason_result[1]) if len(reason_result) > 1 else False
|
|
77
|
-
else:
|
|
78
|
-
reason = str(reason_result or '').strip()
|
|
79
|
-
|
|
80
|
-
if cancelled:
|
|
65
|
+
reason = cli_text_input('Reason (and let OpenHands know why): ').strip()
|
|
66
|
+
except (EOFError, KeyboardInterrupt):
|
|
81
67
|
return ConfirmationResult(decision=UserConfirmation.DEFER)
|
|
82
68
|
|
|
83
69
|
return ConfirmationResult(decision=UserConfirmation.REJECT, reason=reason)
|
|
84
|
-
elif index ==
|
|
70
|
+
elif index == 2:
|
|
85
71
|
return ConfirmationResult(
|
|
86
72
|
decision=UserConfirmation.ACCEPT, policy_change=NeverConfirm()
|
|
87
73
|
)
|
|
88
|
-
elif index ==
|
|
74
|
+
elif index == 3:
|
|
89
75
|
return ConfirmationResult(
|
|
90
76
|
decision=UserConfirmation.ACCEPT,
|
|
91
77
|
policy_change=ConfirmRisky(threshold=SecurityRisk.HIGH),
|
|
@@ -123,9 +123,15 @@ def prompt_api_key(
|
|
|
123
123
|
validator = NonEmptyValueValidator()
|
|
124
124
|
|
|
125
125
|
question = helper_text + step_counter.next_step(question)
|
|
126
|
-
|
|
126
|
+
user_input = cli_text_input(
|
|
127
127
|
question, escapable=escapable, validator=validator, is_password=True
|
|
128
128
|
)
|
|
129
|
+
|
|
130
|
+
# If user pressed ENTER with existing key (empty input), return the existing key
|
|
131
|
+
if existing_api_key and not user_input.strip():
|
|
132
|
+
return existing_api_key.get_secret_value()
|
|
133
|
+
|
|
134
|
+
return user_input
|
|
129
135
|
|
|
130
136
|
|
|
131
137
|
# Advanced settings functions
|
|
@@ -4,7 +4,7 @@ requires = [ "hatchling>=1.25" ]
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "openhands"
|
|
7
|
-
version = "1.0.
|
|
7
|
+
version = "1.0.2"
|
|
8
8
|
description = "OpenHands CLI - Terminal User Interface for OpenHands AI Agent"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -15,15 +15,16 @@ classifiers = [
|
|
|
15
15
|
"Programming Language :: Python :: 3.12",
|
|
16
16
|
"Programming Language :: Python :: 3.13",
|
|
17
17
|
]
|
|
18
|
+
# Using Git URLs for dependencies so installs from PyPI pull from GitHub
|
|
19
|
+
# TODO: pin package versions once agent-sdk has published PyPI packages
|
|
18
20
|
dependencies = [
|
|
19
|
-
"openhands-sdk",
|
|
20
|
-
"openhands-tools",
|
|
21
|
+
"openhands-sdk==1.0.0a3",
|
|
22
|
+
"openhands-tools==1.0.0a3",
|
|
21
23
|
"prompt-toolkit>=3",
|
|
22
24
|
"typer>=0.17.4",
|
|
23
25
|
]
|
|
24
26
|
|
|
25
|
-
|
|
26
|
-
scripts.openhands = "openhands_cli.simple_main:main"
|
|
27
|
+
scripts = { openhands = "openhands_cli.simple_main:main" }
|
|
27
28
|
|
|
28
29
|
[dependency-groups]
|
|
29
30
|
# Hatchling wheel target: include the package directory
|
|
@@ -42,6 +43,9 @@ dev = [
|
|
|
42
43
|
"ruff>=0.11.8",
|
|
43
44
|
]
|
|
44
45
|
|
|
46
|
+
[tool.hatch.metadata]
|
|
47
|
+
allow-direct-references = true
|
|
48
|
+
|
|
45
49
|
[tool.hatch.build.targets.wheel]
|
|
46
50
|
packages = [ "openhands_cli" ]
|
|
47
51
|
|
|
@@ -95,6 +99,8 @@ warn_unused_configs = true
|
|
|
95
99
|
disallow_untyped_defs = true
|
|
96
100
|
ignore_missing_imports = true
|
|
97
101
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
102
|
+
# UNCOMMENT TO USE EXACT COMMIT FROM AGENT-SDK
|
|
103
|
+
|
|
104
|
+
# [tool.uv.sources]
|
|
105
|
+
# openhands-sdk = { git = "https://github.com/All-Hands-AI/agent-sdk.git", subdirectory = "openhands-sdk", rev = "512399d896521aee3131eea4bb59087fb9dfa243" }
|
|
106
|
+
# openhands-tools = { git = "https://github.com/All-Hands-AI/agent-sdk.git", subdirectory = "openhands-tools", rev = "512399d896521aee3131eea4bb59087fb9dfa243" }
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Test for API key preservation bug when updating settings."""
|
|
2
|
+
|
|
3
|
+
from unittest.mock import patch
|
|
4
|
+
import pytest
|
|
5
|
+
from pydantic import SecretStr
|
|
6
|
+
|
|
7
|
+
from openhands_cli.user_actions.settings_action import prompt_api_key
|
|
8
|
+
from openhands_cli.tui.utils import StepCounter
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_api_key_preservation_when_user_presses_enter():
|
|
12
|
+
"""Test that API key is preserved when user presses ENTER to keep current key.
|
|
13
|
+
|
|
14
|
+
This test replicates the bug where API keys disappear when updating settings.
|
|
15
|
+
When a user presses ENTER to keep the current API key, the function should
|
|
16
|
+
return the existing API key, not an empty string.
|
|
17
|
+
"""
|
|
18
|
+
step_counter = StepCounter(1)
|
|
19
|
+
existing_api_key = SecretStr("sk-existing-key-123")
|
|
20
|
+
|
|
21
|
+
# Mock cli_text_input to return empty string (simulating user pressing ENTER)
|
|
22
|
+
with patch('openhands_cli.user_actions.settings_action.cli_text_input', return_value=''):
|
|
23
|
+
result = prompt_api_key(
|
|
24
|
+
step_counter=step_counter,
|
|
25
|
+
provider='openai',
|
|
26
|
+
existing_api_key=existing_api_key,
|
|
27
|
+
escapable=True
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
# The bug: result is empty string instead of the existing key
|
|
31
|
+
# This test will fail initially, demonstrating the bug
|
|
32
|
+
assert result == existing_api_key.get_secret_value(), (
|
|
33
|
+
f"Expected existing API key '{existing_api_key.get_secret_value()}' "
|
|
34
|
+
f"but got '{result}'. API key should be preserved when user presses ENTER."
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_api_key_update_when_user_enters_new_key():
|
|
39
|
+
"""Test that API key is updated when user enters a new key."""
|
|
40
|
+
step_counter = StepCounter(1)
|
|
41
|
+
existing_api_key = SecretStr("sk-existing-key-123")
|
|
42
|
+
new_api_key = "sk-new-key-456"
|
|
43
|
+
|
|
44
|
+
# Mock cli_text_input to return new API key
|
|
45
|
+
with patch('openhands_cli.user_actions.settings_action.cli_text_input', return_value=new_api_key):
|
|
46
|
+
result = prompt_api_key(
|
|
47
|
+
step_counter=step_counter,
|
|
48
|
+
provider='openai',
|
|
49
|
+
existing_api_key=existing_api_key,
|
|
50
|
+
escapable=True
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# Should return the new API key
|
|
54
|
+
assert result == new_api_key
|
|
55
|
+
|
|
56
|
+
|
|
@@ -147,10 +147,12 @@ class TestConfirmationMode:
|
|
|
147
147
|
assert result.policy_change is None
|
|
148
148
|
assert result.policy_change is None
|
|
149
149
|
|
|
150
|
+
@patch('openhands_cli.user_actions.agent_action.cli_text_input')
|
|
150
151
|
@patch('openhands_cli.user_actions.agent_action.cli_confirm')
|
|
151
|
-
def test_ask_user_confirmation_no(self, mock_cli_confirm: Any) -> None:
|
|
152
|
-
"""Test that ask_user_confirmation returns REJECT when user selects
|
|
153
|
-
mock_cli_confirm.return_value = 1 # Second option (
|
|
152
|
+
def test_ask_user_confirmation_no(self, mock_cli_confirm: Any, mock_cli_text_input: Any) -> None:
|
|
153
|
+
"""Test that ask_user_confirmation returns REJECT when user selects reject without reason."""
|
|
154
|
+
mock_cli_confirm.return_value = 1 # Second option (Reject)
|
|
155
|
+
mock_cli_text_input.return_value = '' # Empty reason (reject without reason)
|
|
154
156
|
|
|
155
157
|
mock_action = MagicMock()
|
|
156
158
|
mock_action.tool_name = 'bash'
|
|
@@ -163,6 +165,7 @@ class TestConfirmationMode:
|
|
|
163
165
|
assert result.reason == ''
|
|
164
166
|
assert result.policy_change is None
|
|
165
167
|
assert result.policy_change is None
|
|
168
|
+
mock_cli_text_input.assert_called_once_with('Reason (and let OpenHands know why): ')
|
|
166
169
|
|
|
167
170
|
@patch('openhands_cli.user_actions.agent_action.cli_confirm')
|
|
168
171
|
def test_ask_user_confirmation_y_shorthand(self, mock_cli_confirm: Any) -> None:
|
|
@@ -179,10 +182,12 @@ class TestConfirmationMode:
|
|
|
179
182
|
assert result.reason == ''
|
|
180
183
|
assert result.policy_change is None
|
|
181
184
|
|
|
185
|
+
@patch('openhands_cli.user_actions.agent_action.cli_text_input')
|
|
182
186
|
@patch('openhands_cli.user_actions.agent_action.cli_confirm')
|
|
183
|
-
def test_ask_user_confirmation_n_shorthand(self, mock_cli_confirm: Any) -> None:
|
|
184
|
-
"""Test that ask_user_confirmation accepts second option as
|
|
185
|
-
mock_cli_confirm.return_value = 1 # Second option (
|
|
187
|
+
def test_ask_user_confirmation_n_shorthand(self, mock_cli_confirm: Any, mock_cli_text_input: Any) -> None:
|
|
188
|
+
"""Test that ask_user_confirmation accepts second option as reject."""
|
|
189
|
+
mock_cli_confirm.return_value = 1 # Second option (Reject)
|
|
190
|
+
mock_cli_text_input.return_value = '' # Empty reason (reject without reason)
|
|
186
191
|
|
|
187
192
|
mock_action = MagicMock()
|
|
188
193
|
mock_action.tool_name = 'bash'
|
|
@@ -193,6 +198,7 @@ class TestConfirmationMode:
|
|
|
193
198
|
assert isinstance(result, ConfirmationResult)
|
|
194
199
|
assert result.reason == ''
|
|
195
200
|
assert result.policy_change is None
|
|
201
|
+
mock_cli_text_input.assert_called_once_with('Reason (and let OpenHands know why): ')
|
|
196
202
|
|
|
197
203
|
@patch('openhands_cli.user_actions.agent_action.cli_confirm')
|
|
198
204
|
def test_ask_user_confirmation_invalid_then_yes(
|
|
@@ -278,9 +284,9 @@ class TestConfirmationMode:
|
|
|
278
284
|
def test_ask_user_confirmation_no_with_reason(
|
|
279
285
|
self, mock_cli_confirm: Any, mock_cli_text_input: Any
|
|
280
286
|
) -> None:
|
|
281
|
-
"""Test that ask_user_confirmation returns REJECT when user selects '
|
|
282
|
-
mock_cli_confirm.return_value =
|
|
283
|
-
mock_cli_text_input.return_value =
|
|
287
|
+
"""Test that ask_user_confirmation returns REJECT when user selects 'Reject' and provides a reason."""
|
|
288
|
+
mock_cli_confirm.return_value = 1 # Second option (Reject)
|
|
289
|
+
mock_cli_text_input.return_value = 'This action is too risky'
|
|
284
290
|
|
|
285
291
|
mock_action = MagicMock()
|
|
286
292
|
mock_action.tool_name = 'bash'
|
|
@@ -291,7 +297,7 @@ class TestConfirmationMode:
|
|
|
291
297
|
assert result.decision == UserConfirmation.REJECT
|
|
292
298
|
assert result.reason == 'This action is too risky'
|
|
293
299
|
assert result.policy_change is None
|
|
294
|
-
mock_cli_text_input.
|
|
300
|
+
mock_cli_text_input.assert_called_once_with('Reason (and let OpenHands know why): ')
|
|
295
301
|
|
|
296
302
|
@patch('openhands_cli.user_actions.agent_action.cli_text_input')
|
|
297
303
|
@patch('openhands_cli.user_actions.agent_action.cli_confirm')
|
|
@@ -299,8 +305,8 @@ class TestConfirmationMode:
|
|
|
299
305
|
self, mock_cli_confirm: Any, mock_cli_text_input: Any
|
|
300
306
|
) -> None:
|
|
301
307
|
"""Test that ask_user_confirmation falls back to DEFER when reason input is cancelled."""
|
|
302
|
-
mock_cli_confirm.return_value =
|
|
303
|
-
mock_cli_text_input.
|
|
308
|
+
mock_cli_confirm.return_value = 1 # Second option (Reject)
|
|
309
|
+
mock_cli_text_input.side_effect = KeyboardInterrupt() # User cancelled reason input
|
|
304
310
|
|
|
305
311
|
mock_action = MagicMock()
|
|
306
312
|
mock_action.tool_name = 'bash'
|
|
@@ -311,7 +317,27 @@ class TestConfirmationMode:
|
|
|
311
317
|
assert isinstance(result, ConfirmationResult)
|
|
312
318
|
assert result.reason == ''
|
|
313
319
|
assert result.policy_change is None
|
|
314
|
-
mock_cli_text_input.
|
|
320
|
+
mock_cli_text_input.assert_called_once_with('Reason (and let OpenHands know why): ')
|
|
321
|
+
|
|
322
|
+
@patch('openhands_cli.user_actions.agent_action.cli_text_input')
|
|
323
|
+
@patch('openhands_cli.user_actions.agent_action.cli_confirm')
|
|
324
|
+
def test_ask_user_confirmation_reject_empty_reason(
|
|
325
|
+
self, mock_cli_confirm: Any, mock_cli_text_input: Any
|
|
326
|
+
) -> None:
|
|
327
|
+
"""Test that ask_user_confirmation handles empty reason input correctly."""
|
|
328
|
+
mock_cli_confirm.return_value = 1 # Second option (Reject)
|
|
329
|
+
mock_cli_text_input.return_value = ' ' # Whitespace-only reason (should be treated as empty)
|
|
330
|
+
|
|
331
|
+
mock_action = MagicMock()
|
|
332
|
+
mock_action.tool_name = 'bash'
|
|
333
|
+
mock_action.action = 'dangerous command'
|
|
334
|
+
|
|
335
|
+
result = ask_user_confirmation([mock_action])
|
|
336
|
+
assert result.decision == UserConfirmation.REJECT
|
|
337
|
+
assert isinstance(result, ConfirmationResult)
|
|
338
|
+
assert result.reason == '' # Should be empty after stripping whitespace
|
|
339
|
+
assert result.policy_change is None
|
|
340
|
+
mock_cli_text_input.assert_called_once_with('Reason (and let OpenHands know why): ')
|
|
315
341
|
|
|
316
342
|
def test_user_confirmation_is_escapable_e2e(
|
|
317
343
|
self, monkeypatch: pytest.MonkeyPatch
|
|
@@ -358,8 +384,8 @@ class TestConfirmationMode:
|
|
|
358
384
|
|
|
359
385
|
@patch('openhands_cli.user_actions.agent_action.cli_confirm')
|
|
360
386
|
def test_ask_user_confirmation_always_accept(self, mock_cli_confirm: Any) -> None:
|
|
361
|
-
"""Test that ask_user_confirmation returns ACCEPT with NeverConfirm policy when user selects
|
|
362
|
-
mock_cli_confirm.return_value =
|
|
387
|
+
"""Test that ask_user_confirmation returns ACCEPT with NeverConfirm policy when user selects third option."""
|
|
388
|
+
mock_cli_confirm.return_value = 2 # Third option (Always proceed)
|
|
363
389
|
|
|
364
390
|
mock_action = MagicMock()
|
|
365
391
|
mock_action.tool_name = 'bash'
|
|
@@ -408,7 +434,7 @@ class TestConfirmationMode:
|
|
|
408
434
|
new_mock_conversation.id = mock_conversation.id
|
|
409
435
|
new_mock_conversation.is_confirmation_mode_active = False
|
|
410
436
|
mock_setup.return_value = new_mock_conversation
|
|
411
|
-
|
|
437
|
+
|
|
412
438
|
result = runner._handle_confirmation_request()
|
|
413
439
|
|
|
414
440
|
# Verify that confirmation mode was disabled
|
|
@@ -426,9 +452,9 @@ class TestConfirmationMode:
|
|
|
426
452
|
def test_ask_user_confirmation_auto_confirm_safe(
|
|
427
453
|
self, mock_cli_confirm: Any
|
|
428
454
|
) -> None:
|
|
429
|
-
"""Test that ask_user_confirmation returns ACCEPT with policy_change when user selects
|
|
455
|
+
"""Test that ask_user_confirmation returns ACCEPT with policy_change when user selects fourth option."""
|
|
430
456
|
mock_cli_confirm.return_value = (
|
|
431
|
-
|
|
457
|
+
3 # Fourth option (Auto-confirm LOW/MEDIUM, ask for HIGH)
|
|
432
458
|
)
|
|
433
459
|
|
|
434
460
|
mock_action = MagicMock()
|
|
@@ -111,8 +111,6 @@ class TestLaunchGuiServer:
|
|
|
111
111
|
[
|
|
112
112
|
# Docker pull failure
|
|
113
113
|
(subprocess.CalledProcessError(1, 'docker pull'), None, 1, False, False),
|
|
114
|
-
# Docker pull timeout
|
|
115
|
-
(subprocess.TimeoutExpired('docker pull', 300), None, 1, False, False),
|
|
116
114
|
# Docker run failure
|
|
117
115
|
(MagicMock(returncode=0), subprocess.CalledProcessError(1, 'docker run'), 1, False, False),
|
|
118
116
|
# KeyboardInterrupt during run
|