|
1 | 1 | from __future__ import annotations |
2 | 2 |
|
3 | 3 | import logging |
4 | | - |
5 | | -from server.indexer.pipeline import _build_bm25_text, _build_embedding_text |
6 | | -from server.parser.base import CodeSymbol |
| 4 | +from unittest.mock import AsyncMock, patch |
| 5 | + |
| 6 | +import server.indexer.pipeline as pipeline_module |
| 7 | +from server.config import ServiceConfig |
| 8 | +from server.indexer.github_source import GitHubFile |
| 9 | +from server.indexer.pipeline import ( |
| 10 | + IndexPipeline, |
| 11 | + _build_bm25_text, |
| 12 | + _build_embedding_text, |
| 13 | +) |
| 14 | +from server.parser.base import CodeSymbol, ParseError |
7 | 15 |
|
8 | 16 | _TRUNCATION_MARKER = "// ... (truncated)" |
9 | 17 |
|
10 | 18 |
|
| 19 | +class _StubEmbedder: |
| 20 | + dimensions = 1 |
| 21 | + |
| 22 | + async def embed_batch(self, texts: list[str]) -> list[list[float]]: |
| 23 | + return [[0.0]] * len(texts) |
| 24 | + |
| 25 | + |
| 26 | +def _make_pipeline(store) -> IndexPipeline: |
| 27 | + pipeline = IndexPipeline.__new__(IndexPipeline) |
| 28 | + pipeline._store = store |
| 29 | + pipeline._embedder = _StubEmbedder() |
| 30 | + pipeline._sparse_embedder = _StubEmbedder() |
| 31 | + return pipeline |
| 32 | + |
| 33 | + |
11 | 34 | def _sym_with_source(source: str, docstring: str = "") -> CodeSymbol: |
12 | 35 | return CodeSymbol( |
13 | 36 | name="fn", |
@@ -127,3 +150,33 @@ def test_max_chars_is_configurable() -> None: |
127 | 150 | sym = _sym_with_source("z" * 5_000) |
128 | 151 | assert _TRUNCATION_MARKER not in _build_embedding_text(sym, "svc", max_chars=6000) |
129 | 152 | assert _TRUNCATION_MARKER in _build_embedding_text(sym, "svc", max_chars=1000) |
| 153 | + |
| 154 | + |
| 155 | +async def test_parser_failure_preserves_existing_index_entries() -> None: |
| 156 | + svc = ServiceConfig(name="svc", github_repo="org/repo", exclude=[]) |
| 157 | + github_file = GitHubFile(rel_path="broken.py", blob_sha="sha1") |
| 158 | + |
| 159 | + store = AsyncMock() |
| 160 | + store.get_indexed_file_hashes.return_value = {"svc/broken.py": "old-sha"} |
| 161 | + |
| 162 | + pipeline = _make_pipeline(store) |
| 163 | + |
| 164 | + with ( |
| 165 | + patch.object( |
| 166 | + type(pipeline_module.settings), "load_services", return_value=[svc] |
| 167 | + ), |
| 168 | + patch.object( |
| 169 | + pipeline_module, "list_github_files", AsyncMock(return_value=[github_file]) |
| 170 | + ), |
| 171 | + patch.object( |
| 172 | + pipeline_module, "fetch_blob_content", AsyncMock(return_value=b"garbage") |
| 173 | + ), |
| 174 | + patch.object( |
| 175 | + pipeline_module, "parse_file", side_effect=ParseError("svc/broken.py") |
| 176 | + ), |
| 177 | + ): |
| 178 | + result = await pipeline.index_service("svc", force=True) |
| 179 | + |
| 180 | + store.delete_by_file.assert_not_called() |
| 181 | + store.upsert_chunks.assert_not_called() |
| 182 | + assert result["files"] == 0 |
0 commit comments