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.
- tests/conftest.py +12 -0
- tests/test_config.py +133 -0
- tests/test_embeddings.py +257 -0
- tests/test_indexing.py +631 -0
- tests/test_live_indexer.py +160 -0
- tests/test_query.py +253 -0
- tests/test_server.py +457 -0
- tests/test_smoke.py +38 -0
- trailhead/__init__.py +4 -0
- trailhead/__main__.py +5 -0
- trailhead/cli/__init__.py +3 -0
- trailhead/cli/__main__.py +5 -0
- trailhead/cli/app.py +28 -0
- trailhead/cli/commands/__init__.py +1 -0
- trailhead/cli/commands/embed.py +58 -0
- trailhead/cli/commands/index.py +243 -0
- trailhead/cli/commands/query.py +198 -0
- trailhead/cli/commands/serve.py +158 -0
- trailhead/server/__init__.py +5 -0
- trailhead/server/__main__.py +3 -0
- trailhead/server/app.py +348 -0
- trailhead/server/rate_limit.py +51 -0
- trailhead/services/__init__.py +1 -0
- trailhead/services/config/__init__.py +7 -0
- trailhead/services/config/cache.py +10 -0
- trailhead/services/config/models.py +31 -0
- trailhead/services/embeddings/__init__.py +17 -0
- trailhead/services/embeddings/generator.py +44 -0
- trailhead/services/embeddings/model_store.py +44 -0
- trailhead/services/indexing/__init__.py +33 -0
- trailhead/services/indexing/adapters/__init__.py +87 -0
- trailhead/services/indexing/adapters/base.py +162 -0
- trailhead/services/indexing/adapters/bash.py +97 -0
- trailhead/services/indexing/adapters/c.py +183 -0
- trailhead/services/indexing/adapters/cpp.py +178 -0
- trailhead/services/indexing/adapters/csharp.py +196 -0
- trailhead/services/indexing/adapters/go.py +243 -0
- trailhead/services/indexing/adapters/html.py +176 -0
- trailhead/services/indexing/adapters/java.py +180 -0
- trailhead/services/indexing/adapters/javascript.py +220 -0
- trailhead/services/indexing/adapters/php.py +219 -0
- trailhead/services/indexing/adapters/python.py +374 -0
- trailhead/services/indexing/adapters/registry.py +46 -0
- trailhead/services/indexing/adapters/ruby.py +179 -0
- trailhead/services/indexing/adapters/rust.py +229 -0
- trailhead/services/indexing/adapters/typescript.py +283 -0
- trailhead/services/indexing/graph.py +110 -0
- trailhead/services/indexing/graph_query.py +176 -0
- trailhead/services/indexing/live_indexer.py +195 -0
- trailhead/services/indexing/parser.py +4 -0
- trailhead/services/indexing/query.py +112 -0
- trailhead/services/indexing/query_templates.py +554 -0
- trailhead/services/indexing/sqlite_store.py +542 -0
- trailhead/services/indexing/walker.py +45 -0
- trailhead-0.1.0.dist-info/METADATA +520 -0
- trailhead-0.1.0.dist-info/RECORD +60 -0
- trailhead-0.1.0.dist-info/WHEEL +5 -0
- trailhead-0.1.0.dist-info/entry_points.txt +2 -0
- trailhead-0.1.0.dist-info/licenses/LICENSE +21 -0
- 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
|
tests/test_embeddings.py
ADDED
|
@@ -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
|
+
]
|