openhands 1.0.3__tar.gz → 1.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.
Potentially problematic release.
This version of openhands might be problematic. Click here for more details.
- {openhands-1.0.3 → openhands-1.0.5}/PKG-INFO +1 -1
- {openhands-1.0.3 → openhands-1.0.5}/build.py +16 -12
- {openhands-1.0.3 → openhands-1.0.5}/openhands_cli/agent_chat.py +38 -11
- {openhands-1.0.3 → openhands-1.0.5}/openhands_cli/gui_launcher.py +2 -2
- {openhands-1.0.3 → openhands-1.0.5}/openhands_cli/setup.py +33 -51
- {openhands-1.0.3 → openhands-1.0.5}/openhands_cli/tui/settings/settings_screen.py +18 -6
- {openhands-1.0.3 → openhands-1.0.5}/pyproject.toml +1 -1
- {openhands-1.0.3 → openhands-1.0.5}/tests/commands/test_new_command.py +34 -35
- openhands-1.0.5/tests/commands/test_resume_command.py +147 -0
- openhands-1.0.5/tests/commands/test_settings_command.py +57 -0
- {openhands-1.0.3 → openhands-1.0.5}/tests/settings/test_settings_workflow.py +32 -0
- {openhands-1.0.3 → openhands-1.0.5}/tests/test_confirmation_mode.py +3 -2
- {openhands-1.0.3 → openhands-1.0.5}/tests/test_gui_launcher.py +1 -1
- {openhands-1.0.3 → openhands-1.0.5}/uv.lock +1 -1
- {openhands-1.0.3 → openhands-1.0.5}/.gitignore +0 -0
- {openhands-1.0.3 → openhands-1.0.5}/Makefile +0 -0
- {openhands-1.0.3 → openhands-1.0.5}/README.md +0 -0
- {openhands-1.0.3 → openhands-1.0.5}/build.sh +0 -0
- {openhands-1.0.3 → openhands-1.0.5}/hooks/rthook_profile_imports.py +0 -0
- {openhands-1.0.3 → openhands-1.0.5}/openhands.spec +0 -0
- {openhands-1.0.3 → openhands-1.0.5}/openhands_cli/__init__.py +0 -0
- {openhands-1.0.3 → openhands-1.0.5}/openhands_cli/argparsers/main_parser.py +0 -0
- {openhands-1.0.3 → openhands-1.0.5}/openhands_cli/argparsers/serve_parser.py +0 -0
- {openhands-1.0.3 → openhands-1.0.5}/openhands_cli/listeners/__init__.py +0 -0
- {openhands-1.0.3 → openhands-1.0.5}/openhands_cli/listeners/loading_listener.py +0 -0
- {openhands-1.0.3 → openhands-1.0.5}/openhands_cli/listeners/pause_listener.py +0 -0
- {openhands-1.0.3 → openhands-1.0.5}/openhands_cli/locations.py +0 -0
- {openhands-1.0.3 → openhands-1.0.5}/openhands_cli/pt_style.py +0 -0
- {openhands-1.0.3 → openhands-1.0.5}/openhands_cli/runner.py +0 -0
- {openhands-1.0.3 → openhands-1.0.5}/openhands_cli/simple_main.py +0 -0
- {openhands-1.0.3 → openhands-1.0.5}/openhands_cli/tui/__init__.py +0 -0
- {openhands-1.0.3 → openhands-1.0.5}/openhands_cli/tui/settings/mcp_screen.py +0 -0
- {openhands-1.0.3 → openhands-1.0.5}/openhands_cli/tui/settings/store.py +0 -0
- {openhands-1.0.3 → openhands-1.0.5}/openhands_cli/tui/status.py +0 -0
- {openhands-1.0.3 → openhands-1.0.5}/openhands_cli/tui/tui.py +0 -0
- {openhands-1.0.3 → openhands-1.0.5}/openhands_cli/tui/utils.py +0 -0
- {openhands-1.0.3 → openhands-1.0.5}/openhands_cli/user_actions/__init__.py +0 -0
- {openhands-1.0.3 → openhands-1.0.5}/openhands_cli/user_actions/agent_action.py +0 -0
- {openhands-1.0.3 → openhands-1.0.5}/openhands_cli/user_actions/exit_session.py +0 -0
- {openhands-1.0.3 → openhands-1.0.5}/openhands_cli/user_actions/settings_action.py +0 -0
- {openhands-1.0.3 → openhands-1.0.5}/openhands_cli/user_actions/types.py +0 -0
- {openhands-1.0.3 → openhands-1.0.5}/openhands_cli/user_actions/utils.py +0 -0
- {openhands-1.0.3 → openhands-1.0.5}/openhands_cli/utils.py +0 -0
- {openhands-1.0.3 → openhands-1.0.5}/tests/__init__.py +0 -0
- {openhands-1.0.3 → openhands-1.0.5}/tests/commands/test_confirm_command.py +0 -0
- {openhands-1.0.3 → openhands-1.0.5}/tests/commands/test_status_command.py +0 -0
- {openhands-1.0.3 → openhands-1.0.5}/tests/conftest.py +0 -0
- {openhands-1.0.3 → openhands-1.0.5}/tests/settings/test_api_key_preservation.py +0 -0
- {openhands-1.0.3 → openhands-1.0.5}/tests/settings/test_default_agent_security_analyzer.py +0 -0
- {openhands-1.0.3 → openhands-1.0.5}/tests/settings/test_first_time_user_settings.py +0 -0
- {openhands-1.0.3 → openhands-1.0.5}/tests/settings/test_settings_input.py +0 -0
- {openhands-1.0.3 → openhands-1.0.5}/tests/test_conversation_runner.py +0 -0
- {openhands-1.0.3 → openhands-1.0.5}/tests/test_directory_separation.py +0 -0
- {openhands-1.0.3 → openhands-1.0.5}/tests/test_exit_session_confirmation.py +0 -0
- {openhands-1.0.3 → openhands-1.0.5}/tests/test_loading.py +0 -0
- {openhands-1.0.3 → openhands-1.0.5}/tests/test_main.py +0 -0
- {openhands-1.0.3 → openhands-1.0.5}/tests/test_mcp_config_validation.py +0 -0
- {openhands-1.0.3 → openhands-1.0.5}/tests/test_pause_listener.py +0 -0
- {openhands-1.0.3 → openhands-1.0.5}/tests/test_session_prompter.py +0 -0
- {openhands-1.0.3 → openhands-1.0.5}/tests/test_tui.py +0 -0
- {openhands-1.0.3 → openhands-1.0.5}/tests/utils.py +0 -0
|
@@ -20,15 +20,6 @@ from openhands_cli.locations import AGENT_SETTINGS_PATH, PERSISTENCE_DIR
|
|
|
20
20
|
|
|
21
21
|
from openhands.sdk import LLM
|
|
22
22
|
|
|
23
|
-
dummy_agent = get_default_cli_agent(
|
|
24
|
-
llm=LLM(
|
|
25
|
-
model='dummy-model',
|
|
26
|
-
api_key='dummy-key',
|
|
27
|
-
metadata=get_llm_metadata(model_name='dummy-model', llm_type='openhands'),
|
|
28
|
-
),
|
|
29
|
-
cli_mode=True,
|
|
30
|
-
)
|
|
31
|
-
|
|
32
23
|
# =================================================
|
|
33
24
|
# SECTION: Build Binary
|
|
34
25
|
# =================================================
|
|
@@ -126,7 +117,7 @@ def _is_welcome(line: str) -> bool:
|
|
|
126
117
|
return any(marker in s for marker in WELCOME_MARKERS)
|
|
127
118
|
|
|
128
119
|
|
|
129
|
-
def test_executable() -> bool:
|
|
120
|
+
def test_executable(dummy_agent) -> bool:
|
|
130
121
|
"""Test the built executable, measuring boot time and total test time."""
|
|
131
122
|
print('🧪 Testing the built executable...')
|
|
132
123
|
|
|
@@ -274,7 +265,14 @@ def main() -> int:
|
|
|
274
265
|
|
|
275
266
|
# Test the executable
|
|
276
267
|
if not args.no_test:
|
|
277
|
-
|
|
268
|
+
dummy_agent = get_default_cli_agent(
|
|
269
|
+
llm=LLM(
|
|
270
|
+
model='dummy-model',
|
|
271
|
+
api_key='dummy-key',
|
|
272
|
+
metadata=get_llm_metadata(model_name='dummy-model', llm_type='openhands'),
|
|
273
|
+
)
|
|
274
|
+
)
|
|
275
|
+
if not test_executable(dummy_agent):
|
|
278
276
|
print('❌ Executable test failed, build process failed')
|
|
279
277
|
return 1
|
|
280
278
|
|
|
@@ -285,4 +283,10 @@ def main() -> int:
|
|
|
285
283
|
|
|
286
284
|
|
|
287
285
|
if __name__ == '__main__':
|
|
288
|
-
|
|
286
|
+
try:
|
|
287
|
+
sys.exit(main())
|
|
288
|
+
except Exception as e:
|
|
289
|
+
print(e)
|
|
290
|
+
print('❌ Executable test failed')
|
|
291
|
+
sys.exit(1)
|
|
292
|
+
|
|
@@ -6,6 +6,7 @@ Provides a conversation interface with an AI agent using OpenHands patterns.
|
|
|
6
6
|
|
|
7
7
|
import sys
|
|
8
8
|
from datetime import datetime
|
|
9
|
+
import uuid
|
|
9
10
|
|
|
10
11
|
from openhands.sdk import (
|
|
11
12
|
Message,
|
|
@@ -16,7 +17,11 @@ from prompt_toolkit import print_formatted_text
|
|
|
16
17
|
from prompt_toolkit.formatted_text import HTML
|
|
17
18
|
|
|
18
19
|
from openhands_cli.runner import ConversationRunner
|
|
19
|
-
from openhands_cli.setup import
|
|
20
|
+
from openhands_cli.setup import (
|
|
21
|
+
MissingAgentSpec,
|
|
22
|
+
setup_conversation,
|
|
23
|
+
verify_agent_exists_or_setup_agent
|
|
24
|
+
)
|
|
20
25
|
from openhands_cli.tui.settings.mcp_screen import MCPScreen
|
|
21
26
|
from openhands_cli.tui.settings.settings_screen import SettingsScreen
|
|
22
27
|
from openhands_cli.tui.status import display_status
|
|
@@ -65,21 +70,33 @@ def run_cli_entry(resume_conversation_id: str | None = None) -> None:
|
|
|
65
70
|
EOFError: If EOF is encountered
|
|
66
71
|
"""
|
|
67
72
|
|
|
73
|
+
conversation_id = uuid.uuid4()
|
|
74
|
+
if resume_conversation_id:
|
|
75
|
+
try:
|
|
76
|
+
conversation_id = uuid.UUID(resume_conversation_id)
|
|
77
|
+
except ValueError as e:
|
|
78
|
+
print_formatted_text(
|
|
79
|
+
HTML(
|
|
80
|
+
f"<yellow>Warning: '{resume_conversation_id}' is not a valid UUID.</yellow>"
|
|
81
|
+
)
|
|
82
|
+
)
|
|
83
|
+
return
|
|
84
|
+
|
|
68
85
|
try:
|
|
69
|
-
|
|
86
|
+
initialized_agent = verify_agent_exists_or_setup_agent()
|
|
70
87
|
except MissingAgentSpec:
|
|
71
88
|
print_formatted_text(HTML('\n<yellow>Setup is required to use OpenHands CLI.</yellow>'))
|
|
72
89
|
print_formatted_text(HTML('\n<yellow>Goodbye! 👋</yellow>'))
|
|
73
90
|
return
|
|
74
91
|
|
|
75
92
|
|
|
76
|
-
display_welcome(
|
|
93
|
+
display_welcome(conversation_id, bool(resume_conversation_id))
|
|
77
94
|
|
|
78
95
|
# Track session start time for uptime calculation
|
|
79
96
|
session_start_time = datetime.now()
|
|
80
97
|
|
|
81
98
|
# Create conversation runner to handle state machine logic
|
|
82
|
-
runner =
|
|
99
|
+
runner = None
|
|
83
100
|
session = get_session_prompter()
|
|
84
101
|
|
|
85
102
|
# Main chat loop
|
|
@@ -106,29 +123,29 @@ def run_cli_entry(resume_conversation_id: str | None = None) -> None:
|
|
|
106
123
|
exit_confirmation = exit_session_confirmation()
|
|
107
124
|
if exit_confirmation == UserConfirmation.ACCEPT:
|
|
108
125
|
print_formatted_text(HTML('\n<yellow>Goodbye! 👋</yellow>'))
|
|
109
|
-
_print_exit_hint(
|
|
126
|
+
_print_exit_hint(conversation_id)
|
|
110
127
|
break
|
|
111
128
|
|
|
112
129
|
elif command == '/settings':
|
|
113
|
-
settings_screen = SettingsScreen(conversation)
|
|
130
|
+
settings_screen = SettingsScreen(runner.conversation if runner else None)
|
|
114
131
|
settings_screen.display_settings()
|
|
115
132
|
continue
|
|
116
133
|
|
|
117
134
|
elif command == '/mcp':
|
|
118
135
|
mcp_screen = MCPScreen()
|
|
119
|
-
mcp_screen.display_mcp_info(
|
|
136
|
+
mcp_screen.display_mcp_info(initialized_agent)
|
|
120
137
|
continue
|
|
121
138
|
|
|
122
139
|
elif command == '/clear':
|
|
123
|
-
display_welcome(
|
|
140
|
+
display_welcome(conversation_id)
|
|
124
141
|
continue
|
|
125
142
|
|
|
126
143
|
elif command == '/new':
|
|
127
144
|
try:
|
|
128
145
|
# Start a fresh conversation (no resume ID = new conversation)
|
|
129
|
-
conversation = setup_conversation()
|
|
146
|
+
conversation = setup_conversation(conversation_id)
|
|
130
147
|
runner = ConversationRunner(conversation)
|
|
131
|
-
display_welcome(
|
|
148
|
+
display_welcome(conversation_id, resume=False)
|
|
132
149
|
print_formatted_text(
|
|
133
150
|
HTML('<green>✓ Started fresh conversation</green>')
|
|
134
151
|
)
|
|
@@ -158,6 +175,13 @@ def run_cli_entry(resume_conversation_id: str | None = None) -> None:
|
|
|
158
175
|
continue
|
|
159
176
|
|
|
160
177
|
elif command == '/resume':
|
|
178
|
+
if not runner:
|
|
179
|
+
print_formatted_text(
|
|
180
|
+
HTML('<yellow>No active conversation running...</yellow>')
|
|
181
|
+
)
|
|
182
|
+
continue
|
|
183
|
+
|
|
184
|
+
conversation = runner.conversation
|
|
161
185
|
if not (
|
|
162
186
|
conversation.state.agent_status == AgentExecutionStatus.PAUSED
|
|
163
187
|
or conversation.state.agent_status
|
|
@@ -171,6 +195,9 @@ def run_cli_entry(resume_conversation_id: str | None = None) -> None:
|
|
|
171
195
|
# Resume without new message
|
|
172
196
|
message = None
|
|
173
197
|
|
|
198
|
+
if not runner:
|
|
199
|
+
conversation = setup_conversation(conversation_id)
|
|
200
|
+
runner = ConversationRunner(conversation)
|
|
174
201
|
runner.process_message(message)
|
|
175
202
|
|
|
176
203
|
print() # Add spacing
|
|
@@ -179,7 +206,7 @@ def run_cli_entry(resume_conversation_id: str | None = None) -> None:
|
|
|
179
206
|
exit_confirmation = exit_session_confirmation()
|
|
180
207
|
if exit_confirmation == UserConfirmation.ACCEPT:
|
|
181
208
|
print_formatted_text(HTML('\n<yellow>Goodbye! 👋</yellow>'))
|
|
182
|
-
_print_exit_hint(
|
|
209
|
+
_print_exit_hint(conversation_id)
|
|
183
210
|
break
|
|
184
211
|
|
|
185
212
|
# Clean up terminal state
|
|
@@ -104,8 +104,8 @@ def launch_gui_server(mount_cwd: bool = False, gpu: bool = False) -> None:
|
|
|
104
104
|
|
|
105
105
|
# Get the current version for the Docker image
|
|
106
106
|
version = get_openhands_version()
|
|
107
|
-
runtime_image = f'docker.
|
|
108
|
-
app_image = f'docker.
|
|
107
|
+
runtime_image = f'docker.openhands.dev/openhands/runtime:{version}-nikolaik'
|
|
108
|
+
app_image = f'docker.openhands.dev/openhands/openhands:{version}'
|
|
109
109
|
|
|
110
110
|
print_formatted_text(HTML('<grey>Pulling required Docker images...</grey>'))
|
|
111
111
|
|
|
@@ -2,7 +2,7 @@ import uuid
|
|
|
2
2
|
|
|
3
3
|
from prompt_toolkit import HTML, print_formatted_text
|
|
4
4
|
|
|
5
|
-
from openhands.sdk import BaseConversation, Conversation, Workspace, register_tool
|
|
5
|
+
from openhands.sdk import Agent, BaseConversation, Conversation, Workspace, register_tool
|
|
6
6
|
from openhands.tools.execute_bash import BashTool
|
|
7
7
|
from openhands.tools.file_editor import FileEditorTool
|
|
8
8
|
from openhands.tools.task_tracker import TaskTrackerTool
|
|
@@ -26,8 +26,38 @@ class MissingAgentSpec(Exception):
|
|
|
26
26
|
pass
|
|
27
27
|
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
|
|
30
|
+
def load_agent_specs(
|
|
30
31
|
conversation_id: str | None = None,
|
|
32
|
+
) -> Agent:
|
|
33
|
+
agent_store = AgentStore()
|
|
34
|
+
agent = agent_store.load(session_id=conversation_id)
|
|
35
|
+
if not agent:
|
|
36
|
+
raise MissingAgentSpec(
|
|
37
|
+
'Agent specification not found. Please configure your agent settings.'
|
|
38
|
+
)
|
|
39
|
+
return agent
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def verify_agent_exists_or_setup_agent() -> Agent:
|
|
43
|
+
"""Verify agent specs exists by attempting to load it.
|
|
44
|
+
|
|
45
|
+
"""
|
|
46
|
+
settings_screen = SettingsScreen()
|
|
47
|
+
try:
|
|
48
|
+
agent = load_agent_specs()
|
|
49
|
+
return agent
|
|
50
|
+
except MissingAgentSpec:
|
|
51
|
+
# For first-time users, show the full settings flow with choice between basic/advanced
|
|
52
|
+
settings_screen.configure_settings(first_time=True)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# Try once again after settings setup attempt
|
|
56
|
+
return load_agent_specs()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def setup_conversation(
|
|
60
|
+
conversation_id: uuid,
|
|
31
61
|
include_security_analyzer: bool = True
|
|
32
62
|
) -> BaseConversation:
|
|
33
63
|
"""
|
|
@@ -40,28 +70,8 @@ def setup_conversation(
|
|
|
40
70
|
MissingAgentSpec: If agent specification is not found or invalid.
|
|
41
71
|
"""
|
|
42
72
|
|
|
43
|
-
# Use provided conversation_id or generate a random one
|
|
44
|
-
if conversation_id is None:
|
|
45
|
-
conversation_id = uuid.uuid4()
|
|
46
|
-
elif isinstance(conversation_id, str):
|
|
47
|
-
try:
|
|
48
|
-
conversation_id = uuid.UUID(conversation_id)
|
|
49
|
-
except ValueError as e:
|
|
50
|
-
print_formatted_text(
|
|
51
|
-
HTML(
|
|
52
|
-
f"<yellow>Warning: '{conversation_id}' is not a valid UUID.</yellow>"
|
|
53
|
-
)
|
|
54
|
-
)
|
|
55
|
-
raise e
|
|
56
|
-
|
|
57
73
|
with LoadingContext('Initializing OpenHands agent...'):
|
|
58
|
-
|
|
59
|
-
agent = agent_store.load(session_id=str(conversation_id))
|
|
60
|
-
if not agent:
|
|
61
|
-
raise MissingAgentSpec(
|
|
62
|
-
'Agent specification not found. Please configure your agent settings.'
|
|
63
|
-
)
|
|
64
|
-
|
|
74
|
+
agent = load_agent_specs(str(conversation_id))
|
|
65
75
|
|
|
66
76
|
if not include_security_analyzer:
|
|
67
77
|
# Remove security analyzer from agent spec
|
|
@@ -86,31 +96,3 @@ def setup_conversation(
|
|
|
86
96
|
)
|
|
87
97
|
return conversation
|
|
88
98
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
def start_fresh_conversation(
|
|
92
|
-
resume_conversation_id: str | None = None
|
|
93
|
-
) -> BaseConversation:
|
|
94
|
-
"""Start a fresh conversation by creating a new conversation instance.
|
|
95
|
-
|
|
96
|
-
Handles the complete conversation setup process including settings screen
|
|
97
|
-
if agent configuration is missing.
|
|
98
|
-
|
|
99
|
-
Args:
|
|
100
|
-
resume_conversation_id: Optional conversation ID to resume
|
|
101
|
-
|
|
102
|
-
Returns:
|
|
103
|
-
BaseConversation: A new conversation instance
|
|
104
|
-
"""
|
|
105
|
-
conversation = None
|
|
106
|
-
settings_screen = SettingsScreen()
|
|
107
|
-
try:
|
|
108
|
-
conversation = setup_conversation(resume_conversation_id)
|
|
109
|
-
return conversation
|
|
110
|
-
except MissingAgentSpec:
|
|
111
|
-
# For first-time users, show the full settings flow with choice between basic/advanced
|
|
112
|
-
settings_screen.configure_settings(first_time=True)
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
# Try once again after settings setup attempt
|
|
116
|
-
return setup_conversation(resume_conversation_id)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import os
|
|
2
2
|
|
|
3
|
-
from openhands.sdk import LLM, BaseConversation, LocalFileStore
|
|
3
|
+
from openhands.sdk import LLM, BaseConversation, LLMSummarizingCondenser, LocalFileStore
|
|
4
4
|
from prompt_toolkit import HTML, print_formatted_text
|
|
5
5
|
from prompt_toolkit.shortcuts import print_container
|
|
6
6
|
from prompt_toolkit.widgets import Frame, TextArea
|
|
@@ -33,9 +33,6 @@ class SettingsScreen:
|
|
|
33
33
|
agent_spec = self.agent_store.load()
|
|
34
34
|
if not agent_spec:
|
|
35
35
|
return
|
|
36
|
-
assert self.conversation is not None, (
|
|
37
|
-
'Conversation must be set to display settings.'
|
|
38
|
-
)
|
|
39
36
|
|
|
40
37
|
llm = agent_spec.llm
|
|
41
38
|
advanced_llm_settings = True if llm.base_url else False
|
|
@@ -62,12 +59,20 @@ class SettingsScreen:
|
|
|
62
59
|
labels_and_values.extend(
|
|
63
60
|
[
|
|
64
61
|
(' API Key', '********' if llm.api_key else 'Not Set'),
|
|
62
|
+
]
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
if self.conversation:
|
|
66
|
+
labels_and_values.extend([
|
|
65
67
|
(
|
|
66
68
|
' Confirmation Mode',
|
|
67
69
|
'Enabled'
|
|
68
70
|
if self.conversation.is_confirmation_mode_active
|
|
69
71
|
else 'Disabled',
|
|
70
|
-
)
|
|
72
|
+
)
|
|
73
|
+
])
|
|
74
|
+
|
|
75
|
+
labels_and_values.extend([
|
|
71
76
|
(
|
|
72
77
|
' Memory Condensation',
|
|
73
78
|
'Enabled' if agent_spec.condenser else 'Disabled',
|
|
@@ -153,7 +158,7 @@ class SettingsScreen:
|
|
|
153
158
|
api_key = prompt_api_key(
|
|
154
159
|
step_counter,
|
|
155
160
|
custom_model.split('/')[0] if len(custom_model.split('/')) > 1 else '',
|
|
156
|
-
self.conversation.agent.llm.api_key if self.conversation else None,
|
|
161
|
+
self.conversation.state.agent.llm.api_key if self.conversation else None,
|
|
157
162
|
escapable=escapable,
|
|
158
163
|
)
|
|
159
164
|
memory_condensation = choose_memory_condensation(step_counter)
|
|
@@ -182,7 +187,14 @@ class SettingsScreen:
|
|
|
182
187
|
if not agent:
|
|
183
188
|
agent = get_default_cli_agent(llm=llm)
|
|
184
189
|
|
|
190
|
+
# Must update all LLMs
|
|
185
191
|
agent = agent.model_copy(update={'llm': llm})
|
|
192
|
+
condenser = LLMSummarizingCondenser(
|
|
193
|
+
llm=llm.model_copy(
|
|
194
|
+
update={"usage_id": "condenser"}
|
|
195
|
+
)
|
|
196
|
+
)
|
|
197
|
+
agent = agent.model_copy(update={'condenser': condenser})
|
|
186
198
|
self.agent_store.save(agent)
|
|
187
199
|
|
|
188
200
|
def _save_advanced_settings(
|
|
@@ -4,51 +4,49 @@ from unittest.mock import MagicMock, patch
|
|
|
4
4
|
from uuid import UUID
|
|
5
5
|
from prompt_toolkit.input.defaults import create_pipe_input
|
|
6
6
|
from prompt_toolkit.output.defaults import DummyOutput
|
|
7
|
-
from openhands_cli.setup import MissingAgentSpec,
|
|
7
|
+
from openhands_cli.setup import MissingAgentSpec, verify_agent_exists_or_setup_agent, setup_conversation
|
|
8
8
|
from openhands_cli.user_actions import UserConfirmation
|
|
9
9
|
|
|
10
|
-
@patch('openhands_cli.setup.
|
|
11
|
-
def
|
|
12
|
-
"""Test that
|
|
13
|
-
# Mock the
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
mock_setup_conversation.return_value = mock_conversation
|
|
10
|
+
@patch('openhands_cli.setup.load_agent_specs')
|
|
11
|
+
def test_verify_agent_exists_or_setup_agent_success(mock_load_agent_specs):
|
|
12
|
+
"""Test that verify_agent_exists_or_setup_agent returns agent successfully."""
|
|
13
|
+
# Mock the agent object
|
|
14
|
+
mock_agent = MagicMock()
|
|
15
|
+
mock_load_agent_specs.return_value = mock_agent
|
|
17
16
|
|
|
18
17
|
# Call the function
|
|
19
|
-
result =
|
|
18
|
+
result = verify_agent_exists_or_setup_agent()
|
|
20
19
|
|
|
21
20
|
# Verify the result
|
|
22
|
-
assert result ==
|
|
23
|
-
|
|
21
|
+
assert result == mock_agent
|
|
22
|
+
mock_load_agent_specs.assert_called_once_with()
|
|
24
23
|
|
|
25
24
|
|
|
26
25
|
@patch('openhands_cli.setup.SettingsScreen')
|
|
27
|
-
@patch('openhands_cli.setup.
|
|
28
|
-
def
|
|
29
|
-
|
|
26
|
+
@patch('openhands_cli.setup.load_agent_specs')
|
|
27
|
+
def test_verify_agent_exists_or_setup_agent_missing_agent_spec(
|
|
28
|
+
mock_load_agent_specs,
|
|
30
29
|
mock_settings_screen_class
|
|
31
30
|
):
|
|
32
|
-
"""Test that
|
|
31
|
+
"""Test that verify_agent_exists_or_setup_agent handles MissingAgentSpec exception."""
|
|
33
32
|
# Mock the SettingsScreen instance
|
|
34
33
|
mock_settings_screen = MagicMock()
|
|
35
34
|
mock_settings_screen_class.return_value = mock_settings_screen
|
|
36
35
|
|
|
37
|
-
# Mock
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
mock_setup_conversation.side_effect = [
|
|
36
|
+
# Mock load_agent_specs to raise MissingAgentSpec on first call, then succeed
|
|
37
|
+
mock_agent = MagicMock()
|
|
38
|
+
mock_load_agent_specs.side_effect = [
|
|
41
39
|
MissingAgentSpec("Agent spec missing"),
|
|
42
|
-
|
|
40
|
+
mock_agent
|
|
43
41
|
]
|
|
44
42
|
|
|
45
43
|
# Call the function
|
|
46
|
-
result =
|
|
44
|
+
result = verify_agent_exists_or_setup_agent()
|
|
47
45
|
|
|
48
46
|
# Verify the result
|
|
49
|
-
assert result ==
|
|
47
|
+
assert result == mock_agent
|
|
50
48
|
# Should be called twice: first fails, second succeeds
|
|
51
|
-
assert
|
|
49
|
+
assert mock_load_agent_specs.call_count == 2
|
|
52
50
|
# Settings screen should be called once with first_time=True (new behavior)
|
|
53
51
|
mock_settings_screen.configure_settings.assert_called_once_with(first_time=True)
|
|
54
52
|
|
|
@@ -59,11 +57,11 @@ def test_start_fresh_conversation_missing_agent_spec(
|
|
|
59
57
|
@patch('openhands_cli.agent_chat.exit_session_confirmation')
|
|
60
58
|
@patch('openhands_cli.agent_chat.get_session_prompter')
|
|
61
59
|
@patch('openhands_cli.agent_chat.setup_conversation')
|
|
62
|
-
@patch('openhands_cli.agent_chat.
|
|
60
|
+
@patch('openhands_cli.agent_chat.verify_agent_exists_or_setup_agent')
|
|
63
61
|
@patch('openhands_cli.agent_chat.ConversationRunner')
|
|
64
62
|
def test_new_command_resets_confirmation_mode(
|
|
65
63
|
mock_runner_cls,
|
|
66
|
-
|
|
64
|
+
mock_verify_agent,
|
|
67
65
|
mock_setup_conversation,
|
|
68
66
|
mock_get_session_prompter,
|
|
69
67
|
mock_exit_confirm,
|
|
@@ -71,15 +69,17 @@ def test_new_command_resets_confirmation_mode(
|
|
|
71
69
|
# Auto-accept the exit prompt to avoid interactive UI and EOFError
|
|
72
70
|
mock_exit_confirm.return_value = UserConfirmation.ACCEPT
|
|
73
71
|
|
|
72
|
+
# Mock agent verification to succeed
|
|
73
|
+
mock_agent = MagicMock()
|
|
74
|
+
mock_verify_agent.return_value = mock_agent
|
|
75
|
+
|
|
76
|
+
# Mock conversation - only one is created when /new is called
|
|
74
77
|
conv1 = MagicMock(); conv1.id = UUID('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa')
|
|
75
|
-
|
|
76
|
-
mock_start_fresh_conversation.return_value = conv1
|
|
77
|
-
mock_setup_conversation.side_effect = [conv2]
|
|
78
|
+
mock_setup_conversation.return_value = conv1
|
|
78
79
|
|
|
79
|
-
#
|
|
80
|
+
# One runner instance for the conversation
|
|
80
81
|
runner1 = MagicMock(); runner1.is_confirmation_mode_active = True
|
|
81
|
-
|
|
82
|
-
mock_runner_cls.side_effect = [runner1, runner2]
|
|
82
|
+
mock_runner_cls.return_value = runner1
|
|
83
83
|
|
|
84
84
|
# Real session fed by a pipe (no interactive confirmation now)
|
|
85
85
|
from openhands_cli.user_actions.utils import get_session_prompter as real_get_session_prompter
|
|
@@ -89,13 +89,12 @@ def test_new_command_resets_confirmation_mode(
|
|
|
89
89
|
mock_get_session_prompter.return_value = session
|
|
90
90
|
|
|
91
91
|
from openhands_cli.agent_chat import run_cli_entry
|
|
92
|
-
# Trigger /new, then /
|
|
92
|
+
# Trigger /new, then /exit (exit will be auto-accepted)
|
|
93
93
|
for ch in "/new\r/exit\r":
|
|
94
94
|
pipe.send_text(ch)
|
|
95
95
|
|
|
96
96
|
run_cli_entry(None)
|
|
97
97
|
|
|
98
|
-
# Assert we
|
|
99
|
-
assert mock_runner_cls.call_count ==
|
|
98
|
+
# Assert we created one runner for the conversation when /new was called
|
|
99
|
+
assert mock_runner_cls.call_count == 1
|
|
100
100
|
assert mock_runner_cls.call_args_list[0].args[0] is conv1
|
|
101
|
-
assert mock_runner_cls.call_args_list[1].args[0] is conv2
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""Tests for the /resume command functionality."""
|
|
2
|
+
|
|
3
|
+
from unittest.mock import MagicMock, patch
|
|
4
|
+
from uuid import UUID
|
|
5
|
+
import pytest
|
|
6
|
+
from prompt_toolkit.input.defaults import create_pipe_input
|
|
7
|
+
from prompt_toolkit.output.defaults import DummyOutput
|
|
8
|
+
|
|
9
|
+
from openhands.sdk.conversation.state import AgentExecutionStatus
|
|
10
|
+
from openhands_cli.user_actions import UserConfirmation
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# ---------- Fixtures & helpers ----------
|
|
14
|
+
|
|
15
|
+
@pytest.fixture
|
|
16
|
+
def mock_agent():
|
|
17
|
+
"""Mock agent for verification."""
|
|
18
|
+
return MagicMock()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@pytest.fixture
|
|
22
|
+
def mock_conversation():
|
|
23
|
+
"""Mock conversation with default settings."""
|
|
24
|
+
conv = MagicMock()
|
|
25
|
+
conv.id = UUID('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa')
|
|
26
|
+
return conv
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@pytest.fixture
|
|
30
|
+
def mock_runner():
|
|
31
|
+
"""Mock conversation runner."""
|
|
32
|
+
return MagicMock()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def run_resume_command_test(commands, agent_status=None, expect_runner_created=True):
|
|
36
|
+
"""Helper function to run resume command tests with common setup."""
|
|
37
|
+
with patch('openhands_cli.agent_chat.exit_session_confirmation') as mock_exit_confirm, \
|
|
38
|
+
patch('openhands_cli.agent_chat.get_session_prompter') as mock_get_session_prompter, \
|
|
39
|
+
patch('openhands_cli.agent_chat.setup_conversation') as mock_setup_conversation, \
|
|
40
|
+
patch('openhands_cli.agent_chat.verify_agent_exists_or_setup_agent') as mock_verify_agent, \
|
|
41
|
+
patch('openhands_cli.agent_chat.ConversationRunner') as mock_runner_cls:
|
|
42
|
+
|
|
43
|
+
# Auto-accept the exit prompt to avoid interactive UI
|
|
44
|
+
mock_exit_confirm.return_value = UserConfirmation.ACCEPT
|
|
45
|
+
|
|
46
|
+
# Mock agent verification to succeed
|
|
47
|
+
mock_agent = MagicMock()
|
|
48
|
+
mock_verify_agent.return_value = mock_agent
|
|
49
|
+
|
|
50
|
+
# Mock conversation setup
|
|
51
|
+
conv = MagicMock()
|
|
52
|
+
conv.id = UUID('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa')
|
|
53
|
+
if agent_status:
|
|
54
|
+
conv.state.agent_status = agent_status
|
|
55
|
+
mock_setup_conversation.return_value = conv
|
|
56
|
+
|
|
57
|
+
# Mock runner
|
|
58
|
+
runner = MagicMock()
|
|
59
|
+
runner.conversation = conv
|
|
60
|
+
mock_runner_cls.return_value = runner
|
|
61
|
+
|
|
62
|
+
# Real session fed by a pipe
|
|
63
|
+
from openhands_cli.user_actions.utils import get_session_prompter as real_get_session_prompter
|
|
64
|
+
with create_pipe_input() as pipe:
|
|
65
|
+
output = DummyOutput()
|
|
66
|
+
session = real_get_session_prompter(input=pipe, output=output)
|
|
67
|
+
mock_get_session_prompter.return_value = session
|
|
68
|
+
|
|
69
|
+
from openhands_cli.agent_chat import run_cli_entry
|
|
70
|
+
|
|
71
|
+
# Send commands
|
|
72
|
+
for ch in commands:
|
|
73
|
+
pipe.send_text(ch)
|
|
74
|
+
|
|
75
|
+
# Capture printed output
|
|
76
|
+
with patch('openhands_cli.agent_chat.print_formatted_text') as mock_print:
|
|
77
|
+
run_cli_entry(None)
|
|
78
|
+
|
|
79
|
+
return mock_runner_cls, runner, mock_print
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# ---------- Warning tests (parametrized) ----------
|
|
83
|
+
|
|
84
|
+
@pytest.mark.parametrize(
|
|
85
|
+
"commands,expected_warning,expect_runner_created",
|
|
86
|
+
[
|
|
87
|
+
# No active conversation - /resume immediately
|
|
88
|
+
("/resume\r/exit\r", "No active conversation running", False),
|
|
89
|
+
# Conversation exists but not in paused state - send message first, then /resume
|
|
90
|
+
("hello\r/resume\r/exit\r", "No paused conversation to resume", True),
|
|
91
|
+
],
|
|
92
|
+
)
|
|
93
|
+
def test_resume_command_warnings(commands, expected_warning, expect_runner_created):
|
|
94
|
+
"""Test /resume command shows appropriate warnings."""
|
|
95
|
+
# Set agent status to FINISHED for the "conversation exists but not paused" test
|
|
96
|
+
agent_status = AgentExecutionStatus.FINISHED if expect_runner_created else None
|
|
97
|
+
|
|
98
|
+
mock_runner_cls, runner, mock_print = run_resume_command_test(
|
|
99
|
+
commands, agent_status=agent_status, expect_runner_created=expect_runner_created
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
# Verify warning message was printed
|
|
103
|
+
warning_calls = [call for call in mock_print.call_args_list
|
|
104
|
+
if expected_warning in str(call)]
|
|
105
|
+
assert len(warning_calls) > 0, f"Expected warning about {expected_warning}"
|
|
106
|
+
|
|
107
|
+
# Verify runner creation expectation
|
|
108
|
+
if expect_runner_created:
|
|
109
|
+
assert mock_runner_cls.call_count == 1
|
|
110
|
+
runner.process_message.assert_called()
|
|
111
|
+
else:
|
|
112
|
+
assert mock_runner_cls.call_count == 0
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# ---------- Successful resume tests (parametrized) ----------
|
|
116
|
+
|
|
117
|
+
@pytest.mark.parametrize(
|
|
118
|
+
"agent_status",
|
|
119
|
+
[
|
|
120
|
+
AgentExecutionStatus.PAUSED,
|
|
121
|
+
AgentExecutionStatus.WAITING_FOR_CONFIRMATION,
|
|
122
|
+
],
|
|
123
|
+
)
|
|
124
|
+
def test_resume_command_successful_resume(agent_status):
|
|
125
|
+
"""Test /resume command successfully resumes paused/waiting conversations."""
|
|
126
|
+
commands = "hello\r/resume\r/exit\r"
|
|
127
|
+
|
|
128
|
+
mock_runner_cls, runner, mock_print = run_resume_command_test(
|
|
129
|
+
commands, agent_status=agent_status, expect_runner_created=True
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# Verify runner was created and process_message was called
|
|
133
|
+
assert mock_runner_cls.call_count == 1
|
|
134
|
+
|
|
135
|
+
# Verify process_message was called twice: once with the initial message, once with None for resume
|
|
136
|
+
assert runner.process_message.call_count == 2
|
|
137
|
+
|
|
138
|
+
# Check the calls to process_message
|
|
139
|
+
calls = runner.process_message.call_args_list
|
|
140
|
+
|
|
141
|
+
# First call should have a message (the "hello" message)
|
|
142
|
+
first_call_args = calls[0][0]
|
|
143
|
+
assert first_call_args[0] is not None, "First call should have a message"
|
|
144
|
+
|
|
145
|
+
# Second call should have None (the /resume command)
|
|
146
|
+
second_call_args = calls[1][0]
|
|
147
|
+
assert second_call_args[0] is None, "Second call should have None message for resume"
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Test for the /settings command functionality."""
|
|
2
|
+
|
|
3
|
+
from unittest.mock import MagicMock, patch
|
|
4
|
+
from prompt_toolkit.input.defaults import create_pipe_input
|
|
5
|
+
from prompt_toolkit.output.defaults import DummyOutput
|
|
6
|
+
|
|
7
|
+
from openhands_cli.agent_chat import run_cli_entry
|
|
8
|
+
from openhands_cli.user_actions import UserConfirmation
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@patch('openhands_cli.agent_chat.exit_session_confirmation')
|
|
12
|
+
@patch('openhands_cli.agent_chat.get_session_prompter')
|
|
13
|
+
@patch('openhands_cli.agent_chat.setup_conversation')
|
|
14
|
+
@patch('openhands_cli.agent_chat.verify_agent_exists_or_setup_agent')
|
|
15
|
+
@patch('openhands_cli.agent_chat.ConversationRunner')
|
|
16
|
+
@patch('openhands_cli.agent_chat.SettingsScreen')
|
|
17
|
+
def test_settings_command_works_without_conversation(
|
|
18
|
+
mock_settings_screen_class,
|
|
19
|
+
mock_runner_cls,
|
|
20
|
+
mock_verify_agent,
|
|
21
|
+
mock_setup_conversation,
|
|
22
|
+
mock_get_session_prompter,
|
|
23
|
+
mock_exit_confirm,
|
|
24
|
+
):
|
|
25
|
+
"""Test that /settings command works when no conversation is active (bug fix scenario)."""
|
|
26
|
+
# Auto-accept the exit prompt to avoid interactive UI
|
|
27
|
+
mock_exit_confirm.return_value = UserConfirmation.ACCEPT
|
|
28
|
+
|
|
29
|
+
# Mock agent verification to succeed
|
|
30
|
+
mock_agent = MagicMock()
|
|
31
|
+
mock_verify_agent.return_value = mock_agent
|
|
32
|
+
|
|
33
|
+
# Mock the SettingsScreen instance
|
|
34
|
+
mock_settings_screen = MagicMock()
|
|
35
|
+
mock_settings_screen_class.return_value = mock_settings_screen
|
|
36
|
+
|
|
37
|
+
# No runner initially (simulates starting CLI without a conversation)
|
|
38
|
+
mock_runner_cls.return_value = None
|
|
39
|
+
|
|
40
|
+
# Real session fed by a pipe
|
|
41
|
+
from openhands_cli.user_actions.utils import get_session_prompter as real_get_session_prompter
|
|
42
|
+
with create_pipe_input() as pipe:
|
|
43
|
+
output = DummyOutput()
|
|
44
|
+
session = real_get_session_prompter(input=pipe, output=output)
|
|
45
|
+
mock_get_session_prompter.return_value = session
|
|
46
|
+
|
|
47
|
+
# Trigger /settings, then /exit (exit will be auto-accepted)
|
|
48
|
+
for ch in "/settings\r/exit\r":
|
|
49
|
+
pipe.send_text(ch)
|
|
50
|
+
|
|
51
|
+
run_cli_entry(None)
|
|
52
|
+
|
|
53
|
+
# Assert SettingsScreen was created with None conversation (the bug fix)
|
|
54
|
+
mock_settings_screen_class.assert_called_once_with(None)
|
|
55
|
+
|
|
56
|
+
# Assert display_settings was called (settings screen was shown)
|
|
57
|
+
mock_settings_screen.display_settings.assert_called_once()
|
|
@@ -121,6 +121,38 @@ def test_update_existing_settings_workflow(tmp_path: Path):
|
|
|
121
121
|
assert True # If we get here, the workflow completed successfully
|
|
122
122
|
|
|
123
123
|
|
|
124
|
+
def test_all_llms_in_agent_are_updated():
|
|
125
|
+
"""Test that modifying LLM settings creates multiple LLMs with same API key but different usage_ids."""
|
|
126
|
+
# Create a screen with existing agent settings
|
|
127
|
+
screen = SettingsScreen(conversation=None)
|
|
128
|
+
initial_llm = LLM(model='openai/gpt-3.5-turbo', api_key=SecretStr('sk-initial'), usage_id='test-service')
|
|
129
|
+
initial_agent = get_default_cli_agent(llm=initial_llm)
|
|
130
|
+
|
|
131
|
+
# Mock the agent store to return the initial agent and capture the save call
|
|
132
|
+
with (
|
|
133
|
+
patch.object(screen.agent_store, 'load', return_value=initial_agent),
|
|
134
|
+
patch.object(screen.agent_store, 'save') as mock_save
|
|
135
|
+
):
|
|
136
|
+
# Modify the LLM settings with new API key
|
|
137
|
+
screen._save_llm_settings(model='openai/gpt-4o-mini', api_key='sk-updated-123')
|
|
138
|
+
mock_save.assert_called_once()
|
|
139
|
+
|
|
140
|
+
# Get the saved agent from the mock
|
|
141
|
+
saved_agent = mock_save.call_args[0][0]
|
|
142
|
+
all_llms = list(saved_agent.get_all_llms())
|
|
143
|
+
assert len(all_llms) >= 2, f"Expected at least 2 LLMs, got {len(all_llms)}"
|
|
144
|
+
|
|
145
|
+
# Verify all LLMs have the same API key
|
|
146
|
+
api_keys = [llm.api_key.get_secret_value() for llm in all_llms]
|
|
147
|
+
assert all(api_key == 'sk-updated-123' for api_key in api_keys), \
|
|
148
|
+
f"Not all LLMs have the same API key: {api_keys}"
|
|
149
|
+
|
|
150
|
+
# Verify none of the usage_id attributes match
|
|
151
|
+
usage_ids = [llm.usage_id for llm in all_llms]
|
|
152
|
+
assert len(set(usage_ids)) == len(usage_ids), \
|
|
153
|
+
f"Some usage_ids are duplicated: {usage_ids}"
|
|
154
|
+
|
|
155
|
+
|
|
124
156
|
@pytest.mark.parametrize(
|
|
125
157
|
'step_to_cancel',
|
|
126
158
|
['type', 'provider', 'model', 'apikey', 'save'],
|
|
@@ -4,6 +4,7 @@ Tests for confirmation mode functionality in OpenHands CLI.
|
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
import os
|
|
7
|
+
import uuid
|
|
7
8
|
from concurrent.futures import ThreadPoolExecutor
|
|
8
9
|
from typing import Any
|
|
9
10
|
from unittest.mock import ANY, MagicMock, patch
|
|
@@ -60,7 +61,7 @@ class TestConfirmationMode:
|
|
|
60
61
|
mock_conversation_instance = MagicMock()
|
|
61
62
|
mock_conversation_class.return_value = mock_conversation_instance
|
|
62
63
|
|
|
63
|
-
result = setup_conversation()
|
|
64
|
+
result = setup_conversation(mock_conversation_id)
|
|
64
65
|
|
|
65
66
|
# Verify conversation was created and returned
|
|
66
67
|
assert result == mock_conversation_instance
|
|
@@ -87,7 +88,7 @@ class TestConfirmationMode:
|
|
|
87
88
|
|
|
88
89
|
# Should raise MissingAgentSpec
|
|
89
90
|
with pytest.raises(MissingAgentSpec) as exc_info:
|
|
90
|
-
setup_conversation()
|
|
91
|
+
setup_conversation(uuid.uuid4())
|
|
91
92
|
|
|
92
93
|
assert 'Agent specification not found' in str(exc_info.value)
|
|
93
94
|
mock_agent_store_class.assert_called_once()
|
|
@@ -182,7 +182,7 @@ class TestLaunchGuiServer:
|
|
|
182
182
|
# Check pull command
|
|
183
183
|
pull_call = mock_run.call_args_list[0]
|
|
184
184
|
pull_cmd = pull_call[0][0]
|
|
185
|
-
assert pull_cmd[0:3] == ['docker', 'pull', 'docker.
|
|
185
|
+
assert pull_cmd[0:3] == ['docker', 'pull', 'docker.openhands.dev/openhands/runtime:latest-nikolaik']
|
|
186
186
|
|
|
187
187
|
# Check run command
|
|
188
188
|
run_call = mock_run.call_args_list[1]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|