mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-21 03:39:54 +00:00
23344a9a3c
Ship LSP semantic diagnostics as a bundled plugin (plugins/lsp/) using existing hook system. Zero lines of core code modified. Plugin wiring: - pre_tool_call: capture LSP baseline before write_file/patch - transform_tool_result: inject diagnostics into tool result JSON - on_session_start/on_session_end + atexit: lifecycle management Key design: - Baselines keyed by (session_id, abs_path) for concurrent safety - Diagnostics added as 'lsp_diagnostics' JSON field (preserves shape) - Per-file workspace detection (no static session-start gate) - V4A multi-file patch skipped for MVP - Short timeout (3s) — cold start degrades gracefully - os.path.exists heuristic for Docker/SSH backend skip - First relevant write with no server → INFO log with install hint Tests: 77/77 pass including: - Protocol framing, reporter formatting, workspace resolution - Client E2E against mock LSP server (live_system_guard_bypass) - Eventlog steady-state silence contract - Backend-gate heuristic (local vs non-local paths) - Full hook flow integration (pre→write→transform with diagnostics) Source: PR #24168 by @teknium1, PR #24155 by @OutThisLife Co-authored-by: Teknium <127238744+teknium1@users.noreply.github.com>
140 lines
3.8 KiB
Python
140 lines
3.8 KiB
Python
"""Tests for workspace + project-root resolution."""
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from plugins.lsp.workspace import (
|
|
clear_cache,
|
|
find_git_worktree,
|
|
is_inside_workspace,
|
|
nearest_root,
|
|
normalize_path,
|
|
resolve_workspace_for_file,
|
|
)
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _clear():
|
|
clear_cache()
|
|
yield
|
|
clear_cache()
|
|
|
|
|
|
def test_find_git_worktree_returns_none_outside_repo(tmp_path: Path):
|
|
sub = tmp_path / "sub"
|
|
sub.mkdir()
|
|
assert find_git_worktree(str(sub)) is None
|
|
|
|
|
|
def test_find_git_worktree_finds_dotgit(tmp_path: Path):
|
|
repo = tmp_path / "repo"
|
|
repo.mkdir()
|
|
(repo / ".git").mkdir()
|
|
sub = repo / "src" / "deep"
|
|
sub.mkdir(parents=True)
|
|
assert find_git_worktree(str(sub)) == str(repo)
|
|
|
|
|
|
def test_find_git_worktree_handles_dotgit_file(tmp_path: Path):
|
|
"""``.git`` can also be a file (gitfile pointing into a worktree)."""
|
|
repo = tmp_path / "repo"
|
|
repo.mkdir()
|
|
(repo / ".git").write_text("gitdir: /elsewhere\n")
|
|
assert find_git_worktree(str(repo)) == str(repo)
|
|
|
|
|
|
def test_is_inside_workspace_true_for_subpath(tmp_path: Path):
|
|
root = tmp_path / "p"
|
|
root.mkdir()
|
|
sub = root / "x" / "y.py"
|
|
sub.parent.mkdir(parents=True)
|
|
sub.write_text("")
|
|
assert is_inside_workspace(str(sub), str(root))
|
|
|
|
|
|
def test_is_inside_workspace_false_for_unrelated(tmp_path: Path):
|
|
a = tmp_path / "a"
|
|
b = tmp_path / "b"
|
|
a.mkdir()
|
|
b.mkdir()
|
|
f = b / "x.py"
|
|
f.write_text("")
|
|
assert not is_inside_workspace(str(f), str(a))
|
|
|
|
|
|
def test_nearest_root_finds_first_marker(tmp_path: Path):
|
|
root = tmp_path / "p"
|
|
deep = root / "src" / "pkg"
|
|
deep.mkdir(parents=True)
|
|
(root / "pyproject.toml").write_text("")
|
|
found = nearest_root(str(deep / "mod.py"), ["pyproject.toml"])
|
|
assert found == str(root)
|
|
|
|
|
|
def test_nearest_root_excludes_take_priority(tmp_path: Path):
|
|
"""If an exclude marker matches first, return None."""
|
|
root = tmp_path / "p"
|
|
sub = root / "deno-app"
|
|
sub.mkdir(parents=True)
|
|
(sub / "deno.json").write_text("{}")
|
|
(root / "package.json").write_text("{}") # would match if not for exclude
|
|
found = nearest_root(
|
|
str(sub / "main.ts"),
|
|
["package.json"],
|
|
excludes=["deno.json"],
|
|
)
|
|
assert found is None
|
|
|
|
|
|
def test_nearest_root_returns_none_when_no_marker(tmp_path: Path):
|
|
f = tmp_path / "x.py"
|
|
f.write_text("")
|
|
assert nearest_root(str(f), ["pyproject.toml"]) is None
|
|
|
|
|
|
def test_resolve_workspace_for_file_uses_cwd_first(tmp_path: Path, monkeypatch):
|
|
repo = tmp_path / "repo"
|
|
(repo / ".git").mkdir(parents=True)
|
|
file_path = repo / "x.py"
|
|
file_path.write_text("")
|
|
# cwd is inside the repo
|
|
monkeypatch.chdir(str(repo))
|
|
root, gated = resolve_workspace_for_file(str(file_path))
|
|
assert root == str(repo)
|
|
assert gated is True
|
|
|
|
|
|
def test_resolve_workspace_for_file_no_repo_returns_none(tmp_path: Path, monkeypatch):
|
|
monkeypatch.chdir(str(tmp_path))
|
|
f = tmp_path / "x.py"
|
|
f.write_text("")
|
|
root, gated = resolve_workspace_for_file(str(f))
|
|
assert root is None
|
|
assert gated is False
|
|
|
|
|
|
def test_resolve_workspace_falls_back_to_file_location(tmp_path: Path, monkeypatch):
|
|
"""When cwd isn't a git repo but the file is inside one, we still
|
|
discover the workspace from the file's path."""
|
|
not_a_repo = tmp_path / "loose"
|
|
not_a_repo.mkdir()
|
|
monkeypatch.chdir(str(not_a_repo))
|
|
|
|
repo = tmp_path / "actual-repo"
|
|
(repo / ".git").mkdir(parents=True)
|
|
f = repo / "x.py"
|
|
f.write_text("")
|
|
|
|
root, gated = resolve_workspace_for_file(str(f))
|
|
assert root == str(repo)
|
|
assert gated is True
|
|
|
|
|
|
def test_normalize_path_expands_tilde(monkeypatch):
|
|
monkeypatch.setenv("HOME", "/home/user")
|
|
p = normalize_path("~/x.py")
|
|
assert p == os.path.abspath("/home/user/x.py")
|