trailhead 0.1.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 (60) hide show
  1. tests/conftest.py +12 -0
  2. tests/test_config.py +133 -0
  3. tests/test_embeddings.py +257 -0
  4. tests/test_indexing.py +631 -0
  5. tests/test_live_indexer.py +160 -0
  6. tests/test_query.py +253 -0
  7. tests/test_server.py +457 -0
  8. tests/test_smoke.py +38 -0
  9. trailhead/__init__.py +4 -0
  10. trailhead/__main__.py +5 -0
  11. trailhead/cli/__init__.py +3 -0
  12. trailhead/cli/__main__.py +5 -0
  13. trailhead/cli/app.py +28 -0
  14. trailhead/cli/commands/__init__.py +1 -0
  15. trailhead/cli/commands/embed.py +58 -0
  16. trailhead/cli/commands/index.py +243 -0
  17. trailhead/cli/commands/query.py +198 -0
  18. trailhead/cli/commands/serve.py +158 -0
  19. trailhead/server/__init__.py +5 -0
  20. trailhead/server/__main__.py +3 -0
  21. trailhead/server/app.py +348 -0
  22. trailhead/server/rate_limit.py +51 -0
  23. trailhead/services/__init__.py +1 -0
  24. trailhead/services/config/__init__.py +7 -0
  25. trailhead/services/config/cache.py +10 -0
  26. trailhead/services/config/models.py +31 -0
  27. trailhead/services/embeddings/__init__.py +17 -0
  28. trailhead/services/embeddings/generator.py +44 -0
  29. trailhead/services/embeddings/model_store.py +44 -0
  30. trailhead/services/indexing/__init__.py +33 -0
  31. trailhead/services/indexing/adapters/__init__.py +87 -0
  32. trailhead/services/indexing/adapters/base.py +162 -0
  33. trailhead/services/indexing/adapters/bash.py +97 -0
  34. trailhead/services/indexing/adapters/c.py +183 -0
  35. trailhead/services/indexing/adapters/cpp.py +178 -0
  36. trailhead/services/indexing/adapters/csharp.py +196 -0
  37. trailhead/services/indexing/adapters/go.py +243 -0
  38. trailhead/services/indexing/adapters/html.py +176 -0
  39. trailhead/services/indexing/adapters/java.py +180 -0
  40. trailhead/services/indexing/adapters/javascript.py +220 -0
  41. trailhead/services/indexing/adapters/php.py +219 -0
  42. trailhead/services/indexing/adapters/python.py +374 -0
  43. trailhead/services/indexing/adapters/registry.py +46 -0
  44. trailhead/services/indexing/adapters/ruby.py +179 -0
  45. trailhead/services/indexing/adapters/rust.py +229 -0
  46. trailhead/services/indexing/adapters/typescript.py +283 -0
  47. trailhead/services/indexing/graph.py +110 -0
  48. trailhead/services/indexing/graph_query.py +176 -0
  49. trailhead/services/indexing/live_indexer.py +195 -0
  50. trailhead/services/indexing/parser.py +4 -0
  51. trailhead/services/indexing/query.py +112 -0
  52. trailhead/services/indexing/query_templates.py +554 -0
  53. trailhead/services/indexing/sqlite_store.py +542 -0
  54. trailhead/services/indexing/walker.py +45 -0
  55. trailhead-0.1.0.dist-info/METADATA +520 -0
  56. trailhead-0.1.0.dist-info/RECORD +60 -0
  57. trailhead-0.1.0.dist-info/WHEEL +5 -0
  58. trailhead-0.1.0.dist-info/entry_points.txt +2 -0
  59. trailhead-0.1.0.dist-info/licenses/LICENSE +21 -0
  60. trailhead-0.1.0.dist-info/top_level.txt +2 -0
tests/conftest.py ADDED
@@ -0,0 +1,12 @@
1
+ from __future__ import annotations
2
+
3
+ import pytest
4
+
5
+ from trailhead.services.embeddings import clear_embedding_model_cache
6
+
7
+
8
+ @pytest.fixture(autouse=True)
9
+ def clear_embedding_cache_between_tests():
10
+ clear_embedding_model_cache()
11
+ yield
12
+ clear_embedding_model_cache()
tests/test_config.py ADDED
@@ -0,0 +1,133 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ class TestIsModelAllowed:
5
+ def test_allowlisted_model_is_permitted(self):
6
+ from trailhead.services.config import is_model_allowed
7
+
8
+ assert is_model_allowed("sentence-transformers/all-MiniLM-L6-v2") is True
9
+
10
+ def test_unknown_model_is_rejected_by_default(self):
11
+ from trailhead.services.config import is_model_allowed
12
+
13
+ assert is_model_allowed("some-org/custom-model") is False
14
+
15
+ def test_allow_any_flag_bypasses_allowlist(self):
16
+ from trailhead.services.config import is_model_allowed
17
+
18
+ assert is_model_allowed("some-org/custom-model", allow_any=True) is True
19
+
20
+ def test_env_var_bypasses_allowlist(self, monkeypatch):
21
+ from trailhead.services.config import is_model_allowed
22
+
23
+ monkeypatch.setenv("CINDEX_ALLOW_ANY_MODEL", "1")
24
+ assert is_model_allowed("some-org/custom-model") is True
25
+
26
+ def test_env_var_true_string_bypasses_allowlist(self, monkeypatch):
27
+ from trailhead.services.config import is_model_allowed
28
+
29
+ monkeypatch.setenv("CINDEX_ALLOW_ANY_MODEL", "true")
30
+ assert is_model_allowed("some-org/custom-model") is True
31
+
32
+ def test_env_var_unset_does_not_bypass(self, monkeypatch):
33
+ from trailhead.services.config import is_model_allowed
34
+
35
+ monkeypatch.delenv("CINDEX_ALLOW_ANY_MODEL", raising=False)
36
+ assert is_model_allowed("some-org/custom-model") is False
37
+
38
+ def test_env_var_arbitrary_value_does_not_bypass(self, monkeypatch):
39
+ from trailhead.services.config import is_model_allowed
40
+
41
+ monkeypatch.setenv("CINDEX_ALLOW_ANY_MODEL", "maybe")
42
+ assert is_model_allowed("some-org/custom-model") is False
43
+
44
+ def test_all_allowlisted_models_are_permitted(self):
45
+ from trailhead.services.config import ALLOWED_MODELS
46
+ from trailhead.services.config import is_model_allowed
47
+
48
+ for model in ALLOWED_MODELS:
49
+ assert is_model_allowed(model) is True, f"{model} should be allowed"
50
+
51
+
52
+ class TestServeCommandModelValidation:
53
+ def test_serve_rejects_unlisted_model(self, monkeypatch):
54
+ from trailhead.cli.commands import serve
55
+
56
+ monkeypatch.delenv("CINDEX_ALLOW_ANY_MODEL", raising=False)
57
+
58
+ rc = serve.run(
59
+ type(
60
+ "Args",
61
+ (),
62
+ {
63
+ "directory": ".",
64
+ "host": "127.0.0.1",
65
+ "port": 9000,
66
+ "model": "unknown-org/mystery-model",
67
+ "cache_dir": None,
68
+ "sqlite_db": "data.db",
69
+ "no_preload": False,
70
+ "allow_any_model": False,
71
+ "cors_origins": None,
72
+ "rate_limit": 0,
73
+ },
74
+ )()
75
+ )
76
+
77
+ assert rc == 1
78
+
79
+ def test_serve_accepts_unlisted_model_with_flag(self, monkeypatch, tmp_path):
80
+ from trailhead.cli.commands import serve
81
+
82
+ monkeypatch.delenv("CINDEX_ALLOW_ANY_MODEL", raising=False)
83
+ monkeypatch.setattr("trailhead.server.app.create_app", lambda **kw: object())
84
+ monkeypatch.setattr("uvicorn.run", lambda *a, **kw: None)
85
+
86
+ rc = serve.run(
87
+ type(
88
+ "Args",
89
+ (),
90
+ {
91
+ "directory": ".",
92
+ "host": "127.0.0.1",
93
+ "port": 9000,
94
+ "model": "unknown-org/mystery-model",
95
+ "cache_dir": None,
96
+ "sqlite_db": str(tmp_path / "data.db"),
97
+ "no_preload": False,
98
+ "allow_any_model": True,
99
+ "cors_origins": None,
100
+ "rate_limit": 0,
101
+ },
102
+ )()
103
+ )
104
+
105
+ assert rc == 0
106
+
107
+ def test_serve_accepts_unlisted_model_via_env_var(self, monkeypatch, tmp_path):
108
+ from trailhead.cli.commands import serve
109
+
110
+ monkeypatch.setenv("CINDEX_ALLOW_ANY_MODEL", "1")
111
+ monkeypatch.setattr("trailhead.server.app.create_app", lambda **kw: object())
112
+ monkeypatch.setattr("uvicorn.run", lambda *a, **kw: None)
113
+
114
+ rc = serve.run(
115
+ type(
116
+ "Args",
117
+ (),
118
+ {
119
+ "directory": ".",
120
+ "host": "127.0.0.1",
121
+ "port": 9000,
122
+ "model": "unknown-org/mystery-model",
123
+ "cache_dir": None,
124
+ "sqlite_db": str(tmp_path / "data.db"),
125
+ "no_preload": False,
126
+ "allow_any_model": False,
127
+ "cors_origins": None,
128
+ "rate_limit": 0,
129
+ },
130
+ )()
131
+ )
132
+
133
+ assert rc == 0
@@ -0,0 +1,257 @@
1
+ """Tests for embedding generation service."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import pytest
6
+
7
+
8
+ class TestGenerateEmbedding:
9
+ """Test suite for generate_embedding functionality."""
10
+
11
+ def test_generate_embedding_with_list_input(self, monkeypatch) -> None:
12
+ """Test generate_embedding converts numpy array to list."""
13
+ import numpy as np
14
+
15
+ from trailhead.services.embeddings import generate_embedding
16
+
17
+ mock_embedding = np.array([0.1, 0.2, 0.3, 0.4, 0.5])
18
+
19
+ class MockModel:
20
+ def encode(self, text: str):
21
+ return mock_embedding
22
+
23
+ def mock_sentence_transformer(model_name: str, cache_folder: str | None = None):
24
+ assert model_name == "test-model"
25
+ return MockModel()
26
+
27
+ monkeypatch.setattr(
28
+ "sentence_transformers.SentenceTransformer",
29
+ mock_sentence_transformer,
30
+ )
31
+
32
+ result = generate_embedding("hello", "test-model")
33
+ assert result == [0.1, 0.2, 0.3, 0.4, 0.5]
34
+ assert isinstance(result, list)
35
+ assert all(isinstance(v, float) for v in result)
36
+
37
+ def test_generate_embedding_with_plain_list(self, monkeypatch) -> None:
38
+ """Test generate_embedding handles plain lists (no .tolist() method)."""
39
+ from trailhead.services.embeddings import generate_embedding
40
+
41
+ plain_list = [0.5, 0.6, 0.7]
42
+
43
+ class MockModel:
44
+ def encode(self, text: str):
45
+ return plain_list
46
+
47
+ def mock_sentence_transformer(model_name: str, cache_folder: str | None = None):
48
+ return MockModel()
49
+
50
+ monkeypatch.setattr(
51
+ "sentence_transformers.SentenceTransformer",
52
+ mock_sentence_transformer,
53
+ )
54
+
55
+ result = generate_embedding("test", "model")
56
+ assert result == [0.5, 0.6, 0.7]
57
+
58
+ def test_generate_embedding_passes_cache_folder(self, monkeypatch) -> None:
59
+ """Test that cache_folder is passed to SentenceTransformer."""
60
+ from trailhead.services.embeddings import generate_embedding
61
+
62
+ recorded = {}
63
+
64
+ class MockModel:
65
+ def encode(self, text: str):
66
+ return [0.1]
67
+
68
+ def mock_sentence_transformer(model_name: str, cache_folder: str | None = None):
69
+ recorded["model_name"] = model_name
70
+ recorded["cache_folder"] = cache_folder
71
+ return MockModel()
72
+
73
+ monkeypatch.setattr(
74
+ "sentence_transformers.SentenceTransformer",
75
+ mock_sentence_transformer,
76
+ )
77
+
78
+ generate_embedding("text", "my-model", cache_folder="/custom/cache")
79
+
80
+ assert recorded["model_name"] == "my-model"
81
+ assert recorded["cache_folder"] == "/custom/cache"
82
+
83
+ def test_generate_embedding_default_cache_folder(self, monkeypatch) -> None:
84
+ """Test that cache_folder defaults to None."""
85
+ from trailhead.services.embeddings import generate_embedding
86
+
87
+ recorded = {}
88
+
89
+ class MockModel:
90
+ def encode(self, text: str):
91
+ return [0.1]
92
+
93
+ def mock_sentence_transformer(model_name: str, cache_folder: str | None = None):
94
+ recorded["cache_folder"] = cache_folder
95
+ return MockModel()
96
+
97
+ monkeypatch.setattr(
98
+ "sentence_transformers.SentenceTransformer",
99
+ mock_sentence_transformer,
100
+ )
101
+
102
+ generate_embedding("text", "model")
103
+
104
+ assert recorded["cache_folder"] is None
105
+
106
+ def test_generate_embedding_with_different_texts(self, monkeypatch) -> None:
107
+ """Test generate_embedding encodes different texts."""
108
+ from trailhead.services.embeddings import generate_embedding
109
+
110
+ texts_encoded = []
111
+
112
+ class MockModel:
113
+ def encode(self, text: str):
114
+ texts_encoded.append(text)
115
+ return [0.1, 0.2]
116
+
117
+ def mock_sentence_transformer(model_name: str, cache_folder: str | None = None):
118
+ return MockModel()
119
+
120
+ monkeypatch.setattr(
121
+ "sentence_transformers.SentenceTransformer",
122
+ mock_sentence_transformer,
123
+ )
124
+
125
+ generate_embedding("hello world", "model")
126
+ generate_embedding("goodbye world", "model")
127
+
128
+ assert texts_encoded == ["hello world", "goodbye world"]
129
+
130
+ def test_generate_embedding_returns_floats(self, monkeypatch) -> None:
131
+ """Test that all values in result are floats, even from integer arrays."""
132
+ import numpy as np
133
+
134
+ from trailhead.services.embeddings import generate_embedding
135
+
136
+ mock_embedding = np.array([1, 2, 3], dtype=np.int32)
137
+
138
+ class MockModel:
139
+ def encode(self, text: str):
140
+ return mock_embedding
141
+
142
+ def mock_sentence_transformer(model_name: str, cache_folder: str | None = None):
143
+ return MockModel()
144
+
145
+ monkeypatch.setattr(
146
+ "sentence_transformers.SentenceTransformer",
147
+ mock_sentence_transformer,
148
+ )
149
+
150
+ result = generate_embedding("text", "model")
151
+
152
+ assert result == [1.0, 2.0, 3.0]
153
+ assert all(isinstance(v, float) for v in result)
154
+
155
+ def test_generate_embedding_empty_text(self, monkeypatch) -> None:
156
+ """Test generate_embedding with empty text."""
157
+ from trailhead.services.embeddings import generate_embedding
158
+
159
+ class MockModel:
160
+ def encode(self, text: str):
161
+ return [0.0] * 384
162
+
163
+ def mock_sentence_transformer(model_name: str, cache_folder: str | None = None):
164
+ return MockModel()
165
+
166
+ monkeypatch.setattr(
167
+ "sentence_transformers.SentenceTransformer",
168
+ mock_sentence_transformer,
169
+ )
170
+
171
+ result = generate_embedding("", "model")
172
+
173
+ assert len(result) == 384
174
+ assert all(v == 0.0 for v in result)
175
+
176
+ def test_generate_embedding_long_text(self, monkeypatch) -> None:
177
+ """Test generate_embedding with long text."""
178
+ from trailhead.services.embeddings import generate_embedding
179
+
180
+ long_text = "word " * 1000
181
+
182
+ recorded_text = None
183
+
184
+ class MockModel:
185
+ def encode(self, text: str):
186
+ nonlocal recorded_text
187
+ recorded_text = text
188
+ return [0.1, 0.2]
189
+
190
+ def mock_sentence_transformer(model_name: str, cache_folder: str | None = None):
191
+ return MockModel()
192
+
193
+ monkeypatch.setattr(
194
+ "sentence_transformers.SentenceTransformer",
195
+ mock_sentence_transformer,
196
+ )
197
+
198
+ result = generate_embedding(long_text, "model")
199
+
200
+ assert recorded_text == long_text
201
+ assert result == [0.1, 0.2]
202
+
203
+ def test_generate_embedding_special_characters(self, monkeypatch) -> None:
204
+ """Test generate_embedding with special characters."""
205
+ from trailhead.services.embeddings import generate_embedding
206
+
207
+ special_text = "Hello 你好 مرحبا 🎉 @#$%^&*()"
208
+
209
+ recorded_text = None
210
+
211
+ class MockModel:
212
+ def encode(self, text: str):
213
+ nonlocal recorded_text
214
+ recorded_text = text
215
+ return [0.1]
216
+
217
+ def mock_sentence_transformer(model_name: str, cache_folder: str | None = None):
218
+ return MockModel()
219
+
220
+ monkeypatch.setattr(
221
+ "sentence_transformers.SentenceTransformer",
222
+ mock_sentence_transformer,
223
+ )
224
+
225
+ result = generate_embedding(special_text, "model")
226
+
227
+ assert recorded_text == special_text
228
+ assert result == [0.1]
229
+
230
+ def test_generate_embedding_different_model_names(self, monkeypatch) -> None:
231
+ """Test generate_embedding with different model names."""
232
+ from trailhead.services.embeddings import generate_embedding
233
+
234
+ models_loaded = []
235
+
236
+ class MockModel:
237
+ def encode(self, text: str):
238
+ return [0.1]
239
+
240
+ def mock_sentence_transformer(model_name: str, cache_folder: str | None = None):
241
+ models_loaded.append(model_name)
242
+ return MockModel()
243
+
244
+ monkeypatch.setattr(
245
+ "sentence_transformers.SentenceTransformer",
246
+ mock_sentence_transformer,
247
+ )
248
+
249
+ generate_embedding("text", "sentence-transformers/all-MiniLM-L6-v2")
250
+ generate_embedding("text", "sentence-transformers/all-mpnet-base-v2")
251
+ generate_embedding("text", "my-custom-model")
252
+
253
+ assert models_loaded == [
254
+ "sentence-transformers/all-MiniLM-L6-v2",
255
+ "sentence-transformers/all-mpnet-base-v2",
256
+ "my-custom-model",
257
+ ]