akseljoonas HF Staff commited on
Commit
54049e9
·
1 Parent(s): ee20af8

Remove stale tests directory

Browse files
tests/__init__.py DELETED
File without changes
tests/integration/__init__.py DELETED
File without changes
tests/integration/tools/__init__.py DELETED
File without changes
tests/integration/tools/test_jobs_integration.py DELETED
@@ -1,452 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- Integration tests for refactored HF Jobs Tool
4
- Tests with real HF API using HF_TOKEN from environment
5
- """
6
- import os
7
- import sys
8
- import asyncio
9
- import time
10
-
11
- # Add parent directory to path
12
- sys.path.insert(0, '.')
13
-
14
- from agent.tools.jobs_tool import HfJobsTool
15
-
16
- # ANSI color codes for better output
17
- GREEN = '\033[92m'
18
- YELLOW = '\033[93m'
19
- RED = '\033[91m'
20
- BLUE = '\033[94m'
21
- RESET = '\033[0m'
22
-
23
-
24
- def print_test(msg):
25
- """Print test message in blue"""
26
- print(f"{BLUE}[TEST]{RESET} {msg}")
27
-
28
-
29
- def print_success(msg):
30
- """Print success message in green"""
31
- print(f"{GREEN}✓{RESET} {msg}")
32
-
33
-
34
- def print_warning(msg):
35
- """Print warning message in yellow"""
36
- print(f"{YELLOW}⚠{RESET} {msg}")
37
-
38
-
39
- def print_error(msg):
40
- """Print error message in red"""
41
- print(f"{RED}✗{RESET} {msg}")
42
-
43
-
44
- async def test_basic_job_run(tool):
45
- """Test running a basic job"""
46
- print_test("Running a simple Python job...")
47
-
48
- result = await tool.execute({
49
- "operation": "run",
50
- "args": {
51
- "image": "python:3.12",
52
- "command": ["python", "-c", "print('Hello from HF Jobs!')"],
53
- "flavor": "cpu-basic",
54
- "timeout": "5m",
55
- "detach": True # Don't wait for completion
56
- }
57
- })
58
-
59
- if result.get("isError"):
60
- print_error(f"Failed to run job: {result['formatted']}")
61
- return None
62
-
63
- # Extract job ID from response
64
- import re
65
- job_id_match = re.search(r'\*\*Job ID:\*\* (\S+)', result['formatted'])
66
- if job_id_match:
67
- job_id = job_id_match.group(1)
68
- print_success(f"Job started with ID: {job_id}")
69
- return job_id
70
-
71
- print_error("Could not extract job ID from response")
72
- return None
73
-
74
-
75
- async def test_list_jobs(tool):
76
- """Test listing jobs"""
77
- print_test("Listing running jobs...")
78
-
79
- result = await tool.execute({
80
- "operation": "ps",
81
- "args": {}
82
- })
83
-
84
- if result.get("isError"):
85
- print_error(f"Failed to list jobs: {result['formatted']}")
86
- return False
87
-
88
- print_success(f"Listed jobs: {result['totalResults']} running")
89
- if result['totalResults'] > 0:
90
- print(f" {result['formatted'][:200]}...")
91
- return True
92
-
93
-
94
- async def test_inspect_job(tool, job_id):
95
- """Test inspecting a specific job"""
96
- print_test(f"Inspecting job {job_id}...")
97
-
98
- result = await tool.execute({
99
- "operation": "inspect",
100
- "args": {
101
- "job_id": job_id
102
- }
103
- })
104
-
105
- if result.get("isError"):
106
- print_error(f"Failed to inspect job: {result['formatted']}")
107
- return False
108
-
109
- print_success(f"Inspected job successfully")
110
- return True
111
-
112
-
113
- async def test_get_logs(tool, job_id):
114
- """Test fetching job logs"""
115
- print_test(f"Fetching logs for job {job_id}...")
116
-
117
- # Wait a bit for logs to be available
118
- await asyncio.sleep(2)
119
-
120
- result = await tool.execute({
121
- "operation": "logs",
122
- "args": {
123
- "job_id": job_id
124
- }
125
- })
126
-
127
- if result.get("isError"):
128
- print_warning(f"Could not fetch logs (might be too early): {result['formatted'][:100]}")
129
- return False
130
-
131
- print_success(f"Fetched logs successfully")
132
- if "Hello from HF Jobs!" in result['formatted']:
133
- print_success(" Found expected output in logs!")
134
- return True
135
-
136
-
137
- async def test_cancel_job(tool, job_id):
138
- """Test cancelling a job"""
139
- print_test(f"Cancelling job {job_id}...")
140
-
141
- result = await tool.execute({
142
- "operation": "cancel",
143
- "args": {
144
- "job_id": job_id
145
- }
146
- })
147
-
148
- if result.get("isError"):
149
- print_error(f"Failed to cancel job: {result['formatted']}")
150
- return False
151
-
152
- print_success(f"Cancelled job successfully")
153
- return True
154
-
155
-
156
- async def test_uv_job(tool):
157
- """Test running a UV job"""
158
- print_test("Running a UV Python script job...")
159
-
160
- result = await tool.execute({
161
- "operation": "uv",
162
- "args": {
163
- "script": "print('Hello from UV!')\nimport sys\nprint(f'Python version: {sys.version}')",
164
- "flavor": "cpu-basic",
165
- "timeout": "5m",
166
- "detach": True
167
- }
168
- })
169
-
170
- if result.get("isError"):
171
- print_error(f"Failed to run UV job: {result['formatted']}")
172
- return None
173
-
174
- # Extract job ID
175
- import re
176
- job_id_match = re.search(r'UV Job started: (\S+)', result['formatted'])
177
- if job_id_match:
178
- job_id = job_id_match.group(1)
179
- print_success(f"UV job started with ID: {job_id}")
180
- return job_id
181
-
182
- print_error("Could not extract job ID from response")
183
- return None
184
-
185
-
186
- async def test_list_all_jobs(tool):
187
- """Test listing all jobs (including completed)"""
188
- print_test("Listing all jobs (including completed)...")
189
-
190
- result = await tool.execute({
191
- "operation": "ps",
192
- "args": {
193
- "all": True
194
- }
195
- })
196
-
197
- if result.get("isError"):
198
- print_error(f"Failed to list all jobs: {result['formatted']}")
199
- return False
200
-
201
- print_success(f"Listed all jobs: {result['totalResults']} total")
202
- return True
203
-
204
-
205
- async def test_scheduled_job(tool):
206
- """Test creating and managing a scheduled job"""
207
- print_test("Creating a scheduled job (daily at midnight)...")
208
-
209
- result = await tool.execute({
210
- "operation": "scheduled run",
211
- "args": {
212
- "image": "python:3.12",
213
- "command": ["python", "-c", "print('Scheduled job running!')"],
214
- "schedule": "@daily",
215
- "flavor": "cpu-basic",
216
- "timeout": "5m"
217
- }
218
- })
219
-
220
- if result.get("isError"):
221
- print_error(f"Failed to create scheduled job: {result['formatted']}")
222
- return None
223
-
224
- # Extract scheduled job ID
225
- import re
226
- job_id_match = re.search(r'\*\*Scheduled Job ID:\*\* (\S+)', result['formatted'])
227
- if not job_id_match:
228
- print_error("Could not extract scheduled job ID")
229
- return None
230
-
231
- scheduled_job_id = job_id_match.group(1)
232
- print_success(f"Scheduled job created with ID: {scheduled_job_id}")
233
- return scheduled_job_id
234
-
235
-
236
- async def test_list_scheduled_jobs(tool):
237
- """Test listing scheduled jobs"""
238
- print_test("Listing scheduled jobs...")
239
-
240
- result = await tool.execute({
241
- "operation": "scheduled ps",
242
- "args": {}
243
- })
244
-
245
- if result.get("isError"):
246
- print_error(f"Failed to list scheduled jobs: {result['formatted']}")
247
- return False
248
-
249
- print_success(f"Listed scheduled jobs: {result['totalResults']} active")
250
- return True
251
-
252
-
253
- async def test_inspect_scheduled_job(tool, scheduled_job_id):
254
- """Test inspecting a scheduled job"""
255
- print_test(f"Inspecting scheduled job {scheduled_job_id}...")
256
-
257
- result = await tool.execute({
258
- "operation": "scheduled inspect",
259
- "args": {
260
- "scheduled_job_id": scheduled_job_id
261
- }
262
- })
263
-
264
- if result.get("isError"):
265
- print_error(f"Failed to inspect scheduled job: {result['formatted']}")
266
- return False
267
-
268
- print_success(f"Inspected scheduled job successfully")
269
- return True
270
-
271
-
272
- async def test_suspend_scheduled_job(tool, scheduled_job_id):
273
- """Test suspending a scheduled job"""
274
- print_test(f"Suspending scheduled job {scheduled_job_id}...")
275
-
276
- result = await tool.execute({
277
- "operation": "scheduled suspend",
278
- "args": {
279
- "scheduled_job_id": scheduled_job_id
280
- }
281
- })
282
-
283
- if result.get("isError"):
284
- print_error(f"Failed to suspend scheduled job: {result['formatted']}")
285
- return False
286
-
287
- print_success(f"Suspended scheduled job successfully")
288
- return True
289
-
290
-
291
- async def test_resume_scheduled_job(tool, scheduled_job_id):
292
- """Test resuming a scheduled job"""
293
- print_test(f"Resuming scheduled job {scheduled_job_id}...")
294
-
295
- result = await tool.execute({
296
- "operation": "scheduled resume",
297
- "args": {
298
- "scheduled_job_id": scheduled_job_id
299
- }
300
- })
301
-
302
- if result.get("isError"):
303
- print_error(f"Failed to resume scheduled job: {result['formatted']}")
304
- return False
305
-
306
- print_success(f"Resumed scheduled job successfully")
307
- return True
308
-
309
-
310
- async def test_delete_scheduled_job(tool, scheduled_job_id):
311
- """Test deleting a scheduled job"""
312
- print_test(f"Deleting scheduled job {scheduled_job_id}...")
313
-
314
- result = await tool.execute({
315
- "operation": "scheduled delete",
316
- "args": {
317
- "scheduled_job_id": scheduled_job_id
318
- }
319
- })
320
-
321
- if result.get("isError"):
322
- print_error(f"Failed to delete scheduled job: {result['formatted']}")
323
- return False
324
-
325
- print_success(f"Deleted scheduled job successfully")
326
- return True
327
-
328
-
329
- async def main():
330
- """Run all integration tests"""
331
- print("=" * 70)
332
- print(f"{BLUE}HF Jobs Tool - Integration Tests{RESET}")
333
- print("=" * 70)
334
- print()
335
-
336
- # Check for HF_TOKEN
337
- hf_token = os.environ.get('HF_TOKEN')
338
- if not hf_token:
339
- print_error("HF_TOKEN not found in environment variables!")
340
- print_warning("Set it with: export HF_TOKEN='your_token_here'")
341
- sys.exit(1)
342
-
343
- print_success(f"Found HF_TOKEN (length: {len(hf_token)})")
344
- print()
345
-
346
- # Initialize tool with token
347
- tool = HfJobsTool(hf_token=hf_token)
348
-
349
- # Track job IDs for cleanup
350
- job_ids = []
351
- scheduled_job_ids = []
352
-
353
- try:
354
- # Test 1: Run basic job
355
- print(f"\n{YELLOW}{'=' * 70}{RESET}")
356
- print(f"{YELLOW}Test Suite 1: Regular Jobs{RESET}")
357
- print(f"{YELLOW}{'=' * 70}{RESET}\n")
358
-
359
- job_id = await test_basic_job_run(tool)
360
- if job_id:
361
- job_ids.append(job_id)
362
-
363
- # Wait a moment for job to register
364
- await asyncio.sleep(1)
365
-
366
- # Test 2: List jobs
367
- await test_list_jobs(tool)
368
-
369
- # Test 3: Inspect job
370
- await test_inspect_job(tool, job_id)
371
-
372
- # Test 4: Get logs
373
- await test_get_logs(tool, job_id)
374
-
375
- # Test 5: Cancel job (cleanup)
376
- await test_cancel_job(tool, job_id)
377
-
378
- # Test 6: UV job
379
- print()
380
- uv_job_id = await test_uv_job(tool)
381
- if uv_job_id:
382
- job_ids.append(uv_job_id)
383
- await asyncio.sleep(1)
384
- await test_cancel_job(tool, uv_job_id)
385
-
386
- # Test 7: List all jobs
387
- print()
388
- await test_list_all_jobs(tool)
389
-
390
- # Test Suite 2: Scheduled Jobs
391
- print(f"\n{YELLOW}{'=' * 70}{RESET}")
392
- print(f"{YELLOW}Test Suite 2: Scheduled Jobs{RESET}")
393
- print(f"{YELLOW}{'=' * 70}{RESET}\n")
394
-
395
- scheduled_job_id = await test_scheduled_job(tool)
396
- if scheduled_job_id:
397
- scheduled_job_ids.append(scheduled_job_id)
398
-
399
- # Wait a moment for job to register
400
- await asyncio.sleep(1)
401
-
402
- # Test scheduled job operations
403
- await test_list_scheduled_jobs(tool)
404
- print()
405
- await test_inspect_scheduled_job(tool, scheduled_job_id)
406
- print()
407
- await test_suspend_scheduled_job(tool, scheduled_job_id)
408
- print()
409
- await test_resume_scheduled_job(tool, scheduled_job_id)
410
- print()
411
-
412
- # Cleanup: Delete scheduled job
413
- await test_delete_scheduled_job(tool, scheduled_job_id)
414
-
415
- # Final summary
416
- print(f"\n{YELLOW}{'=' * 70}{RESET}")
417
- print(f"{GREEN}✓ All integration tests completed!{RESET}")
418
- print(f"{YELLOW}{'=' * 70}{RESET}\n")
419
-
420
- print_success("Refactored implementation works correctly with real HF API")
421
- print_success("All 13 operations tested and verified")
422
- print()
423
- print(f"{BLUE}Summary:{RESET}")
424
- print(f" • Regular jobs: ✓ run, list, inspect, logs, cancel")
425
- print(f" • UV jobs: ✓ run")
426
- print(f" • Scheduled jobs: ✓ create, list, inspect, suspend, resume, delete")
427
- print()
428
-
429
- except Exception as e:
430
- print_error(f"Test failed with exception: {str(e)}")
431
- import traceback
432
- traceback.print_exc()
433
-
434
- # Attempt cleanup
435
- print(f"\n{YELLOW}Attempting cleanup...{RESET}")
436
- for job_id in job_ids:
437
- try:
438
- await test_cancel_job(tool, job_id)
439
- except:
440
- pass
441
-
442
- for scheduled_job_id in scheduled_job_ids:
443
- try:
444
- await test_delete_scheduled_job(tool, scheduled_job_id)
445
- except:
446
- pass
447
-
448
- sys.exit(1)
449
-
450
-
451
- if __name__ == "__main__":
452
- asyncio.run(main())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/integration/tools/test_papers_integration.py DELETED
@@ -1,572 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- Integration tests for HF Papers Tool
4
- Tests with real HF and arXiv APIs — all endpoints are public, no auth required.
5
-
6
- Run: python tests/integration/tools/test_papers_integration.py
7
- """
8
- import asyncio
9
- import re
10
- import sys
11
-
12
- sys.path.insert(0, ".")
13
-
14
- from agent.tools.papers_tool import hf_papers_handler
15
-
16
- # ANSI color codes
17
- GREEN = "\033[92m"
18
- YELLOW = "\033[93m"
19
- RED = "\033[91m"
20
- BLUE = "\033[94m"
21
- DIM = "\033[2m"
22
- RESET = "\033[0m"
23
-
24
- assertions_passed = 0
25
- assertions_failed = 0
26
-
27
-
28
- def print_test(msg):
29
- print(f"\n{BLUE}{'─' * 70}{RESET}")
30
- print(f"{BLUE}[TEST]{RESET} {msg}")
31
- print(f"{BLUE}{'─' * 70}{RESET}")
32
-
33
-
34
- def print_success(msg):
35
- print(f"{GREEN} ✓ {msg}{RESET}")
36
-
37
-
38
- def print_error(msg):
39
- print(f"{RED} ✗ {msg}{RESET}")
40
-
41
-
42
- def print_output(output: str, max_lines: int = 40):
43
- """Print the full tool output, indented, with line limit."""
44
- lines = output.split("\n")
45
- for line in lines[:max_lines]:
46
- print(f"{DIM} │ {RESET}{line}")
47
- if len(lines) > max_lines:
48
- print(f"{DIM} │ ... ({len(lines) - max_lines} more lines){RESET}")
49
-
50
-
51
- def assert_true(condition: bool, msg: str) -> bool:
52
- """Assert and print result. Returns True if passed."""
53
- global assertions_passed, assertions_failed
54
- if condition:
55
- print_success(msg)
56
- assertions_passed += 1
57
- return True
58
- else:
59
- print_error(msg)
60
- assertions_failed += 1
61
- return False
62
-
63
-
64
- async def run(args: dict) -> tuple[str, bool]:
65
- return await hf_papers_handler(args)
66
-
67
-
68
- # ---------------------------------------------------------------------------
69
- # Test Suite 1: Paper Discovery
70
- # ---------------------------------------------------------------------------
71
-
72
-
73
- async def test_trending():
74
- print_test("trending (limit=3)")
75
- output, success = await run({"operation": "trending", "limit": 3})
76
- print_output(output)
77
-
78
- ok = True
79
- ok &= assert_true(success, "success=True")
80
- ok &= assert_true("# Trending Papers" in output, "has '# Trending Papers' heading")
81
- ok &= assert_true("Showing 3 paper(s)" in output, "shows exactly 3 papers")
82
-
83
- # Check that each paper has an arxiv_id line
84
- arxiv_ids = re.findall(r"\*\*arxiv_id:\*\* (\S+)", output)
85
- ok &= assert_true(len(arxiv_ids) == 3, f"found 3 arxiv IDs: {arxiv_ids}")
86
-
87
- # Check that IDs look valid (digits and dots)
88
- for aid in arxiv_ids:
89
- ok &= assert_true(
90
- re.match(r"\d{4}\.\d{4,5}", aid) is not None,
91
- f"arxiv_id '{aid}' looks valid (NNNN.NNNNN format)",
92
- )
93
-
94
- # Check each paper has an HF URL
95
- hf_urls = re.findall(r"https://huggingface\.co/papers/\S+", output)
96
- ok &= assert_true(len(hf_urls) == 3, f"found 3 HF paper URLs")
97
-
98
- return ok
99
-
100
-
101
- async def test_trending_with_query():
102
- print_test("trending with query='language' (limit=5)")
103
- output, success = await run({"operation": "trending", "query": "language", "limit": 5})
104
- print_output(output)
105
-
106
- ok = True
107
- ok &= assert_true(success, "success=True")
108
- ok &= assert_true("Filtered by: 'language'" in output, "shows filter applied")
109
-
110
- # The filter may return 0-5 results depending on today's papers
111
- match = re.search(r"Showing (\d+) paper\(s\)", output)
112
- ok &= assert_true(match is not None, "has 'Showing N paper(s)' line")
113
- if match:
114
- count = int(match.group(1))
115
- ok &= assert_true(count <= 5, f"returned {count} papers (within limit)")
116
- # If we got results, verify they mention language somewhere
117
- if count > 0:
118
- print_success(f"got {count} filtered results")
119
-
120
- return ok
121
-
122
-
123
- async def test_search():
124
- print_test("search 'direct preference optimization' (limit=3)")
125
- output, success = await run(
126
- {"operation": "search", "query": "direct preference optimization", "limit": 3}
127
- )
128
- print_output(output)
129
-
130
- ok = True
131
- ok &= assert_true(success, "success=True")
132
- ok &= assert_true("Papers matching" in output, "has matching header")
133
-
134
- arxiv_ids = re.findall(r"\*\*arxiv_id:\*\* (\S+)", output)
135
- ok &= assert_true(len(arxiv_ids) == 3, f"found 3 results: {arxiv_ids}")
136
-
137
- # At least one result should mention "preference" in title or summary
138
- ok &= assert_true(
139
- "preference" in output.lower(),
140
- "results mention 'preference' (relevant to query)",
141
- )
142
-
143
- return ok
144
-
145
-
146
- async def test_paper_details():
147
- print_test("paper_details for 2305.18290 (DPO paper)")
148
- output, success = await run({"operation": "paper_details", "arxiv_id": "2305.18290"})
149
- print_output(output)
150
-
151
- ok = True
152
- ok &= assert_true(success, "success=True")
153
- ok &= assert_true("Direct Preference Optimization" in output, "title contains 'Direct Preference Optimization'")
154
- ok &= assert_true("2305.18290" in output, "contains arxiv_id")
155
- ok &= assert_true("https://arxiv.org/abs/2305.18290" in output, "has arxiv URL")
156
- ok &= assert_true("https://huggingface.co/papers/2305.18290" in output, "has HF URL")
157
- ok &= assert_true("**Authors:**" in output, "has authors section")
158
- ok &= assert_true("**upvotes:**" in output, "has upvotes")
159
-
160
- # Check for abstract or AI summary
161
- ok &= assert_true(
162
- "## Abstract" in output or "## AI Summary" in output,
163
- "has Abstract or AI Summary section",
164
- )
165
-
166
- # Check for next steps hint
167
- ok &= assert_true("read_paper" in output, "mentions read_paper as next step")
168
- ok &= assert_true("find_all_resources" in output, "mentions find_all_resources as next step")
169
-
170
- return ok
171
-
172
-
173
- # ---------------------------------------------------------------------------
174
- # Test Suite 2: Read Paper
175
- # ---------------------------------------------------------------------------
176
-
177
-
178
- async def test_read_paper_toc():
179
- print_test("read_paper TOC for 2305.18290 (no section → should return abstract + sections)")
180
- output, success = await run({"operation": "read_paper", "arxiv_id": "2305.18290"})
181
- print_output(output)
182
-
183
- ok = True
184
- ok &= assert_true(success, "success=True")
185
- ok &= assert_true("## Abstract" in output, "has Abstract section")
186
- ok &= assert_true("## Sections" in output, "has Sections heading (TOC)")
187
-
188
- # Check that sections are listed with bold titles
189
- section_titles = re.findall(r"- \*\*(.+?)\*\*:", output)
190
- ok &= assert_true(len(section_titles) >= 5, f"found {len(section_titles)} sections (expect >=5 for a full paper)")
191
- if section_titles:
192
- print_success(f"sections found: {section_titles[:5]}{'...' if len(section_titles) > 5 else ''}")
193
-
194
- # Check that expected DPO paper sections are present
195
- section_text = " ".join(section_titles).lower()
196
- ok &= assert_true("introduction" in section_text, "'Introduction' section present")
197
- ok &= assert_true("experiment" in section_text, "'Experiment' section present")
198
-
199
- # Check for the tip about reading specific sections
200
- ok &= assert_true("section=" in output, "has tip about using section parameter")
201
-
202
- # Check the abstract has actual content (not empty)
203
- abstract_match = re.search(r"## Abstract\n(.+?)(?:\n##|\n\*\*Tip)", output, re.DOTALL)
204
- if abstract_match:
205
- abstract_text = abstract_match.group(1).strip()
206
- ok &= assert_true(len(abstract_text) > 100, f"abstract has real content ({len(abstract_text)} chars)")
207
- else:
208
- ok &= assert_true(False, "could extract abstract text")
209
-
210
- return ok
211
-
212
-
213
- async def test_read_paper_section_by_number():
214
- print_test("read_paper section='4' for 2305.18290")
215
- output, success = await run(
216
- {"operation": "read_paper", "arxiv_id": "2305.18290", "section": "4"}
217
- )
218
- print_output(output, max_lines=30)
219
-
220
- ok = True
221
- ok &= assert_true(success, "success=True")
222
- ok &= assert_true("https://arxiv.org/abs/2305.18290" in output, "has arxiv URL")
223
-
224
- # Should have a section heading at top
225
- ok &= assert_true(output.startswith("# "), "starts with heading")
226
-
227
- # Should have substantial content
228
- ok &= assert_true(len(output) > 500, f"section has substantial content ({len(output)} chars)")
229
-
230
- # Should NOT have TOC structure (this is a single section, not the TOC)
231
- ok &= assert_true("## Sections" not in output, "is a single section (not TOC)")
232
-
233
- return ok
234
-
235
-
236
- async def test_read_paper_section_by_name():
237
- print_test("read_paper section='Experiments' for 2305.18290")
238
- output, success = await run(
239
- {"operation": "read_paper", "arxiv_id": "2305.18290", "section": "Experiments"}
240
- )
241
- print_output(output, max_lines=30)
242
-
243
- ok = True
244
- ok &= assert_true(success, "success=True")
245
-
246
- # Title should contain "Experiments"
247
- first_line = output.split("\n")[0]
248
- ok &= assert_true(
249
- "experiment" in first_line.lower(),
250
- f"heading contains 'Experiments': '{first_line}'",
251
- )
252
-
253
- ok &= assert_true(len(output) > 500, f"section has substantial content ({len(output)} chars)")
254
-
255
- return ok
256
-
257
-
258
- async def test_read_paper_old_paper():
259
- print_test("read_paper for 1706.03762 (Attention Is All You Need — 2017 paper)")
260
- output, success = await run({"operation": "read_paper", "arxiv_id": "1706.03762"})
261
- print_output(output, max_lines=30)
262
-
263
- ok = True
264
- ok &= assert_true(success, "success=True")
265
- ok &= assert_true("attention" in output.lower(), "mentions 'attention' (relevant content)")
266
-
267
- # Either we get sections (HTML available) or abstract fallback
268
- has_sections = "## Sections" in output
269
- has_abstract_fallback = "HTML version not available" in output
270
- ok &= assert_true(
271
- has_sections or has_abstract_fallback or "## Abstract" in output,
272
- "got either full sections, or abstract fallback",
273
- )
274
- if has_sections:
275
- print_success("HTML version available — got full sections")
276
- elif has_abstract_fallback:
277
- print_success("HTML not available — graceful fallback to abstract")
278
-
279
- return ok
280
-
281
-
282
- # ---------------------------------------------------------------------------
283
- # Test Suite 3: Linked Resources
284
- # ---------------------------------------------------------------------------
285
-
286
-
287
- async def test_find_datasets():
288
- print_test("find_datasets for 2305.18290 (limit=5, sort=downloads)")
289
- output, success = await run(
290
- {"operation": "find_datasets", "arxiv_id": "2305.18290", "limit": 5}
291
- )
292
- print_output(output)
293
-
294
- ok = True
295
- ok &= assert_true(success, "success=True")
296
- ok &= assert_true("Datasets linked to paper 2305.18290" in output, "has correct heading")
297
- ok &= assert_true("sorted by downloads" in output, "sorted by downloads (default)")
298
-
299
- # Check we got dataset entries with IDs
300
- dataset_ids = re.findall(r"\[([^\]]+)\]\(https://huggingface\.co/datasets/", output)
301
- ok &= assert_true(len(dataset_ids) > 0, f"found {len(dataset_ids)} dataset links")
302
- if dataset_ids:
303
- print_success(f"dataset IDs: {dataset_ids}")
304
-
305
- # Check download counts are present
306
- downloads = re.findall(r"Downloads: ([\d,]+)", output)
307
- ok &= assert_true(len(downloads) > 0, f"found download counts: {downloads}")
308
-
309
- # Check for inspect hint
310
- ok &= assert_true("hf_inspect_dataset" in output, "has inspect dataset hint")
311
-
312
- return ok
313
-
314
-
315
- async def test_find_datasets_sort_likes():
316
- print_test("find_datasets for 2305.18290 (sort=likes, limit=3)")
317
- output, success = await run(
318
- {"operation": "find_datasets", "arxiv_id": "2305.18290", "limit": 3, "sort": "likes"}
319
- )
320
- print_output(output)
321
-
322
- ok = True
323
- ok &= assert_true(success, "success=True")
324
- ok &= assert_true("sorted by likes" in output, "sorted by likes")
325
-
326
- return ok
327
-
328
-
329
- async def test_find_models():
330
- print_test("find_models for 2305.18290 (limit=5)")
331
- output, success = await run(
332
- {"operation": "find_models", "arxiv_id": "2305.18290", "limit": 5}
333
- )
334
- print_output(output)
335
-
336
- ok = True
337
- ok &= assert_true(success, "success=True")
338
- ok &= assert_true("Models linked to paper 2305.18290" in output, "has correct heading")
339
-
340
- # Check model links
341
- model_ids = re.findall(r"\[([^\]]+)\]\(https://huggingface\.co/", output)
342
- ok &= assert_true(len(model_ids) > 0, f"found {len(model_ids)} model links")
343
- if model_ids:
344
- print_success(f"model IDs: {model_ids}")
345
-
346
- # Check for pipeline_tag / library info
347
- has_task = "Task:" in output
348
- has_library = "Library:" in output
349
- ok &= assert_true(has_task or has_library, "has Task or Library metadata")
350
-
351
- return ok
352
-
353
-
354
- async def test_find_collections():
355
- print_test("find_collections for 2305.18290")
356
- output, success = await run(
357
- {"operation": "find_collections", "arxiv_id": "2305.18290"}
358
- )
359
- print_output(output)
360
-
361
- ok = True
362
- ok &= assert_true(success, "success=True")
363
- ok &= assert_true("Collections containing paper" in output, "has correct heading")
364
-
365
- # Check collection entries
366
- collection_urls = re.findall(r"https://huggingface\.co/collections/\S+", output)
367
- ok &= assert_true(len(collection_urls) > 0, f"found {len(collection_urls)} collection URLs")
368
-
369
- # Check for metadata
370
- ok &= assert_true("Upvotes:" in output, "has upvote counts")
371
- ok &= assert_true("Items:" in output, "has item counts")
372
-
373
- return ok
374
-
375
-
376
- async def test_find_all_resources():
377
- print_test("find_all_resources for 2305.18290 (parallel fan-out)")
378
- output, success = await run(
379
- {"operation": "find_all_resources", "arxiv_id": "2305.18290"}
380
- )
381
- print_output(output)
382
-
383
- ok = True
384
- ok &= assert_true(success, "success=True")
385
- ok &= assert_true("# Resources linked to paper 2305.18290" in output, "has unified heading")
386
- ok &= assert_true("https://huggingface.co/papers/2305.18290" in output, "has paper URL")
387
-
388
- # All three sections should be present
389
- ok &= assert_true("## Datasets" in output, "has Datasets section")
390
- ok &= assert_true("## Models" in output, "has Models section")
391
- ok &= assert_true("## Collections" in output, "has Collections section")
392
-
393
- # Check that sections have actual entries (not just "None found")
394
- ok &= assert_true("downloads)" in output, "datasets/models have download counts")
395
-
396
- return ok
397
-
398
-
399
- # ---------------------------------------------------------------------------
400
- # Test Suite 4: Edge Cases
401
- # ---------------------------------------------------------------------------
402
-
403
-
404
- async def test_search_no_results():
405
- print_test("search with gibberish query → should return empty gracefully")
406
- output, success = await run(
407
- {"operation": "search", "query": "xyzzyplugh_nonexistent_topic_9999"}
408
- )
409
- print_output(output)
410
-
411
- ok = True
412
- ok &= assert_true(success, "success=True (empty results is not an error)")
413
- ok &= assert_true("No papers found" in output, "says 'No papers found'")
414
-
415
- return ok
416
-
417
-
418
- async def test_missing_query():
419
- print_test("search without query → should error")
420
- output, success = await run({"operation": "search"})
421
- print_output(output)
422
-
423
- ok = True
424
- ok &= assert_true(not success, "success=False (missing required param)")
425
- ok &= assert_true("required" in output.lower(), "error mentions 'required'")
426
-
427
- return ok
428
-
429
-
430
- async def test_missing_arxiv_id():
431
- print_test("find_datasets without arxiv_id → should error")
432
- output, success = await run({"operation": "find_datasets"})
433
- print_output(output)
434
-
435
- ok = True
436
- ok &= assert_true(not success, "success=False")
437
- ok &= assert_true("required" in output.lower(), "error mentions 'required'")
438
-
439
- return ok
440
-
441
-
442
- async def test_invalid_arxiv_id():
443
- print_test("paper_details with nonexistent arxiv ID")
444
- output, success = await run({"operation": "paper_details", "arxiv_id": "0000.00000"})
445
- print_output(output)
446
-
447
- ok = True
448
- ok &= assert_true(not success, "success=False (API returns error)")
449
-
450
- return ok
451
-
452
-
453
- async def test_invalid_operation():
454
- print_test("invalid operation name → should error")
455
- output, success = await run({"operation": "nonexistent_op"})
456
- print_output(output)
457
-
458
- ok = True
459
- ok &= assert_true(not success, "success=False")
460
- ok &= assert_true("Unknown operation" in output, "says 'Unknown operation'")
461
- ok &= assert_true("trending" in output, "lists valid operations")
462
-
463
- return ok
464
-
465
-
466
- async def test_read_paper_bad_section():
467
- print_test("read_paper with nonexistent section → should error with available sections")
468
- output, success = await run(
469
- {"operation": "read_paper", "arxiv_id": "2305.18290", "section": "Nonexistent Section XYZ"}
470
- )
471
- print_output(output)
472
-
473
- ok = True
474
- ok &= assert_true(not success, "success=False")
475
- ok &= assert_true("not found" in output.lower(), "says section 'not found'")
476
- ok &= assert_true("Introduction" in output, "lists available sections (includes Introduction)")
477
-
478
- return ok
479
-
480
-
481
- # ---------------------------------------------------------------------------
482
- # Main
483
- # ---------------------------------------------------------------------------
484
-
485
-
486
- async def main():
487
- print("=" * 70)
488
- print(f"{BLUE}HF Papers Tool — Integration Tests{RESET}")
489
- print(f"{BLUE}All APIs are public, no authentication required.{RESET}")
490
- print("=" * 70)
491
-
492
- all_tests = [
493
- # Suite 1: Paper Discovery
494
- ("Paper Discovery", [
495
- test_trending,
496
- test_trending_with_query,
497
- test_search,
498
- test_paper_details,
499
- ]),
500
- # Suite 2: Read Paper
501
- ("Read Paper", [
502
- test_read_paper_toc,
503
- test_read_paper_section_by_number,
504
- test_read_paper_section_by_name,
505
- test_read_paper_old_paper,
506
- ]),
507
- # Suite 3: Linked Resources
508
- ("Linked Resources", [
509
- test_find_datasets,
510
- test_find_datasets_sort_likes,
511
- test_find_models,
512
- test_find_collections,
513
- test_find_all_resources,
514
- ]),
515
- # Suite 4: Edge Cases
516
- ("Edge Cases", [
517
- test_search_no_results,
518
- test_missing_query,
519
- test_missing_arxiv_id,
520
- test_invalid_arxiv_id,
521
- test_invalid_operation,
522
- test_read_paper_bad_section,
523
- ]),
524
- ]
525
-
526
- global assertions_passed, assertions_failed
527
- suite_results = []
528
-
529
- for suite_name, tests in all_tests:
530
- print(f"\n{YELLOW}{'=' * 70}{RESET}")
531
- print(f"{YELLOW}Test Suite: {suite_name} ({len(tests)} tests){RESET}")
532
- print(f"{YELLOW}{'=' * 70}{RESET}")
533
-
534
- suite_pass = 0
535
- suite_fail = 0
536
-
537
- for test_fn in tests:
538
- try:
539
- test_ok = await test_fn()
540
- if test_ok:
541
- suite_pass += 1
542
- else:
543
- suite_fail += 1
544
- except Exception as e:
545
- print_error(f"CRASHED: {e}")
546
- import traceback
547
- traceback.print_exc()
548
- suite_fail += 1
549
-
550
- suite_results.append((suite_name, suite_pass, suite_fail))
551
-
552
- # Summary
553
- print(f"\n{'=' * 70}")
554
- print(f"{BLUE}Summary{RESET}")
555
- print(f"{'=' * 70}")
556
- for suite_name, sp, sf in suite_results:
557
- icon = f"{GREEN}✓{RESET}" if sf == 0 else f"{RED}✗{RESET}"
558
- print(f" {icon} {suite_name}: {sp}/{sp + sf} tests passed")
559
-
560
- print(f"{'─' * 70}")
561
- total_tests = sum(sp + sf for _, sp, sf in suite_results)
562
- total_failed = sum(sf for _, _, sf in suite_results)
563
- print(f" Assertions: {assertions_passed} passed, {assertions_failed} failed")
564
- print(f" Tests: {total_tests - total_failed}/{total_tests} passed")
565
- print(f"{'=' * 70}\n")
566
-
567
- if total_failed > 0 or assertions_failed > 0:
568
- sys.exit(1)
569
-
570
-
571
- if __name__ == "__main__":
572
- asyncio.run(main())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/unit/__init__.py DELETED
File without changes
tests/unit/tools/__init__.py DELETED
File without changes
tests/unit/tools/test_jobs_tool.py DELETED
@@ -1,537 +0,0 @@
1
- """
2
- Tests for HF Jobs Tool
3
-
4
- Tests the refactored jobs tool implementation using huggingface-hub library
5
- """
6
-
7
- from unittest.mock import AsyncMock, patch
8
-
9
- import pytest
10
-
11
- from agent.tools.jobs_tool import HfJobsTool, hf_jobs_handler
12
-
13
-
14
- def create_mock_job_info(
15
- job_id="test-job-1",
16
- stage="RUNNING",
17
- command=None,
18
- docker_image="python:3.12",
19
- ):
20
- """Create a mock JobInfo object"""
21
- from huggingface_hub._jobs_api import JobInfo
22
-
23
- if command is None:
24
- command = ["echo", "test"]
25
-
26
- return JobInfo(
27
- id=job_id,
28
- created_at="2024-01-01T00:00:00.000000Z",
29
- docker_image=docker_image,
30
- space_id=None,
31
- command=command,
32
- arguments=[],
33
- environment={},
34
- secrets={},
35
- flavor="cpu-basic",
36
- status={"stage": stage, "message": None},
37
- owner={"id": "123", "name": "test-user", "type": "user"},
38
- endpoint="https://huggingface.co",
39
- url=f"https://huggingface.co/jobs/test-user/{job_id}",
40
- )
41
-
42
-
43
- def create_mock_scheduled_job_info(
44
- job_id="sched-job-1",
45
- schedule="@daily",
46
- suspend=False,
47
- ):
48
- """Create a mock ScheduledJobInfo object"""
49
- from huggingface_hub._jobs_api import ScheduledJobInfo
50
-
51
- return ScheduledJobInfo(
52
- id=job_id,
53
- created_at="2024-01-01T00:00:00.000000Z",
54
- job_spec={
55
- "docker_image": "python:3.12",
56
- "space_id": None,
57
- "command": ["python", "backup.py"],
58
- "arguments": [],
59
- "environment": {},
60
- "secrets": {},
61
- "flavor": "cpu-basic",
62
- "timeout": 1800,
63
- "tags": None,
64
- "arch": None,
65
- },
66
- schedule=schedule,
67
- suspend=suspend,
68
- concurrency=False,
69
- status={
70
- "last_job": None,
71
- "next_job_run_at": "2024-01-02T00:00:00.000000Z",
72
- },
73
- owner={"id": "123", "name": "test-user", "type": "user"},
74
- )
75
-
76
-
77
- @pytest.mark.asyncio
78
- async def test_show_help():
79
- """Test that help message is shown when no operation specified"""
80
- tool = HfJobsTool()
81
- result = await tool.execute({})
82
-
83
- assert "HuggingFace Jobs API" in result["formatted"]
84
- assert "Available Commands" in result["formatted"]
85
- assert result["totalResults"] == 1
86
- assert not result.get("isError", False)
87
-
88
-
89
- @pytest.mark.asyncio
90
- async def test_show_operation_help():
91
- """Test operation-specific help"""
92
- tool = HfJobsTool()
93
- result = await tool.execute({"operation": "run", "args": {"help": True}})
94
-
95
- assert "Help for operation" in result["formatted"]
96
- assert result["totalResults"] == 1
97
-
98
-
99
- @pytest.mark.asyncio
100
- async def test_invalid_operation():
101
- """Test invalid operation handling"""
102
- tool = HfJobsTool()
103
- result = await tool.execute({"operation": "invalid_op"})
104
-
105
- assert result.get("isError") == True
106
- assert "Unknown operation" in result["formatted"]
107
-
108
-
109
- @pytest.mark.asyncio
110
- async def test_run_job_missing_command():
111
- """Test run job with missing required parameter"""
112
- tool = HfJobsTool()
113
-
114
- # Mock the HfApi.run_job to raise an error
115
- with patch.object(tool.api, "run_job") as mock_run:
116
- mock_run.side_effect = Exception("command parameter is required")
117
-
118
- result = await tool.execute(
119
- {"operation": "run", "args": {"image": "python:3.12"}}
120
- )
121
-
122
- assert result.get("isError") == True
123
-
124
-
125
- @pytest.mark.asyncio
126
- async def test_list_jobs_mock():
127
- """Test list jobs with mock API"""
128
- tool = HfJobsTool()
129
-
130
- # Create mock job objects
131
- running_job = create_mock_job_info("test-job-1", "RUNNING")
132
- completed_job = create_mock_job_info(
133
- "test-job-2", "COMPLETED", ["python", "script.py"]
134
- )
135
-
136
- # Mock the HfApi.list_jobs method
137
- with patch.object(tool.api, "list_jobs") as mock_list:
138
- mock_list.return_value = [running_job, completed_job]
139
-
140
- # Test listing only running jobs (default)
141
- result = await tool.execute({"operation": "ps"})
142
-
143
- assert not result.get("isError", False)
144
- assert "test-job-1" in result["formatted"]
145
- assert "test-job-2" not in result["formatted"] # COMPLETED jobs filtered out
146
- assert result["totalResults"] == 1
147
- assert result["resultsShared"] == 1
148
-
149
- # Test listing all jobs
150
- result = await tool.execute({"operation": "ps", "args": {"all": True}})
151
-
152
- assert not result.get("isError", False)
153
- assert "test-job-1" in result["formatted"]
154
- assert "test-job-2" in result["formatted"]
155
- assert result["totalResults"] == 2
156
- assert result["resultsShared"] == 2
157
-
158
-
159
- @pytest.mark.asyncio
160
- async def test_inspect_job_mock():
161
- """Test inspect job with mock API"""
162
- tool = HfJobsTool()
163
-
164
- mock_job = create_mock_job_info("test-job-1", "RUNNING")
165
-
166
- with patch.object(tool.api, "inspect_job") as mock_inspect:
167
- mock_inspect.return_value = mock_job
168
-
169
- result = await tool.execute(
170
- {"operation": "inspect", "args": {"job_id": "test-job-1"}}
171
- )
172
-
173
- assert not result.get("isError", False)
174
- assert "test-job-1" in result["formatted"]
175
- assert "Job Details" in result["formatted"]
176
- mock_inspect.assert_called_once()
177
-
178
-
179
- @pytest.mark.asyncio
180
- async def test_cancel_job_mock():
181
- """Test cancel job with mock API"""
182
- tool = HfJobsTool()
183
-
184
- with patch.object(tool.api, "cancel_job") as mock_cancel:
185
- mock_cancel.return_value = None
186
-
187
- result = await tool.execute(
188
- {"operation": "cancel", "args": {"job_id": "test-job-1"}}
189
- )
190
-
191
- assert not result.get("isError", False)
192
- assert "cancelled" in result["formatted"]
193
- assert "test-job-1" in result["formatted"]
194
- mock_cancel.assert_called_once()
195
-
196
-
197
- @pytest.mark.asyncio
198
- async def test_run_job_mock():
199
- """Test run job with mock API"""
200
- tool = HfJobsTool()
201
-
202
- mock_job = create_mock_job_info("new-job-123", "RUNNING")
203
-
204
- with patch.object(tool.api, "run_job") as mock_run:
205
- mock_run.return_value = mock_job
206
-
207
- result = await tool.execute(
208
- {
209
- "operation": "run",
210
- "args": {
211
- "image": "python:3.12",
212
- "command": ["python", "-c", "print('test')"],
213
- "flavor": "cpu-basic",
214
- "detach": True,
215
- },
216
- }
217
- )
218
-
219
- assert not result.get("isError", False)
220
- assert "new-job-123" in result["formatted"]
221
- assert "Job started" in result["formatted"]
222
- mock_run.assert_called_once()
223
-
224
-
225
- @pytest.mark.asyncio
226
- async def test_run_uv_job_mock():
227
- """Test run UV job with mock API"""
228
- tool = HfJobsTool()
229
-
230
- mock_job = create_mock_job_info("uv-job-456", "RUNNING")
231
-
232
- with patch.object(tool.api, "run_uv_job") as mock_run:
233
- mock_run.return_value = mock_job
234
-
235
- result = await tool.execute(
236
- {
237
- "operation": "uv",
238
- "args": {
239
- "script": "print('Hello UV')",
240
- "flavor": "cpu-basic",
241
- },
242
- }
243
- )
244
-
245
- assert not result.get("isError", False)
246
- assert "uv-job-456" in result["formatted"]
247
- assert "UV Job started" in result["formatted"]
248
- mock_run.assert_called_once()
249
-
250
-
251
- @pytest.mark.asyncio
252
- async def test_get_logs_mock():
253
- """Test get logs with mock API"""
254
- tool = HfJobsTool()
255
-
256
- # Mock fetch_job_logs to return a generator
257
- def log_generator():
258
- yield "Log line 1"
259
- yield "Log line 2"
260
- yield "Hello from HF Jobs!"
261
-
262
- with patch.object(tool.api, "fetch_job_logs") as mock_logs:
263
- mock_logs.return_value = log_generator()
264
-
265
- result = await tool.execute(
266
- {"operation": "logs", "args": {"job_id": "test-job-1"}}
267
- )
268
-
269
- assert not result.get("isError", False)
270
- assert "Log line 1" in result["formatted"]
271
- assert "Hello from HF Jobs!" in result["formatted"]
272
-
273
-
274
- @pytest.mark.asyncio
275
- async def test_handler():
276
- """Test the handler function"""
277
- with patch("agent.tools.jobs_tool.HfJobsTool") as MockTool:
278
- mock_tool_instance = MockTool.return_value
279
- mock_tool_instance.execute = AsyncMock(
280
- return_value={
281
- "formatted": "Test output",
282
- "totalResults": 1,
283
- "resultsShared": 1,
284
- "isError": False,
285
- }
286
- )
287
-
288
- output, success = await hf_jobs_handler({"operation": "ps"})
289
-
290
- assert success == True
291
- assert "Test output" in output
292
-
293
-
294
- @pytest.mark.asyncio
295
- async def test_handler_error():
296
- """Test handler with error"""
297
- with patch("agent.tools.jobs_tool.HfJobsTool") as MockTool:
298
- MockTool.side_effect = Exception("Test error")
299
-
300
- output, success = await hf_jobs_handler({})
301
-
302
- assert success == False
303
- assert "Error" in output
304
-
305
-
306
- @pytest.mark.asyncio
307
- async def test_scheduled_jobs_mock():
308
- """Test scheduled jobs operations with mock API"""
309
- tool = HfJobsTool()
310
-
311
- mock_scheduled_job = create_mock_scheduled_job_info()
312
-
313
- # Test list scheduled jobs
314
- with patch.object(tool.api, "list_scheduled_jobs") as mock_list:
315
- mock_list.return_value = [mock_scheduled_job]
316
-
317
- result = await tool.execute({"operation": "scheduled ps"})
318
-
319
- assert not result.get("isError", False)
320
- assert "sched-job-1" in result["formatted"]
321
- assert "Scheduled Jobs" in result["formatted"]
322
-
323
-
324
- @pytest.mark.asyncio
325
- async def test_create_scheduled_job_mock():
326
- """Test create scheduled job with mock API"""
327
- tool = HfJobsTool()
328
-
329
- mock_scheduled_job = create_mock_scheduled_job_info()
330
-
331
- with patch.object(tool.api, "create_scheduled_job") as mock_create:
332
- mock_create.return_value = mock_scheduled_job
333
-
334
- result = await tool.execute(
335
- {
336
- "operation": "scheduled run",
337
- "args": {
338
- "image": "python:3.12",
339
- "command": ["python", "backup.py"],
340
- "schedule": "@daily",
341
- "flavor": "cpu-basic",
342
- },
343
- }
344
- )
345
-
346
- assert not result.get("isError", False)
347
- assert "sched-job-1" in result["formatted"]
348
- assert "Scheduled job created" in result["formatted"]
349
- mock_create.assert_called_once()
350
-
351
-
352
- @pytest.mark.asyncio
353
- async def test_inspect_scheduled_job_mock():
354
- """Test inspect scheduled job with mock API"""
355
- tool = HfJobsTool()
356
-
357
- mock_scheduled_job = create_mock_scheduled_job_info()
358
-
359
- with patch.object(tool.api, "inspect_scheduled_job") as mock_inspect:
360
- mock_inspect.return_value = mock_scheduled_job
361
-
362
- result = await tool.execute(
363
- {
364
- "operation": "scheduled inspect",
365
- "args": {"scheduled_job_id": "sched-job-1"},
366
- }
367
- )
368
-
369
- assert not result.get("isError", False)
370
- assert "sched-job-1" in result["formatted"]
371
- assert "Scheduled Job Details" in result["formatted"]
372
-
373
-
374
- @pytest.mark.asyncio
375
- async def test_suspend_scheduled_job_mock():
376
- """Test suspend scheduled job with mock API"""
377
- tool = HfJobsTool()
378
-
379
- with patch.object(tool.api, "suspend_scheduled_job") as mock_suspend:
380
- mock_suspend.return_value = None
381
-
382
- result = await tool.execute(
383
- {
384
- "operation": "scheduled suspend",
385
- "args": {"scheduled_job_id": "sched-job-1"},
386
- }
387
- )
388
-
389
- assert not result.get("isError", False)
390
- assert "suspended" in result["formatted"]
391
- assert "sched-job-1" in result["formatted"]
392
-
393
-
394
- @pytest.mark.asyncio
395
- async def test_resume_scheduled_job_mock():
396
- """Test resume scheduled job with mock API"""
397
- tool = HfJobsTool()
398
-
399
- with patch.object(tool.api, "resume_scheduled_job") as mock_resume:
400
- mock_resume.return_value = None
401
-
402
- result = await tool.execute(
403
- {
404
- "operation": "scheduled resume",
405
- "args": {"scheduled_job_id": "sched-job-1"},
406
- }
407
- )
408
-
409
- assert not result.get("isError", False)
410
- assert "resumed" in result["formatted"]
411
- assert "sched-job-1" in result["formatted"]
412
-
413
-
414
- @pytest.mark.asyncio
415
- async def test_delete_scheduled_job_mock():
416
- """Test delete scheduled job with mock API"""
417
- tool = HfJobsTool()
418
-
419
- with patch.object(tool.api, "delete_scheduled_job") as mock_delete:
420
- mock_delete.return_value = None
421
-
422
- result = await tool.execute(
423
- {
424
- "operation": "scheduled delete",
425
- "args": {"scheduled_job_id": "sched-job-1"},
426
- }
427
- )
428
-
429
- assert not result.get("isError", False)
430
- assert "deleted" in result["formatted"]
431
- assert "sched-job-1" in result["formatted"]
432
-
433
-
434
- @pytest.mark.asyncio
435
- async def test_list_jobs_with_status_filter():
436
- """Test list jobs with status filter"""
437
- tool = HfJobsTool()
438
-
439
- running_job = create_mock_job_info("job-1", "RUNNING")
440
- completed_job = create_mock_job_info("job-2", "COMPLETED")
441
- error_job = create_mock_job_info("job-3", "ERROR")
442
-
443
- with patch.object(tool.api, "list_jobs") as mock_list:
444
- mock_list.return_value = [running_job, completed_job, error_job]
445
-
446
- # Filter by status
447
- result = await tool.execute(
448
- {"operation": "ps", "args": {"all": True, "status": "ERROR"}}
449
- )
450
-
451
- assert not result.get("isError", False)
452
- assert "job-3" in result["formatted"]
453
- assert "job-1" not in result["formatted"]
454
- assert result["resultsShared"] == 1
455
-
456
-
457
- def test_filter_uv_install_output():
458
- """Test filtering of UV package installation output"""
459
- from agent.tools.jobs_tool import _filter_uv_install_output
460
-
461
- # Test case 1: Logs with UV installation output
462
- logs_with_install = [
463
- "Resolved 68 packages in 1.01s",
464
- "Installed 68 packages in 251ms",
465
- "Hello from the script!",
466
- "Script execution completed",
467
- ]
468
-
469
- filtered = _filter_uv_install_output(logs_with_install)
470
- assert len(filtered) == 4
471
- assert filtered[0] == "[installs truncated]"
472
- assert filtered[1] == "Installed 68 packages in 251ms"
473
- assert filtered[2] == "Hello from the script!"
474
- assert filtered[3] == "Script execution completed"
475
-
476
- # Test case 2: Logs without UV installation output
477
- logs_without_install = [
478
- "Script started",
479
- "Processing data...",
480
- "Done!",
481
- ]
482
-
483
- filtered = _filter_uv_install_output(logs_without_install)
484
- assert len(filtered) == 3
485
- assert filtered == logs_without_install
486
-
487
- # Test case 3: Empty logs
488
- assert _filter_uv_install_output([]) == []
489
-
490
- # Test case 4: Different time formats (ms vs s)
491
- logs_with_seconds = [
492
- "Downloading packages...",
493
- "Installed 10 packages in 2s",
494
- "Running main.py",
495
- ]
496
-
497
- filtered = _filter_uv_install_output(logs_with_seconds)
498
- assert len(filtered) == 3
499
- assert filtered[0] == "[installs truncated]"
500
- assert filtered[1] == "Installed 10 packages in 2s"
501
- assert filtered[2] == "Running main.py"
502
-
503
- # Test case 5: Single package
504
- logs_single_package = [
505
- "Resolving dependencies",
506
- "Installed 1 package in 50ms",
507
- "Import successful",
508
- ]
509
-
510
- filtered = _filter_uv_install_output(logs_single_package)
511
- assert len(filtered) == 3
512
- assert filtered[0] == "[installs truncated]"
513
- assert filtered[1] == "Installed 1 package in 50ms"
514
- assert filtered[2] == "Import successful"
515
-
516
- # Test case 6: Decimal time values
517
- logs_decimal_time = [
518
- "Starting installation",
519
- "Installed 25 packages in 125.5ms",
520
- "All dependencies ready",
521
- ]
522
-
523
- filtered = _filter_uv_install_output(logs_decimal_time)
524
- assert len(filtered) == 3
525
- assert filtered[0] == "[installs truncated]"
526
- assert filtered[1] == "Installed 25 packages in 125.5ms"
527
- assert filtered[2] == "All dependencies ready"
528
-
529
- # Test case 7: "Installed" line is first (no truncation needed)
530
- logs_install_first = [
531
- "Installed 5 packages in 100ms",
532
- "Running script...",
533
- ]
534
-
535
- filtered = _filter_uv_install_output(logs_install_first)
536
- # No truncation message if "Installed" is the first line
537
- assert filtered == logs_install_first