codexa 0.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (189) hide show
  1. codexa-0.4.0.dist-info/METADATA +650 -0
  2. codexa-0.4.0.dist-info/RECORD +189 -0
  3. codexa-0.4.0.dist-info/WHEEL +5 -0
  4. codexa-0.4.0.dist-info/entry_points.txt +2 -0
  5. codexa-0.4.0.dist-info/licenses/LICENSE +21 -0
  6. codexa-0.4.0.dist-info/top_level.txt +1 -0
  7. semantic_code_intelligence/__init__.py +5 -0
  8. semantic_code_intelligence/analysis/__init__.py +21 -0
  9. semantic_code_intelligence/analysis/ai_features.py +351 -0
  10. semantic_code_intelligence/bridge/__init__.py +28 -0
  11. semantic_code_intelligence/bridge/context_provider.py +245 -0
  12. semantic_code_intelligence/bridge/protocol.py +167 -0
  13. semantic_code_intelligence/bridge/server.py +348 -0
  14. semantic_code_intelligence/bridge/vscode.py +271 -0
  15. semantic_code_intelligence/ci/__init__.py +13 -0
  16. semantic_code_intelligence/ci/hooks.py +98 -0
  17. semantic_code_intelligence/ci/hotspots.py +272 -0
  18. semantic_code_intelligence/ci/impact.py +246 -0
  19. semantic_code_intelligence/ci/metrics.py +591 -0
  20. semantic_code_intelligence/ci/pr.py +412 -0
  21. semantic_code_intelligence/ci/quality.py +557 -0
  22. semantic_code_intelligence/ci/templates.py +164 -0
  23. semantic_code_intelligence/ci/trace.py +224 -0
  24. semantic_code_intelligence/cli/__init__.py +0 -0
  25. semantic_code_intelligence/cli/commands/__init__.py +0 -0
  26. semantic_code_intelligence/cli/commands/ask_cmd.py +153 -0
  27. semantic_code_intelligence/cli/commands/benchmark_cmd.py +303 -0
  28. semantic_code_intelligence/cli/commands/chat_cmd.py +252 -0
  29. semantic_code_intelligence/cli/commands/ci_gen_cmd.py +74 -0
  30. semantic_code_intelligence/cli/commands/context_cmd.py +120 -0
  31. semantic_code_intelligence/cli/commands/cross_refactor_cmd.py +113 -0
  32. semantic_code_intelligence/cli/commands/deps_cmd.py +91 -0
  33. semantic_code_intelligence/cli/commands/docs_cmd.py +101 -0
  34. semantic_code_intelligence/cli/commands/doctor_cmd.py +147 -0
  35. semantic_code_intelligence/cli/commands/evolve_cmd.py +171 -0
  36. semantic_code_intelligence/cli/commands/explain_cmd.py +112 -0
  37. semantic_code_intelligence/cli/commands/gate_cmd.py +135 -0
  38. semantic_code_intelligence/cli/commands/grep_cmd.py +234 -0
  39. semantic_code_intelligence/cli/commands/hotspots_cmd.py +119 -0
  40. semantic_code_intelligence/cli/commands/impact_cmd.py +131 -0
  41. semantic_code_intelligence/cli/commands/index_cmd.py +138 -0
  42. semantic_code_intelligence/cli/commands/init_cmd.py +152 -0
  43. semantic_code_intelligence/cli/commands/investigate_cmd.py +163 -0
  44. semantic_code_intelligence/cli/commands/languages_cmd.py +101 -0
  45. semantic_code_intelligence/cli/commands/lsp_cmd.py +49 -0
  46. semantic_code_intelligence/cli/commands/mcp_cmd.py +50 -0
  47. semantic_code_intelligence/cli/commands/metrics_cmd.py +264 -0
  48. semantic_code_intelligence/cli/commands/models_cmd.py +157 -0
  49. semantic_code_intelligence/cli/commands/plugin_cmd.py +275 -0
  50. semantic_code_intelligence/cli/commands/pr_summary_cmd.py +178 -0
  51. semantic_code_intelligence/cli/commands/quality_cmd.py +208 -0
  52. semantic_code_intelligence/cli/commands/refactor_cmd.py +103 -0
  53. semantic_code_intelligence/cli/commands/review_cmd.py +88 -0
  54. semantic_code_intelligence/cli/commands/search_cmd.py +236 -0
  55. semantic_code_intelligence/cli/commands/serve_cmd.py +117 -0
  56. semantic_code_intelligence/cli/commands/suggest_cmd.py +100 -0
  57. semantic_code_intelligence/cli/commands/summary_cmd.py +78 -0
  58. semantic_code_intelligence/cli/commands/tool_cmd.py +282 -0
  59. semantic_code_intelligence/cli/commands/trace_cmd.py +123 -0
  60. semantic_code_intelligence/cli/commands/tui_cmd.py +58 -0
  61. semantic_code_intelligence/cli/commands/viz_cmd.py +127 -0
  62. semantic_code_intelligence/cli/commands/watch_cmd.py +72 -0
  63. semantic_code_intelligence/cli/commands/web_cmd.py +61 -0
  64. semantic_code_intelligence/cli/commands/workspace_cmd.py +250 -0
  65. semantic_code_intelligence/cli/main.py +65 -0
  66. semantic_code_intelligence/cli/router.py +92 -0
  67. semantic_code_intelligence/config/__init__.py +0 -0
  68. semantic_code_intelligence/config/settings.py +260 -0
  69. semantic_code_intelligence/context/__init__.py +19 -0
  70. semantic_code_intelligence/context/engine.py +429 -0
  71. semantic_code_intelligence/context/memory.py +253 -0
  72. semantic_code_intelligence/daemon/__init__.py +1 -0
  73. semantic_code_intelligence/daemon/watcher.py +515 -0
  74. semantic_code_intelligence/docs/__init__.py +1080 -0
  75. semantic_code_intelligence/embeddings/__init__.py +0 -0
  76. semantic_code_intelligence/embeddings/enhanced.py +131 -0
  77. semantic_code_intelligence/embeddings/generator.py +149 -0
  78. semantic_code_intelligence/embeddings/model_registry.py +100 -0
  79. semantic_code_intelligence/evolution/__init__.py +1 -0
  80. semantic_code_intelligence/evolution/budget_guard.py +111 -0
  81. semantic_code_intelligence/evolution/commit_manager.py +88 -0
  82. semantic_code_intelligence/evolution/context_builder.py +131 -0
  83. semantic_code_intelligence/evolution/engine.py +249 -0
  84. semantic_code_intelligence/evolution/patch_generator.py +229 -0
  85. semantic_code_intelligence/evolution/task_selector.py +214 -0
  86. semantic_code_intelligence/evolution/test_runner.py +111 -0
  87. semantic_code_intelligence/indexing/__init__.py +0 -0
  88. semantic_code_intelligence/indexing/chunker.py +174 -0
  89. semantic_code_intelligence/indexing/parallel.py +86 -0
  90. semantic_code_intelligence/indexing/scanner.py +146 -0
  91. semantic_code_intelligence/indexing/semantic_chunker.py +337 -0
  92. semantic_code_intelligence/llm/__init__.py +62 -0
  93. semantic_code_intelligence/llm/cache.py +219 -0
  94. semantic_code_intelligence/llm/cached_provider.py +145 -0
  95. semantic_code_intelligence/llm/conversation.py +190 -0
  96. semantic_code_intelligence/llm/cross_refactor.py +272 -0
  97. semantic_code_intelligence/llm/investigation.py +274 -0
  98. semantic_code_intelligence/llm/mock_provider.py +77 -0
  99. semantic_code_intelligence/llm/ollama_provider.py +122 -0
  100. semantic_code_intelligence/llm/openai_provider.py +100 -0
  101. semantic_code_intelligence/llm/provider.py +92 -0
  102. semantic_code_intelligence/llm/rate_limiter.py +164 -0
  103. semantic_code_intelligence/llm/reasoning.py +438 -0
  104. semantic_code_intelligence/llm/safety.py +110 -0
  105. semantic_code_intelligence/llm/streaming.py +251 -0
  106. semantic_code_intelligence/lsp/__init__.py +609 -0
  107. semantic_code_intelligence/mcp/__init__.py +393 -0
  108. semantic_code_intelligence/parsing/__init__.py +19 -0
  109. semantic_code_intelligence/parsing/parser.py +375 -0
  110. semantic_code_intelligence/plugins/__init__.py +255 -0
  111. semantic_code_intelligence/plugins/examples/__init__.py +1 -0
  112. semantic_code_intelligence/plugins/examples/code_quality.py +73 -0
  113. semantic_code_intelligence/plugins/examples/search_annotator.py +56 -0
  114. semantic_code_intelligence/scalability/__init__.py +205 -0
  115. semantic_code_intelligence/search/__init__.py +0 -0
  116. semantic_code_intelligence/search/formatter.py +123 -0
  117. semantic_code_intelligence/search/grep.py +361 -0
  118. semantic_code_intelligence/search/hybrid_search.py +170 -0
  119. semantic_code_intelligence/search/keyword_search.py +311 -0
  120. semantic_code_intelligence/search/section_expander.py +103 -0
  121. semantic_code_intelligence/services/__init__.py +0 -0
  122. semantic_code_intelligence/services/indexing_service.py +630 -0
  123. semantic_code_intelligence/services/search_service.py +269 -0
  124. semantic_code_intelligence/storage/__init__.py +0 -0
  125. semantic_code_intelligence/storage/chunk_hash_store.py +86 -0
  126. semantic_code_intelligence/storage/hash_store.py +66 -0
  127. semantic_code_intelligence/storage/index_manifest.py +85 -0
  128. semantic_code_intelligence/storage/index_stats.py +138 -0
  129. semantic_code_intelligence/storage/query_history.py +160 -0
  130. semantic_code_intelligence/storage/symbol_registry.py +209 -0
  131. semantic_code_intelligence/storage/vector_store.py +297 -0
  132. semantic_code_intelligence/tests/__init__.py +0 -0
  133. semantic_code_intelligence/tests/test_ai_features.py +351 -0
  134. semantic_code_intelligence/tests/test_chunker.py +119 -0
  135. semantic_code_intelligence/tests/test_cli.py +188 -0
  136. semantic_code_intelligence/tests/test_config.py +154 -0
  137. semantic_code_intelligence/tests/test_context.py +381 -0
  138. semantic_code_intelligence/tests/test_embeddings.py +73 -0
  139. semantic_code_intelligence/tests/test_endtoend.py +1142 -0
  140. semantic_code_intelligence/tests/test_enhanced_embeddings.py +92 -0
  141. semantic_code_intelligence/tests/test_hash_store.py +79 -0
  142. semantic_code_intelligence/tests/test_logging.py +55 -0
  143. semantic_code_intelligence/tests/test_new_cli.py +138 -0
  144. semantic_code_intelligence/tests/test_parser.py +495 -0
  145. semantic_code_intelligence/tests/test_phase10.py +355 -0
  146. semantic_code_intelligence/tests/test_phase11.py +593 -0
  147. semantic_code_intelligence/tests/test_phase12.py +375 -0
  148. semantic_code_intelligence/tests/test_phase13.py +663 -0
  149. semantic_code_intelligence/tests/test_phase14.py +568 -0
  150. semantic_code_intelligence/tests/test_phase15.py +814 -0
  151. semantic_code_intelligence/tests/test_phase16.py +792 -0
  152. semantic_code_intelligence/tests/test_phase17.py +815 -0
  153. semantic_code_intelligence/tests/test_phase18.py +934 -0
  154. semantic_code_intelligence/tests/test_phase19.py +986 -0
  155. semantic_code_intelligence/tests/test_phase20.py +2753 -0
  156. semantic_code_intelligence/tests/test_phase20b.py +2058 -0
  157. semantic_code_intelligence/tests/test_phase20c.py +962 -0
  158. semantic_code_intelligence/tests/test_phase21.py +428 -0
  159. semantic_code_intelligence/tests/test_phase22.py +799 -0
  160. semantic_code_intelligence/tests/test_phase23.py +783 -0
  161. semantic_code_intelligence/tests/test_phase24.py +715 -0
  162. semantic_code_intelligence/tests/test_phase25.py +496 -0
  163. semantic_code_intelligence/tests/test_phase26.py +251 -0
  164. semantic_code_intelligence/tests/test_phase27.py +531 -0
  165. semantic_code_intelligence/tests/test_phase8.py +592 -0
  166. semantic_code_intelligence/tests/test_phase9.py +643 -0
  167. semantic_code_intelligence/tests/test_plugins.py +293 -0
  168. semantic_code_intelligence/tests/test_priority_features.py +727 -0
  169. semantic_code_intelligence/tests/test_router.py +41 -0
  170. semantic_code_intelligence/tests/test_scalability.py +138 -0
  171. semantic_code_intelligence/tests/test_scanner.py +125 -0
  172. semantic_code_intelligence/tests/test_search.py +160 -0
  173. semantic_code_intelligence/tests/test_semantic_chunker.py +255 -0
  174. semantic_code_intelligence/tests/test_tools.py +182 -0
  175. semantic_code_intelligence/tests/test_vector_store.py +151 -0
  176. semantic_code_intelligence/tests/test_watcher.py +211 -0
  177. semantic_code_intelligence/tools/__init__.py +442 -0
  178. semantic_code_intelligence/tools/executor.py +232 -0
  179. semantic_code_intelligence/tools/protocol.py +200 -0
  180. semantic_code_intelligence/tui/__init__.py +454 -0
  181. semantic_code_intelligence/utils/__init__.py +0 -0
  182. semantic_code_intelligence/utils/logging.py +112 -0
  183. semantic_code_intelligence/version.py +3 -0
  184. semantic_code_intelligence/web/__init__.py +11 -0
  185. semantic_code_intelligence/web/api.py +289 -0
  186. semantic_code_intelligence/web/server.py +397 -0
  187. semantic_code_intelligence/web/ui.py +659 -0
  188. semantic_code_intelligence/web/visualize.py +226 -0
  189. semantic_code_intelligence/workspace/__init__.py +427 -0
@@ -0,0 +1,293 @@
1
+ """Tests for the plugin architecture SDK."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ import pytest
9
+
10
+ from semantic_code_intelligence.plugins import (
11
+ PluginBase,
12
+ PluginHook,
13
+ PluginManager,
14
+ PluginMetadata,
15
+ )
16
+
17
+
18
+ # ---------------------------------------------------------------------------
19
+ # Test Plugin Implementation
20
+ # ---------------------------------------------------------------------------
21
+
22
+ class SamplePlugin(PluginBase):
23
+ """A simple plugin for testing."""
24
+
25
+ def __init__(self, name: str = "test-plugin") -> None:
26
+ self._name = name
27
+ self.activated = False
28
+ self.deactivated = False
29
+ self.hooks_received: list[tuple[PluginHook, dict]] = []
30
+
31
+ def metadata(self) -> PluginMetadata:
32
+ return PluginMetadata(
33
+ name=self._name,
34
+ version="1.0.0",
35
+ description="Test plugin",
36
+ hooks=[PluginHook.PRE_INDEX, PluginHook.POST_INDEX],
37
+ )
38
+
39
+ def activate(self, context: dict[str, Any]) -> None:
40
+ self.activated = True
41
+
42
+ def deactivate(self) -> None:
43
+ self.deactivated = True
44
+
45
+ def on_hook(self, hook: PluginHook, data: dict[str, Any]) -> dict[str, Any]:
46
+ self.hooks_received.append((hook, dict(data)))
47
+ data["processed_by"] = self._name
48
+ return data
49
+
50
+
51
+ class FailingPlugin(PluginBase):
52
+ """A plugin that raises during hook dispatch."""
53
+
54
+ def metadata(self) -> PluginMetadata:
55
+ return PluginMetadata(
56
+ name="failing-plugin",
57
+ hooks=[PluginHook.PRE_INDEX],
58
+ )
59
+
60
+ def on_hook(self, hook: PluginHook, data: dict[str, Any]) -> dict[str, Any]:
61
+ raise RuntimeError("Plugin error!")
62
+
63
+
64
+ # ---------------------------------------------------------------------------
65
+ # PluginMetadata
66
+ # ---------------------------------------------------------------------------
67
+
68
+ class TestPluginMetadata:
69
+ def test_to_dict(self):
70
+ meta = PluginMetadata(
71
+ name="my-plugin",
72
+ version="2.0",
73
+ description="Test",
74
+ hooks=[PluginHook.PRE_SEARCH],
75
+ )
76
+ d = meta.to_dict()
77
+ assert d["name"] == "my-plugin"
78
+ assert d["version"] == "2.0"
79
+ assert "pre_search" in d["hooks"]
80
+
81
+ def test_defaults(self):
82
+ meta = PluginMetadata(name="minimal")
83
+ assert meta.version == "0.1.0"
84
+ assert meta.hooks == []
85
+
86
+
87
+ # ---------------------------------------------------------------------------
88
+ # PluginHook Enum
89
+ # ---------------------------------------------------------------------------
90
+
91
+ class TestPluginHook:
92
+ def test_values(self):
93
+ assert PluginHook.PRE_INDEX.value == "pre_index"
94
+ assert PluginHook.POST_SEARCH.value == "post_search"
95
+ assert PluginHook.ON_FILE_CHANGE.value == "on_file_change"
96
+
97
+ def test_all_hooks_are_strings(self):
98
+ for hook in PluginHook:
99
+ assert isinstance(hook.value, str)
100
+
101
+
102
+ # ---------------------------------------------------------------------------
103
+ # PluginManager — Registration
104
+ # ---------------------------------------------------------------------------
105
+
106
+ class TestPluginManagerRegistration:
107
+ def test_register(self):
108
+ mgr = PluginManager()
109
+ plugin = SamplePlugin()
110
+ mgr.register(plugin)
111
+ assert "test-plugin" in mgr.registered_plugins
112
+
113
+ def test_register_duplicate_replaces(self):
114
+ mgr = PluginManager()
115
+ p1 = SamplePlugin("dup")
116
+ p2 = SamplePlugin("dup")
117
+ mgr.register(p1)
118
+ mgr.register(p2)
119
+ assert mgr.registered_plugins.count("dup") == 1
120
+
121
+ def test_unregister(self):
122
+ mgr = PluginManager()
123
+ plugin = SamplePlugin()
124
+ mgr.register(plugin)
125
+ mgr.unregister("test-plugin")
126
+ assert "test-plugin" not in mgr.registered_plugins
127
+
128
+ def test_unregister_nonexistent(self):
129
+ mgr = PluginManager()
130
+ mgr.unregister("nope") # should not raise
131
+
132
+
133
+ # ---------------------------------------------------------------------------
134
+ # PluginManager — Activation
135
+ # ---------------------------------------------------------------------------
136
+
137
+ class TestPluginManagerActivation:
138
+ def test_activate(self):
139
+ mgr = PluginManager()
140
+ plugin = SamplePlugin()
141
+ mgr.register(plugin)
142
+ mgr.activate("test-plugin")
143
+ assert "test-plugin" in mgr.active_plugins
144
+ assert plugin.activated
145
+
146
+ def test_deactivate(self):
147
+ mgr = PluginManager()
148
+ plugin = SamplePlugin()
149
+ mgr.register(plugin)
150
+ mgr.activate("test-plugin")
151
+ mgr.deactivate("test-plugin")
152
+ assert "test-plugin" not in mgr.active_plugins
153
+ assert plugin.deactivated
154
+
155
+ def test_activate_unregistered(self):
156
+ mgr = PluginManager()
157
+ with pytest.raises(ValueError):
158
+ mgr.activate("nope")
159
+
160
+ def test_unregister_active_deactivates(self):
161
+ mgr = PluginManager()
162
+ plugin = SamplePlugin()
163
+ mgr.register(plugin)
164
+ mgr.activate("test-plugin")
165
+ mgr.unregister("test-plugin")
166
+ assert plugin.deactivated
167
+
168
+
169
+ # ---------------------------------------------------------------------------
170
+ # PluginManager — Hook Dispatch
171
+ # ---------------------------------------------------------------------------
172
+
173
+ class TestPluginManagerDispatch:
174
+ def test_dispatch_basic(self):
175
+ mgr = PluginManager()
176
+ plugin = SamplePlugin()
177
+ mgr.register(plugin)
178
+ mgr.activate("test-plugin")
179
+
180
+ result = mgr.dispatch(PluginHook.PRE_INDEX, {"file": "test.py"})
181
+ assert result["processed_by"] == "test-plugin"
182
+ assert len(plugin.hooks_received) == 1
183
+
184
+ def test_dispatch_skips_inactive(self):
185
+ mgr = PluginManager()
186
+ plugin = SamplePlugin()
187
+ mgr.register(plugin)
188
+ # Not activated
189
+ result = mgr.dispatch(PluginHook.PRE_INDEX, {"file": "test.py"})
190
+ assert "processed_by" not in result
191
+ assert len(plugin.hooks_received) == 0
192
+
193
+ def test_dispatch_unregistered_hook(self):
194
+ mgr = PluginManager()
195
+ plugin = SamplePlugin() # only PRE_INDEX and POST_INDEX
196
+ mgr.register(plugin)
197
+ mgr.activate("test-plugin")
198
+
199
+ result = mgr.dispatch(PluginHook.PRE_SEARCH, {"query": "test"})
200
+ assert "processed_by" not in result # plugin not registered for this hook
201
+
202
+ def test_dispatch_chain(self):
203
+ mgr = PluginManager()
204
+ p1 = SamplePlugin("plugin-a")
205
+ p2 = SamplePlugin("plugin-b")
206
+ mgr.register(p1)
207
+ mgr.register(p2)
208
+ mgr.activate("plugin-a")
209
+ mgr.activate("plugin-b")
210
+
211
+ result = mgr.dispatch(PluginHook.PRE_INDEX, {"count": 0})
212
+ # Last plugin wins for processed_by
213
+ assert result["processed_by"] == "plugin-b"
214
+
215
+ def test_dispatch_failing_plugin_continues(self):
216
+ mgr = PluginManager()
217
+ failing = FailingPlugin()
218
+ good = SamplePlugin()
219
+ mgr.register(failing)
220
+ mgr.register(good)
221
+ mgr.activate("failing-plugin")
222
+ mgr.activate("test-plugin")
223
+
224
+ # Should not raise; failing plugin error is caught
225
+ result = mgr.dispatch(PluginHook.PRE_INDEX, {"x": 1})
226
+ assert result["processed_by"] == "test-plugin"
227
+
228
+
229
+ # ---------------------------------------------------------------------------
230
+ # PluginManager — Info
231
+ # ---------------------------------------------------------------------------
232
+
233
+ class TestPluginManagerInfo:
234
+ def test_get_plugin_info(self):
235
+ mgr = PluginManager()
236
+ plugin = SamplePlugin()
237
+ mgr.register(plugin)
238
+ info = mgr.get_plugin_info("test-plugin")
239
+ assert info is not None
240
+ assert info["name"] == "test-plugin"
241
+ assert info["active"] is False
242
+
243
+ def test_get_plugin_info_active(self):
244
+ mgr = PluginManager()
245
+ plugin = SamplePlugin()
246
+ mgr.register(plugin)
247
+ mgr.activate("test-plugin")
248
+ info = mgr.get_plugin_info("test-plugin")
249
+ assert info["active"] is True
250
+
251
+ def test_get_plugin_info_missing(self):
252
+ mgr = PluginManager()
253
+ assert mgr.get_plugin_info("nope") is None
254
+
255
+
256
+ # ---------------------------------------------------------------------------
257
+ # Plugin Discovery
258
+ # ---------------------------------------------------------------------------
259
+
260
+ class TestPluginDiscovery:
261
+ def test_discover_empty_dir(self, tmp_path):
262
+ mgr = PluginManager()
263
+ count = mgr.discover_from_directory(tmp_path)
264
+ assert count == 0
265
+
266
+ def test_discover_nonexistent_dir(self, tmp_path):
267
+ mgr = PluginManager()
268
+ count = mgr.discover_from_directory(tmp_path / "nope")
269
+ assert count == 0
270
+
271
+ def test_discover_valid_plugin(self, tmp_path):
272
+ plugin_code = '''\
273
+ from semantic_code_intelligence.plugins import PluginBase, PluginMetadata, PluginHook
274
+
275
+ class MyPlugin(PluginBase):
276
+ def metadata(self):
277
+ return PluginMetadata(name="discovered", hooks=[PluginHook.PRE_INDEX])
278
+
279
+ def create_plugin():
280
+ return MyPlugin()
281
+ '''
282
+ (tmp_path / "my_plugin.py").write_text(plugin_code, encoding="utf-8")
283
+
284
+ mgr = PluginManager()
285
+ count = mgr.discover_from_directory(tmp_path)
286
+ assert count == 1
287
+ assert "discovered" in mgr.registered_plugins
288
+
289
+ def test_discover_skips_underscore_files(self, tmp_path):
290
+ (tmp_path / "_private.py").write_text("x = 1", encoding="utf-8")
291
+ mgr = PluginManager()
292
+ count = mgr.discover_from_directory(tmp_path)
293
+ assert count == 0