java-functional-lsp 0.9.10__tar.gz → 0.9.11__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 (69) hide show
  1. {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.11}/PKG-INFO +1 -1
  2. {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.11}/pyproject.toml +1 -1
  3. {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.11}/src/java_functional_lsp/__init__.py +1 -1
  4. {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.11}/src/java_functional_lsp/proxy.py +92 -27
  5. {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.11}/tests/test_proxy.py +52 -0
  6. {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.11}/tests/test_server.py +34 -13
  7. {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.11}/.claude-plugin/plugin.json +0 -0
  8. {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.11}/.githooks/pre-commit +0 -0
  9. {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.11}/.githooks/pre-push +0 -0
  10. {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.11}/.github/CODEOWNERS +0 -0
  11. {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.11}/.github/ISSUE_TEMPLATE/bug-report.md +0 -0
  12. {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.11}/.github/ISSUE_TEMPLATE/feature-request.md +0 -0
  13. {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.11}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  14. {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.11}/.github/SECURITY.md +0 -0
  15. {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.11}/.github/dependabot.yml +0 -0
  16. {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.11}/.github/release-drafter.yml +0 -0
  17. {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.11}/.github/workflows/publish.yml +0 -0
  18. {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.11}/.github/workflows/release-drafter.yml +0 -0
  19. {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.11}/.github/workflows/stale.yml +0 -0
  20. {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.11}/.github/workflows/test.yml +0 -0
  21. {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.11}/.github/workflows/update-homebrew.yml +0 -0
  22. {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.11}/.github/workflows/vscode-ext.yml +0 -0
  23. {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.11}/.gitignore +0 -0
  24. {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.11}/CONTRIBUTING.md +0 -0
  25. {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.11}/LICENSE +0 -0
  26. {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.11}/README.md +0 -0
  27. {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.11}/SKILL.md +0 -0
  28. {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.11}/commands/lint-java.md +0 -0
  29. {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.11}/editors/intellij/README.md +0 -0
  30. {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.11}/editors/intellij/lsp4ij-template.json +0 -0
  31. {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.11}/editors/vscode/.vscodeignore +0 -0
  32. {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.11}/editors/vscode/README.md +0 -0
  33. {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.11}/editors/vscode/package-lock.json +0 -0
  34. {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.11}/editors/vscode/package.json +0 -0
  35. {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.11}/editors/vscode/src/extension.ts +0 -0
  36. {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.11}/editors/vscode/tsconfig.json +0 -0
  37. {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.11}/hooks/hooks.json +0 -0
  38. {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.11}/hooks/java_linter_reminder.py +0 -0
  39. {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.11}/scripts/ensure-lsp.sh +0 -0
  40. {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.11}/scripts/generate-formula.py +0 -0
  41. {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.11}/src/java_functional_lsp/__main__.py +0 -0
  42. {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.11}/src/java_functional_lsp/analyzers/__init__.py +0 -0
  43. {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.11}/src/java_functional_lsp/analyzers/base.py +0 -0
  44. {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.11}/src/java_functional_lsp/analyzers/exception_checker.py +0 -0
  45. {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.11}/src/java_functional_lsp/analyzers/functional_checker.py +0 -0
  46. {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.11}/src/java_functional_lsp/analyzers/mutation_checker.py +0 -0
  47. {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.11}/src/java_functional_lsp/analyzers/null_checker.py +0 -0
  48. {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.11}/src/java_functional_lsp/analyzers/spring_checker.py +0 -0
  49. {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.11}/src/java_functional_lsp/cli.py +0 -0
  50. {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.11}/src/java_functional_lsp/fixes.py +0 -0
  51. {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.11}/src/java_functional_lsp/merkle.py +0 -0
  52. {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.11}/src/java_functional_lsp/server.py +0 -0
  53. {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.11}/tests/__init__.py +0 -0
  54. {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.11}/tests/conftest.py +0 -0
  55. {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.11}/tests/test_base.py +0 -0
  56. {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.11}/tests/test_cli.py +0 -0
  57. {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.11}/tests/test_config.py +0 -0
  58. {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.11}/tests/test_e2e.py +0 -0
  59. {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.11}/tests/test_e2e_jdtls.py +0 -0
  60. {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.11}/tests/test_exception_checker.py +0 -0
  61. {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.11}/tests/test_fixes.py +0 -0
  62. {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.11}/tests/test_functional_checker.py +0 -0
  63. {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.11}/tests/test_merkle.py +0 -0
  64. {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.11}/tests/test_merkle_proxy.py +0 -0
  65. {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.11}/tests/test_mutation_checker.py +0 -0
  66. {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.11}/tests/test_null_checker.py +0 -0
  67. {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.11}/tests/test_spring_checker.py +0 -0
  68. {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.11}/tests/test_suppress.py +0 -0
  69. {java_functional_lsp-0.9.10 → java_functional_lsp-0.9.11}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: java-functional-lsp
3
- Version: 0.9.10
3
+ Version: 0.9.11
4
4
  Summary: Java LSP server enforcing functional programming best practices — null safety, immutability, no exceptions
5
5
  Project-URL: Homepage, https://github.com/aviadshiber/java-functional-lsp
6
6
  Project-URL: Repository, https://github.com/aviadshiber/java-functional-lsp
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "java-functional-lsp"
7
- version = "0.9.10"
7
+ version = "0.9.11"
8
8
  description = "Java LSP server enforcing functional programming best practices — null safety, immutability, no exceptions"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -1,3 +1,3 @@
1
1
  """java-functional-lsp: A Java LSP server enforcing functional programming best practices."""
2
2
 
3
- __version__ = "0.9.10"
3
+ __version__ = "0.9.11"
@@ -30,6 +30,27 @@ DEFAULT_JVM_MAX_HEAP = "4g"
30
30
  _STDERR_LINE_MAX = 1000
31
31
  _MAX_EXPANDED_GROUPS: int = 5 # hard cap on concurrent Maven group workspace folders (prevents jdtls OOM)
32
32
 
33
+ # Default jdtls initialization settings. Sent via initializationOptions.settings
34
+ # so they apply BEFORE the Maven import scan (didChangeConfiguration is too late).
35
+ # Users can override via .java-functional-lsp.json: {"jdtls": {"settings": {...}}}
36
+ _DEFAULT_JDTLS_SETTINGS: dict[str, Any] = {
37
+ "java": {
38
+ "import": {
39
+ "maven": {"enabled": True},
40
+ "gradle": {"enabled": False},
41
+ "exclusions": [
42
+ "**/node_modules/**",
43
+ "**/.metadata/**",
44
+ "**/archetype-resources/**",
45
+ "**/META-INF/maven/**",
46
+ "**/target/**",
47
+ ],
48
+ },
49
+ "configuration": {"updateBuildConfiguration": "automatic"},
50
+ "maven": {"downloadSources": False},
51
+ }
52
+ }
53
+
33
54
  #: Minimum Java major version required by jdtls 1.57+. Anything below this
34
55
  #: is rejected by the jdtls Python launcher with ``"jdtls requires at least
35
56
  #: Java 21"`` before the server even starts.
@@ -558,25 +579,43 @@ def _wipe_data_dir(data_dir: Path) -> None:
558
579
  logger.info("jdtls: wiped data-dir %s after init timeout", _redact_path(str(data_dir)))
559
580
 
560
581
 
561
- def _clear_cache_on_version_change(cache_root: Path) -> None:
562
- """Clear jdtls data cache when the server version changes.
582
+ def _compute_cache_marker() -> str:
583
+ """Compute a stable cache marker from jdtls version path + Java version.
563
584
 
564
- Stores a ``.version`` marker in the cache root. On version mismatch
565
- (e.g., Lombok agent added, jdtls config changed), wipes the entire
566
- cache so jdtls re-indexes from scratch with the current configuration.
585
+ Our Python package version changes (pip upgrades) do NOT affect the jdtls
586
+ Eclipse workspace index — only jdtls binary upgrades or Java version
587
+ changes do. Resolves through symlinks so Homebrew Cellar version changes
588
+ (e.g. ``/opt/homebrew/Cellar/jdtls/1.58.0/...``) are detected even when
589
+ the wrapper script at ``/opt/homebrew/bin/jdtls`` doesn't change.
567
590
  """
568
- from . import __version__
591
+ parts: list[str] = []
592
+ jdtls_path = shutil.which("jdtls")
593
+ if jdtls_path:
594
+ resolved = str(Path(jdtls_path).resolve())
595
+ parts.append(hashlib.sha256(resolved.encode()).hexdigest()[:12])
596
+ parts.append(str(_MIN_JDTLS_JAVA_MAJOR))
597
+ return "|".join(parts)
598
+
599
+
600
+ def _clear_cache_on_version_change(cache_root: Path) -> None:
601
+ """Clear jdtls data cache when the jdtls installation changes.
569
602
 
603
+ Stores a ``.version`` marker in the cache root. The marker is derived
604
+ from the resolved jdtls binary path + Java version requirement — NOT
605
+ from our Python ``__version__``. This preserves warm caches across
606
+ java-functional-lsp upgrades (which don't affect the Eclipse index).
607
+ """
608
+ current_marker = _compute_cache_marker()
570
609
  marker = cache_root / ".version"
571
610
  try:
572
- if marker.exists() and marker.read_text().strip() == __version__:
611
+ if marker.exists() and marker.read_text().strip() == current_marker:
573
612
  return
574
- # Version changed or first run — clear stale caches.
613
+ # jdtls or Java version changed, or first run — clear stale caches.
575
614
  if cache_root.exists():
576
615
  shutil.rmtree(cache_root)
577
- logger.info("jdtls: cleared stale cache (version changed to %s)", __version__)
616
+ logger.info("jdtls: cleared stale cache (marker changed to %s)", current_marker)
578
617
  cache_root.mkdir(parents=True, exist_ok=True)
579
- marker.write_text(__version__)
618
+ marker.write_text(current_marker)
580
619
  except OSError as e:
581
620
  logger.warning("jdtls: failed to manage cache version marker: %s", e)
582
621
 
@@ -622,6 +661,48 @@ def _find_lombok_jar(config: Mapping[str, Any] | None = None) -> str | None:
622
661
  return None
623
662
 
624
663
 
664
+ def _build_effective_params(
665
+ init_params: dict[str, Any],
666
+ module_root_uri: str | None,
667
+ original_root: str,
668
+ config: Mapping[str, Any] | None,
669
+ ) -> dict[str, Any]:
670
+ """Build the ``initialize`` request params for jdtls.
671
+
672
+ Deep-copies *init_params*, scopes to *module_root_uri* if provided,
673
+ injects workspaceFolders capability, and injects Maven/Gradle settings
674
+ from *config* (or defaults) into ``initializationOptions.settings``.
675
+ """
676
+ from pygls.uris import to_fs_path
677
+
678
+ effective_params = copy.deepcopy(init_params)
679
+ if module_root_uri:
680
+ effective_params["rootUri"] = module_root_uri
681
+ effective_params["rootPath"] = to_fs_path(module_root_uri)
682
+ logger.info(
683
+ "jdtls: scoping to module %s (full root: %s)",
684
+ _redact_path(module_root_uri),
685
+ _redact_path(original_root),
686
+ )
687
+
688
+ # Inject workspaceFolders capability for later expansion.
689
+ caps = effective_params.setdefault("capabilities", {})
690
+ ws = caps.setdefault("workspace", {})
691
+ ws["workspaceFolders"] = True
692
+
693
+ # Inject jdtls Maven/Gradle settings into initializationOptions.
694
+ # Must be in initializationOptions (not didChangeConfiguration) so they
695
+ # apply before the Maven import scan starts.
696
+ jdtls_settings = copy.deepcopy(_DEFAULT_JDTLS_SETTINGS)
697
+ if config and "jdtls" in config and "settings" in config["jdtls"]:
698
+ jdtls_settings = copy.deepcopy(config["jdtls"]["settings"])
699
+ init_opts = effective_params.setdefault("initializationOptions", {})
700
+ init_opts["settings"] = jdtls_settings
701
+ logger.debug("jdtls: initializationOptions.settings = %s", jdtls_settings)
702
+
703
+ return effective_params
704
+
705
+
625
706
  class JdtlsProxy:
626
707
  """Manages a jdtls subprocess and provides async request/notification forwarding."""
627
708
 
@@ -724,23 +805,7 @@ class JdtlsProxy:
724
805
  data_dir = cache_root / data_hash
725
806
  data_dir.mkdir(parents=True, exist_ok=True)
726
807
 
727
- # Deep copy to avoid mutating server._init_params.
728
- effective_params = copy.deepcopy(init_params)
729
- if module_root_uri:
730
- effective_params["rootUri"] = module_root_uri
731
- from pygls.uris import to_fs_path
732
-
733
- effective_params["rootPath"] = to_fs_path(module_root_uri)
734
- logger.info(
735
- "jdtls: scoping to module %s (full root: %s)",
736
- _redact_path(module_root_uri),
737
- _redact_path(original_root),
738
- )
739
-
740
- # Inject workspaceFolders capability for later expansion.
741
- caps = effective_params.setdefault("capabilities", {})
742
- ws = caps.setdefault("workspace", {})
743
- ws["workspaceFolders"] = True
808
+ effective_params = _build_effective_params(init_params, module_root_uri, original_root, config)
744
809
 
745
810
  jdtls_cmd: list[str] = [
746
811
  jdtls_path,
@@ -1417,6 +1417,58 @@ class TestLazyStart:
1417
1417
  # ---------------------------------------------------------------------------
1418
1418
 
1419
1419
 
1420
+ class TestBuildEffectiveParams:
1421
+ """Tests for _build_effective_params."""
1422
+
1423
+ def test_defaults_injected_when_no_config(self) -> None:
1424
+ from java_functional_lsp.proxy import _build_effective_params
1425
+
1426
+ result = _build_effective_params({"rootUri": "file:///ws"}, None, "file:///ws", None)
1427
+ settings = result["initializationOptions"]["settings"]
1428
+ assert settings["java"]["import"]["maven"]["enabled"] is True
1429
+ assert settings["java"]["import"]["gradle"]["enabled"] is False
1430
+ assert "**/target/**" in settings["java"]["import"]["exclusions"]
1431
+
1432
+ def test_config_override_replaces_defaults(self) -> None:
1433
+ from java_functional_lsp.proxy import _build_effective_params
1434
+
1435
+ custom = {"java": {"custom": "value"}}
1436
+ config = {"jdtls": {"settings": custom}}
1437
+ result = _build_effective_params({"rootUri": "file:///ws"}, None, "file:///ws", config)
1438
+ settings = result["initializationOptions"]["settings"]
1439
+ assert settings == {"java": {"custom": "value"}}
1440
+ assert "import" not in settings.get("java", {})
1441
+
1442
+ def test_does_not_mutate_original_init_params(self) -> None:
1443
+ from java_functional_lsp.proxy import _build_effective_params
1444
+
1445
+ original = {"rootUri": "file:///ws", "capabilities": {}}
1446
+ _build_effective_params(original, "file:///mod", "file:///ws", None)
1447
+ assert "initializationOptions" not in original
1448
+ assert original["rootUri"] == "file:///ws"
1449
+
1450
+ def test_module_root_uri_scopes_root(self) -> None:
1451
+ from java_functional_lsp.proxy import _build_effective_params
1452
+
1453
+ result = _build_effective_params({"rootUri": "file:///ws"}, "file:///mod", "file:///ws", None)
1454
+ assert result["rootUri"] == "file:///mod"
1455
+
1456
+ def test_workspace_folders_capability_always_set(self) -> None:
1457
+ from java_functional_lsp.proxy import _build_effective_params
1458
+
1459
+ result = _build_effective_params({"rootUri": "file:///ws"}, None, "file:///ws", None)
1460
+ assert result["capabilities"]["workspace"]["workspaceFolders"] is True
1461
+
1462
+ def test_deepcopy_prevents_default_mutation(self) -> None:
1463
+ from java_functional_lsp.proxy import _DEFAULT_JDTLS_SETTINGS, _build_effective_params
1464
+
1465
+ result = _build_effective_params({"rootUri": "file:///ws"}, None, "file:///ws", None)
1466
+ # Mutate the returned settings
1467
+ result["initializationOptions"]["settings"]["java"]["import"]["exclusions"].append("MUTATED")
1468
+ # Module-level defaults must be unchanged
1469
+ assert "MUTATED" not in _DEFAULT_JDTLS_SETTINGS["java"]["import"]["exclusions"]
1470
+
1471
+
1420
1472
  class TestWipeDataDir:
1421
1473
  def test_wipe_removes_directory(self, tmp_path: Path) -> None:
1422
1474
  """Directory is removed and info is logged."""
@@ -1548,48 +1548,69 @@ class TestFindLombokJar:
1548
1548
 
1549
1549
 
1550
1550
  class TestCacheClear:
1551
- """Tests for _clear_cache_on_version_change()."""
1551
+ """Tests for _clear_cache_on_version_change() and _compute_cache_marker()."""
1552
1552
 
1553
1553
  def test_clears_on_version_mismatch(self, tmp_path: Any) -> None:
1554
- from java_functional_lsp.proxy import _clear_cache_on_version_change
1554
+ from java_functional_lsp.proxy import _clear_cache_on_version_change, _compute_cache_marker
1555
1555
 
1556
1556
  cache = tmp_path / "jdtls-data"
1557
1557
  cache.mkdir()
1558
1558
  stale_dir = cache / "abc123"
1559
1559
  stale_dir.mkdir()
1560
- (cache / ".version").write_text("0.7.9")
1560
+ (cache / ".version").write_text("old-stale-marker")
1561
1561
 
1562
1562
  _clear_cache_on_version_change(cache)
1563
1563
 
1564
1564
  assert not stale_dir.exists(), "Stale data-dir should be cleared"
1565
1565
  assert cache.exists(), "Cache root should be recreated"
1566
- from java_functional_lsp import __version__
1566
+ assert (cache / ".version").read_text() == _compute_cache_marker()
1567
1567
 
1568
- assert (cache / ".version").read_text() == __version__
1569
-
1570
- def test_keeps_cache_on_same_version(self, tmp_path: Any) -> None:
1571
- from java_functional_lsp import __version__
1572
- from java_functional_lsp.proxy import _clear_cache_on_version_change
1568
+ def test_keeps_cache_on_same_marker(self, tmp_path: Any) -> None:
1569
+ from java_functional_lsp.proxy import _clear_cache_on_version_change, _compute_cache_marker
1573
1570
 
1574
1571
  cache = tmp_path / "jdtls-data"
1575
1572
  cache.mkdir()
1576
1573
  existing_dir = cache / "abc123"
1577
1574
  existing_dir.mkdir()
1578
- (cache / ".version").write_text(__version__)
1575
+ (cache / ".version").write_text(_compute_cache_marker())
1579
1576
 
1580
1577
  _clear_cache_on_version_change(cache)
1581
1578
 
1582
1579
  assert existing_dir.exists(), "Existing data-dir should be preserved"
1583
1580
 
1584
1581
  def test_creates_marker_on_first_run(self, tmp_path: Any) -> None:
1585
- from java_functional_lsp.proxy import _clear_cache_on_version_change
1582
+ from java_functional_lsp.proxy import _clear_cache_on_version_change, _compute_cache_marker
1586
1583
 
1587
1584
  cache = tmp_path / "jdtls-data"
1588
1585
  _clear_cache_on_version_change(cache)
1589
1586
 
1590
- from java_functional_lsp import __version__
1587
+ assert (cache / ".version").read_text() == _compute_cache_marker()
1588
+
1589
+ def test_python_version_bump_preserves_cache(self) -> None:
1590
+ """Our __version__ is NOT in the cache marker — pip upgrades don't wipe."""
1591
+ from unittest.mock import patch
1592
+
1593
+ from java_functional_lsp.proxy import _compute_cache_marker
1594
+
1595
+ marker_before = _compute_cache_marker()
1596
+ with patch("java_functional_lsp.__version__", "99.99.99"):
1597
+ marker_after = _compute_cache_marker()
1598
+ assert marker_before == marker_after, "Changing __version__ must not change the cache marker"
1599
+
1600
+ def test_different_jdtls_paths_produce_different_markers(self) -> None:
1601
+ """Different resolved jdtls paths produce different cache markers."""
1602
+ from pathlib import Path
1603
+ from unittest.mock import patch
1604
+
1605
+ from java_functional_lsp.proxy import _compute_cache_marker
1591
1606
 
1592
- assert (cache / ".version").read_text() == __version__
1607
+ with patch("java_functional_lsp.proxy.shutil.which", return_value="/opt/jdtls/1.57/bin/jdtls"):
1608
+ with patch("java_functional_lsp.proxy.Path.resolve", return_value=Path("/opt/jdtls/1.57/bin/jdtls")):
1609
+ marker_a = _compute_cache_marker()
1610
+ with patch("java_functional_lsp.proxy.shutil.which", return_value="/opt/jdtls/1.58/bin/jdtls"):
1611
+ with patch("java_functional_lsp.proxy.Path.resolve", return_value=Path("/opt/jdtls/1.58/bin/jdtls")):
1612
+ marker_b = _compute_cache_marker()
1613
+ assert marker_a != marker_b
1593
1614
 
1594
1615
 
1595
1616
  # Subprocess-based tests — zero mocks, real LSP transport