codecortex 0.23.1__tar.gz → 0.23.3__tar.gz
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.
- {codecortex-0.23.1/src/codecortex.egg-info → codecortex-0.23.3}/PKG-INFO +1 -1
- {codecortex-0.23.1 → codecortex-0.23.3/src/codecortex.egg-info}/PKG-INFO +1 -1
- codecortex-0.23.3/src/codeintel/__init__.py +1 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/src/codeintel/gateway.py +62 -34
- {codecortex-0.23.1 → codecortex-0.23.3}/src/codeintel/indexer.py +13 -1
- {codecortex-0.23.1 → codecortex-0.23.3}/src/codeintel/provider.py +17 -6
- {codecortex-0.23.1 → codecortex-0.23.3}/src/codeintel/providers/lsp.py +92 -1
- {codecortex-0.23.1 → codecortex-0.23.3}/tests/test_doctor.py +21 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/tests/test_gateway.py +1 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/tests/test_incompleteness.py +30 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/tests/test_index_failure_reason.py +25 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/tests/test_lsp_provider.py +86 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/tests/test_null_reason_and_ripple.py +64 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/tests/test_semantic_provider.py +1 -0
- codecortex-0.23.1/src/codeintel/__init__.py +0 -1
- {codecortex-0.23.1 → codecortex-0.23.3}/LICENSE +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/README.md +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/pyproject.toml +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/setup.cfg +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/src/codecortex.egg-info/SOURCES.txt +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/src/codecortex.egg-info/dependency_links.txt +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/src/codecortex.egg-info/entry_points.txt +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/src/codecortex.egg-info/requires.txt +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/src/codecortex.egg-info/top_level.txt +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/src/codeintel/__main__.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/src/codeintel/agent_prompt.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/src/codeintel/auth.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/src/codeintel/c4.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/src/codeintel/c4_check.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/src/codeintel/c4_layers.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/src/codeintel/cache.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/src/codeintel/commands/__init__.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/src/codeintel/commands/_common.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/src/codeintel/commands/c4.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/src/codeintel/commands/doctor.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/src/codeintel/commands/gen_token.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/src/codeintel/commands/graph.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/src/codeintel/commands/index.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/src/codeintel/commands/install.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/src/codeintel/commands/map.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/src/codeintel/commands/prompt.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/src/codeintel/commands/query.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/src/codeintel/commands/reset.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/src/codeintel/commands/serve.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/src/codeintel/commands/serve_http.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/src/codeintel/commands/setup.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/src/codeintel/commands/status.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/src/codeintel/config.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/src/codeintel/containment.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/src/codeintel/doctor.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/src/codeintel/graph_backend.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/src/codeintel/graph_render.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/src/codeintel/graph_resolution.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/src/codeintel/grapher.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/src/codeintel/http_server.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/src/codeintel/injector.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/src/codeintel/installer.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/src/codeintel/lang_config.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/src/codeintel/loc.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/src/codeintel/logconfig.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/src/codeintel/mapper.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/src/codeintel/metrics.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/src/codeintel/onboarding.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/src/codeintel/outcome.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/src/codeintel/paths.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/src/codeintel/policy.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/src/codeintel/progress.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/src/codeintel/providers/__init__.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/src/codeintel/providers/graph.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/src/codeintel/providers/none.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/src/codeintel/providers/semantic.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/src/codeintel/py.typed +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/src/codeintel/query_ops.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/src/codeintel/redact.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/src/codeintel/reindexer.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/src/codeintel/reset.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/src/codeintel/searcher.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/src/codeintel/semantic_db.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/src/codeintel/server.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/src/codeintel/source_kind.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/src/codeintel/term.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/src/codeintel/verify.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/src/codeintel/viewer/__init__.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/src/codeintel/viewer/graph_template.html +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/src/codeintel/wire_text.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/tests/test_agent_prompt.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/tests/test_backend_reaper.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/tests/test_background_index_failure.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/tests/test_bench_oracle.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/tests/test_bench_oracle_ts.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/tests/test_c4.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/tests/test_c4_check.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/tests/test_c4_layers.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/tests/test_c4_model.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/tests/test_c4_output.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/tests/test_cache.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/tests/test_chunking.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/tests/test_cli_commands.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/tests/test_cli_help.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/tests/test_cli_install_dry_run.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/tests/test_cli_install_offer_injection.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/tests/test_cold_process.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/tests/test_config.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/tests/test_corpus.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/tests/test_could_not_ask_semantic.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/tests/test_docs_ci_claims.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/tests/test_docs_deadcode_withdrawal.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/tests/test_e2e.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/tests/test_edge_confidence.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/tests/test_engine_adoption.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/tests/test_enterprise.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/tests/test_graph_failure_population.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/tests/test_graph_provider.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/tests/test_graph_real.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/tests/test_graph_stdin.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/tests/test_grapher.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/tests/test_hard_exit.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/tests/test_hardening.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/tests/test_http_auth.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/tests/test_http_server.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/tests/test_index_progress.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/tests/test_indexer_binary.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/tests/test_installer.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/tests/test_integration.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/tests/test_lang_config.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/tests/test_loc_census.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/tests/test_lsp_language_coverage.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/tests/test_lsp_real.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/tests/test_mapper.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/tests/test_mcp_handshake.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/tests/test_mcp_server.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/tests/test_model_dimension.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/tests/test_never_raise.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/tests/test_onboarding.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/tests/test_rbac.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/tests/test_redaction_boundary.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/tests/test_reindexer.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/tests/test_rerank.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/tests/test_reset.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/tests/test_source_kind.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/tests/test_staleness.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/tests/test_symbol_context.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/tests/test_term.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/tests/test_treesitter.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/tests/test_verify.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/tests/test_verify_call.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/tests/test_version_skew.py +0 -0
- {codecortex-0.23.1 → codecortex-0.23.3}/tests/test_wire_text.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codecortex
|
|
3
|
-
Version: 0.23.
|
|
3
|
+
Version: 0.23.3
|
|
4
4
|
Summary: Local-first, MCP-native code-intelligence server — graph, LSP, and semantic search behind one safe code.query tool for coding agents.
|
|
5
5
|
Author: Shammai Hamilton
|
|
6
6
|
License-Expression: MIT
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codecortex
|
|
3
|
-
Version: 0.23.
|
|
3
|
+
Version: 0.23.3
|
|
4
4
|
Summary: Local-first, MCP-native code-intelligence server — graph, LSP, and semantic search behind one safe code.query tool for coding agents.
|
|
5
5
|
Author: Shammai Hamilton
|
|
6
6
|
License-Expression: MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.23.3"
|
|
@@ -174,7 +174,7 @@ class Gateway:
|
|
|
174
174
|
self, result: Result, op: str, target: str, budget: Any,
|
|
175
175
|
project_root: Any, was_auto: bool,
|
|
176
176
|
) -> Result:
|
|
177
|
-
"""Ask the LSP when the graph's
|
|
177
|
+
"""Ask the LSP when the graph's caller answer carries a known binding failure.
|
|
178
178
|
|
|
179
179
|
This closes the routing gap that let the worst failure through. `_AUTO_ENGINE` is a static
|
|
180
180
|
op→engine map, not a cascade: `callers` goes to the graph and, uniquely among the ops, has
|
|
@@ -185,48 +185,71 @@ class Gateway:
|
|
|
185
185
|
being confidently wrong is a bug; no second engine ever being consulted is the design that
|
|
186
186
|
let it reach the caller.
|
|
187
187
|
|
|
188
|
-
Deliberately narrow. It fires
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
not call edges), so presenting its
|
|
193
|
-
another. And it never fires for an
|
|
194
|
-
said which engine they want."""
|
|
188
|
+
Deliberately narrow. It fires when every graph row was name-resolved, or when a caller used
|
|
189
|
+
a file hint and the graph found same-named symbols but no edge for that exact definition.
|
|
190
|
+
The latter catches property calls that the TypeScript graph resolver failed to bind, while
|
|
191
|
+
avoiding an LSP call for ordinary healthy answers. It APPENDS rather than replaces: the LSP
|
|
192
|
+
answers a related but different question (references, not call edges), so presenting its
|
|
193
|
+
list as the graph's would substitute one over-claim for another. And it never fires for an
|
|
194
|
+
explicitly pinned `--engine graph`, where the caller has said which engine they want."""
|
|
195
195
|
try:
|
|
196
196
|
if not was_auto or op not in self._CROSS_CHECKED_OPS:
|
|
197
197
|
return result
|
|
198
198
|
if result.get("result") is None or self.lsp is None:
|
|
199
199
|
return result
|
|
200
200
|
gaps = result.get("gaps") or []
|
|
201
|
-
|
|
202
|
-
|
|
201
|
+
kinds = {
|
|
202
|
+
str(g.get("kind") or "") for g in gaps if isinstance(g, dict)
|
|
203
|
+
}
|
|
204
|
+
all_name_resolved = "all-rows-name-resolved" in kinds
|
|
205
|
+
exact_target_unbound = "target-hint-unmatched" in kinds
|
|
206
|
+
if not (all_name_resolved or exact_target_unbound):
|
|
203
207
|
return result
|
|
208
|
+
graph_problem = (
|
|
209
|
+
"every graph row was resolved by name"
|
|
210
|
+
if all_name_resolved
|
|
211
|
+
else "the graph found no caller edge for the file-qualified symbol"
|
|
212
|
+
)
|
|
204
213
|
probe = self._dispatch_single(
|
|
205
214
|
self.lsp, "symbol", target, budget, project_root, "lsp")
|
|
206
215
|
body = probe.get("result")
|
|
207
|
-
|
|
216
|
+
probe_gaps = probe.get("gaps") or []
|
|
217
|
+
reference_gap = next((
|
|
218
|
+
gap for gap in probe_gaps
|
|
219
|
+
if isinstance(gap, dict) and gap.get("section") == "references"
|
|
220
|
+
), None)
|
|
221
|
+
if not body or reference_gap is not None:
|
|
208
222
|
# Silence from the LSP is not agreement. Say which check did not happen — and
|
|
209
223
|
# separate "not yet booted" from "had nothing", because only the first is fixed by
|
|
210
224
|
# asking again. A one-shot CLI process meets a cold serena on every invocation; the
|
|
211
225
|
# long-lived MCP server keeps the session warm and takes this branch once at most.
|
|
212
226
|
# Waiting here is deliberately NOT done: it would hold back a graph answer that is
|
|
213
227
|
# already complete, to append a section that is only advisory.
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
228
|
+
unavailable_reason = (
|
|
229
|
+
str(reference_gap.get("kind") or "not-asked")
|
|
230
|
+
if reference_gap is not None else str(probe.get("reason") or "no-result")
|
|
231
|
+
)
|
|
232
|
+
warming = unavailable_reason in {"warming", "timeout"}
|
|
233
|
+
why = (
|
|
234
|
+
"the language server had not finished booting"
|
|
235
|
+
if unavailable_reason == "warming"
|
|
236
|
+
else str(reference_gap.get("detail") or "the reference lookup did not answer")
|
|
237
|
+
if reference_gap is not None
|
|
238
|
+
else "the LSP engine reported nothing for this symbol"
|
|
239
|
+
)
|
|
217
240
|
nxt = (" Ask again once it is warm and this section will be filled in."
|
|
218
241
|
if warming else
|
|
219
242
|
" Check it yourself with `--engine lsp --op symbol`.")
|
|
220
243
|
return {**result, "gaps": [*gaps, {
|
|
221
244
|
"section": op, "kind": "cross-check-unavailable",
|
|
222
245
|
"engine": "lsp",
|
|
223
|
-
"reason":
|
|
224
|
-
"detail": f"
|
|
225
|
-
f"
|
|
246
|
+
"reason": unavailable_reason,
|
|
247
|
+
"detail": f"{graph_problem} and the LSP could not provide an independent "
|
|
248
|
+
f"reference check ({why}), so the graph answer remains unverified"
|
|
226
249
|
+ (" — retry" if warming else ""),
|
|
227
250
|
**({"retry_after_s": 2} if warming else {}),
|
|
228
251
|
}], "result": str(result["result"]) + (
|
|
229
|
-
f"\n\n> Cross-check unavailable:
|
|
252
|
+
f"\n\n> Cross-check unavailable: {graph_problem}, and "
|
|
230
253
|
f"{why}, so nothing here has been confirmed against a second engine.{nxt}"
|
|
231
254
|
)}
|
|
232
255
|
refs = self._reference_lines(str(body))
|
|
@@ -236,15 +259,15 @@ class Gateway:
|
|
|
236
259
|
merged = (
|
|
237
260
|
f"{result['result']}\n\n## Cross-check — LSP references to `{target}` "
|
|
238
261
|
f"({len(refs)})\n"
|
|
239
|
-
f"_The
|
|
240
|
-
f"language server, which resolves
|
|
241
|
-
f"but absent here is
|
|
242
|
-
f"is a
|
|
262
|
+
f"_The graph answer has a known binding gap: {graph_problem}. These locations come "
|
|
263
|
+
f"from the language server, which resolves the exact definition. A caller listed "
|
|
264
|
+
f"above but absent here is likely a name collision; a location here but missing "
|
|
265
|
+
f"above is a reference the graph could not bind._\n" + listing + more
|
|
243
266
|
)
|
|
244
267
|
return {**result, "result": merged, "gaps": [*gaps, {
|
|
245
268
|
"section": op, "kind": "cross-checked-with-lsp",
|
|
246
269
|
"engine": "lsp",
|
|
247
|
-
"detail": f"
|
|
270
|
+
"detail": f"{graph_problem}, so the LSP was consulted "
|
|
248
271
|
f"independently and reported {len(refs)} reference location(s); the two "
|
|
249
272
|
f"lists answer related but different questions and are shown separately",
|
|
250
273
|
}]}
|
|
@@ -292,17 +315,22 @@ class Gateway:
|
|
|
292
315
|
# codebase throws away the could-not-ask / asked-and-found-nothing distinction it is
|
|
293
316
|
# otherwise careful to preserve per-provider.
|
|
294
317
|
reasons = {eng: str(r.get("reason") or "no-result") for eng, r in results.items()}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
318
|
+
# Providers have already classified their reason into the public outcome taxonomy.
|
|
319
|
+
# Repeating a private reason allow-list here guaranteed drift: the first new reason,
|
|
320
|
+
# ``source-unreadable``, was correctly marked unavailable by LSP and then collapsed
|
|
321
|
+
# back to ``not_found`` by this merge. Failed and unavailable both mean no engine was
|
|
322
|
+
# able to answer; an actual miss is the explicit ``not_found`` outcome.
|
|
323
|
+
# Older/custom providers may not have adopted ``outcome`` yet. Route their reason
|
|
324
|
+
# through the SAME central classifier rather than reintroducing a local allow-list.
|
|
325
|
+
classified = [
|
|
326
|
+
r.get("outcome") or safe_null_result(
|
|
327
|
+
op_str, target_str, engine=eng, reason=reasons[eng]
|
|
328
|
+
).get("outcome")
|
|
329
|
+
for eng, r in results.items()
|
|
330
|
+
]
|
|
331
|
+
all_unreachable = bool(classified) and all(
|
|
332
|
+
outcome in ("unavailable", "failed") for outcome in classified
|
|
333
|
+
)
|
|
306
334
|
summary = "engines-unavailable" if all_unreachable else "no-result"
|
|
307
335
|
detail = ", ".join(f"{eng}: {why}" for eng, why in sorted(reasons.items()))
|
|
308
336
|
return safe_null_result(
|
|
@@ -390,7 +390,19 @@ class Indexer:
|
|
|
390
390
|
root_real = real_root(str(root))
|
|
391
391
|
except Exception:
|
|
392
392
|
root_real = str(root)
|
|
393
|
-
|
|
393
|
+
def fail_on_unreadable_tree(error: OSError) -> None:
|
|
394
|
+
"""Do not turn an unreadable subtree into a successful empty index.
|
|
395
|
+
|
|
396
|
+
``os.walk`` silently ignores directory-enumeration failures unless an ``onerror``
|
|
397
|
+
callback is supplied. On macOS this is particularly dangerous for folders protected
|
|
398
|
+
by Files & Folders / Full Disk Access: the root can pass ``isdir`` while every source
|
|
399
|
+
directory raises ``EPERM``, leaving the CLI to report "Nothing new to index" at exit
|
|
400
|
+
zero. Propagating the original error preserves its path and lets ``index()`` expose a
|
|
401
|
+
truthful, actionable failure.
|
|
402
|
+
"""
|
|
403
|
+
raise error
|
|
404
|
+
|
|
405
|
+
for dirpath, dirnames, filenames in os.walk(root, onerror=fail_on_unreadable_tree):
|
|
394
406
|
at_root = os.path.realpath(dirpath) == os.path.realpath(real_root_str)
|
|
395
407
|
dirnames[:] = [
|
|
396
408
|
d for d in dirnames
|
|
@@ -75,20 +75,31 @@ def safe_null_result(
|
|
|
75
75
|
hint: str | None = None,
|
|
76
76
|
) -> Result:
|
|
77
77
|
failed_reasons = {
|
|
78
|
-
"backend-error", "backend-incompatible", "boot-failed", "error", "
|
|
79
|
-
"provider-error", "
|
|
78
|
+
"backend-error", "backend-incompatible", "boot-failed", "error", "gateway-error",
|
|
79
|
+
"handler-error", "index-failed", "provider-error", "query-failed", "timeout",
|
|
80
|
+
"unparsable",
|
|
80
81
|
}
|
|
81
82
|
unavailable_reasons = {
|
|
82
|
-
"engine-unavailable", "engines-unavailable", "indexing-in-progress",
|
|
83
|
-
"no-
|
|
84
|
-
"
|
|
83
|
+
"backend-unreachable", "engine-unavailable", "engines-unavailable", "indexing-in-progress",
|
|
84
|
+
"index-stale", "no-engine", "no-project-root", "op-not-allowed-for-role",
|
|
85
|
+
"op-not-supported", "op-withdrawn", "project-not-indexed",
|
|
86
|
+
"project-not-indexed-standalone", "root-not-allowed-for-role", "source-unreadable",
|
|
87
|
+
"unknown-engine", "unsupported-op", "warming",
|
|
88
|
+
}
|
|
89
|
+
not_found_reasons = {
|
|
90
|
+
"below-floor", "no-edges", "no-index", "no-result", "not-found", "not-in-graph",
|
|
85
91
|
}
|
|
86
92
|
if reason in failed_reasons:
|
|
87
93
|
outcome = "failed"
|
|
88
94
|
elif reason in unavailable_reasons:
|
|
89
95
|
outcome = "unavailable"
|
|
90
|
-
|
|
96
|
+
elif reason in not_found_reasons:
|
|
91
97
|
outcome = "not_found"
|
|
98
|
+
else:
|
|
99
|
+
# New failure reasons must fail closed. Defaulting every unknown string to ``not_found``
|
|
100
|
+
# turned a newly introduced ``source-unreadable`` reason into an assertion that the symbol
|
|
101
|
+
# was absent — exactly the ambiguity the explicit outcome field was added to remove.
|
|
102
|
+
outcome = "failed"
|
|
92
103
|
r: Result = {
|
|
93
104
|
"ok": True,
|
|
94
105
|
"op": str(op or ""),
|
|
@@ -67,6 +67,30 @@ _BACKEND_ERROR_MARKERS = (
|
|
|
67
67
|
)
|
|
68
68
|
|
|
69
69
|
|
|
70
|
+
def _split_target_file_hint(target: str) -> tuple[str, str]:
|
|
71
|
+
"""Return Serena's symbol pattern and an optional repo-relative file hint.
|
|
72
|
+
|
|
73
|
+
Graph operations accept ``name@path`` to disambiguate duplicate symbols. The gateway may pass
|
|
74
|
+
that exact target to the LSP for an independent reference cross-check, but Serena understands
|
|
75
|
+
only the name-path portion. Keep the syntax aligned here without coupling the LSP provider to
|
|
76
|
+
graph.py's private target parser.
|
|
77
|
+
"""
|
|
78
|
+
raw = str(target or "").strip()
|
|
79
|
+
if "@" not in raw:
|
|
80
|
+
return raw, ""
|
|
81
|
+
head, _, tail = raw.rpartition("@")
|
|
82
|
+
if not head.strip() or not tail.strip():
|
|
83
|
+
return raw, ""
|
|
84
|
+
return head.strip(), tail.strip()
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _file_hint_matches(file_path: Any, hint: str) -> bool:
|
|
88
|
+
"""Match a file hint on path-segment boundaries, consistently with the graph provider."""
|
|
89
|
+
have = str(file_path or "").replace("\\", "/").strip().strip("/").lower()
|
|
90
|
+
want = str(hint or "").replace("\\", "/").strip().strip("/").lower()
|
|
91
|
+
return bool(have and want and (have == want or have.endswith("/" + want)))
|
|
92
|
+
|
|
93
|
+
|
|
70
94
|
def _looks_like_backend_error(text: str) -> bool:
|
|
71
95
|
"""Whether *text* is a backend failure message rather than a result.
|
|
72
96
|
|
|
@@ -127,6 +151,29 @@ def _serena_launch_args(cmd: str, project_root: str) -> list[str]:
|
|
|
127
151
|
return [cmd, *common]
|
|
128
152
|
|
|
129
153
|
|
|
154
|
+
def _project_access_failure(project_root: str) -> tuple[str, str] | None:
|
|
155
|
+
"""Return a safe diagnosis when the host cannot enumerate the project root.
|
|
156
|
+
|
|
157
|
+
Serena's stdio transport wraps an early subprocess exit in an ``ExceptionGroup``. When the
|
|
158
|
+
real cause is a macOS Files & Folders denial, reporting that wrapper as a network/bootstrap
|
|
159
|
+
failure sends the user in exactly the wrong direction. A one-entry scan is cheap, local, and
|
|
160
|
+
establishes the prerequisite Serena itself needs without forwarding any backend-controlled
|
|
161
|
+
prose to the calling agent.
|
|
162
|
+
"""
|
|
163
|
+
try:
|
|
164
|
+
with os.scandir(project_root) as entries:
|
|
165
|
+
next(entries, None)
|
|
166
|
+
except OSError as exc:
|
|
167
|
+
location = str(getattr(exc, "filename", None) or project_root)
|
|
168
|
+
detail = f"repository root is not readable by codeintel ({type(exc).__name__}: {location})"
|
|
169
|
+
remediation = (
|
|
170
|
+
"grant the codeintel host read access to the repository, then retry; on macOS check "
|
|
171
|
+
"System Settings > Privacy & Security > Files and Folders (or Full Disk Access)"
|
|
172
|
+
)
|
|
173
|
+
return detail, remediation
|
|
174
|
+
return None
|
|
175
|
+
|
|
176
|
+
|
|
130
177
|
class _State(enum.Enum):
|
|
131
178
|
WARMING = "WARMING"
|
|
132
179
|
READY = "READY"
|
|
@@ -433,6 +480,12 @@ class LspProvider:
|
|
|
433
480
|
"detail": "serena session is warming for this repo", "remediation": None}
|
|
434
481
|
|
|
435
482
|
# deep: boot (or reuse) a session and poll to a hard deadline — never hangs.
|
|
483
|
+
access_failure = _project_access_failure(project_root)
|
|
484
|
+
if access_failure is not None:
|
|
485
|
+
detail, remediation = access_failure
|
|
486
|
+
return {"installed": True, "runnable": False, "repo_indexed": None,
|
|
487
|
+
"detail": detail, "remediation": remediation}
|
|
488
|
+
|
|
436
489
|
session = self._get_or_create_session(project_root)
|
|
437
490
|
deadline = time.monotonic() + timeout_s
|
|
438
491
|
while time.monotonic() < deadline:
|
|
@@ -502,6 +555,21 @@ class LspProvider:
|
|
|
502
555
|
if not self.available:
|
|
503
556
|
return safe_null_result(op_str, target_str, engine="lsp", reason="engine-unavailable")
|
|
504
557
|
|
|
558
|
+
# A protected macOS folder can still satisfy ``isdir`` while denying enumeration.
|
|
559
|
+
# Detect that locally before spawning Serena: otherwise its early exit is wrapped by
|
|
560
|
+
# the MCP transport as a generic boot failure and the first-query hint talks about a
|
|
561
|
+
# cold uvx cache instead of the permission the user actually needs to grant. Keep the
|
|
562
|
+
# ``isdir`` guard so synthetic/nonexistent roots used by embedders retain the existing
|
|
563
|
+
# never-raise backend behaviour.
|
|
564
|
+
if os.path.isdir(root_str):
|
|
565
|
+
access_failure = _project_access_failure(root_str)
|
|
566
|
+
if access_failure is not None:
|
|
567
|
+
detail, remediation = access_failure
|
|
568
|
+
return safe_null_result(
|
|
569
|
+
op_str, target_str, engine="lsp", reason="source-unreadable",
|
|
570
|
+
hint=f"{detail}; {remediation}",
|
|
571
|
+
)
|
|
572
|
+
|
|
505
573
|
try:
|
|
506
574
|
budget_ms = int(budget) if budget else 0
|
|
507
575
|
except Exception:
|
|
@@ -729,10 +797,18 @@ class LspProvider:
|
|
|
729
797
|
self, session: _LspSession, target: str, root: str, timeout_s: float
|
|
730
798
|
) -> str | None:
|
|
731
799
|
try:
|
|
800
|
+
symbol_target, file_hint = _split_target_file_hint(target)
|
|
732
801
|
def_out = self._call_tool(
|
|
733
802
|
session,
|
|
734
803
|
"find_symbol",
|
|
735
|
-
{
|
|
804
|
+
{
|
|
805
|
+
"name_path_pattern": symbol_target,
|
|
806
|
+
"include_body": True,
|
|
807
|
+
# A file hint exists specifically because the name is ambiguous. Retrieve
|
|
808
|
+
# enough definitions to find the requested file instead of silently selecting
|
|
809
|
+
# whichever five Serena happens to return first.
|
|
810
|
+
"max_matches": 50 if file_hint else 5,
|
|
811
|
+
},
|
|
736
812
|
timeout_s,
|
|
737
813
|
)
|
|
738
814
|
if isinstance(def_out, Missing):
|
|
@@ -745,6 +821,21 @@ class LspProvider:
|
|
|
745
821
|
def_raw = def_out.value
|
|
746
822
|
def_text = self._extract_text(def_raw)
|
|
747
823
|
matches = self._loads(def_text)
|
|
824
|
+
if isinstance(matches, list) and file_hint:
|
|
825
|
+
matches = [
|
|
826
|
+
match for match in matches
|
|
827
|
+
if isinstance(match, dict)
|
|
828
|
+
and _file_hint_matches(match.get("relative_path"), file_hint)
|
|
829
|
+
]
|
|
830
|
+
if not matches:
|
|
831
|
+
# Do not fall through and render the original JSON containing definitions
|
|
832
|
+
# from other files. More importantly, the reference lookup below must remain
|
|
833
|
+
# "not asked", rather than turning an unresolved exact definition into a
|
|
834
|
+
# confident zero-reference answer.
|
|
835
|
+
def_text = (
|
|
836
|
+
f"> The language server found no definition for `{symbol_target}` in "
|
|
837
|
+
f"`{file_hint}`."
|
|
838
|
+
)
|
|
748
839
|
|
|
749
840
|
first: dict | None = None
|
|
750
841
|
if isinstance(matches, list) and matches:
|
|
@@ -316,6 +316,27 @@ def test_lsp_probe_deep_boot_failure_is_bounded(monkeypatch):
|
|
|
316
316
|
assert r["runnable"] in (False, None) # failed to boot (or timed out) — never True
|
|
317
317
|
|
|
318
318
|
|
|
319
|
+
def test_lsp_probe_reports_repository_access_before_blaming_network(monkeypatch):
|
|
320
|
+
monkeypatch.setattr("codeintel.providers.lsp.shutil.which", lambda x: "/fake/uvx")
|
|
321
|
+
monkeypatch.setattr(
|
|
322
|
+
"codeintel.providers.lsp.os.scandir",
|
|
323
|
+
lambda root: (_ for _ in ()).throw(PermissionError(1, "denied", str(root))),
|
|
324
|
+
)
|
|
325
|
+
p = LspProvider()
|
|
326
|
+
monkeypatch.setattr(
|
|
327
|
+
p, "_get_or_create_session",
|
|
328
|
+
lambda root: pytest.fail("an unreadable repository must not launch serena"),
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
r = p.probe("/protected/repo", deep=True)
|
|
332
|
+
|
|
333
|
+
assert r["runnable"] is False
|
|
334
|
+
assert "PermissionError" in r["detail"]
|
|
335
|
+
assert "/protected/repo" in r["detail"]
|
|
336
|
+
assert "Privacy & Security" in r["remediation"]
|
|
337
|
+
assert "network" not in r["remediation"].lower()
|
|
338
|
+
|
|
339
|
+
|
|
319
340
|
# --------------------------------------------------------------------------- #
|
|
320
341
|
# handler + hint plumbing
|
|
321
342
|
# --------------------------------------------------------------------------- #
|
|
@@ -548,6 +548,7 @@ def test_a_fanout_where_engines_ran_and_found_nothing_still_reports_no_result():
|
|
|
548
548
|
lsp=_NullProvider("lsp", "not-found"))
|
|
549
549
|
r = gw.query(op="context", target="nope", engine="both")
|
|
550
550
|
assert r["reason"] == "no-result"
|
|
551
|
+
assert r["outcome"] == "not_found"
|
|
551
552
|
assert "NOT evidence" not in (r.get("hint") or "")
|
|
552
553
|
|
|
553
554
|
|
|
@@ -371,8 +371,13 @@ def test_null_result_outcome_is_machine_readable():
|
|
|
371
371
|
from codeintel.provider import safe_null_result
|
|
372
372
|
|
|
373
373
|
assert safe_null_result("x", "y", reason="not-in-graph")["outcome"] == "not_found"
|
|
374
|
+
assert safe_null_result("x", "y", reason="not-found")["outcome"] == "not_found"
|
|
375
|
+
assert safe_null_result("x", "y", reason="no-index")["outcome"] == "not_found"
|
|
374
376
|
assert safe_null_result("x", "y", reason="engine-unavailable")["outcome"] == "unavailable"
|
|
375
377
|
assert safe_null_result("x", "y", reason="backend-error")["outcome"] == "failed"
|
|
378
|
+
assert safe_null_result("x", "y", reason="source-unreadable")["outcome"] == "unavailable"
|
|
379
|
+
assert safe_null_result("x", "y", reason="backend-unreachable")["outcome"] == "unavailable"
|
|
380
|
+
assert safe_null_result("x", "y", reason="new-unclassified-reason")["outcome"] == "failed"
|
|
376
381
|
|
|
377
382
|
|
|
378
383
|
def test_a_fanned_out_answer_reports_the_engine_that_could_not_be_asked():
|
|
@@ -398,6 +403,31 @@ def test_a_fanned_out_answer_reports_the_engine_that_could_not_be_asked():
|
|
|
398
403
|
assert "engine-unavailable" in kinds, kinds
|
|
399
404
|
|
|
400
405
|
|
|
406
|
+
def test_fan_out_preserves_source_unreadable_as_unavailable():
|
|
407
|
+
"""The merge must consume the public outcome, not maintain another reason allow-list. If all
|
|
408
|
+
engines could not answer, a newly introduced reason must never collapse back to ``not_found``.
|
|
409
|
+
"""
|
|
410
|
+
from codeintel.gateway import Gateway
|
|
411
|
+
from codeintel.provider import safe_null_result
|
|
412
|
+
|
|
413
|
+
merged = Gateway()._merge(
|
|
414
|
+
{
|
|
415
|
+
"lsp": safe_null_result(
|
|
416
|
+
"context", "createSession", engine="lsp", reason="source-unreadable"
|
|
417
|
+
),
|
|
418
|
+
"graph": safe_null_result(
|
|
419
|
+
"context", "createSession", engine="graph", reason="project-not-indexed"
|
|
420
|
+
),
|
|
421
|
+
},
|
|
422
|
+
"context", "createSession",
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
assert merged["result"] is None
|
|
426
|
+
assert merged["reason"] == "engines-unavailable"
|
|
427
|
+
assert merged["outcome"] == "unavailable"
|
|
428
|
+
assert "NOT evidence" in merged["hint"]
|
|
429
|
+
|
|
430
|
+
|
|
401
431
|
def test_a_constituent_gap_survives_the_merge():
|
|
402
432
|
from codeintel.gateway import Gateway
|
|
403
433
|
|
|
@@ -326,6 +326,31 @@ def test_other_failures_keep_their_type_prefix(monkeypatch, tmp_path):
|
|
|
326
326
|
assert indexer.last_error == "PermissionError: read-only file system"
|
|
327
327
|
|
|
328
328
|
|
|
329
|
+
def test_an_unreadable_subtree_is_not_a_successful_empty_walk(monkeypatch, tmp_path):
|
|
330
|
+
"""``os.walk`` ignores enumeration errors by default. A macOS-protected repository could
|
|
331
|
+
therefore scan as zero files and make the CLI print "Nothing new to index" at exit zero even
|
|
332
|
+
though no source was readable. The original permission error, including its path, must escape
|
|
333
|
+
the walk so ``Indexer.index`` can report it as an unrecoverable failure.
|
|
334
|
+
"""
|
|
335
|
+
import codeintel.indexer as indexer_module
|
|
336
|
+
from codeintel.indexer import Indexer
|
|
337
|
+
|
|
338
|
+
protected = tmp_path / "protected-repo"
|
|
339
|
+
|
|
340
|
+
def denied_walk(root, *, onerror=None):
|
|
341
|
+
assert onerror is not None
|
|
342
|
+
onerror(PermissionError(1, "Operation not permitted", str(protected)))
|
|
343
|
+
return []
|
|
344
|
+
|
|
345
|
+
monkeypatch.setattr(indexer_module.os, "walk", denied_walk)
|
|
346
|
+
monkeypatch.setattr(indexer_module, "load_gitattributes_globs", lambda root: [])
|
|
347
|
+
indexer = Indexer.__new__(Indexer)
|
|
348
|
+
monkeypatch.setattr(indexer, "_load_gitignore", lambda root: set())
|
|
349
|
+
|
|
350
|
+
with pytest.raises(PermissionError, match="protected-repo"):
|
|
351
|
+
list(indexer._walk_files(tmp_path))
|
|
352
|
+
|
|
353
|
+
|
|
329
354
|
def test_the_searcher_reports_it_too(monkeypatch):
|
|
330
355
|
"""A query is the other way a cold cache is discovered — stage-qualified, message verbatim."""
|
|
331
356
|
from codeintel.searcher import Searcher
|
|
@@ -6,6 +6,8 @@ import time
|
|
|
6
6
|
from typing import Any
|
|
7
7
|
from unittest.mock import MagicMock
|
|
8
8
|
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
9
11
|
from codeintel.outcome import Missing, Ok
|
|
10
12
|
from codeintel.providers.lsp import LspProvider, _State
|
|
11
13
|
from codeintel.server import code_status_handler
|
|
@@ -161,6 +163,64 @@ def test_lsp_provider_ready_symbol(monkeypatch):
|
|
|
161
163
|
assert "parse_result" in r["result"]
|
|
162
164
|
|
|
163
165
|
|
|
166
|
+
def test_file_qualified_symbol_selects_the_matching_definition(monkeypatch):
|
|
167
|
+
"""A graph file hint must survive the LSP cross-check instead of selecting a same-name method."""
|
|
168
|
+
monkeypatch.setattr("codeintel.providers.lsp.shutil.which", lambda x: "/fake/uvx")
|
|
169
|
+
p = LspProvider()
|
|
170
|
+
p._sessions["/my/repo"] = _make_fake_session(_State.READY)
|
|
171
|
+
calls: list[tuple[str, dict]] = []
|
|
172
|
+
|
|
173
|
+
def _fake_call_tool(session, tool, args, timeout_s):
|
|
174
|
+
calls.append((tool, args))
|
|
175
|
+
if tool == "find_symbol":
|
|
176
|
+
return Ok('[{"name_path":"Other/createSession","kind":"Method",'
|
|
177
|
+
'"relative_path":"src/other.ts","body_location":{"start_line":1,'
|
|
178
|
+
'"end_line":2},"body":"wrong"},{"name_path":"WsSessionHandler/createSession",'
|
|
179
|
+
'"kind":"Method","relative_path":"backend/src/session.handler.ts",'
|
|
180
|
+
'"body_location":{"start_line":138,"end_line":177},"body":"right"}]')
|
|
181
|
+
if tool == "find_referencing_symbols":
|
|
182
|
+
assert args["name_path"] == "WsSessionHandler/createSession"
|
|
183
|
+
assert args["relative_path"] == "backend/src/session.handler.ts"
|
|
184
|
+
return Ok('{"backend/src/gateway.ts":{"Method":[{"name_path":"Gateway/routeMessage",'
|
|
185
|
+
'"content_around_reference":"> 612: await handler.createSession()"}]}}')
|
|
186
|
+
return Missing("backend-error", "unstubbed tool")
|
|
187
|
+
|
|
188
|
+
monkeypatch.setattr(p, "_call_tool", _fake_call_tool)
|
|
189
|
+
r = p.build_result(
|
|
190
|
+
"symbol", "createSession@backend/src/session.handler.ts", [], 30000, "/my/repo"
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
assert r["confidence"] == "complete"
|
|
194
|
+
assert "right" in r["result"] and "wrong" not in r["result"]
|
|
195
|
+
assert "backend/src/gateway.ts:613" in r["result"]
|
|
196
|
+
assert calls[0][1]["name_path_pattern"] == "createSession"
|
|
197
|
+
assert calls[0][1]["max_matches"] == 50
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def test_file_qualified_symbol_does_not_use_a_definition_from_another_file(monkeypatch):
|
|
201
|
+
monkeypatch.setattr("codeintel.providers.lsp.shutil.which", lambda x: "/fake/uvx")
|
|
202
|
+
p = LspProvider()
|
|
203
|
+
p._sessions["/my/repo"] = _make_fake_session(_State.READY)
|
|
204
|
+
calls: list[str] = []
|
|
205
|
+
|
|
206
|
+
def _fake_call_tool(session, tool, args, timeout_s):
|
|
207
|
+
calls.append(tool)
|
|
208
|
+
return Ok('[{"name_path":"Other/createSession","kind":"Method",'
|
|
209
|
+
'"relative_path":"src/other.ts","body_location":{"start_line":1,'
|
|
210
|
+
'"end_line":2},"body":"wrong"}]')
|
|
211
|
+
|
|
212
|
+
monkeypatch.setattr(p, "_call_tool", _fake_call_tool)
|
|
213
|
+
r = p.build_result(
|
|
214
|
+
"symbol", "createSession@backend/src/session.handler.ts", [], 30000, "/my/repo"
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
assert r["confidence"] == "partial"
|
|
218
|
+
assert "found no definition" in r["result"] and "wrong" not in r["result"]
|
|
219
|
+
assert "References — not retrieved" in r["result"]
|
|
220
|
+
assert calls == ["find_symbol"]
|
|
221
|
+
assert any(g["section"] == "references" and g["kind"] == "not-asked" for g in r["gaps"])
|
|
222
|
+
|
|
223
|
+
|
|
164
224
|
# ---------------------------------------------------------------------------
|
|
165
225
|
# Group 8 — Unsupported op when READY
|
|
166
226
|
# ---------------------------------------------------------------------------
|
|
@@ -462,6 +522,32 @@ def test_boot_failed_on_a_cold_uvx_first_attempt_reads_as_retry(monkeypatch):
|
|
|
462
522
|
assert "download" in hint
|
|
463
523
|
|
|
464
524
|
|
|
525
|
+
def test_an_unreadable_repository_never_launches_serena(monkeypatch):
|
|
526
|
+
monkeypatch.setattr("codeintel.providers.lsp.shutil.which",
|
|
527
|
+
lambda x: "/fake/uvx" if x == "uvx" else None)
|
|
528
|
+
monkeypatch.setattr("codeintel.providers.lsp.os.path.isdir", lambda root: True)
|
|
529
|
+
monkeypatch.setattr(
|
|
530
|
+
"codeintel.providers.lsp._project_access_failure",
|
|
531
|
+
lambda root: (
|
|
532
|
+
f"repository root is not readable by codeintel (PermissionError: {root})",
|
|
533
|
+
"grant repository access",
|
|
534
|
+
),
|
|
535
|
+
)
|
|
536
|
+
p = LspProvider()
|
|
537
|
+
monkeypatch.setattr(
|
|
538
|
+
p, "_get_or_create_session",
|
|
539
|
+
lambda root: pytest.fail("an unreadable repository must not launch serena"),
|
|
540
|
+
)
|
|
541
|
+
|
|
542
|
+
r = p.build_result("symbol", "createSession", [], 1000, "/protected/repo")
|
|
543
|
+
|
|
544
|
+
assert r["result"] is None
|
|
545
|
+
assert r["reason"] == "source-unreadable"
|
|
546
|
+
assert r["outcome"] == "unavailable"
|
|
547
|
+
assert "PermissionError" in r["hint"]
|
|
548
|
+
assert "grant repository access" in r["hint"]
|
|
549
|
+
|
|
550
|
+
|
|
465
551
|
def test_boot_failed_on_a_respawn_does_not_blame_a_cold_cache(monkeypatch):
|
|
466
552
|
"""`uvx` populated its cache on the first attempt, so "still downloading" is only true once."""
|
|
467
553
|
monkeypatch.setattr("codeintel.providers.lsp.shutil.which",
|