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.

Files changed (59) hide show
  1. {openhands-1.0.0 → openhands-1.0.2}/PKG-INFO +3 -3
  2. {openhands-1.0.0 → openhands-1.0.2}/build.py +1 -1
  3. {openhands-1.0.0 → openhands-1.0.2}/openhands.spec +2 -2
  4. openhands-1.0.2/openhands_cli/__init__.py +8 -0
  5. {openhands-1.0.0 → openhands-1.0.2}/openhands_cli/agent_chat.py +1 -0
  6. {openhands-1.0.0 → openhands-1.0.2}/openhands_cli/gui_launcher.py +1 -10
  7. {openhands-1.0.0 → openhands-1.0.2}/openhands_cli/tui/tui.py +0 -2
  8. {openhands-1.0.0 → openhands-1.0.2}/openhands_cli/user_actions/agent_action.py +8 -22
  9. {openhands-1.0.0 → openhands-1.0.2}/openhands_cli/user_actions/settings_action.py +7 -1
  10. {openhands-1.0.0 → openhands-1.0.2}/pyproject.toml +14 -8
  11. openhands-1.0.2/tests/settings/test_api_key_preservation.py +56 -0
  12. {openhands-1.0.0 → openhands-1.0.2}/tests/test_confirmation_mode.py +44 -18
  13. {openhands-1.0.0 → openhands-1.0.2}/tests/test_gui_launcher.py +0 -2
  14. {openhands-1.0.0 → openhands-1.0.2}/uv.lock +1342 -942
  15. openhands-1.0.0/openhands_cli/__init__.py +0 -3
  16. {openhands-1.0.0 → openhands-1.0.2}/.gitignore +0 -0
  17. {openhands-1.0.0 → openhands-1.0.2}/Makefile +0 -0
  18. {openhands-1.0.0 → openhands-1.0.2}/README.md +0 -0
  19. {openhands-1.0.0 → openhands-1.0.2}/build.sh +0 -0
  20. {openhands-1.0.0 → openhands-1.0.2}/hooks/rthook_profile_imports.py +0 -0
  21. {openhands-1.0.0 → openhands-1.0.2}/openhands_cli/argparsers/main_parser.py +0 -0
  22. {openhands-1.0.0 → openhands-1.0.2}/openhands_cli/argparsers/serve_parser.py +0 -0
  23. {openhands-1.0.0 → openhands-1.0.2}/openhands_cli/listeners/__init__.py +0 -0
  24. {openhands-1.0.0 → openhands-1.0.2}/openhands_cli/listeners/loading_listener.py +0 -0
  25. {openhands-1.0.0 → openhands-1.0.2}/openhands_cli/listeners/pause_listener.py +0 -0
  26. {openhands-1.0.0 → openhands-1.0.2}/openhands_cli/llm_utils.py +0 -0
  27. {openhands-1.0.0 → openhands-1.0.2}/openhands_cli/locations.py +0 -0
  28. {openhands-1.0.0 → openhands-1.0.2}/openhands_cli/pt_style.py +0 -0
  29. {openhands-1.0.0 → openhands-1.0.2}/openhands_cli/runner.py +0 -0
  30. {openhands-1.0.0 → openhands-1.0.2}/openhands_cli/setup.py +0 -0
  31. {openhands-1.0.0 → openhands-1.0.2}/openhands_cli/simple_main.py +0 -0
  32. {openhands-1.0.0 → openhands-1.0.2}/openhands_cli/tui/__init__.py +0 -0
  33. {openhands-1.0.0 → openhands-1.0.2}/openhands_cli/tui/settings/mcp_screen.py +0 -0
  34. {openhands-1.0.0 → openhands-1.0.2}/openhands_cli/tui/settings/settings_screen.py +0 -0
  35. {openhands-1.0.0 → openhands-1.0.2}/openhands_cli/tui/settings/store.py +0 -0
  36. {openhands-1.0.0 → openhands-1.0.2}/openhands_cli/tui/status.py +0 -0
  37. {openhands-1.0.0 → openhands-1.0.2}/openhands_cli/tui/utils.py +0 -0
  38. {openhands-1.0.0 → openhands-1.0.2}/openhands_cli/user_actions/__init__.py +0 -0
  39. {openhands-1.0.0 → openhands-1.0.2}/openhands_cli/user_actions/exit_session.py +0 -0
  40. {openhands-1.0.0 → openhands-1.0.2}/openhands_cli/user_actions/types.py +0 -0
  41. {openhands-1.0.0 → openhands-1.0.2}/openhands_cli/user_actions/utils.py +0 -0
  42. {openhands-1.0.0 → openhands-1.0.2}/tests/__init__.py +0 -0
  43. {openhands-1.0.0 → openhands-1.0.2}/tests/commands/test_confirm_command.py +0 -0
  44. {openhands-1.0.0 → openhands-1.0.2}/tests/commands/test_new_command.py +0 -0
  45. {openhands-1.0.0 → openhands-1.0.2}/tests/commands/test_status_command.py +0 -0
  46. {openhands-1.0.0 → openhands-1.0.2}/tests/conftest.py +0 -0
  47. {openhands-1.0.0 → openhands-1.0.2}/tests/settings/test_first_time_user_settings.py +0 -0
  48. {openhands-1.0.0 → openhands-1.0.2}/tests/settings/test_settings_input.py +0 -0
  49. {openhands-1.0.0 → openhands-1.0.2}/tests/settings/test_settings_workflow.py +0 -0
  50. {openhands-1.0.0 → openhands-1.0.2}/tests/test_conversation_runner.py +0 -0
  51. {openhands-1.0.0 → openhands-1.0.2}/tests/test_directory_separation.py +0 -0
  52. {openhands-1.0.0 → openhands-1.0.2}/tests/test_exit_session_confirmation.py +0 -0
  53. {openhands-1.0.0 → openhands-1.0.2}/tests/test_loading.py +0 -0
  54. {openhands-1.0.0 → openhands-1.0.2}/tests/test_main.py +0 -0
  55. {openhands-1.0.0 → openhands-1.0.2}/tests/test_mcp_config_validation.py +0 -0
  56. {openhands-1.0.0 → openhands-1.0.2}/tests/test_pause_listener.py +0 -0
  57. {openhands-1.0.0 → openhands-1.0.2}/tests/test_session_prompter.py +0 -0
  58. {openhands-1.0.0 → openhands-1.0.2}/tests/test_tui.py +0 -0
  59. {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.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
@@ -164,7 +164,7 @@ def test_executable() -> bool:
164
164
  )
165
165
 
166
166
  # --- Wait for welcome ---
167
- deadline = boot_start + 30
167
+ deadline = boot_start + 60
168
168
  saw_welcome = False
169
169
  captured = []
170
170
 
@@ -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 Jinja prompt templates required by the agent SDK
36
- *collect_data_files('openhands.sdk.agent', includes=['prompts/*.j2']),
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
  ],
@@ -0,0 +1,8 @@
1
+ """OpenHands package."""
2
+
3
+ from importlib.metadata import version, PackageNotFoundError
4
+
5
+ try:
6
+ __version__ = version("openhands")
7
+ except PackageNotFoundError:
8
+ __version__ = "0.0.0"
@@ -54,6 +54,7 @@ def _print_exit_hint(conversation_id: str) -> None:
54
54
  )
55
55
 
56
56
 
57
+
57
58
  def run_cli_entry(resume_conversation_id: str | None = None) -> None:
58
59
  """Run the agent chat session using the agent SDK.
59
60
 
@@ -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(
@@ -57,8 +57,6 @@ def display_banner(conversation_id: str, resume: bool = False) -> None:
57
57
  style=DEFAULT_STYLE,
58
58
  )
59
59
 
60
- print_formatted_text(HTML(f'<grey>OpenHands CLI v{__version__}</grey>'))
61
-
62
60
  print_formatted_text('')
63
61
  if not resume:
64
62
  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
- 'No, reject (w/o reason)',
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
- return ConfirmationResult(decision=UserConfirmation.REJECT)
64
- elif index == 2:
63
+ # Handle "Reject" option with optional reason
65
64
  try:
66
- reason_result = cli_text_input(
67
- 'Please enter your reason for rejecting these actions: '
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 == 3:
70
+ elif index == 2:
85
71
  return ConfirmationResult(
86
72
  decision=UserConfirmation.ACCEPT, policy_change=NeverConfirm()
87
73
  )
88
- elif index == 4:
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
- return cli_text_input(
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.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
- # Dev-only tools with uv groups: `uv sync --group dev`
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
- [tool.uv.sources]
99
- openhands-sdk = { git = "https://github.com/All-Hands-AI/agent-sdk.git", subdirectory = "openhands/sdk", rev = "189979a5013751aa86852ab41afe9a79555e62ac" }
100
- openhands-tools = { git = "https://github.com/All-Hands-AI/agent-sdk.git", subdirectory = "openhands/tools", rev = "189979a5013751aa86852ab41afe9a79555e62ac" }
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 no."""
153
- mock_cli_confirm.return_value = 1 # Second option (No, reject)
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 no."""
185
- mock_cli_confirm.return_value = 1 # Second option (No, reject)
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 'No (with reason)'."""
282
- mock_cli_confirm.return_value = 2 # Third option (No, with reason)
283
- mock_cli_text_input.return_value = ('This action is too risky', False)
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.assert_called_once()
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 = 2 # Third option (No, with reason)
303
- mock_cli_text_input.return_value = ('', True) # User cancelled reason 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.assert_called_once()
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 fourth option."""
362
- mock_cli_confirm.return_value = 3 # Fourth option (Always proceed)
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 fifth option."""
455
+ """Test that ask_user_confirmation returns ACCEPT with policy_change when user selects fourth option."""
430
456
  mock_cli_confirm.return_value = (
431
- 4 # Fifth option (Auto-confirm LOW/MEDIUM, ask for HIGH)
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