forked from OpenHands/OpenHands
-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathtest_microagent_utils.py
More file actions
366 lines (285 loc) Β· 11.5 KB
/
test_microagent_utils.py
File metadata and controls
366 lines (285 loc) Β· 11.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
"""Tests for the microagent system."""
import tempfile
from pathlib import Path
import pytest
from openhands.microagent import (
BaseMicroagent,
KnowledgeMicroagent,
MicroagentMetadata,
MicroagentType,
RepoMicroagent,
load_microagents_from_dir,
)
CONTENT = '# dummy header\ndummy content\n## dummy subheader\ndummy subcontent\n'
def test_legacy_micro_agent_load(tmp_path):
"""Test loading of legacy microagents."""
legacy_file = tmp_path / '.openhands_instructions'
legacy_file.write_text(CONTENT)
# Pass microagent_dir (tmp_path in this case) to load
micro_agent = BaseMicroagent.load(legacy_file, tmp_path)
assert isinstance(micro_agent, RepoMicroagent)
assert micro_agent.name == 'repo_legacy' # Legacy name is hardcoded
assert micro_agent.content == CONTENT
assert micro_agent.type == MicroagentType.REPO_KNOWLEDGE
@pytest.fixture
def temp_microagents_dir():
"""Create a temporary directory with test microagents."""
with tempfile.TemporaryDirectory() as temp_dir:
root = Path(temp_dir)
# Create test knowledge agent (type inferred from triggers)
knowledge_agent = """---
# type: knowledge
version: 1.0.0
agent: CodeActAgent
triggers:
- test
- pytest
---
# Test Guidelines
Testing best practices and guidelines.
"""
(root / 'knowledge.md').write_text(knowledge_agent)
# Create test repo agent (type inferred from lack of triggers)
repo_agent = """---
# type: repo
version: 1.0.0
agent: CodeActAgent
---
# Test Repository Agent
Repository-specific test instructions.
"""
(root / 'repo.md').write_text(repo_agent)
yield root
def test_knowledge_agent():
"""Test knowledge agent functionality."""
# Note: We still pass type to the constructor here, as it expects it.
# The loader infers the type before calling the constructor.
agent = KnowledgeMicroagent(
name='test',
content='Test content',
metadata=MicroagentMetadata(name='test', triggers=['test', 'pytest']),
source='test.md',
type=MicroagentType.KNOWLEDGE, # Constructor still needs type
)
assert agent.match_trigger('running a test') == 'test'
assert agent.match_trigger('using pytest') == 'test'
assert agent.match_trigger('no match here') is None
assert agent.triggers == ['test', 'pytest']
def test_load_microagents(temp_microagents_dir):
"""Test loading microagents from directory."""
repo_agents, knowledge_agents = load_microagents_from_dir(temp_microagents_dir)
# Check knowledge agents (name derived from filename: knowledge.md -> 'knowledge')
assert len(knowledge_agents) == 1
agent_k = knowledge_agents['knowledge']
assert isinstance(agent_k, KnowledgeMicroagent)
assert agent_k.type == MicroagentType.KNOWLEDGE # Check inferred type
assert 'test' in agent_k.triggers
# Check repo agents (name derived from filename: repo.md -> 'repo')
assert len(repo_agents) == 1
agent_r = repo_agents['repo']
assert isinstance(agent_r, RepoMicroagent)
assert agent_r.type == MicroagentType.REPO_KNOWLEDGE # Check inferred type
def test_load_microagents_with_nested_dirs(temp_microagents_dir):
"""Test loading microagents from nested directories."""
# Create nested knowledge agent
nested_dir = temp_microagents_dir / 'nested' / 'dir'
nested_dir.mkdir(parents=True)
nested_agent = """---
# type: knowledge
version: 1.0.0
agent: CodeActAgent
triggers:
- nested
---
# Nested Test Guidelines
Testing nested directory loading.
"""
(nested_dir / 'nested.md').write_text(nested_agent)
repo_agents, knowledge_agents = load_microagents_from_dir(temp_microagents_dir)
# Check that we can find the nested agent (name derived from path: nested/dir/nested.md -> 'nested/dir/nested')
assert (
len(knowledge_agents) == 2
) # Original ('knowledge') + nested ('nested/dir/nested')
agent_n = knowledge_agents['nested/dir/nested']
assert isinstance(agent_n, KnowledgeMicroagent)
assert agent_n.type == MicroagentType.KNOWLEDGE # Check inferred type
assert 'nested' in agent_n.triggers
def test_load_microagents_with_trailing_slashes(temp_microagents_dir):
"""Test loading microagents when directory paths have trailing slashes."""
# Create a directory with trailing slash
knowledge_dir = temp_microagents_dir / 'test_knowledge/'
knowledge_dir.mkdir(exist_ok=True)
knowledge_agent = """---
# type: knowledge
version: 1.0.0
agent: CodeActAgent
triggers:
- trailing
---
# Trailing Slash Test
Testing loading with trailing slashes.
"""
(knowledge_dir / 'trailing.md').write_text(knowledge_agent)
repo_agents, knowledge_agents = load_microagents_from_dir(
str(temp_microagents_dir) + '/' # Add trailing slash to test
)
# Check that we can find the agent despite trailing slashes (name derived from path: test_knowledge/trailing.md -> 'test_knowledge/trailing')
assert (
len(knowledge_agents) == 2
) # Original ('knowledge') + trailing ('test_knowledge/trailing')
agent_t = knowledge_agents['test_knowledge/trailing']
assert isinstance(agent_t, KnowledgeMicroagent)
assert agent_t.type == MicroagentType.KNOWLEDGE # Check inferred type
assert 'trailing' in agent_t.triggers
def test_invalid_microagent_type(temp_microagents_dir):
"""Test loading a microagent with an invalid type."""
# Create a microagent with an invalid type
invalid_agent = """---
name: invalid_type_agent
type: invalid_type
version: 1.0.0
agent: CodeActAgent
triggers:
- test
---
# Invalid Type Test
This microagent has an invalid type.
"""
invalid_file = temp_microagents_dir / 'invalid_type.md'
invalid_file.write_text(invalid_agent)
# Attempt to load the microagent should raise a MicroagentValidationError
from openhands.core.exceptions import MicroagentValidationError
with pytest.raises(MicroagentValidationError) as excinfo:
load_microagents_from_dir(temp_microagents_dir)
# Check that the error message contains helpful information
error_msg = str(excinfo.value)
assert 'invalid_type.md' in error_msg
assert 'Invalid "type" value: "invalid_type"' in error_msg
assert 'Valid types are:' in error_msg
assert '"knowledge"' in error_msg
assert '"repo"' in error_msg
assert '"task"' in error_msg
def test_cursorrules_file_load():
"""Test loading .cursorrules file as a RepoMicroagent."""
cursorrules_content = """Always use Python for new files.
Follow the existing code style.
Add proper error handling."""
cursorrules_path = Path('.cursorrules')
# Test loading .cursorrules file directly
agent = BaseMicroagent.load(cursorrules_path, file_content=cursorrules_content)
# Verify it's loaded as a RepoMicroagent
assert isinstance(agent, RepoMicroagent)
assert agent.name == 'cursorrules'
assert agent.content == cursorrules_content
assert agent.type == MicroagentType.REPO_KNOWLEDGE
assert agent.metadata.name == 'cursorrules'
assert agent.source == str(cursorrules_path)
def test_microagent_version_as_integer():
"""Test loading a microagent with version as integer (reproduces the bug)."""
# Create a microagent with version as an unquoted integer
# This should be parsed as an integer by YAML but converted to string by our code
microagent_content = """---
name: test_agent
type: knowledge
version: 2512312
agent: CodeActAgent
triggers:
- test
---
# Test Agent
This is a test agent with integer version.
"""
test_path = Path('test_agent.md')
# This should not raise an error even though version is an integer in YAML
agent = BaseMicroagent.load(test_path, file_content=microagent_content)
# Verify the agent was loaded correctly
assert isinstance(agent, KnowledgeMicroagent)
assert agent.name == 'test_agent'
assert agent.metadata.version == '2512312' # Should be converted to string
assert isinstance(agent.metadata.version, str) # Ensure it's actually a string
assert agent.type == MicroagentType.KNOWLEDGE
def test_microagent_version_as_float():
"""Test loading a microagent with version as float."""
# Create a microagent with version as an unquoted float
microagent_content = """---
name: test_agent_float
type: knowledge
version: 1.5
agent: CodeActAgent
triggers:
- test
---
# Test Agent Float
This is a test agent with float version.
"""
test_path = Path('test_agent_float.md')
# This should not raise an error even though version is a float in YAML
agent = BaseMicroagent.load(test_path, file_content=microagent_content)
# Verify the agent was loaded correctly
assert isinstance(agent, KnowledgeMicroagent)
assert agent.name == 'test_agent_float'
assert agent.metadata.version == '1.5' # Should be converted to string
assert isinstance(agent.metadata.version, str) # Ensure it's actually a string
assert agent.type == MicroagentType.KNOWLEDGE
def test_microagent_version_as_string_unchanged():
"""Test loading a microagent with version as string (should remain unchanged)."""
# Create a microagent with version as a quoted string
microagent_content = """---
name: test_agent_string
type: knowledge
version: "1.0.0"
agent: CodeActAgent
triggers:
- test
---
# Test Agent String
This is a test agent with string version.
"""
test_path = Path('test_agent_string.md')
# This should work normally
agent = BaseMicroagent.load(test_path, file_content=microagent_content)
# Verify the agent was loaded correctly
assert isinstance(agent, KnowledgeMicroagent)
assert agent.name == 'test_agent_string'
assert agent.metadata.version == '1.0.0' # Should remain as string
assert isinstance(agent.metadata.version, str) # Ensure it's actually a string
assert agent.type == MicroagentType.KNOWLEDGE
@pytest.fixture
def temp_microagents_dir_with_cursorrules():
"""Create a temporary directory with test microagents and .cursorrules file."""
with tempfile.TemporaryDirectory() as temp_dir:
root = Path(temp_dir)
# Create .openhands/microagents directory structure
microagents_dir = root / '.openhands' / 'microagents'
microagents_dir.mkdir(parents=True, exist_ok=True)
# Create .cursorrules file in repository root
cursorrules_content = """Always use TypeScript for new files.
Follow the existing code style."""
(root / '.cursorrules').write_text(cursorrules_content)
# Create test repo agent
repo_agent = """---
# type: repo
version: 1.0.0
agent: CodeActAgent
---
# Test Repository Agent
Repository-specific test instructions.
"""
(microagents_dir / 'repo.md').write_text(repo_agent)
yield root
def test_load_microagents_with_cursorrules(temp_microagents_dir_with_cursorrules):
"""Test loading microagents when .cursorrules file exists."""
microagents_dir = (
temp_microagents_dir_with_cursorrules / '.openhands' / 'microagents'
)
repo_agents, knowledge_agents = load_microagents_from_dir(microagents_dir)
# Verify that .cursorrules file was loaded as a RepoMicroagent
assert len(repo_agents) == 2 # repo.md + .cursorrules
assert 'repo' in repo_agents
assert 'cursorrules' in repo_agents
# Check .cursorrules agent
cursorrules_agent = repo_agents['cursorrules']
assert isinstance(cursorrules_agent, RepoMicroagent)
assert cursorrules_agent.name == 'cursorrules'
assert 'Always use TypeScript for new files' in cursorrules_agent.content
assert cursorrules_agent.type == MicroagentType.REPO_KNOWLEDGE