Files
wehub-resource-sync a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:33:42 +08:00

241 lines
7.2 KiB
Go

package lsp
import (
"strings"
"testing"
"github.com/zzet/gortex/internal/semantic"
)
// TestSpecForExtension verifies the per-extension lookup table is
// populated for every claimed extension and that case is normalised.
func TestSpecForExtension(t *testing.T) {
cases := []struct {
ext string
wantName string
}{
{".go", "gopls"},
{".ts", "typescript-language-server"},
{".tsx", "typescript-language-server"},
{".py", "pyright"},
{".pyi", "pyright"},
{".rs", "rust-analyzer"},
{".cpp", "clangd"},
{".HPP", "clangd"}, // case folding
{".java", "jdtls"},
{".kt", "kotlin-language-server"},
{".cs", "omnisharp"},
{".rb", "ruby-lsp"},
{".php", "phpactor"},
{".lua", "lua-language-server"},
{".swift", "sourcekit-lsp"},
{".hs", "haskell-language-server"},
{".ex", "elixir-ls"},
{".ml", "ocamllsp"},
{".zig", "zls"},
}
for _, c := range cases {
t.Run(c.ext, func(t *testing.T) {
s := SpecForExtension(c.ext)
if s == nil {
t.Fatalf("no spec for %q", c.ext)
}
if s.Name != c.wantName {
t.Fatalf("ext %q: got %s, want %s", c.ext, s.Name, c.wantName)
}
})
}
if SpecForExtension(".unknown_ext") != nil {
t.Fatal("expected nil for unknown ext")
}
if SpecForExtension("") != nil {
t.Fatal("empty ext should not map")
}
}
// TestSpecForPath verifies path-based routing extracts the extension
// even from messy paths.
func TestSpecForPath(t *testing.T) {
if s := SpecForPath("/abs/path/to/main.go"); s == nil || s.Name != "gopls" {
t.Fatalf("unexpected spec for go path: %v", s)
}
if s := SpecForPath("README.md"); s != nil {
t.Fatalf("expected nil spec for markdown, got %v", s)
}
if s := SpecForPath("/x.RS"); s == nil || s.Name != "rust-analyzer" {
t.Fatalf("uppercase rust path: %v", s)
}
}
// TestLanguageIDForPath checks per-extension languageId resolution.
func TestLanguageIDForPath(t *testing.T) {
cases := map[string]string{
"app.tsx": "typescriptreact",
"main.ts": "typescript",
"a.jsx": "javascriptreact",
"foo.py": "python",
"pkg/x.rs": "rust",
"a.cpp": "cpp",
"a.h": "c", // mapping wins
}
for path, want := range cases {
got := LanguageIDForPath(path)
if got != want {
t.Errorf("path %q: got %q, want %q", path, got, want)
}
}
if LanguageIDForPath("README.md") != "" {
t.Fatal("expected empty languageId for unknown ext")
}
}
// TestSpecByName ensures every entry in Servers is reachable by name.
func TestSpecByName(t *testing.T) {
for _, want := range Servers {
got := SpecByName(want.Name)
if got == nil {
t.Errorf("SpecByName(%q) = nil", want.Name)
continue
}
if got.Name != want.Name {
t.Errorf("SpecByName(%q).Name = %q", want.Name, got.Name)
}
}
if SpecByName("nope") != nil {
t.Fatal("unknown name should return nil")
}
}
// TestRegistryContributesDefaultProviders proves that initialisation
// adds an entry per spec to the semantic.DefaultConfig provider list.
func TestRegistryContributesDefaultProviders(t *testing.T) {
cfg := semantic.DefaultConfig()
names := make(map[string]bool, len(cfg.Providers))
for _, p := range cfg.Providers {
names[p.Name] = true
}
for _, s := range Servers {
if !names[s.Name] {
t.Errorf("DefaultConfig missing %q", s.Name)
}
}
}
// TestExtensionsCoverEverySpec asserts every spec lists at least one
// extension — a spec with no extensions can't be routed to.
func TestExtensionsCoverEverySpec(t *testing.T) {
for _, s := range Servers {
if len(s.Extensions) == 0 {
t.Errorf("spec %q has no extensions", s.Name)
}
for _, e := range s.Extensions {
if !strings.HasPrefix(e, ".") {
t.Errorf("spec %q ext %q must start with '.'", s.Name, e)
}
}
}
}
// TestPyreflyAndTsgoSpecs verifies the pyrefly and tsgo LSP servers are
// registered as first-class specs without disturbing the default
// Python / TypeScript routing.
func TestPyreflyAndTsgoSpecs(t *testing.T) {
pyrefly := SpecByName("pyrefly")
if pyrefly == nil {
t.Fatal("pyrefly spec not registered")
}
if pyrefly.Command != "pyrefly" || len(pyrefly.Args) == 0 {
t.Errorf("pyrefly invocation: %s %v", pyrefly.Command, pyrefly.Args)
}
if len(pyrefly.Languages) == 0 || pyrefly.Languages[0] != "python" {
t.Errorf("pyrefly should cover python, got %v", pyrefly.Languages)
}
tsgo := SpecByName("tsgo")
if tsgo == nil {
t.Fatal("tsgo spec not registered")
}
if tsgo.Command != "tsgo" || len(tsgo.Args) == 0 {
t.Errorf("tsgo invocation: %s %v", tsgo.Command, tsgo.Args)
}
// Default extension routing must be unchanged — the incumbents
// register earlier in Servers and keep .py / .ts.
if s := SpecForExtension(".py"); s == nil || s.Name != "pyright" {
t.Errorf(".py should still route to pyright, got %v", s)
}
if s := SpecForExtension(".ts"); s == nil || s.Name != "typescript-language-server" {
t.Errorf(".ts should still route to typescript-language-server, got %v", s)
}
// Both must be contributed to the default provider list.
cfg := semantic.DefaultConfig()
have := map[string]bool{}
for _, p := range cfg.Providers {
have[p.Name] = true
}
if !have["pyrefly"] || !have["tsgo"] {
t.Errorf("pyrefly/tsgo missing from DefaultConfig providers")
}
}
// TestSpecWithOverrides verifies .gortex.yaml command / args / env
// overrides are applied to a copy without mutating the built-in spec —
// the path by which jdtls gets a pinned JRE and launcher args.
func TestSpecWithOverrides(t *testing.T) {
if SpecWithOverrides(nil, "x", nil, nil) != nil {
t.Error("nil base must return nil")
}
base := SpecByName("jdtls")
if base == nil {
t.Fatal("jdtls spec missing")
}
// No overrides → a copy with inherited values.
cp := SpecWithOverrides(base, "", nil, nil)
if cp == base {
t.Error("must return a copy, not the same pointer")
}
if cp.Command != base.Command {
t.Errorf("command should be inherited, got %q", cp.Command)
}
// Overrides applied.
cp = SpecWithOverrides(base, "/opt/jdtls/bin/jdtls",
[]string{"--jvm-arg=-Xmx4G"}, []string{"JAVA_HOME=/opt/jdk21"})
if cp.Command != "/opt/jdtls/bin/jdtls" {
t.Errorf("command override not applied: %q", cp.Command)
}
if len(cp.Args) != 1 || cp.Args[0] != "--jvm-arg=-Xmx4G" {
t.Errorf("args override not applied: %v", cp.Args)
}
if len(cp.Env) != 1 || cp.Env[0] != "JAVA_HOME=/opt/jdk21" {
t.Errorf("env override not applied: %v", cp.Env)
}
// The built-in spec must stay pristine.
if len(base.Env) != 0 || base.Command != "jdtls" {
t.Errorf("base spec mutated: command=%q env=%v", base.Command, base.Env)
}
}
// TestClangdDisablesClangTidy pins that the clangd enrichment spec turns off
// clang-tidy: Gortex consumes semantic graph signal, not lint diagnostics, and
// a broad repo .clang-tidy can crash clangd into a reconnect/high-CPU loop.
func TestClangdDisablesClangTidy(t *testing.T) {
spec := SpecByName("clangd")
if spec == nil {
t.Fatal("clangd spec missing")
}
joined := strings.Join(spec.Args, " ")
if !strings.Contains(joined, "--clang-tidy=false") {
t.Errorf("clangd args must disable clang-tidy, got %v", spec.Args)
}
// Cross-file references still need the background index — it must NOT be
// disabled.
if strings.Contains(joined, "--background-index=false") {
t.Errorf("clangd must keep --background-index for cross-file references, got %v", spec.Args)
}
}