FossilRepo

fossilrepo / tests / test_mcp_server.py
Blame History Raw 711 lines
1
"""Tests for MCP server tool definitions and handlers.
2
3
Covers:
4
- Tool registry: all 17 tools registered with correct schemas
5
- Tool dispatch: execute_tool routes to correct handler
6
- Read handlers: list_projects, get_project, browse_code, read_file,
7
get_timeline, get_checkin, search_code, list_tickets, get_ticket,
8
list_wiki_pages, get_wiki_page, list_branches, get_file_blame,
9
get_file_history, sql_query
10
- Write handlers: create_ticket, update_ticket
11
- Error handling: unknown tool, missing project, exceptions
12
"""
13
14
from datetime import UTC, datetime
15
from unittest.mock import MagicMock, patch
16
17
import pytest
18
19
from fossil.models import FossilRepository
20
from fossil.reader import (
21
CheckinDetail,
22
FileEntry,
23
RepoMetadata,
24
TicketEntry,
25
TimelineEntry,
26
WikiPage,
27
)
28
from mcp_server.tools import TOOLS, execute_tool
29
30
# Patch targets -- tools.py does deferred imports inside handler functions,
31
# so we patch at the source module rather than at the consumer.
32
_READER = "fossil.reader.FossilReader"
33
_CLI = "fossil.cli.FossilCLI"
34
35
# ---------------------------------------------------------------------------
36
# Fixtures
37
# ---------------------------------------------------------------------------
38
39
40
@pytest.fixture
41
def fossil_repo_obj(sample_project):
42
"""Return the auto-created FossilRepository for sample_project."""
43
return FossilRepository.objects.get(project=sample_project, deleted_at__isnull=True)
44
45
46
def _mock_reader():
47
"""Return a context-manager mock for FossilReader."""
48
reader = MagicMock()
49
reader.__enter__ = MagicMock(return_value=reader)
50
reader.__exit__ = MagicMock(return_value=False)
51
return reader
52
53
54
# ---------------------------------------------------------------------------
55
# Tool registry tests
56
# ---------------------------------------------------------------------------
57
58
59
class TestToolRegistry:
60
def test_all_17_tools_registered(self):
61
assert len(TOOLS) == 17
62
63
def test_tool_names_are_unique(self):
64
names = [t.name for t in TOOLS]
65
assert len(names) == len(set(names))
66
67
def test_every_tool_has_input_schema(self):
68
for tool in TOOLS:
69
assert tool.inputSchema is not None
70
assert tool.inputSchema.get("type") == "object"
71
72
def test_every_tool_has_description(self):
73
for tool in TOOLS:
74
assert tool.description
75
assert len(tool.description) > 10
76
77
def test_expected_tools_present(self):
78
names = {t.name for t in TOOLS}
79
expected = {
80
"list_projects",
81
"get_project",
82
"browse_code",
83
"read_file",
84
"get_timeline",
85
"get_checkin",
86
"search_code",
87
"list_tickets",
88
"get_ticket",
89
"create_ticket",
90
"update_ticket",
91
"list_wiki_pages",
92
"get_wiki_page",
93
"list_branches",
94
"get_file_blame",
95
"get_file_history",
96
"sql_query",
97
}
98
assert names == expected
99
100
def test_slug_required_for_project_scoped_tools(self):
101
"""All tools except list_projects require a slug parameter."""
102
for tool in TOOLS:
103
if tool.name == "list_projects":
104
assert "slug" not in tool.inputSchema.get("required", [])
105
else:
106
assert "slug" in tool.inputSchema.get("required", []), f"{tool.name} should require slug"
107
108
109
# ---------------------------------------------------------------------------
110
# Dispatch tests
111
# ---------------------------------------------------------------------------
112
113
114
class TestDispatch:
115
def test_unknown_tool_returns_error(self):
116
result = execute_tool("nonexistent_tool", {})
117
assert "error" in result
118
assert "Unknown tool" in result["error"]
119
120
@pytest.mark.django_db
121
def test_missing_project_returns_error(self):
122
result = execute_tool("get_project", {"slug": "does-not-exist"})
123
assert "error" in result
124
125
@pytest.mark.django_db
126
def test_exception_in_handler_returns_error(self, sample_project):
127
with patch("mcp_server.tools._get_repo", side_effect=RuntimeError("boom")):
128
result = execute_tool("get_project", {"slug": sample_project.slug})
129
assert "error" in result
130
assert "boom" in result["error"]
131
132
133
# ---------------------------------------------------------------------------
134
# list_projects
135
# ---------------------------------------------------------------------------
136
137
138
@pytest.mark.django_db
139
class TestListProjects:
140
def test_returns_all_active_projects(self, sample_project):
141
result = execute_tool("list_projects", {})
142
assert "projects" in result
143
slugs = [p["slug"] for p in result["projects"]]
144
assert sample_project.slug in slugs
145
146
def test_excludes_deleted_projects(self, sample_project, admin_user):
147
sample_project.soft_delete(user=admin_user)
148
result = execute_tool("list_projects", {})
149
slugs = [p["slug"] for p in result["projects"]]
150
assert sample_project.slug not in slugs
151
152
153
# ---------------------------------------------------------------------------
154
# get_project
155
# ---------------------------------------------------------------------------
156
157
158
@pytest.mark.django_db
159
class TestGetProject:
160
@patch(_READER)
161
def test_returns_project_details(self, mock_reader_cls, sample_project, fossil_repo_obj):
162
reader = _mock_reader()
163
reader.get_metadata.return_value = RepoMetadata(
164
project_name="Test",
165
checkin_count=10,
166
ticket_count=3,
167
wiki_page_count=2,
168
)
169
mock_reader_cls.return_value = reader
170
171
with patch.object(type(fossil_repo_obj), "exists_on_disk", new_callable=lambda: property(lambda s: True)):
172
result = execute_tool("get_project", {"slug": sample_project.slug})
173
174
assert result["name"] == sample_project.name
175
assert result["slug"] == sample_project.slug
176
assert result["visibility"] == sample_project.visibility
177
178
def test_nonexistent_slug_returns_error(self):
179
result = execute_tool("get_project", {"slug": "no-such-project"})
180
assert "error" in result
181
182
183
# ---------------------------------------------------------------------------
184
# browse_code
185
# ---------------------------------------------------------------------------
186
187
188
@pytest.mark.django_db
189
class TestBrowseCode:
190
@patch(_READER)
191
def test_lists_files_at_root(self, mock_reader_cls, sample_project, fossil_repo_obj):
192
reader = _mock_reader()
193
reader.get_latest_checkin_uuid.return_value = "abc123"
194
reader.get_files_at_checkin.return_value = [
195
FileEntry(name="README.md", uuid="f1", size=100),
196
FileEntry(name="src/main.py", uuid="f2", size=200),
197
]
198
mock_reader_cls.return_value = reader
199
200
result = execute_tool("browse_code", {"slug": sample_project.slug})
201
assert len(result["files"]) == 2
202
assert result["checkin"] == "abc123"
203
204
@patch(_READER)
205
def test_filters_by_path(self, mock_reader_cls, sample_project, fossil_repo_obj):
206
reader = _mock_reader()
207
reader.get_latest_checkin_uuid.return_value = "abc123"
208
reader.get_files_at_checkin.return_value = [
209
FileEntry(name="README.md", uuid="f1", size=100),
210
FileEntry(name="src/main.py", uuid="f2", size=200),
211
FileEntry(name="src/utils.py", uuid="f3", size=150),
212
]
213
mock_reader_cls.return_value = reader
214
215
result = execute_tool("browse_code", {"slug": sample_project.slug, "path": "src"})
216
assert len(result["files"]) == 2
217
assert all(f["name"].startswith("src/") for f in result["files"])
218
219
@patch(_READER)
220
def test_empty_repo_returns_error(self, mock_reader_cls, sample_project, fossil_repo_obj):
221
reader = _mock_reader()
222
reader.get_latest_checkin_uuid.return_value = None
223
mock_reader_cls.return_value = reader
224
225
result = execute_tool("browse_code", {"slug": sample_project.slug})
226
assert "error" in result
227
228
229
# ---------------------------------------------------------------------------
230
# read_file
231
# ---------------------------------------------------------------------------
232
233
234
@pytest.mark.django_db
235
class TestReadFile:
236
@patch(_READER)
237
def test_reads_text_file(self, mock_reader_cls, sample_project, fossil_repo_obj):
238
reader = _mock_reader()
239
reader.get_latest_checkin_uuid.return_value = "abc123"
240
reader.get_files_at_checkin.return_value = [
241
FileEntry(name="README.md", uuid="f1", size=100),
242
]
243
reader.get_file_content.return_value = b"# Hello World"
244
mock_reader_cls.return_value = reader
245
246
result = execute_tool("read_file", {"slug": sample_project.slug, "filepath": "README.md"})
247
assert result["filepath"] == "README.md"
248
assert result["content"] == "# Hello World"
249
250
@patch(_READER)
251
def test_binary_file_returns_metadata(self, mock_reader_cls, sample_project, fossil_repo_obj):
252
reader = _mock_reader()
253
reader.get_latest_checkin_uuid.return_value = "abc123"
254
reader.get_files_at_checkin.return_value = [
255
FileEntry(name="image.png", uuid="f1", size=5000),
256
]
257
reader.get_file_content.return_value = b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00"
258
mock_reader_cls.return_value = reader
259
260
result = execute_tool("read_file", {"slug": sample_project.slug, "filepath": "image.png"})
261
assert result["binary"] is True
262
assert result["size"] > 0
263
264
@patch(_READER)
265
def test_file_not_found(self, mock_reader_cls, sample_project, fossil_repo_obj):
266
reader = _mock_reader()
267
reader.get_latest_checkin_uuid.return_value = "abc123"
268
reader.get_files_at_checkin.return_value = []
269
mock_reader_cls.return_value = reader
270
271
result = execute_tool("read_file", {"slug": sample_project.slug, "filepath": "nope.txt"})
272
assert "error" in result
273
assert "not found" in result["error"].lower()
274
275
276
# ---------------------------------------------------------------------------
277
# get_timeline
278
# ---------------------------------------------------------------------------
279
280
281
@pytest.mark.django_db
282
class TestGetTimeline:
283
@patch(_READER)
284
def test_returns_checkins(self, mock_reader_cls, sample_project, fossil_repo_obj):
285
reader = _mock_reader()
286
reader.get_timeline.return_value = [
287
TimelineEntry(
288
rid=1,
289
uuid="abc123",
290
event_type="ci",
291
timestamp=datetime(2025, 1, 15, 10, 0, 0, tzinfo=UTC),
292
user="alice",
293
comment="Initial commit",
294
branch="trunk",
295
),
296
]
297
mock_reader_cls.return_value = reader
298
299
result = execute_tool("get_timeline", {"slug": sample_project.slug})
300
assert len(result["checkins"]) == 1
301
assert result["checkins"][0]["uuid"] == "abc123"
302
assert result["checkins"][0]["user"] == "alice"
303
304
@patch(_READER)
305
def test_branch_filter(self, mock_reader_cls, sample_project, fossil_repo_obj):
306
reader = _mock_reader()
307
reader.get_timeline.return_value = [
308
TimelineEntry(
309
rid=1,
310
uuid="a1",
311
event_type="ci",
312
timestamp=datetime(2025, 1, 15, tzinfo=UTC),
313
user="alice",
314
comment="on trunk",
315
branch="trunk",
316
),
317
TimelineEntry(
318
rid=2,
319
uuid="b2",
320
event_type="ci",
321
timestamp=datetime(2025, 1, 14, tzinfo=UTC),
322
user="bob",
323
comment="on feature",
324
branch="feature-x",
325
),
326
]
327
mock_reader_cls.return_value = reader
328
329
result = execute_tool("get_timeline", {"slug": sample_project.slug, "branch": "trunk"})
330
assert len(result["checkins"]) == 1
331
assert result["checkins"][0]["branch"] == "trunk"
332
333
334
# ---------------------------------------------------------------------------
335
# get_checkin
336
# ---------------------------------------------------------------------------
337
338
339
@pytest.mark.django_db
340
class TestGetCheckin:
341
@patch(_READER)
342
def test_returns_checkin_detail(self, mock_reader_cls, sample_project, fossil_repo_obj):
343
reader = _mock_reader()
344
reader.get_checkin_detail.return_value = CheckinDetail(
345
uuid="abc123full",
346
timestamp=datetime(2025, 1, 15, 10, 0, 0, tzinfo=UTC),
347
user="alice",
348
comment="Initial commit",
349
branch="trunk",
350
parent_uuid="parent000",
351
files_changed=[{"name": "README.md", "change_type": "added", "uuid": "f1", "prev_uuid": ""}],
352
)
353
mock_reader_cls.return_value = reader
354
355
result = execute_tool("get_checkin", {"slug": sample_project.slug, "uuid": "abc123"})
356
assert result["uuid"] == "abc123full"
357
assert len(result["files_changed"]) == 1
358
359
@patch(_READER)
360
def test_checkin_not_found(self, mock_reader_cls, sample_project, fossil_repo_obj):
361
reader = _mock_reader()
362
reader.get_checkin_detail.return_value = None
363
mock_reader_cls.return_value = reader
364
365
result = execute_tool("get_checkin", {"slug": sample_project.slug, "uuid": "nonexistent"})
366
assert "error" in result
367
368
369
# ---------------------------------------------------------------------------
370
# search_code
371
# ---------------------------------------------------------------------------
372
373
374
@pytest.mark.django_db
375
class TestSearchCode:
376
@patch(_READER)
377
def test_returns_search_results(self, mock_reader_cls, sample_project, fossil_repo_obj):
378
reader = _mock_reader()
379
reader.search.return_value = {
380
"checkins": [{"uuid": "abc", "timestamp": datetime(2025, 1, 15, tzinfo=UTC), "user": "alice", "comment": "fix bug"}],
381
"tickets": [{"uuid": "tkt1", "title": "Bug report", "status": "Open", "created": datetime(2025, 1, 10, tzinfo=UTC)}],
382
"wiki": [{"name": "Debugging"}],
383
}
384
mock_reader_cls.return_value = reader
385
386
result = execute_tool("search_code", {"slug": sample_project.slug, "query": "bug"})
387
assert len(result["checkins"]) == 1
388
assert len(result["tickets"]) == 1
389
assert len(result["wiki"]) == 1
390
# Timestamps should be serialized to strings
391
assert isinstance(result["checkins"][0]["timestamp"], str)
392
393
394
# ---------------------------------------------------------------------------
395
# list_tickets / get_ticket
396
# ---------------------------------------------------------------------------
397
398
399
@pytest.mark.django_db
400
class TestTickets:
401
@patch(_READER)
402
def test_list_tickets(self, mock_reader_cls, sample_project, fossil_repo_obj):
403
reader = _mock_reader()
404
reader.get_tickets.return_value = [
405
TicketEntry(
406
uuid="tkt-001",
407
title="Fix bug",
408
status="Open",
409
type="Code_Defect",
410
created=datetime(2025, 1, 10, tzinfo=UTC),
411
owner="alice",
412
priority="High",
413
),
414
]
415
mock_reader_cls.return_value = reader
416
417
result = execute_tool("list_tickets", {"slug": sample_project.slug})
418
assert len(result["tickets"]) == 1
419
assert result["tickets"][0]["uuid"] == "tkt-001"
420
421
@patch(_READER)
422
def test_get_ticket_detail(self, mock_reader_cls, sample_project, fossil_repo_obj):
423
reader = _mock_reader()
424
reader.get_ticket_detail.return_value = TicketEntry(
425
uuid="tkt-001",
426
title="Fix bug",
427
status="Open",
428
type="Code_Defect",
429
created=datetime(2025, 1, 10, tzinfo=UTC),
430
owner="alice",
431
body="Detailed description",
432
priority="High",
433
severity="Critical",
434
)
435
reader.get_ticket_comments.return_value = [
436
{"timestamp": datetime(2025, 1, 11, tzinfo=UTC), "user": "bob", "comment": "Reproduced", "mimetype": "text/plain"},
437
]
438
mock_reader_cls.return_value = reader
439
440
result = execute_tool("get_ticket", {"slug": sample_project.slug, "uuid": "tkt-001"})
441
assert result["title"] == "Fix bug"
442
assert result["body"] == "Detailed description"
443
assert len(result["comments"]) == 1
444
445
@patch(_READER)
446
def test_ticket_not_found(self, mock_reader_cls, sample_project, fossil_repo_obj):
447
reader = _mock_reader()
448
reader.get_ticket_detail.return_value = None
449
mock_reader_cls.return_value = reader
450
451
result = execute_tool("get_ticket", {"slug": sample_project.slug, "uuid": "nonexistent"})
452
assert "error" in result
453
454
455
# ---------------------------------------------------------------------------
456
# create_ticket / update_ticket
457
# ---------------------------------------------------------------------------
458
459
460
@pytest.mark.django_db
461
class TestWriteTickets:
462
@patch(_CLI)
463
def test_create_ticket(self, mock_cli_cls, sample_project, fossil_repo_obj):
464
cli = MagicMock()
465
cli.ticket_add.return_value = True
466
mock_cli_cls.return_value = cli
467
468
result = execute_tool(
469
"create_ticket",
470
{
471
"slug": sample_project.slug,
472
"title": "New bug",
473
"body": "Something is broken",
474
},
475
)
476
assert result["success"] is True
477
assert result["title"] == "New bug"
478
cli.ticket_add.assert_called_once()
479
call_args = cli.ticket_add.call_args
480
fields = call_args[0][1]
481
assert fields["title"] == "New bug"
482
assert fields["comment"] == "Something is broken"
483
assert fields["status"] == "Open"
484
485
@patch(_CLI)
486
def test_create_ticket_failure(self, mock_cli_cls, sample_project, fossil_repo_obj):
487
cli = MagicMock()
488
cli.ticket_add.return_value = False
489
mock_cli_cls.return_value = cli
490
491
result = execute_tool(
492
"create_ticket",
493
{
494
"slug": sample_project.slug,
495
"title": "Failing",
496
"body": "Will fail",
497
},
498
)
499
assert "error" in result
500
501
@patch(_CLI)
502
def test_update_ticket_status(self, mock_cli_cls, sample_project, fossil_repo_obj):
503
cli = MagicMock()
504
cli.ticket_change.return_value = True
505
mock_cli_cls.return_value = cli
506
507
result = execute_tool(
508
"update_ticket",
509
{
510
"slug": sample_project.slug,
511
"uuid": "tkt-001",
512
"status": "Closed",
513
},
514
)
515
assert result["success"] is True
516
call_args = cli.ticket_change.call_args
517
assert call_args[0][1] == "tkt-001"
518
assert call_args[0][2]["status"] == "Closed"
519
520
@patch(_CLI)
521
def test_update_ticket_comment(self, mock_cli_cls, sample_project, fossil_repo_obj):
522
cli = MagicMock()
523
cli.ticket_change.return_value = True
524
mock_cli_cls.return_value = cli
525
526
result = execute_tool(
527
"update_ticket",
528
{
529
"slug": sample_project.slug,
530
"uuid": "tkt-001",
531
"comment": "Fixed in latest push",
532
},
533
)
534
assert result["success"] is True
535
call_args = cli.ticket_change.call_args
536
assert call_args[0][2]["icomment"] == "Fixed in latest push"
537
538
@patch(_CLI)
539
def test_update_ticket_no_fields(self, mock_cli_cls, sample_project, fossil_repo_obj):
540
cli = MagicMock()
541
mock_cli_cls.return_value = cli
542
543
result = execute_tool(
544
"update_ticket",
545
{
546
"slug": sample_project.slug,
547
"uuid": "tkt-001",
548
},
549
)
550
assert "error" in result
551
assert "No fields" in result["error"]
552
553
554
# ---------------------------------------------------------------------------
555
# wiki handlers
556
# ---------------------------------------------------------------------------
557
558
559
@pytest.mark.django_db
560
class TestWiki:
561
@patch(_READER)
562
def test_list_wiki_pages(self, mock_reader_cls, sample_project, fossil_repo_obj):
563
reader = _mock_reader()
564
reader.get_wiki_pages.return_value = [
565
WikiPage(name="Home", content="", last_modified=datetime(2025, 1, 12, tzinfo=UTC), user="alice"),
566
WikiPage(name="FAQ", content="", last_modified=datetime(2025, 1, 13, tzinfo=UTC), user="bob"),
567
]
568
mock_reader_cls.return_value = reader
569
570
result = execute_tool("list_wiki_pages", {"slug": sample_project.slug})
571
assert len(result["pages"]) == 2
572
assert result["pages"][0]["name"] == "Home"
573
574
@patch(_READER)
575
def test_get_wiki_page(self, mock_reader_cls, sample_project, fossil_repo_obj):
576
reader = _mock_reader()
577
reader.get_wiki_page.return_value = WikiPage(
578
name="Home",
579
content="# Welcome\nThis is home.",
580
last_modified=datetime(2025, 1, 12, tzinfo=UTC),
581
user="alice",
582
)
583
mock_reader_cls.return_value = reader
584
585
result = execute_tool("get_wiki_page", {"slug": sample_project.slug, "page_name": "Home"})
586
assert result["name"] == "Home"
587
assert "Welcome" in result["content"]
588
589
@patch(_READER)
590
def test_wiki_page_not_found(self, mock_reader_cls, sample_project, fossil_repo_obj):
591
reader = _mock_reader()
592
reader.get_wiki_page.return_value = None
593
mock_reader_cls.return_value = reader
594
595
result = execute_tool("get_wiki_page", {"slug": sample_project.slug, "page_name": "Missing"})
596
assert "error" in result
597
598
599
# ---------------------------------------------------------------------------
600
# branches, blame, file history
601
# ---------------------------------------------------------------------------
602
603
604
@pytest.mark.django_db
605
class TestBranchesAndHistory:
606
@patch(_READER)
607
def test_list_branches(self, mock_reader_cls, sample_project, fossil_repo_obj):
608
reader = _mock_reader()
609
reader.get_branches.return_value = [
610
{
611
"name": "trunk",
612
"last_checkin": datetime(2025, 1, 15, tzinfo=UTC),
613
"last_user": "alice",
614
"checkin_count": 30,
615
"last_uuid": "abc123",
616
},
617
]
618
mock_reader_cls.return_value = reader
619
620
result = execute_tool("list_branches", {"slug": sample_project.slug})
621
assert len(result["branches"]) == 1
622
assert result["branches"][0]["name"] == "trunk"
623
624
@patch(_CLI)
625
def test_get_file_blame(self, mock_cli_cls, sample_project, fossil_repo_obj):
626
cli = MagicMock()
627
cli.blame.return_value = [
628
{"uuid": "aaa", "date": "2025-01-15", "user": "alice", "text": "line 1"},
629
{"uuid": "bbb", "date": "2025-01-14", "user": "bob", "text": "line 2"},
630
]
631
mock_cli_cls.return_value = cli
632
633
result = execute_tool("get_file_blame", {"slug": sample_project.slug, "filepath": "main.py"})
634
assert result["filepath"] == "main.py"
635
assert len(result["lines"]) == 2
636
assert result["total"] == 2
637
638
@patch(_READER)
639
def test_get_file_history(self, mock_reader_cls, sample_project, fossil_repo_obj):
640
reader = _mock_reader()
641
reader.get_file_history.return_value = [
642
{"uuid": "c1", "timestamp": datetime(2025, 1, 15, tzinfo=UTC), "user": "alice", "comment": "Update"},
643
{"uuid": "c2", "timestamp": datetime(2025, 1, 14, tzinfo=UTC), "user": "bob", "comment": "Create"},
644
]
645
mock_reader_cls.return_value = reader
646
647
result = execute_tool("get_file_history", {"slug": sample_project.slug, "filepath": "main.py"})
648
assert result["filepath"] == "main.py"
649
assert len(result["history"]) == 2
650
# Timestamps should be serialized
651
assert isinstance(result["history"][0]["timestamp"], str)
652
653
654
# ---------------------------------------------------------------------------
655
# sql_query
656
# ---------------------------------------------------------------------------
657
658
659
@pytest.mark.django_db
660
class TestSqlQuery:
661
def test_rejects_non_select(self, sample_project, fossil_repo_obj):
662
result = execute_tool("sql_query", {"slug": sample_project.slug, "sql": "DELETE FROM ticket"})
663
assert "error" in result
664
assert "SELECT" in result["error"]
665
666
def test_rejects_empty_query(self, sample_project, fossil_repo_obj):
667
result = execute_tool("sql_query", {"slug": sample_project.slug, "sql": ""})
668
assert "error" in result
669
670
def test_rejects_drop(self, sample_project, fossil_repo_obj):
671
result = execute_tool("sql_query", {"slug": sample_project.slug, "sql": "SELECT 1; DROP TABLE ticket"})
672
assert "error" in result
673
674
@patch(_READER)
675
def test_valid_select(self, mock_reader_cls, sample_project, fossil_repo_obj):
676
reader = _mock_reader()
677
mock_cursor = MagicMock()
678
mock_cursor.description = [("tkt_uuid",), ("title",)]
679
mock_cursor.fetchmany.return_value = [("uuid-1", "Bug one"), ("uuid-2", "Bug two")]
680
reader.conn.cursor.return_value = mock_cursor
681
mock_reader_cls.return_value = reader
682
683
result = execute_tool("sql_query", {"slug": sample_project.slug, "sql": "SELECT tkt_uuid, title FROM ticket"})
684
assert result["columns"] == ["tkt_uuid", "title"]
685
assert len(result["rows"]) == 2
686
assert result["count"] == 2
687
688
689
# ---------------------------------------------------------------------------
690
# Server module smoke test
691
# ---------------------------------------------------------------------------
692
693
694
class TestServerModule:
695
def test_server_instance_exists(self):
696
from mcp_server.server import server
697
698
assert server.name == "fossilrepo"
699
700
def test_main_is_coroutine(self):
701
import inspect
702
703
from mcp_server.server import main
704
705
assert inspect.iscoroutinefunction(main)
706
707
def test_entry_point_function_exists(self):
708
from mcp_server.__main__ import run
709
710
assert callable(run)
711

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button