Skip to content

Commit f04efc3

Browse files
aksOpsclaude
andcommitted
Boost server/ui test coverage: extract testable logic, add 35 new tests
- Extract build_filter_js() from explorer.py for testable JS generation - Extract get_tool_map() from mcp_console.py for testable tool lookup - Add tests for _on_page_change, _on_drill_down, _nav_to navigation helpers - Add tests for _get_tool_fn, get_tool_map, and edge cases in parse/coerce - Cover components.py line 98-99 (start_line without end_line branch) - Add test_ui_init.py verifying setup_ui is importable and callable - Result: components 100%, theme 100%, explorer 33%, mcp_console 38%, total 42% - Remaining uncovered lines are pure NiceGUI rendering (ui.card, ui.dialog, etc.) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 55d0689 commit f04efc3

6 files changed

Lines changed: 527 additions & 21 deletions

File tree

src/osscodeiq/server/ui/explorer.py

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -65,25 +65,45 @@ def navigate_to(self, index: int) -> None:
6565
# Search filter JavaScript (client-side, no server round-trip)
6666
# ---------------------------------------------------------------------------
6767

68-
_SEARCH_JS = """
69-
(function(query) {
70-
const cards = document.querySelectorAll('.explorer-card');
68+
_SEARCH_JS_TEMPLATE = """
69+
(function(query) {{
70+
const cards = document.querySelectorAll('{container}');
7171
const lower = query.toLowerCase();
72-
cards.forEach(function(card) {
72+
cards.forEach(function(card) {{
7373
const text = card.textContent.toLowerCase();
74-
if (!lower || text.includes(lower)) {
74+
if (!lower || text.includes(lower)) {{
7575
card.style.opacity = '1';
7676
card.style.pointerEvents = 'auto';
7777
card.style.display = '';
78-
} else {
78+
}} else {{
7979
card.style.opacity = '0.15';
8080
card.style.pointerEvents = 'none';
81-
}
82-
});
83-
})("{query}")
81+
}}
82+
}});
83+
}})("{query}")
8484
"""
8585

8686

87+
def build_filter_js(query: str, container_selector: str = ".explorer-card") -> str:
88+
"""Build a JavaScript snippet that filters cards by text content.
89+
90+
Parameters
91+
----------
92+
query:
93+
The search string to filter by. Double-quotes are escaped.
94+
container_selector:
95+
CSS selector for the card elements to filter.
96+
97+
Returns
98+
-------
99+
A self-executing JavaScript string.
100+
"""
101+
safe_query = query.replace("\\", "\\\\").replace('"', '\\"')
102+
return _SEARCH_JS_TEMPLATE.format(
103+
container=container_selector, query=safe_query
104+
)
105+
106+
87107
# ---------------------------------------------------------------------------
88108
# Detail modal
89109
# ---------------------------------------------------------------------------
@@ -332,7 +352,7 @@ def content() -> None:
332352
search_input.on(
333353
"update:model-value",
334354
lambda e: ui.run_javascript(
335-
_SEARCH_JS.replace("{query}", str(e.args or "").replace('"', '\\"'))
355+
build_filter_js(str(e.args or ""))
336356
),
337357
)
338358

src/osscodeiq/server/ui/mcp_console.py

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -69,15 +69,21 @@ def parse_mcp_command(raw: str) -> tuple[str, dict[str, Any]]:
6969
# ── MCP tool lookup table ──────────────────────────────────────────────────
7070

7171

72-
def _get_tool_fn(name: str):
73-
"""Import and return the MCP tool function by *name*, or None."""
72+
def get_tool_map() -> dict[str, Any]:
73+
"""Build and return the MCP tool name -> function mapping.
74+
75+
This is separated from ``_get_tool_fn`` so it can be tested without a
76+
NiceGUI context. The import is deferred so the module can be loaded
77+
without the full server stack at import time.
78+
"""
7479
from osscodeiq.server.mcp_server import ( # noqa: C0415
7580
find_callers,
7681
find_component_by_file,
7782
find_consumers,
7883
find_cycles,
7984
find_dependencies,
8085
find_dependents,
86+
find_producers,
8187
find_related_endpoints,
8288
find_shortest_path,
8389
generate_flow,
@@ -91,7 +97,7 @@ def _get_tool_fn(name: str):
9197
trace_impact,
9298
)
9399

94-
_TOOL_MAP: dict[str, Any] = {
100+
return {
95101
"get_stats": get_stats,
96102
"query_nodes": query_nodes,
97103
"query_edges": query_edges,
@@ -100,7 +106,7 @@ def _get_tool_fn(name: str):
100106
"find_cycles": find_cycles,
101107
"find_shortest_path": find_shortest_path,
102108
"find_consumers": find_consumers,
103-
"find_producers": find_producers, # noqa: F841 — not importable separately
109+
"find_producers": find_producers,
104110
"find_callers": find_callers,
105111
"find_dependencies": find_dependencies,
106112
"find_dependents": find_dependents,
@@ -112,12 +118,10 @@ def _get_tool_fn(name: str):
112118
"read_file": read_file,
113119
}
114120

115-
# find_producers is imported via find_consumers' sibling — fix the map
116-
from osscodeiq.server.mcp_server import find_producers # noqa: C0415
117121

118-
_TOOL_MAP["find_producers"] = find_producers
119-
120-
return _TOOL_MAP.get(name)
122+
def _get_tool_fn(name: str):
123+
"""Import and return the MCP tool function by *name*, or None."""
124+
return get_tool_map().get(name)
121125

122126

123127
# ── Console builder ────────────────────────────────────────────────────────

tests/server/test_ui_components.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,11 @@ def test_unknown_kind_gets_defaults(self) -> None:
4242
assert result["icon"] == "circle"
4343
assert result["color"].startswith("#")
4444

45+
def test_missing_count_defaults_zero(self) -> None:
46+
kind_info = {"kind": "endpoint"}
47+
result = build_kind_card_data(kind_info)
48+
assert result["count"] == 0
49+
4550

4651
class TestBuildNodeCardData:
4752
def test_basic_transform(self) -> None:
@@ -93,6 +98,23 @@ def test_missing_optional_fields(self) -> None:
9398
assert result["module"] is None
9499
assert result["properties"] == {}
95100

101+
def test_subtitle_empty_when_no_details(self) -> None:
102+
node_info = {
103+
"id": "mod:x.py:module:x",
104+
"name": "x",
105+
}
106+
result = build_node_card_data(node_info)
107+
assert result["subtitle"] == ""
108+
109+
def test_edge_count_zero(self) -> None:
110+
node_info = {
111+
"id": "cls:a.py:class:A",
112+
"name": "A",
113+
"edge_count": 0,
114+
}
115+
result = build_node_card_data(node_info)
116+
assert "0 edges" in result["subtitle"]
117+
96118

97119
class TestBuildDetailData:
98120
def test_basic_transform(self) -> None:
@@ -172,6 +194,41 @@ def test_location_includes_line_numbers(self) -> None:
172194
assert "5" in loc_value
173195
assert "50" in loc_value
174196

197+
def test_location_with_start_line_only(self) -> None:
198+
"""Cover the branch where start_line is set but end_line is None (lines 98-99)."""
199+
detail = {
200+
"id": "cls:app.py:class:Bar",
201+
"name": "Bar",
202+
"kind": "class",
203+
"file_path": "app.py",
204+
"start_line": 42,
205+
# end_line deliberately omitted
206+
"properties": {},
207+
"edges_out": [],
208+
"edges_in": [],
209+
}
210+
result = build_detail_data(detail)
211+
location_props = [p for p in result["properties"] if p[0] == "Location"]
212+
assert len(location_props) == 1
213+
loc_value = location_props[0][1]
214+
assert loc_value == "app.py:42"
215+
216+
def test_location_with_file_path_only(self) -> None:
217+
"""Cover the branch where file_path is set but no line numbers."""
218+
detail = {
219+
"id": "mod:lib.py:module:lib",
220+
"name": "lib",
221+
"kind": "module",
222+
"file_path": "lib.py",
223+
"properties": {},
224+
"edges_out": [],
225+
"edges_in": [],
226+
}
227+
result = build_detail_data(detail)
228+
location_props = [p for p in result["properties"] if p[0] == "Location"]
229+
assert len(location_props) == 1
230+
assert location_props[0][1] == "lib.py"
231+
175232
def test_empty_edges(self) -> None:
176233
detail = {
177234
"id": "mod:x.py:module:x",
@@ -199,3 +256,14 @@ def test_missing_optional_fields(self) -> None:
199256
prop_keys = [p[0] for p in result["properties"]]
200257
# FQN, Module, Layer may be absent but should not error
201258
assert isinstance(result["properties"], list)
259+
260+
def test_missing_edges_defaults_empty(self) -> None:
261+
detail = {
262+
"id": "mod:y.py:module:y",
263+
"name": "y",
264+
"kind": "module",
265+
"properties": {},
266+
}
267+
result = build_detail_data(detail)
268+
assert result["edges_out"] == []
269+
assert result["edges_in"] == []

0 commit comments

Comments
 (0)