codecortex 0.24.0__tar.gz → 0.24.1__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.
Files changed (167) hide show
  1. {codecortex-0.24.0/src/codecortex.egg-info → codecortex-0.24.1}/PKG-INFO +1 -1
  2. {codecortex-0.24.0 → codecortex-0.24.1/src/codecortex.egg-info}/PKG-INFO +1 -1
  3. codecortex-0.24.1/src/codeintel/__init__.py +1 -0
  4. {codecortex-0.24.0 → codecortex-0.24.1}/src/codeintel/commands/index.py +36 -0
  5. {codecortex-0.24.0 → codecortex-0.24.1}/src/codeintel/filelock.py +41 -1
  6. {codecortex-0.24.0 → codecortex-0.24.1}/tests/test_bench_corpus_typed.py +20 -0
  7. {codecortex-0.24.0 → codecortex-0.24.1}/tests/test_cli_commands.py +90 -0
  8. {codecortex-0.24.0 → codecortex-0.24.1}/tests/test_reindex_lock.py +59 -1
  9. codecortex-0.24.0/src/codeintel/__init__.py +0 -1
  10. {codecortex-0.24.0 → codecortex-0.24.1}/LICENSE +0 -0
  11. {codecortex-0.24.0 → codecortex-0.24.1}/README.md +0 -0
  12. {codecortex-0.24.0 → codecortex-0.24.1}/pyproject.toml +0 -0
  13. {codecortex-0.24.0 → codecortex-0.24.1}/setup.cfg +0 -0
  14. {codecortex-0.24.0 → codecortex-0.24.1}/src/codecortex.egg-info/SOURCES.txt +0 -0
  15. {codecortex-0.24.0 → codecortex-0.24.1}/src/codecortex.egg-info/dependency_links.txt +0 -0
  16. {codecortex-0.24.0 → codecortex-0.24.1}/src/codecortex.egg-info/entry_points.txt +0 -0
  17. {codecortex-0.24.0 → codecortex-0.24.1}/src/codecortex.egg-info/requires.txt +0 -0
  18. {codecortex-0.24.0 → codecortex-0.24.1}/src/codecortex.egg-info/top_level.txt +0 -0
  19. {codecortex-0.24.0 → codecortex-0.24.1}/src/codeintel/__main__.py +0 -0
  20. {codecortex-0.24.0 → codecortex-0.24.1}/src/codeintel/agent_prompt.py +0 -0
  21. {codecortex-0.24.0 → codecortex-0.24.1}/src/codeintel/auth.py +0 -0
  22. {codecortex-0.24.0 → codecortex-0.24.1}/src/codeintel/c4.py +0 -0
  23. {codecortex-0.24.0 → codecortex-0.24.1}/src/codeintel/c4_check.py +0 -0
  24. {codecortex-0.24.0 → codecortex-0.24.1}/src/codeintel/c4_layers.py +0 -0
  25. {codecortex-0.24.0 → codecortex-0.24.1}/src/codeintel/cache.py +0 -0
  26. {codecortex-0.24.0 → codecortex-0.24.1}/src/codeintel/commands/__init__.py +0 -0
  27. {codecortex-0.24.0 → codecortex-0.24.1}/src/codeintel/commands/_common.py +0 -0
  28. {codecortex-0.24.0 → codecortex-0.24.1}/src/codeintel/commands/c4.py +0 -0
  29. {codecortex-0.24.0 → codecortex-0.24.1}/src/codeintel/commands/doctor.py +0 -0
  30. {codecortex-0.24.0 → codecortex-0.24.1}/src/codeintel/commands/gen_token.py +0 -0
  31. {codecortex-0.24.0 → codecortex-0.24.1}/src/codeintel/commands/graph.py +0 -0
  32. {codecortex-0.24.0 → codecortex-0.24.1}/src/codeintel/commands/install.py +0 -0
  33. {codecortex-0.24.0 → codecortex-0.24.1}/src/codeintel/commands/map.py +0 -0
  34. {codecortex-0.24.0 → codecortex-0.24.1}/src/codeintel/commands/prompt.py +0 -0
  35. {codecortex-0.24.0 → codecortex-0.24.1}/src/codeintel/commands/query.py +0 -0
  36. {codecortex-0.24.0 → codecortex-0.24.1}/src/codeintel/commands/reset.py +0 -0
  37. {codecortex-0.24.0 → codecortex-0.24.1}/src/codeintel/commands/serve.py +0 -0
  38. {codecortex-0.24.0 → codecortex-0.24.1}/src/codeintel/commands/serve_http.py +0 -0
  39. {codecortex-0.24.0 → codecortex-0.24.1}/src/codeintel/commands/setup.py +0 -0
  40. {codecortex-0.24.0 → codecortex-0.24.1}/src/codeintel/commands/status.py +0 -0
  41. {codecortex-0.24.0 → codecortex-0.24.1}/src/codeintel/commands/uninstall.py +0 -0
  42. {codecortex-0.24.0 → codecortex-0.24.1}/src/codeintel/config.py +0 -0
  43. {codecortex-0.24.0 → codecortex-0.24.1}/src/codeintel/containment.py +0 -0
  44. {codecortex-0.24.0 → codecortex-0.24.1}/src/codeintel/doctor.py +0 -0
  45. {codecortex-0.24.0 → codecortex-0.24.1}/src/codeintel/gateway.py +0 -0
  46. {codecortex-0.24.0 → codecortex-0.24.1}/src/codeintel/graph_answer.py +0 -0
  47. {codecortex-0.24.0 → codecortex-0.24.1}/src/codeintel/graph_backend.py +0 -0
  48. {codecortex-0.24.0 → codecortex-0.24.1}/src/codeintel/graph_confidence.py +0 -0
  49. {codecortex-0.24.0 → codecortex-0.24.1}/src/codeintel/graph_edges.py +0 -0
  50. {codecortex-0.24.0 → codecortex-0.24.1}/src/codeintel/graph_ops.py +0 -0
  51. {codecortex-0.24.0 → codecortex-0.24.1}/src/codeintel/graph_render.py +0 -0
  52. {codecortex-0.24.0 → codecortex-0.24.1}/src/codeintel/graph_resolution.py +0 -0
  53. {codecortex-0.24.0 → codecortex-0.24.1}/src/codeintel/graph_targets.py +0 -0
  54. {codecortex-0.24.0 → codecortex-0.24.1}/src/codeintel/grapher.py +0 -0
  55. {codecortex-0.24.0 → codecortex-0.24.1}/src/codeintel/http_server.py +0 -0
  56. {codecortex-0.24.0 → codecortex-0.24.1}/src/codeintel/indexer.py +0 -0
  57. {codecortex-0.24.0 → codecortex-0.24.1}/src/codeintel/injector.py +0 -0
  58. {codecortex-0.24.0 → codecortex-0.24.1}/src/codeintel/installer.py +0 -0
  59. {codecortex-0.24.0 → codecortex-0.24.1}/src/codeintel/lang_config.py +0 -0
  60. {codecortex-0.24.0 → codecortex-0.24.1}/src/codeintel/loc.py +0 -0
  61. {codecortex-0.24.0 → codecortex-0.24.1}/src/codeintel/logconfig.py +0 -0
  62. {codecortex-0.24.0 → codecortex-0.24.1}/src/codeintel/mapper.py +0 -0
  63. {codecortex-0.24.0 → codecortex-0.24.1}/src/codeintel/metrics.py +0 -0
  64. {codecortex-0.24.0 → codecortex-0.24.1}/src/codeintel/onboarding.py +0 -0
  65. {codecortex-0.24.0 → codecortex-0.24.1}/src/codeintel/outcome.py +0 -0
  66. {codecortex-0.24.0 → codecortex-0.24.1}/src/codeintel/paths.py +0 -0
  67. {codecortex-0.24.0 → codecortex-0.24.1}/src/codeintel/per_thread.py +0 -0
  68. {codecortex-0.24.0 → codecortex-0.24.1}/src/codeintel/policy.py +0 -0
  69. {codecortex-0.24.0 → codecortex-0.24.1}/src/codeintel/progress.py +0 -0
  70. {codecortex-0.24.0 → codecortex-0.24.1}/src/codeintel/provider.py +0 -0
  71. {codecortex-0.24.0 → codecortex-0.24.1}/src/codeintel/providers/__init__.py +0 -0
  72. {codecortex-0.24.0 → codecortex-0.24.1}/src/codeintel/providers/graph.py +0 -0
  73. {codecortex-0.24.0 → codecortex-0.24.1}/src/codeintel/providers/lsp.py +0 -0
  74. {codecortex-0.24.0 → codecortex-0.24.1}/src/codeintel/providers/none.py +0 -0
  75. {codecortex-0.24.0 → codecortex-0.24.1}/src/codeintel/providers/semantic.py +0 -0
  76. {codecortex-0.24.0 → codecortex-0.24.1}/src/codeintel/py.typed +0 -0
  77. {codecortex-0.24.0 → codecortex-0.24.1}/src/codeintel/query_ops.py +0 -0
  78. {codecortex-0.24.0 → codecortex-0.24.1}/src/codeintel/redact.py +0 -0
  79. {codecortex-0.24.0 → codecortex-0.24.1}/src/codeintel/reindexer.py +0 -0
  80. {codecortex-0.24.0 → codecortex-0.24.1}/src/codeintel/reset.py +0 -0
  81. {codecortex-0.24.0 → codecortex-0.24.1}/src/codeintel/searcher.py +0 -0
  82. {codecortex-0.24.0 → codecortex-0.24.1}/src/codeintel/semantic_db.py +0 -0
  83. {codecortex-0.24.0 → codecortex-0.24.1}/src/codeintel/server.py +0 -0
  84. {codecortex-0.24.0 → codecortex-0.24.1}/src/codeintel/source_kind.py +0 -0
  85. {codecortex-0.24.0 → codecortex-0.24.1}/src/codeintel/term.py +0 -0
  86. {codecortex-0.24.0 → codecortex-0.24.1}/src/codeintel/verify.py +0 -0
  87. {codecortex-0.24.0 → codecortex-0.24.1}/src/codeintel/viewer/__init__.py +0 -0
  88. {codecortex-0.24.0 → codecortex-0.24.1}/src/codeintel/viewer/graph_template.html +0 -0
  89. {codecortex-0.24.0 → codecortex-0.24.1}/src/codeintel/wire_text.py +0 -0
  90. {codecortex-0.24.0 → codecortex-0.24.1}/tests/test_agent_prompt.py +0 -0
  91. {codecortex-0.24.0 → codecortex-0.24.1}/tests/test_backend_reaper.py +0 -0
  92. {codecortex-0.24.0 → codecortex-0.24.1}/tests/test_background_index_failure.py +0 -0
  93. {codecortex-0.24.0 → codecortex-0.24.1}/tests/test_bench_gate.py +0 -0
  94. {codecortex-0.24.0 → codecortex-0.24.1}/tests/test_bench_oracle.py +0 -0
  95. {codecortex-0.24.0 → codecortex-0.24.1}/tests/test_bench_oracle_ts.py +0 -0
  96. {codecortex-0.24.0 → codecortex-0.24.1}/tests/test_c4.py +0 -0
  97. {codecortex-0.24.0 → codecortex-0.24.1}/tests/test_c4_check.py +0 -0
  98. {codecortex-0.24.0 → codecortex-0.24.1}/tests/test_c4_layers.py +0 -0
  99. {codecortex-0.24.0 → codecortex-0.24.1}/tests/test_c4_model.py +0 -0
  100. {codecortex-0.24.0 → codecortex-0.24.1}/tests/test_c4_output.py +0 -0
  101. {codecortex-0.24.0 → codecortex-0.24.1}/tests/test_cache.py +0 -0
  102. {codecortex-0.24.0 → codecortex-0.24.1}/tests/test_chunking.py +0 -0
  103. {codecortex-0.24.0 → codecortex-0.24.1}/tests/test_cli_help.py +0 -0
  104. {codecortex-0.24.0 → codecortex-0.24.1}/tests/test_cli_install_dry_run.py +0 -0
  105. {codecortex-0.24.0 → codecortex-0.24.1}/tests/test_cli_install_offer_injection.py +0 -0
  106. {codecortex-0.24.0 → codecortex-0.24.1}/tests/test_cold_process.py +0 -0
  107. {codecortex-0.24.0 → codecortex-0.24.1}/tests/test_config.py +0 -0
  108. {codecortex-0.24.0 → codecortex-0.24.1}/tests/test_corpus.py +0 -0
  109. {codecortex-0.24.0 → codecortex-0.24.1}/tests/test_could_not_ask_semantic.py +0 -0
  110. {codecortex-0.24.0 → codecortex-0.24.1}/tests/test_docs_ci_claims.py +0 -0
  111. {codecortex-0.24.0 → codecortex-0.24.1}/tests/test_docs_deadcode_withdrawal.py +0 -0
  112. {codecortex-0.24.0 → codecortex-0.24.1}/tests/test_docs_trust_claims.py +0 -0
  113. {codecortex-0.24.0 → codecortex-0.24.1}/tests/test_doctor.py +0 -0
  114. {codecortex-0.24.0 → codecortex-0.24.1}/tests/test_e2e.py +0 -0
  115. {codecortex-0.24.0 → codecortex-0.24.1}/tests/test_edge_confidence.py +0 -0
  116. {codecortex-0.24.0 → codecortex-0.24.1}/tests/test_engine_adoption.py +0 -0
  117. {codecortex-0.24.0 → codecortex-0.24.1}/tests/test_enterprise.py +0 -0
  118. {codecortex-0.24.0 → codecortex-0.24.1}/tests/test_gateway.py +0 -0
  119. {codecortex-0.24.0 → codecortex-0.24.1}/tests/test_graph_failure_population.py +0 -0
  120. {codecortex-0.24.0 → codecortex-0.24.1}/tests/test_graph_provider.py +0 -0
  121. {codecortex-0.24.0 → codecortex-0.24.1}/tests/test_graph_real.py +0 -0
  122. {codecortex-0.24.0 → codecortex-0.24.1}/tests/test_graph_stdin.py +0 -0
  123. {codecortex-0.24.0 → codecortex-0.24.1}/tests/test_grapher.py +0 -0
  124. {codecortex-0.24.0 → codecortex-0.24.1}/tests/test_hard_exit.py +0 -0
  125. {codecortex-0.24.0 → codecortex-0.24.1}/tests/test_hardening.py +0 -0
  126. {codecortex-0.24.0 → codecortex-0.24.1}/tests/test_http_auth.py +0 -0
  127. {codecortex-0.24.0 → codecortex-0.24.1}/tests/test_http_server.py +0 -0
  128. {codecortex-0.24.0 → codecortex-0.24.1}/tests/test_incompleteness.py +0 -0
  129. {codecortex-0.24.0 → codecortex-0.24.1}/tests/test_index_failure_reason.py +0 -0
  130. {codecortex-0.24.0 → codecortex-0.24.1}/tests/test_index_incompleteness.py +0 -0
  131. {codecortex-0.24.0 → codecortex-0.24.1}/tests/test_index_progress.py +0 -0
  132. {codecortex-0.24.0 → codecortex-0.24.1}/tests/test_indexer_binary.py +0 -0
  133. {codecortex-0.24.0 → codecortex-0.24.1}/tests/test_installer.py +0 -0
  134. {codecortex-0.24.0 → codecortex-0.24.1}/tests/test_integration.py +0 -0
  135. {codecortex-0.24.0 → codecortex-0.24.1}/tests/test_lang_config.py +0 -0
  136. {codecortex-0.24.0 → codecortex-0.24.1}/tests/test_loc_census.py +0 -0
  137. {codecortex-0.24.0 → codecortex-0.24.1}/tests/test_lsp_language_coverage.py +0 -0
  138. {codecortex-0.24.0 → codecortex-0.24.1}/tests/test_lsp_provider.py +0 -0
  139. {codecortex-0.24.0 → codecortex-0.24.1}/tests/test_lsp_real.py +0 -0
  140. {codecortex-0.24.0 → codecortex-0.24.1}/tests/test_mapper.py +0 -0
  141. {codecortex-0.24.0 → codecortex-0.24.1}/tests/test_mcp_handshake.py +0 -0
  142. {codecortex-0.24.0 → codecortex-0.24.1}/tests/test_mcp_server.py +0 -0
  143. {codecortex-0.24.0 → codecortex-0.24.1}/tests/test_missing_target.py +0 -0
  144. {codecortex-0.24.0 → codecortex-0.24.1}/tests/test_model_dimension.py +0 -0
  145. {codecortex-0.24.0 → codecortex-0.24.1}/tests/test_never_raise.py +0 -0
  146. {codecortex-0.24.0 → codecortex-0.24.1}/tests/test_null_reason_and_ripple.py +0 -0
  147. {codecortex-0.24.0 → codecortex-0.24.1}/tests/test_onboarding.py +0 -0
  148. {codecortex-0.24.0 → codecortex-0.24.1}/tests/test_per_thread.py +0 -0
  149. {codecortex-0.24.0 → codecortex-0.24.1}/tests/test_query_budget.py +0 -0
  150. {codecortex-0.24.0 → codecortex-0.24.1}/tests/test_rbac.py +0 -0
  151. {codecortex-0.24.0 → codecortex-0.24.1}/tests/test_redaction_boundary.py +0 -0
  152. {codecortex-0.24.0 → codecortex-0.24.1}/tests/test_reindexer.py +0 -0
  153. {codecortex-0.24.0 → codecortex-0.24.1}/tests/test_rerank.py +0 -0
  154. {codecortex-0.24.0 → codecortex-0.24.1}/tests/test_reset.py +0 -0
  155. {codecortex-0.24.0 → codecortex-0.24.1}/tests/test_semantic_provider.py +0 -0
  156. {codecortex-0.24.0 → codecortex-0.24.1}/tests/test_source_kind.py +0 -0
  157. {codecortex-0.24.0 → codecortex-0.24.1}/tests/test_staleness.py +0 -0
  158. {codecortex-0.24.0 → codecortex-0.24.1}/tests/test_summary_integrity.py +0 -0
  159. {codecortex-0.24.0 → codecortex-0.24.1}/tests/test_symbol_context.py +0 -0
  160. {codecortex-0.24.0 → codecortex-0.24.1}/tests/test_term.py +0 -0
  161. {codecortex-0.24.0 → codecortex-0.24.1}/tests/test_treesitter.py +0 -0
  162. {codecortex-0.24.0 → codecortex-0.24.1}/tests/test_uninstall.py +0 -0
  163. {codecortex-0.24.0 → codecortex-0.24.1}/tests/test_upgrade_path.py +0 -0
  164. {codecortex-0.24.0 → codecortex-0.24.1}/tests/test_verify.py +0 -0
  165. {codecortex-0.24.0 → codecortex-0.24.1}/tests/test_verify_call.py +0 -0
  166. {codecortex-0.24.0 → codecortex-0.24.1}/tests/test_version_skew.py +0 -0
  167. {codecortex-0.24.0 → codecortex-0.24.1}/tests/test_wire_text.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codecortex
3
- Version: 0.24.0
3
+ Version: 0.24.1
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.24.0
3
+ Version: 0.24.1
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.24.1"
@@ -6,6 +6,11 @@ from typing import Any
6
6
 
7
7
  from codeintel.commands._common import resolve_root
8
8
 
9
+ # How long a foreground `codeintel index` waits for another process's pass over the same repository
10
+ # before indexing anyway. Long enough to outlast a cold index of a large repo (measured at ~10 min
11
+ # for 30k chunks, docs/benchmarks.md); short enough that a hung holder cannot block it forever.
12
+ _LOCK_WAIT_S = 900.0
13
+
9
14
 
10
15
  class _ProgressLogBridge(logging.Handler):
11
16
  """Routes WARNING+ log records from the indexer to a live progress counter's ``notice()``, so a
@@ -52,6 +57,35 @@ def run(args: Any) -> int:
52
57
  quiet = getattr(args, "quiet", False)
53
58
  counter = None if quiet else term.LiveCounter(console)
54
59
 
60
+ # Take the same per-repository lock the background reindexer takes, so a terminal `index` and
61
+ # a running server's background pass do not embed the whole repository twice. It covers the
62
+ # semantic AND graph passes, as the background pass does; the map refresh is not indexing.
63
+ import contextlib
64
+
65
+ from codeintel.filelock import exclusive_waiting, lock_path
66
+
67
+ def _say_waiting() -> None:
68
+ if not quiet: # --quiet promises the result line only, contention or not
69
+ print("waiting for another codeintel process to finish indexing this repository…",
70
+ flush=True)
71
+
72
+ lock = contextlib.ExitStack()
73
+ # Resolving the lock path reads the codeintel home, which raises where there is no resolvable
74
+ # home directory (a container UID with no passwd entry and no CODEINTEL_HOME). That must not
75
+ # turn into a traceback before the semantic pass's own error handling below — which reports
76
+ # exactly that failure as "index failed: …". A lock we cannot place is a lock we do without,
77
+ # by the rule in `filelock`: it may dedupe work, never stop it.
78
+ try:
79
+ path = lock_path(os.path.realpath(project_root))
80
+ except Exception:
81
+ path = None
82
+ if path is not None:
83
+ got_lock = lock.enter_context(
84
+ exclusive_waiting(path, _LOCK_WAIT_S, on_wait=_say_waiting))
85
+ if not got_lock and not quiet:
86
+ print(f"still locked after {int(_LOCK_WAIT_S)}s — indexing anyway; the other pass "
87
+ "may repeat some of this work", flush=True)
88
+
55
89
  failed = False
56
90
  # Wrap the whole semantic pass so a setup failure (e.g. an unresolvable home dir →
57
91
  # Path.home() raising) degrades with a message, like every other subcommand, not a traceback.
@@ -146,6 +180,8 @@ def run(args: Any) -> int:
146
180
  if beat:
147
181
  beat.stop("warn", "skipped")
148
182
 
183
+ lock.close()
184
+
149
185
  # best-effort map refresh after index (fast, kept silent — not worth a checklist row)
150
186
  try:
151
187
  from codeintel.mapper import MapGenerator
@@ -33,7 +33,8 @@ import logging
33
33
  import os
34
34
  import pathlib
35
35
  import re
36
- from collections.abc import Iterator
36
+ import time
37
+ from collections.abc import Callable, Iterator
37
38
 
38
39
  logger = logging.getLogger(__name__)
39
40
 
@@ -99,6 +100,45 @@ def exclusive(path: pathlib.Path) -> Iterator[bool]:
99
100
  os.close(fd) # closing would release it anyway; explicit is clearer
100
101
 
101
102
 
103
+ @contextlib.contextmanager
104
+ def exclusive_waiting(
105
+ path: pathlib.Path,
106
+ timeout_s: float,
107
+ *,
108
+ poll_s: float = 0.25,
109
+ on_wait: Callable[[], None] | None = None,
110
+ ) -> Iterator[bool]:
111
+ """Like `exclusive`, but WAIT up to *timeout_s* for another holder to finish.
112
+
113
+ For work a person asked for. A background pass that loses the race skips, because nobody asked
114
+ for it and the holder is already doing it. A foreground `codeintel index` cannot skip: the user
115
+ asked for an index, and "somebody else is probably doing it" is not one. So it waits, and once
116
+ the holder finishes, its own pass is incremental and mostly finds nothing new.
117
+
118
+ Yields True once the lock is held, and False when *timeout_s* passed with it still held — in
119
+ which case the caller proceeds WITHOUT the lock, by the module's rule that a lock may dedupe work
120
+ but never stop it. A holder that is alive but hung must not block indexing forever.
121
+
122
+ *on_wait* runs once, the first time the lock is found held, so a caller can say why it has
123
+ stopped moving. Without `fcntl` there is nothing to wait on and this yields True at once.
124
+ """
125
+ deadline = time.monotonic() + max(0.0, timeout_s)
126
+ announced = False
127
+ while True:
128
+ with exclusive(path) as got:
129
+ if got:
130
+ yield True
131
+ return
132
+ if not announced and on_wait is not None:
133
+ announced = True
134
+ with contextlib.suppress(Exception):
135
+ on_wait()
136
+ if time.monotonic() >= deadline:
137
+ yield False
138
+ return
139
+ time.sleep(poll_s)
140
+
141
+
102
142
  def held_by_another_process(path: pathlib.Path) -> bool:
103
143
  """True when some other live process holds this lock.
104
144
 
@@ -144,3 +144,23 @@ def test_the_typed_arm_scores_only_the_class_qualified_targets():
144
144
  f"{target} does not look class-qualified; this arm scores only that shape"
145
145
  )
146
146
  assert os.path.exists(os.path.join(_TYPED, path)), path
147
+
148
+
149
+ def test_both_corpora_give_serena_the_same_project_config():
150
+ """The serena config is part of what the language server sees, so it is part of the control.
151
+
152
+ It also stops the churn this repository kept seeing. A newer serena upgrades every REGISTERED
153
+ project's `project.yml` to its current template when it starts — and codeintel launching serena
154
+ with `--project <fixture>` registers the fixtures. `corpus_ts_typed` was generated by that
155
+ newer serena and never drifted; `corpus_ts` held an older template and was rewritten at the
156
+ start of every session. Keeping the two identical (apart from the name) keeps them both on the
157
+ template the other one already proved stable, and makes a future re-template show up here as
158
+ one file differing from its twin rather than as an unexplained diff."""
159
+ def config(root: str, name: str) -> str:
160
+ with open(os.path.join(root, ".serena", "project.yml"), encoding="utf-8") as fh:
161
+ text = fh.read()
162
+ assert f'project_name: "{name}"' in text, f"{root}: project_name is not {name!r}"
163
+ return text.replace(f'project_name: "{name}"', 'project_name: "<name>"')
164
+
165
+ assert config(_TYPED, "corpus_ts_typed") == config(_UNTYPED, "corpus_ts"), (
166
+ "the two fixtures' .serena/project.yml differ beyond project_name — copy one over the other")
@@ -12,6 +12,7 @@ commands, or a command dispatches to nothing.
12
12
  from __future__ import annotations
13
13
 
14
14
  import argparse
15
+ import os
15
16
  import string
16
17
  import subprocess
17
18
  import sys
@@ -930,3 +931,92 @@ def test_query_json_stays_parseable_when_the_query_itself_fails(monkeypatch, cap
930
931
  payload = _json.loads(capsys.readouterr().out) # must not raise
931
932
  assert payload["result"] is None
932
933
  assert "backend exploded" in payload["reason"]
934
+
935
+
936
+ # --------------------------------------------------------------------------- index lock
937
+
938
+ def test_index_holds_the_reindex_lock_for_its_whole_pass(monkeypatch, tmp_path):
939
+ """A server's background reindex skips while this is held, so the repository is embedded once.
940
+ Checked from inside the pass, because holding it "somewhere around" the pass is not enough."""
941
+ from codeintel.filelock import _HAVE_FLOCK, held_by_another_process, lock_path
942
+ if not _HAVE_FLOCK:
943
+ pytest.skip("advisory locking needs POSIX fcntl")
944
+ seen = _stub_index(monkeypatch, tmp_path, count=1, has_graph_backend=True)
945
+ path = lock_path(os.path.realpath(str(tmp_path)))
946
+ held: dict[str, bool] = {}
947
+
948
+ import codeintel.indexer as indexer_mod
949
+ base = indexer_mod.Indexer
950
+
951
+ class _Checking(base): # type: ignore[misc, valid-type]
952
+ def index(self, root):
953
+ held["semantic"] = held_by_another_process(path)
954
+ return super().index(root)
955
+
956
+ monkeypatch.setattr("codeintel.indexer.Indexer", _Checking)
957
+ import codeintel.reindexer as reindexer_mod
958
+ graph_base = reindexer_mod.Reindexer
959
+
960
+ class _CheckingGraph(graph_base): # type: ignore[misc, valid-type]
961
+ def _graph_reindex(self, root):
962
+ held["graph"] = held_by_another_process(path)
963
+ super()._graph_reindex(root)
964
+
965
+ monkeypatch.setattr("codeintel.reindexer.Reindexer", _CheckingGraph)
966
+
967
+ assert import_module("codeintel.commands.index").run(_args(project_root=str(tmp_path))) == 0
968
+ assert held == {"semantic": True, "graph": True}, held
969
+ assert seen["reindexed"]
970
+ assert not held_by_another_process(path), "the lock outlived the command"
971
+
972
+
973
+ def test_index_waits_for_another_pass_then_indexes_anyway_on_timeout(monkeypatch, tmp_path, capsys):
974
+ """Held by someone else: say so, wait, and — past the limit — index anyway. Never skip: the
975
+ user asked for an index, and a hung holder must not be able to stop one."""
976
+ from codeintel.filelock import _HAVE_FLOCK, exclusive, lock_path
977
+ if not _HAVE_FLOCK:
978
+ pytest.skip("advisory locking needs POSIX fcntl")
979
+ _stub_index(monkeypatch, tmp_path, count=7)
980
+ mod = import_module("codeintel.commands.index")
981
+ monkeypatch.setattr(mod, "_LOCK_WAIT_S", 0.3)
982
+
983
+ with exclusive(lock_path(os.path.realpath(str(tmp_path)))) as other:
984
+ assert other
985
+ assert mod.run(_args(project_root=str(tmp_path))) == 0
986
+
987
+ out = capsys.readouterr().out
988
+ assert "waiting for another codeintel process" in out
989
+ assert "indexing anyway" in out
990
+ assert "Indexed 7 chunks" in out
991
+
992
+
993
+ def test_index_quiet_stays_quiet_while_waiting_for_the_lock(monkeypatch, tmp_path, capsys):
994
+ """`--quiet` promises the result line only. Contention is not an exception to that."""
995
+ from codeintel.filelock import _HAVE_FLOCK, exclusive, lock_path
996
+ if not _HAVE_FLOCK:
997
+ pytest.skip("advisory locking needs POSIX fcntl")
998
+ _stub_index(monkeypatch, tmp_path, count=2)
999
+ mod = import_module("codeintel.commands.index")
1000
+ monkeypatch.setattr(mod, "_LOCK_WAIT_S", 0.2)
1001
+
1002
+ with exclusive(lock_path(os.path.realpath(str(tmp_path)))) as other:
1003
+ assert other
1004
+ assert mod.run(_args(project_root=str(tmp_path), quiet=True)) == 0
1005
+
1006
+ assert capsys.readouterr().out.strip() == "Indexed 2 chunks"
1007
+
1008
+
1009
+ def test_index_without_a_resolvable_home_fails_with_a_message_not_a_traceback(
1010
+ monkeypatch, tmp_path, capsys):
1011
+ """A container UID with no home: the lock is skipped, and the failure the semantic pass hits
1012
+ is reported the way it always was — `index failed: …` and exit 1."""
1013
+ _stub_index(monkeypatch, tmp_path)
1014
+
1015
+ def _no_home(*a, **k):
1016
+ raise RuntimeError("could not determine home directory")
1017
+
1018
+ monkeypatch.setattr("codeintel.filelock.lock_path", _no_home)
1019
+ monkeypatch.setattr("codeintel.config.load_config", _no_home)
1020
+
1021
+ assert import_module("codeintel.commands.index").run(_args(project_root=str(tmp_path))) == 1
1022
+ assert "index failed: could not determine home directory" in capsys.readouterr().out
@@ -25,10 +25,18 @@ import pathlib
25
25
  import subprocess
26
26
  import sys
27
27
  import textwrap
28
+ import threading
29
+ import time
28
30
 
29
31
  import pytest
30
32
 
31
- from codeintel.filelock import _HAVE_FLOCK, exclusive, held_by_another_process, lock_path
33
+ from codeintel.filelock import (
34
+ _HAVE_FLOCK,
35
+ exclusive,
36
+ exclusive_waiting,
37
+ held_by_another_process,
38
+ lock_path,
39
+ )
32
40
  from codeintel.reindexer import Reindexer
33
41
 
34
42
  pytestmark = pytest.mark.skipif(not _HAVE_FLOCK, reason="advisory locking needs POSIX fcntl")
@@ -207,3 +215,53 @@ def test_a_probe_that_fails_does_not_claim_a_reindex_is_running(home, monkeypatc
207
215
  monkeypatch.setattr(fl, "exclusive", lambda _p: (_ for _ in ()).throw(OSError("nope")))
208
216
 
209
217
  assert fl.held_by_another_process(path) is False
218
+
219
+
220
+ # ══════════════════════════════════════════════════════════════════════════════════════════════
221
+ # Waiting — for work a person asked for
222
+ # ══════════════════════════════════════════════════════════════════════════════════════════════
223
+
224
+ def test_a_free_lock_is_taken_at_once_without_announcing_a_wait(home):
225
+ waits: list[int] = []
226
+ with exclusive_waiting(lock_path("/repo/one"), 5, on_wait=lambda: waits.append(1)) as got:
227
+ assert got is True
228
+ assert waits == []
229
+
230
+
231
+ def test_a_held_lock_is_waited_for_and_then_taken(home):
232
+ """The foreground case: another pass is running, so wait for it rather than skip or duplicate."""
233
+ path = lock_path("/repo/one")
234
+ waits: list[int] = []
235
+ holding, release = threading.Event(), threading.Event()
236
+
237
+ def holder() -> None:
238
+ with exclusive(path) as got:
239
+ assert got
240
+ holding.set()
241
+ release.wait(5)
242
+
243
+ t = threading.Thread(target=holder)
244
+ t.start()
245
+ holding.wait(5)
246
+ threading.Timer(0.4, release.set).start()
247
+
248
+ started = time.monotonic()
249
+ with exclusive_waiting(path, 10, poll_s=0.05, on_wait=lambda: waits.append(1)) as got:
250
+ waited = time.monotonic() - started
251
+ assert got is True
252
+ assert held_by_another_process(path), "yielded True without actually holding the lock"
253
+ t.join(5)
254
+
255
+ assert waits == [1], "the wait must be announced exactly once"
256
+ assert waited >= 0.3, f"took the lock after {waited:.2f}s while it was still held"
257
+
258
+
259
+ def test_a_lock_held_past_the_timeout_yields_false_so_the_work_still_happens(home):
260
+ """A hung holder must not block indexing forever: time out, say so, proceed."""
261
+ path = lock_path("/repo/one")
262
+ waits: list[int] = []
263
+ with exclusive(path) as first:
264
+ assert first
265
+ with exclusive_waiting(path, 0.2, poll_s=0.05, on_wait=lambda: waits.append(1)) as got:
266
+ assert got is False
267
+ assert waits == [1]
@@ -1 +0,0 @@
1
- __version__ = "0.24.0"
File without changes
File without changes
File without changes
File without changes