Skip to content

Commit 985a49f

Browse files
committed
ci(test): update paths and dependencies for iclaw rename
1 parent 65902a5 commit 985a49f

9 files changed

Lines changed: 826 additions & 17 deletions

File tree

.github/workflows/test.yaml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ on:
44
workflow_dispatch:
55
push:
66
paths:
7-
- 'mini_copilot/**'
7+
- 'iclaw/**'
88
- 'tests/**'
99
pull_request:
1010
paths:
11-
- 'mini_copilot/**'
11+
- 'iclaw/**'
1212
- 'tests/**'
1313

1414
concurrency:
@@ -18,7 +18,7 @@ concurrency:
1818
jobs:
1919
test_job:
2020
runs-on: ubuntu-latest
21-
21+
2222
steps:
2323
- name: Checkout repository
2424
uses: actions/checkout@v4
@@ -32,10 +32,10 @@ jobs:
3232
run: |
3333
python -m pip install --upgrade pip
3434
pip install -r requirements.txt
35-
pip install coverage pytest-cov beautifulsoup4 readability-lxml lxml requests pyperclip
35+
pip install coverage
3636
3737
- name: Run test script
3838
run: |
3939
export PYTHONPATH=$PYTHONPATH:.
4040
python3 -m coverage run -m unittest discover tests
41-
python3 -m coverage report -m mini_copilot/*.py mini_copilot/commands/*.py
41+
python3 -m coverage report -m iclaw/*.py iclaw/commands/*.py iclaw/tools/*.py

requirements.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
11
# Dependencies are declared in pyproject.toml.
22
# To install for development: pip install -e .
33
requests>=2.32.0
4+
pyperclip>=1.8.0
5+
beautifulsoup4>=4.12.0
6+
readability-lxml>=0.8.1
7+
lxml>=5.0.0

tests/test_commands.py

Lines changed: 191 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,203 @@ def test_handle_login_command(self, mock_input, mock_poll, mock_device):
3131

3232
@patch("iclaw.commands.utils.sys")
3333
def test_handle_copy_command(self, mock_sys):
34-
# We'll patch pyperclip inside sys.modules because it's imported inside the function
3534
mock_py = MagicMock()
3635
with patch.dict("sys.modules", {"pyperclip": mock_py}):
3736
with patch("sys.stdout"):
3837
utils.handle_copy_command("text")
3938
mock_py.copy.assert_called_with("text")
4039

40+
# --- New tests for model.py ---
41+
42+
def test_handle_model_command_no_token(self):
43+
with patch("sys.stderr"):
44+
res = model.handle_model_command(None, "gpt-4o")
45+
self.assertEqual(res, "gpt-4o")
46+
47+
@patch("iclaw.commands.model.get_models", side_effect=Exception("fail"))
48+
def test_handle_model_command_fetch_error(self, mock_models):
49+
with patch("sys.stderr"):
50+
res = model.handle_model_command("t", "gpt-4o")
51+
self.assertEqual(res, "gpt-4o")
52+
53+
@patch("iclaw.commands.model.get_models")
54+
@patch("iclaw.commands.model.input", return_value="")
55+
def test_handle_model_command_empty_input(self, mock_input, mock_models):
56+
mock_models.return_value = [{"id": "m1", "owned_by": "o1"}]
57+
with patch("sys.stdout"):
58+
res = model.handle_model_command("t", "curr")
59+
self.assertEqual(res, "curr")
60+
61+
@patch("iclaw.commands.model.get_models")
62+
@patch("iclaw.commands.model.input", return_value="m1")
63+
def test_handle_model_command_by_name(self, mock_input, mock_models):
64+
mock_models.return_value = [{"id": "m1", "owned_by": "o1"}]
65+
with patch("sys.stdout"):
66+
res = model.handle_model_command("t", "curr")
67+
self.assertEqual(res, "m1")
68+
69+
@patch("iclaw.commands.model.get_models")
70+
@patch("iclaw.commands.model.input", return_value="999")
71+
def test_handle_model_command_invalid_number(self, mock_input, mock_models):
72+
mock_models.return_value = [{"id": "m1", "owned_by": "o1"}]
73+
with patch("sys.stdout"):
74+
res = model.handle_model_command("t", "curr")
75+
self.assertEqual(res, "curr")
76+
77+
@patch("iclaw.commands.model.get_models")
78+
@patch("iclaw.commands.model.input", return_value="nonexistent")
79+
def test_handle_model_command_unknown_name(self, mock_input, mock_models):
80+
mock_models.return_value = [{"id": "m1", "owned_by": "o1"}]
81+
with patch("sys.stdout"):
82+
res = model.handle_model_command("t", "curr")
83+
self.assertEqual(res, "curr")
84+
85+
@patch("iclaw.commands.model.get_models")
86+
@patch("iclaw.commands.model.input", side_effect=EOFError)
87+
def test_handle_model_command_eof(self, mock_input, mock_models):
88+
mock_models.return_value = [{"id": "m1", "owned_by": "o1"}]
89+
with patch("sys.stdout"):
90+
res = model.handle_model_command("t", "curr")
91+
self.assertEqual(res, "curr")
92+
93+
# --- New tests for model_provider ---
94+
95+
@patch("iclaw.commands.model.input", return_value="")
96+
def test_model_provider_empty_input(self, mock_input):
97+
with patch("sys.stdout"):
98+
p, t = model.handle_model_provider_command(MagicMock(), "copilot")
99+
self.assertEqual(p, "copilot")
100+
self.assertIsNone(t)
101+
102+
@patch("iclaw.commands.model.handle_login_command", return_value="gh_tok")
103+
@patch("iclaw.commands.model.get_copilot_token", return_value="cp_tok")
104+
@patch("iclaw.commands.model.input", return_value="1")
105+
def test_model_provider_copilot_success(self, mock_input, mock_cp, mock_login):
106+
with patch("sys.stdout"):
107+
p, t = model.handle_model_provider_command(MagicMock(), "copilot")
108+
self.assertEqual(p, "copilot")
109+
self.assertEqual(t, "cp_tok")
110+
111+
@patch("iclaw.commands.model.handle_login_command", return_value="gh_tok")
112+
@patch("iclaw.commands.model.get_copilot_token", side_effect=Exception("err"))
113+
@patch("iclaw.commands.model.input", return_value="1")
114+
def test_model_provider_copilot_error(self, mock_input, mock_cp, mock_login):
115+
with patch("sys.stdout"), patch("sys.stderr"):
116+
p, t = model.handle_model_provider_command(MagicMock(), "copilot")
117+
self.assertEqual(p, "copilot")
118+
self.assertIsNone(t)
119+
120+
@patch("iclaw.commands.model.handle_login_command", return_value=None)
121+
@patch("iclaw.commands.model.input", return_value="1")
122+
def test_model_provider_copilot_no_github_token(self, mock_input, mock_login):
123+
with patch("sys.stdout"):
124+
p, t = model.handle_model_provider_command(MagicMock(), "copilot")
125+
self.assertEqual(p, "copilot")
126+
self.assertIsNone(t)
127+
128+
@patch("iclaw.commands.model.input", return_value="2")
129+
def test_model_provider_others(self, mock_input):
130+
with patch("sys.stdout"):
131+
p, t = model.handle_model_provider_command(MagicMock(), "copilot")
132+
self.assertEqual(p, "copilot")
133+
self.assertIsNone(t)
134+
135+
@patch("iclaw.commands.model.input", return_value="99")
136+
def test_model_provider_invalid(self, mock_input):
137+
with patch("sys.stdout"):
138+
p, t = model.handle_model_provider_command(MagicMock(), "copilot")
139+
self.assertEqual(p, "copilot")
140+
self.assertIsNone(t)
141+
142+
# --- New tests for search_provider ---
143+
144+
@patch("iclaw.commands.search_provider.input", return_value="2")
145+
def test_search_provider_select_startpage(self, mock_input):
146+
with patch("sys.stdout"):
147+
res = search_provider.handle_search_provider_command("duckduckgo")
148+
self.assertEqual(res, "startpage")
149+
150+
@patch("iclaw.commands.search_provider.input", return_value="99")
151+
def test_search_provider_invalid_number(self, mock_input):
152+
with patch("sys.stdout"):
153+
res = search_provider.handle_search_provider_command("duckduckgo")
154+
self.assertEqual(res, "duckduckgo")
155+
156+
@patch("iclaw.commands.search_provider.input", return_value="abc")
157+
def test_search_provider_not_a_number(self, mock_input):
158+
with patch("sys.stdout"):
159+
res = search_provider.handle_search_provider_command("duckduckgo")
160+
self.assertEqual(res, "duckduckgo")
161+
162+
@patch("iclaw.commands.search_provider.input", return_value="")
163+
def test_search_provider_empty(self, mock_input):
164+
with patch("sys.stdout"):
165+
res = search_provider.handle_search_provider_command("duckduckgo")
166+
self.assertEqual(res, "duckduckgo")
167+
168+
@patch("iclaw.commands.search_provider.os.getenv", return_value=None)
169+
@patch("iclaw.commands.search_provider.input", side_effect=["4", "mykey"])
170+
def test_search_provider_tavily_with_key(self, mock_input, mock_getenv):
171+
with patch("sys.stdout"):
172+
res = search_provider.handle_search_provider_command("duckduckgo")
173+
self.assertEqual(res, "tavily")
174+
175+
@patch("iclaw.commands.search_provider.os.getenv", return_value=None)
176+
@patch("iclaw.commands.search_provider.input", side_effect=["4", ""])
177+
def test_search_provider_tavily_no_key(self, mock_input, mock_getenv):
178+
with patch("sys.stdout"):
179+
res = search_provider.handle_search_provider_command("duckduckgo")
180+
self.assertEqual(res, "duckduckgo")
181+
182+
@patch("iclaw.commands.search_provider.input", side_effect=EOFError)
183+
def test_search_provider_eof(self, mock_input):
184+
with patch("sys.stdout"):
185+
res = search_provider.handle_search_provider_command("duckduckgo")
186+
self.assertEqual(res, "duckduckgo")
187+
188+
# --- New tests for auth ---
189+
190+
@patch("iclaw.commands.auth.input", return_value="2")
191+
def test_login_direct_token(self, mock_input):
192+
mock_input.side_effect = ["2", "my_token"]
193+
mock_path = MagicMock()
194+
with patch("sys.stdout"):
195+
res = auth.handle_login_command(mock_path)
196+
self.assertEqual(res, "my_token")
197+
198+
@patch("iclaw.commands.auth.input", side_effect=["2", ""])
199+
def test_login_direct_token_empty(self, mock_input):
200+
mock_path = MagicMock()
201+
with patch("sys.stdout"):
202+
res = auth.handle_login_command(mock_path)
203+
self.assertIsNone(res)
204+
205+
@patch("iclaw.commands.auth.input", return_value="3")
206+
def test_login_invalid_choice(self, mock_input):
207+
mock_path = MagicMock()
208+
with patch("sys.stdout"):
209+
res = auth.handle_login_command(mock_path)
210+
self.assertIsNone(res)
211+
212+
@patch("iclaw.commands.auth.get_device_code", side_effect=Exception("net err"))
213+
@patch("iclaw.commands.auth.input", return_value="1")
214+
def test_login_device_flow_error(self, mock_input, mock_device):
215+
mock_path = MagicMock()
216+
with patch("sys.stdout"), patch("sys.stderr"):
217+
res = auth.handle_login_command(mock_path)
218+
self.assertIsNone(res)
219+
220+
# --- New tests for utils ---
221+
222+
def test_copy_nothing(self):
223+
with patch("sys.stdout"):
224+
utils.handle_copy_command(None)
225+
226+
def test_copy_exception(self):
227+
with patch.dict("sys.modules", {"pyperclip": None}):
228+
with patch("sys.stderr"), patch("sys.stdout"):
229+
utils.handle_copy_command("text")
230+
41231

42232
if __name__ == "__main__":
43233
unittest.main()

tests/test_edit_tool.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,61 @@ def test_multiple_hunks(self):
5757
self.assertEqual(lines[0], "First Line")
5858
self.assertEqual(lines[-1], "Last Line")
5959

60+
def test_new_file(self):
61+
new_file = "test_new_file.txt"
62+
try:
63+
edit = """--- /dev/null
64+
+++ test_new_file.txt
65+
@@ -1,0 +1,2 @@
66+
+New line 1
67+
+New line 2
68+
"""
69+
new_content = EditTool.edit(new_file, edit)
70+
self.assertIn("New line 1", new_content)
71+
self.assertIn("New line 2", new_content)
72+
finally:
73+
if os.path.exists(new_file):
74+
os.remove(new_file)
75+
76+
def test_context_lines(self):
77+
edit = """--- test_file.txt
78+
+++ test_file.txt
79+
@@ -1,3 +1,3 @@
80+
Line 1
81+
-Line 2
82+
+Line TWO
83+
Line 3
84+
"""
85+
new_content = EditTool.edit(self.test_file, edit)
86+
lines = new_content.splitlines()
87+
self.assertEqual(lines[0], "Line 1")
88+
self.assertEqual(lines[1], "Line TWO")
89+
self.assertEqual(lines[2], "Line 3")
90+
91+
def test_delete_lines(self):
92+
edit = """--- test_file.txt
93+
+++ test_file.txt
94+
@@ -2,2 +2,0 @@
95+
-Line 2
96+
-Line 3
97+
"""
98+
new_content = EditTool.edit(self.test_file, edit)
99+
lines = new_content.splitlines()
100+
self.assertEqual(len(lines), 3)
101+
self.assertEqual(lines[0], "Line 1")
102+
self.assertEqual(lines[1], "Line 4")
103+
104+
def test_add_lines(self):
105+
edit = """--- test_file.txt
106+
+++ test_file.txt
107+
@@ -2,0 +2,2 @@
108+
+Inserted A
109+
+Inserted B
110+
"""
111+
new_content = EditTool.edit(self.test_file, edit)
112+
self.assertIn("Inserted A", new_content)
113+
self.assertIn("Inserted B", new_content)
114+
60115

61116
if __name__ == "__main__":
62117
unittest.main()

tests/test_exec_tool.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import unittest
2+
from unittest.mock import patch
23
from iclaw.exec_tool import exec_command
34

45

@@ -12,10 +13,28 @@ def test_exec_ls(self):
1213
self.assertIn("pyproject.toml", output)
1314

1415
def test_exec_error(self):
15-
# Depending on the system, the error message might vary, but it should contain some indication of failure
1616
output = exec_command("non_existent_command_12345")
1717
self.assertIn("not found", output.lower())
1818

19+
def test_exec_empty_output_nonzero(self):
20+
output = exec_command("bash -c 'exit 42'")
21+
self.assertIn("Process exited with code 42", output)
22+
23+
@patch("iclaw.exec_tool.subprocess.run", side_effect=Exception("unexpected"))
24+
def test_exec_exception(self, mock_run):
25+
output = exec_command("anything")
26+
self.assertIn("Error executing command", output)
27+
28+
def test_exec_timeout(self):
29+
import subprocess
30+
31+
with patch(
32+
"iclaw.exec_tool.subprocess.run",
33+
side_effect=subprocess.TimeoutExpired("cmd", 30),
34+
):
35+
output = exec_command("sleep 999")
36+
self.assertIn("timed out", output)
37+
1938

2039
if __name__ == "__main__":
2140
unittest.main()

0 commit comments

Comments
 (0)