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.
- codexa-0.4.0.dist-info/METADATA +650 -0
- codexa-0.4.0.dist-info/RECORD +189 -0
- codexa-0.4.0.dist-info/WHEEL +5 -0
- codexa-0.4.0.dist-info/entry_points.txt +2 -0
- codexa-0.4.0.dist-info/licenses/LICENSE +21 -0
- codexa-0.4.0.dist-info/top_level.txt +1 -0
- semantic_code_intelligence/__init__.py +5 -0
- semantic_code_intelligence/analysis/__init__.py +21 -0
- semantic_code_intelligence/analysis/ai_features.py +351 -0
- semantic_code_intelligence/bridge/__init__.py +28 -0
- semantic_code_intelligence/bridge/context_provider.py +245 -0
- semantic_code_intelligence/bridge/protocol.py +167 -0
- semantic_code_intelligence/bridge/server.py +348 -0
- semantic_code_intelligence/bridge/vscode.py +271 -0
- semantic_code_intelligence/ci/__init__.py +13 -0
- semantic_code_intelligence/ci/hooks.py +98 -0
- semantic_code_intelligence/ci/hotspots.py +272 -0
- semantic_code_intelligence/ci/impact.py +246 -0
- semantic_code_intelligence/ci/metrics.py +591 -0
- semantic_code_intelligence/ci/pr.py +412 -0
- semantic_code_intelligence/ci/quality.py +557 -0
- semantic_code_intelligence/ci/templates.py +164 -0
- semantic_code_intelligence/ci/trace.py +224 -0
- semantic_code_intelligence/cli/__init__.py +0 -0
- semantic_code_intelligence/cli/commands/__init__.py +0 -0
- semantic_code_intelligence/cli/commands/ask_cmd.py +153 -0
- semantic_code_intelligence/cli/commands/benchmark_cmd.py +303 -0
- semantic_code_intelligence/cli/commands/chat_cmd.py +252 -0
- semantic_code_intelligence/cli/commands/ci_gen_cmd.py +74 -0
- semantic_code_intelligence/cli/commands/context_cmd.py +120 -0
- semantic_code_intelligence/cli/commands/cross_refactor_cmd.py +113 -0
- semantic_code_intelligence/cli/commands/deps_cmd.py +91 -0
- semantic_code_intelligence/cli/commands/docs_cmd.py +101 -0
- semantic_code_intelligence/cli/commands/doctor_cmd.py +147 -0
- semantic_code_intelligence/cli/commands/evolve_cmd.py +171 -0
- semantic_code_intelligence/cli/commands/explain_cmd.py +112 -0
- semantic_code_intelligence/cli/commands/gate_cmd.py +135 -0
- semantic_code_intelligence/cli/commands/grep_cmd.py +234 -0
- semantic_code_intelligence/cli/commands/hotspots_cmd.py +119 -0
- semantic_code_intelligence/cli/commands/impact_cmd.py +131 -0
- semantic_code_intelligence/cli/commands/index_cmd.py +138 -0
- semantic_code_intelligence/cli/commands/init_cmd.py +152 -0
- semantic_code_intelligence/cli/commands/investigate_cmd.py +163 -0
- semantic_code_intelligence/cli/commands/languages_cmd.py +101 -0
- semantic_code_intelligence/cli/commands/lsp_cmd.py +49 -0
- semantic_code_intelligence/cli/commands/mcp_cmd.py +50 -0
- semantic_code_intelligence/cli/commands/metrics_cmd.py +264 -0
- semantic_code_intelligence/cli/commands/models_cmd.py +157 -0
- semantic_code_intelligence/cli/commands/plugin_cmd.py +275 -0
- semantic_code_intelligence/cli/commands/pr_summary_cmd.py +178 -0
- semantic_code_intelligence/cli/commands/quality_cmd.py +208 -0
- semantic_code_intelligence/cli/commands/refactor_cmd.py +103 -0
- semantic_code_intelligence/cli/commands/review_cmd.py +88 -0
- semantic_code_intelligence/cli/commands/search_cmd.py +236 -0
- semantic_code_intelligence/cli/commands/serve_cmd.py +117 -0
- semantic_code_intelligence/cli/commands/suggest_cmd.py +100 -0
- semantic_code_intelligence/cli/commands/summary_cmd.py +78 -0
- semantic_code_intelligence/cli/commands/tool_cmd.py +282 -0
- semantic_code_intelligence/cli/commands/trace_cmd.py +123 -0
- semantic_code_intelligence/cli/commands/tui_cmd.py +58 -0
- semantic_code_intelligence/cli/commands/viz_cmd.py +127 -0
- semantic_code_intelligence/cli/commands/watch_cmd.py +72 -0
- semantic_code_intelligence/cli/commands/web_cmd.py +61 -0
- semantic_code_intelligence/cli/commands/workspace_cmd.py +250 -0
- semantic_code_intelligence/cli/main.py +65 -0
- semantic_code_intelligence/cli/router.py +92 -0
- semantic_code_intelligence/config/__init__.py +0 -0
- semantic_code_intelligence/config/settings.py +260 -0
- semantic_code_intelligence/context/__init__.py +19 -0
- semantic_code_intelligence/context/engine.py +429 -0
- semantic_code_intelligence/context/memory.py +253 -0
- semantic_code_intelligence/daemon/__init__.py +1 -0
- semantic_code_intelligence/daemon/watcher.py +515 -0
- semantic_code_intelligence/docs/__init__.py +1080 -0
- semantic_code_intelligence/embeddings/__init__.py +0 -0
- semantic_code_intelligence/embeddings/enhanced.py +131 -0
- semantic_code_intelligence/embeddings/generator.py +149 -0
- semantic_code_intelligence/embeddings/model_registry.py +100 -0
- semantic_code_intelligence/evolution/__init__.py +1 -0
- semantic_code_intelligence/evolution/budget_guard.py +111 -0
- semantic_code_intelligence/evolution/commit_manager.py +88 -0
- semantic_code_intelligence/evolution/context_builder.py +131 -0
- semantic_code_intelligence/evolution/engine.py +249 -0
- semantic_code_intelligence/evolution/patch_generator.py +229 -0
- semantic_code_intelligence/evolution/task_selector.py +214 -0
- semantic_code_intelligence/evolution/test_runner.py +111 -0
- semantic_code_intelligence/indexing/__init__.py +0 -0
- semantic_code_intelligence/indexing/chunker.py +174 -0
- semantic_code_intelligence/indexing/parallel.py +86 -0
- semantic_code_intelligence/indexing/scanner.py +146 -0
- semantic_code_intelligence/indexing/semantic_chunker.py +337 -0
- semantic_code_intelligence/llm/__init__.py +62 -0
- semantic_code_intelligence/llm/cache.py +219 -0
- semantic_code_intelligence/llm/cached_provider.py +145 -0
- semantic_code_intelligence/llm/conversation.py +190 -0
- semantic_code_intelligence/llm/cross_refactor.py +272 -0
- semantic_code_intelligence/llm/investigation.py +274 -0
- semantic_code_intelligence/llm/mock_provider.py +77 -0
- semantic_code_intelligence/llm/ollama_provider.py +122 -0
- semantic_code_intelligence/llm/openai_provider.py +100 -0
- semantic_code_intelligence/llm/provider.py +92 -0
- semantic_code_intelligence/llm/rate_limiter.py +164 -0
- semantic_code_intelligence/llm/reasoning.py +438 -0
- semantic_code_intelligence/llm/safety.py +110 -0
- semantic_code_intelligence/llm/streaming.py +251 -0
- semantic_code_intelligence/lsp/__init__.py +609 -0
- semantic_code_intelligence/mcp/__init__.py +393 -0
- semantic_code_intelligence/parsing/__init__.py +19 -0
- semantic_code_intelligence/parsing/parser.py +375 -0
- semantic_code_intelligence/plugins/__init__.py +255 -0
- semantic_code_intelligence/plugins/examples/__init__.py +1 -0
- semantic_code_intelligence/plugins/examples/code_quality.py +73 -0
- semantic_code_intelligence/plugins/examples/search_annotator.py +56 -0
- semantic_code_intelligence/scalability/__init__.py +205 -0
- semantic_code_intelligence/search/__init__.py +0 -0
- semantic_code_intelligence/search/formatter.py +123 -0
- semantic_code_intelligence/search/grep.py +361 -0
- semantic_code_intelligence/search/hybrid_search.py +170 -0
- semantic_code_intelligence/search/keyword_search.py +311 -0
- semantic_code_intelligence/search/section_expander.py +103 -0
- semantic_code_intelligence/services/__init__.py +0 -0
- semantic_code_intelligence/services/indexing_service.py +630 -0
- semantic_code_intelligence/services/search_service.py +269 -0
- semantic_code_intelligence/storage/__init__.py +0 -0
- semantic_code_intelligence/storage/chunk_hash_store.py +86 -0
- semantic_code_intelligence/storage/hash_store.py +66 -0
- semantic_code_intelligence/storage/index_manifest.py +85 -0
- semantic_code_intelligence/storage/index_stats.py +138 -0
- semantic_code_intelligence/storage/query_history.py +160 -0
- semantic_code_intelligence/storage/symbol_registry.py +209 -0
- semantic_code_intelligence/storage/vector_store.py +297 -0
- semantic_code_intelligence/tests/__init__.py +0 -0
- semantic_code_intelligence/tests/test_ai_features.py +351 -0
- semantic_code_intelligence/tests/test_chunker.py +119 -0
- semantic_code_intelligence/tests/test_cli.py +188 -0
- semantic_code_intelligence/tests/test_config.py +154 -0
- semantic_code_intelligence/tests/test_context.py +381 -0
- semantic_code_intelligence/tests/test_embeddings.py +73 -0
- semantic_code_intelligence/tests/test_endtoend.py +1142 -0
- semantic_code_intelligence/tests/test_enhanced_embeddings.py +92 -0
- semantic_code_intelligence/tests/test_hash_store.py +79 -0
- semantic_code_intelligence/tests/test_logging.py +55 -0
- semantic_code_intelligence/tests/test_new_cli.py +138 -0
- semantic_code_intelligence/tests/test_parser.py +495 -0
- semantic_code_intelligence/tests/test_phase10.py +355 -0
- semantic_code_intelligence/tests/test_phase11.py +593 -0
- semantic_code_intelligence/tests/test_phase12.py +375 -0
- semantic_code_intelligence/tests/test_phase13.py +663 -0
- semantic_code_intelligence/tests/test_phase14.py +568 -0
- semantic_code_intelligence/tests/test_phase15.py +814 -0
- semantic_code_intelligence/tests/test_phase16.py +792 -0
- semantic_code_intelligence/tests/test_phase17.py +815 -0
- semantic_code_intelligence/tests/test_phase18.py +934 -0
- semantic_code_intelligence/tests/test_phase19.py +986 -0
- semantic_code_intelligence/tests/test_phase20.py +2753 -0
- semantic_code_intelligence/tests/test_phase20b.py +2058 -0
- semantic_code_intelligence/tests/test_phase20c.py +962 -0
- semantic_code_intelligence/tests/test_phase21.py +428 -0
- semantic_code_intelligence/tests/test_phase22.py +799 -0
- semantic_code_intelligence/tests/test_phase23.py +783 -0
- semantic_code_intelligence/tests/test_phase24.py +715 -0
- semantic_code_intelligence/tests/test_phase25.py +496 -0
- semantic_code_intelligence/tests/test_phase26.py +251 -0
- semantic_code_intelligence/tests/test_phase27.py +531 -0
- semantic_code_intelligence/tests/test_phase8.py +592 -0
- semantic_code_intelligence/tests/test_phase9.py +643 -0
- semantic_code_intelligence/tests/test_plugins.py +293 -0
- semantic_code_intelligence/tests/test_priority_features.py +727 -0
- semantic_code_intelligence/tests/test_router.py +41 -0
- semantic_code_intelligence/tests/test_scalability.py +138 -0
- semantic_code_intelligence/tests/test_scanner.py +125 -0
- semantic_code_intelligence/tests/test_search.py +160 -0
- semantic_code_intelligence/tests/test_semantic_chunker.py +255 -0
- semantic_code_intelligence/tests/test_tools.py +182 -0
- semantic_code_intelligence/tests/test_vector_store.py +151 -0
- semantic_code_intelligence/tests/test_watcher.py +211 -0
- semantic_code_intelligence/tools/__init__.py +442 -0
- semantic_code_intelligence/tools/executor.py +232 -0
- semantic_code_intelligence/tools/protocol.py +200 -0
- semantic_code_intelligence/tui/__init__.py +454 -0
- semantic_code_intelligence/utils/__init__.py +0 -0
- semantic_code_intelligence/utils/logging.py +112 -0
- semantic_code_intelligence/version.py +3 -0
- semantic_code_intelligence/web/__init__.py +11 -0
- semantic_code_intelligence/web/api.py +289 -0
- semantic_code_intelligence/web/server.py +397 -0
- semantic_code_intelligence/web/ui.py +659 -0
- semantic_code_intelligence/web/visualize.py +226 -0
- semantic_code_intelligence/workspace/__init__.py +427 -0
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
"""Tests for Phase 10 — Multi-Repository Workspace Intelligence.
|
|
2
|
+
|
|
3
|
+
Covers: RepoEntry, WorkspaceManifest, Workspace model (persistence,
|
|
4
|
+
repo management, summary), and CLI workspace subcommands.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from unittest.mock import MagicMock, patch
|
|
12
|
+
|
|
13
|
+
import click
|
|
14
|
+
import pytest
|
|
15
|
+
|
|
16
|
+
from semantic_code_intelligence.workspace import (
|
|
17
|
+
RepoEntry,
|
|
18
|
+
Workspace,
|
|
19
|
+
WorkspaceManifest,
|
|
20
|
+
WORKSPACE_FILE,
|
|
21
|
+
)
|
|
22
|
+
from semantic_code_intelligence.cli.commands.workspace_cmd import workspace_cmd
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# =========================================================================
|
|
26
|
+
# RepoEntry tests
|
|
27
|
+
# =========================================================================
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class TestRepoEntry:
|
|
31
|
+
def test_to_dict(self):
|
|
32
|
+
entry = RepoEntry(name="backend", path="/repos/backend", last_indexed=1000.0, file_count=42, vector_count=100)
|
|
33
|
+
d = entry.to_dict()
|
|
34
|
+
assert d["name"] == "backend"
|
|
35
|
+
assert d["path"] == "/repos/backend"
|
|
36
|
+
assert d["last_indexed"] == 1000.0
|
|
37
|
+
assert d["file_count"] == 42
|
|
38
|
+
assert d["vector_count"] == 100
|
|
39
|
+
|
|
40
|
+
def test_from_dict_full(self):
|
|
41
|
+
data = {"name": "api", "path": "/code/api", "last_indexed": 999.0, "file_count": 5, "vector_count": 20}
|
|
42
|
+
entry = RepoEntry.from_dict(data)
|
|
43
|
+
assert entry.name == "api"
|
|
44
|
+
assert entry.path == "/code/api"
|
|
45
|
+
assert entry.last_indexed == 999.0
|
|
46
|
+
|
|
47
|
+
def test_from_dict_defaults(self):
|
|
48
|
+
data = {"name": "lib", "path": "/code/lib"}
|
|
49
|
+
entry = RepoEntry.from_dict(data)
|
|
50
|
+
assert entry.last_indexed == 0.0
|
|
51
|
+
assert entry.file_count == 0
|
|
52
|
+
assert entry.vector_count == 0
|
|
53
|
+
|
|
54
|
+
def test_roundtrip(self):
|
|
55
|
+
entry = RepoEntry(name="x", path="/x", last_indexed=1.5, file_count=3, vector_count=7)
|
|
56
|
+
reconstructed = RepoEntry.from_dict(entry.to_dict())
|
|
57
|
+
assert reconstructed.name == entry.name
|
|
58
|
+
assert reconstructed.path == entry.path
|
|
59
|
+
assert reconstructed.last_indexed == entry.last_indexed
|
|
60
|
+
assert reconstructed.file_count == entry.file_count
|
|
61
|
+
assert reconstructed.vector_count == entry.vector_count
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# =========================================================================
|
|
65
|
+
# WorkspaceManifest tests
|
|
66
|
+
# =========================================================================
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class TestWorkspaceManifest:
|
|
70
|
+
def test_empty_manifest(self):
|
|
71
|
+
m = WorkspaceManifest()
|
|
72
|
+
assert m.version == "1.0.0"
|
|
73
|
+
assert m.repos == []
|
|
74
|
+
|
|
75
|
+
def test_to_dict(self):
|
|
76
|
+
m = WorkspaceManifest(repos=[
|
|
77
|
+
RepoEntry(name="a", path="/a"),
|
|
78
|
+
RepoEntry(name="b", path="/b"),
|
|
79
|
+
])
|
|
80
|
+
d = m.to_dict()
|
|
81
|
+
assert d["version"] == "1.0.0"
|
|
82
|
+
assert len(d["repos"]) == 2
|
|
83
|
+
assert d["repos"][0]["name"] == "a"
|
|
84
|
+
|
|
85
|
+
def test_from_dict(self):
|
|
86
|
+
data = {"version": "2.0.0", "repos": [{"name": "z", "path": "/z"}]}
|
|
87
|
+
m = WorkspaceManifest.from_dict(data)
|
|
88
|
+
assert m.version == "2.0.0"
|
|
89
|
+
assert len(m.repos) == 1
|
|
90
|
+
assert m.repos[0].name == "z"
|
|
91
|
+
|
|
92
|
+
def test_from_dict_defaults(self):
|
|
93
|
+
m = WorkspaceManifest.from_dict({})
|
|
94
|
+
assert m.version == "1.0.0"
|
|
95
|
+
assert m.repos == []
|
|
96
|
+
|
|
97
|
+
def test_roundtrip(self):
|
|
98
|
+
original = WorkspaceManifest(repos=[
|
|
99
|
+
RepoEntry(name="core", path="/core", file_count=10),
|
|
100
|
+
])
|
|
101
|
+
restored = WorkspaceManifest.from_dict(original.to_dict())
|
|
102
|
+
assert restored.version == original.version
|
|
103
|
+
assert len(restored.repos) == 1
|
|
104
|
+
assert restored.repos[0].name == "core"
|
|
105
|
+
assert restored.repos[0].file_count == 10
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# =========================================================================
|
|
109
|
+
# Workspace model tests
|
|
110
|
+
# =========================================================================
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class TestWorkspaceProperties:
|
|
114
|
+
def test_root_and_directories(self, tmp_path):
|
|
115
|
+
ws = Workspace(tmp_path)
|
|
116
|
+
assert ws.root == tmp_path.resolve()
|
|
117
|
+
assert ws.config_dir == tmp_path.resolve() / ".codexa"
|
|
118
|
+
assert ws.repos_dir == tmp_path.resolve() / ".codexa" / "repos"
|
|
119
|
+
assert ws.manifest_path == tmp_path.resolve() / ".codexa" / WORKSPACE_FILE
|
|
120
|
+
|
|
121
|
+
def test_repos_empty(self, tmp_path):
|
|
122
|
+
ws = Workspace(tmp_path)
|
|
123
|
+
assert ws.repos == []
|
|
124
|
+
|
|
125
|
+
def test_repo_index_dir(self, tmp_path):
|
|
126
|
+
ws = Workspace(tmp_path)
|
|
127
|
+
assert ws.repo_index_dir("myrepo") == ws.repos_dir / "myrepo"
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class TestWorkspacePersistence:
|
|
131
|
+
def test_save_creates_files(self, tmp_path):
|
|
132
|
+
ws = Workspace(tmp_path)
|
|
133
|
+
result_path = ws.save()
|
|
134
|
+
assert result_path.exists()
|
|
135
|
+
assert ws.config_dir.exists()
|
|
136
|
+
assert ws.repos_dir.exists()
|
|
137
|
+
data = json.loads(result_path.read_text())
|
|
138
|
+
assert data["version"] == "1.0.0"
|
|
139
|
+
assert data["repos"] == []
|
|
140
|
+
|
|
141
|
+
def test_load_roundtrip(self, tmp_path):
|
|
142
|
+
ws = Workspace(tmp_path)
|
|
143
|
+
repo_dir = tmp_path / "myrepo"
|
|
144
|
+
repo_dir.mkdir()
|
|
145
|
+
ws.add_repo("myrepo", repo_dir)
|
|
146
|
+
ws.save()
|
|
147
|
+
|
|
148
|
+
loaded = Workspace.load(tmp_path)
|
|
149
|
+
assert len(loaded.repos) == 1
|
|
150
|
+
assert loaded.repos[0].name == "myrepo"
|
|
151
|
+
|
|
152
|
+
def test_load_nonexistent_raises(self, tmp_path):
|
|
153
|
+
with pytest.raises(FileNotFoundError, match="No workspace found"):
|
|
154
|
+
Workspace.load(tmp_path / "nope")
|
|
155
|
+
|
|
156
|
+
def test_load_or_create_new(self, tmp_path):
|
|
157
|
+
ws = Workspace.load_or_create(tmp_path)
|
|
158
|
+
assert ws.manifest_path.exists()
|
|
159
|
+
assert ws.repos == []
|
|
160
|
+
|
|
161
|
+
def test_load_or_create_existing(self, tmp_path):
|
|
162
|
+
ws = Workspace(tmp_path)
|
|
163
|
+
repo_dir = tmp_path / "r"
|
|
164
|
+
repo_dir.mkdir()
|
|
165
|
+
ws.add_repo("r", repo_dir)
|
|
166
|
+
ws.save()
|
|
167
|
+
|
|
168
|
+
ws2 = Workspace.load_or_create(tmp_path)
|
|
169
|
+
assert len(ws2.repos) == 1
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class TestWorkspaceRepoManagement:
|
|
173
|
+
def test_add_repo(self, tmp_path):
|
|
174
|
+
ws = Workspace(tmp_path)
|
|
175
|
+
repo_dir = tmp_path / "repo_a"
|
|
176
|
+
repo_dir.mkdir()
|
|
177
|
+
entry = ws.add_repo("repo_a", repo_dir)
|
|
178
|
+
assert entry.name == "repo_a"
|
|
179
|
+
assert entry.path == str(repo_dir.resolve())
|
|
180
|
+
assert len(ws.repos) == 1
|
|
181
|
+
|
|
182
|
+
def test_add_duplicate_raises(self, tmp_path):
|
|
183
|
+
ws = Workspace(tmp_path)
|
|
184
|
+
repo_dir = tmp_path / "d"
|
|
185
|
+
repo_dir.mkdir()
|
|
186
|
+
ws.add_repo("d", repo_dir)
|
|
187
|
+
with pytest.raises(ValueError, match="already registered"):
|
|
188
|
+
ws.add_repo("d", repo_dir)
|
|
189
|
+
|
|
190
|
+
def test_add_nonexistent_dir_raises(self, tmp_path):
|
|
191
|
+
ws = Workspace(tmp_path)
|
|
192
|
+
with pytest.raises(FileNotFoundError, match="Directory not found"):
|
|
193
|
+
ws.add_repo("missing", tmp_path / "nope")
|
|
194
|
+
|
|
195
|
+
def test_remove_repo(self, tmp_path):
|
|
196
|
+
ws = Workspace(tmp_path)
|
|
197
|
+
repo_dir = tmp_path / "rem"
|
|
198
|
+
repo_dir.mkdir()
|
|
199
|
+
ws.add_repo("rem", repo_dir)
|
|
200
|
+
assert ws.remove_repo("rem") is True
|
|
201
|
+
assert len(ws.repos) == 0
|
|
202
|
+
|
|
203
|
+
def test_remove_nonexistent(self, tmp_path):
|
|
204
|
+
ws = Workspace(tmp_path)
|
|
205
|
+
assert ws.remove_repo("ghost") is False
|
|
206
|
+
|
|
207
|
+
def test_get_repo(self, tmp_path):
|
|
208
|
+
ws = Workspace(tmp_path)
|
|
209
|
+
repo_dir = tmp_path / "g"
|
|
210
|
+
repo_dir.mkdir()
|
|
211
|
+
ws.add_repo("g", repo_dir)
|
|
212
|
+
assert ws.get_repo("g") is not None
|
|
213
|
+
assert ws.get_repo("g").name == "g"
|
|
214
|
+
assert ws.get_repo("nope") is None
|
|
215
|
+
|
|
216
|
+
def test_multiple_repos(self, tmp_path):
|
|
217
|
+
ws = Workspace(tmp_path)
|
|
218
|
+
for name in ["a", "b", "c"]:
|
|
219
|
+
d = tmp_path / name
|
|
220
|
+
d.mkdir()
|
|
221
|
+
ws.add_repo(name, d)
|
|
222
|
+
assert len(ws.repos) == 3
|
|
223
|
+
names = {r.name for r in ws.repos}
|
|
224
|
+
assert names == {"a", "b", "c"}
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
class TestWorkspaceSummary:
|
|
228
|
+
def test_summary_structure(self, tmp_path):
|
|
229
|
+
ws = Workspace(tmp_path)
|
|
230
|
+
repo_dir = tmp_path / "s"
|
|
231
|
+
repo_dir.mkdir()
|
|
232
|
+
ws.add_repo("s", repo_dir)
|
|
233
|
+
info = ws.summary()
|
|
234
|
+
assert info["root"] == str(tmp_path.resolve())
|
|
235
|
+
assert info["repo_count"] == 1
|
|
236
|
+
assert info["version"] == "1.0.0"
|
|
237
|
+
assert len(info["repos"]) == 1
|
|
238
|
+
assert info["repos"][0]["name"] == "s"
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
class TestWorkspaceIndexing:
|
|
242
|
+
def test_index_repo_not_registered(self, tmp_path):
|
|
243
|
+
ws = Workspace(tmp_path)
|
|
244
|
+
with pytest.raises(KeyError, match="not registered"):
|
|
245
|
+
ws.index_repo("nope")
|
|
246
|
+
|
|
247
|
+
@patch("semantic_code_intelligence.workspace.generate_embeddings")
|
|
248
|
+
@patch("semantic_code_intelligence.workspace.scan_repository")
|
|
249
|
+
def test_index_repo_empty(self, mock_scan, mock_embed, tmp_path):
|
|
250
|
+
"""Indexing a repo with no files produces zero vectors."""
|
|
251
|
+
mock_scan.return_value = []
|
|
252
|
+
ws = Workspace(tmp_path)
|
|
253
|
+
repo_dir = tmp_path / "empty_repo"
|
|
254
|
+
repo_dir.mkdir()
|
|
255
|
+
ws.add_repo("empty_repo", repo_dir)
|
|
256
|
+
ws.save()
|
|
257
|
+
result = ws.index_repo("empty_repo")
|
|
258
|
+
assert result.files_indexed == 0
|
|
259
|
+
assert result.chunks_created == 0
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
class TestWorkspaceSearch:
|
|
263
|
+
def test_search_no_repos(self, tmp_path):
|
|
264
|
+
"""Searching with no repos returns empty list."""
|
|
265
|
+
ws = Workspace(tmp_path)
|
|
266
|
+
ws.save()
|
|
267
|
+
with patch("semantic_code_intelligence.workspace.generate_embeddings") as mock_embed:
|
|
268
|
+
import numpy as np
|
|
269
|
+
mock_embed.return_value = np.zeros((1, 384))
|
|
270
|
+
results = ws.search("hello")
|
|
271
|
+
assert results == []
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
# =========================================================================
|
|
275
|
+
# CLI command tests
|
|
276
|
+
# =========================================================================
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
class TestWorkspaceCLI:
|
|
280
|
+
def test_command_group_name(self):
|
|
281
|
+
assert workspace_cmd.name == "workspace"
|
|
282
|
+
|
|
283
|
+
def test_subcommands_exist(self):
|
|
284
|
+
names = list(workspace_cmd.commands.keys())
|
|
285
|
+
assert "init" in names
|
|
286
|
+
assert "add" in names
|
|
287
|
+
assert "remove" in names
|
|
288
|
+
assert "list" in names
|
|
289
|
+
assert "index" in names
|
|
290
|
+
assert "search" in names
|
|
291
|
+
|
|
292
|
+
def test_subcommand_count(self):
|
|
293
|
+
assert len(workspace_cmd.commands) == 6
|
|
294
|
+
|
|
295
|
+
def test_init_creates_workspace(self, tmp_path):
|
|
296
|
+
from click.testing import CliRunner
|
|
297
|
+
runner = CliRunner()
|
|
298
|
+
result = runner.invoke(workspace_cmd, ["init", "--path", str(tmp_path)])
|
|
299
|
+
assert result.exit_code == 0
|
|
300
|
+
assert (tmp_path / ".codexa" / WORKSPACE_FILE).exists()
|
|
301
|
+
|
|
302
|
+
def test_add_without_init_fails(self, tmp_path):
|
|
303
|
+
from click.testing import CliRunner
|
|
304
|
+
runner = CliRunner()
|
|
305
|
+
repo = tmp_path / "repo"
|
|
306
|
+
repo.mkdir()
|
|
307
|
+
result = runner.invoke(workspace_cmd, ["add", "myrepo", str(repo), "--path", str(tmp_path)])
|
|
308
|
+
assert result.exit_code == 0 # click still exits 0 but prints error
|
|
309
|
+
assert "not initialised" in result.output.lower() or "error" in result.output.lower()
|
|
310
|
+
|
|
311
|
+
def test_add_and_list(self, tmp_path):
|
|
312
|
+
from click.testing import CliRunner
|
|
313
|
+
runner = CliRunner()
|
|
314
|
+
# Init
|
|
315
|
+
runner.invoke(workspace_cmd, ["init", "--path", str(tmp_path)])
|
|
316
|
+
# Add
|
|
317
|
+
repo = tmp_path / "backend"
|
|
318
|
+
repo.mkdir()
|
|
319
|
+
result = runner.invoke(workspace_cmd, ["add", "backend", str(repo), "--path", str(tmp_path)])
|
|
320
|
+
assert result.exit_code == 0
|
|
321
|
+
# List JSON
|
|
322
|
+
result = runner.invoke(workspace_cmd, ["list", "--json", "--path", str(tmp_path)])
|
|
323
|
+
assert result.exit_code == 0
|
|
324
|
+
data = json.loads(result.output)
|
|
325
|
+
assert data["repo_count"] == 1
|
|
326
|
+
|
|
327
|
+
def test_remove_repo(self, tmp_path):
|
|
328
|
+
from click.testing import CliRunner
|
|
329
|
+
runner = CliRunner()
|
|
330
|
+
runner.invoke(workspace_cmd, ["init", "--path", str(tmp_path)])
|
|
331
|
+
repo = tmp_path / "api"
|
|
332
|
+
repo.mkdir()
|
|
333
|
+
runner.invoke(workspace_cmd, ["add", "api", str(repo), "--path", str(tmp_path)])
|
|
334
|
+
result = runner.invoke(workspace_cmd, ["remove", "api", "--path", str(tmp_path)])
|
|
335
|
+
assert result.exit_code == 0
|
|
336
|
+
# Verify removed
|
|
337
|
+
result = runner.invoke(workspace_cmd, ["list", "--json", "--path", str(tmp_path)])
|
|
338
|
+
data = json.loads(result.output)
|
|
339
|
+
assert data["repo_count"] == 0
|
|
340
|
+
|
|
341
|
+
def test_remove_nonexistent_warns(self, tmp_path):
|
|
342
|
+
from click.testing import CliRunner
|
|
343
|
+
runner = CliRunner()
|
|
344
|
+
runner.invoke(workspace_cmd, ["init", "--path", str(tmp_path)])
|
|
345
|
+
result = runner.invoke(workspace_cmd, ["remove", "ghost", "--path", str(tmp_path)])
|
|
346
|
+
assert result.exit_code == 0
|
|
347
|
+
assert "not found" in result.output.lower()
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
class TestRouterIncludesWorkspace:
|
|
351
|
+
def test_workspace_command_registered(self):
|
|
352
|
+
from semantic_code_intelligence.cli.router import register_commands
|
|
353
|
+
group = click.Group(name="test")
|
|
354
|
+
register_commands(group)
|
|
355
|
+
assert "workspace" in group.commands
|