test(lsp): improve handler test coverage from 22% to 68%

- Add completion_test.go: tests for top-level, ingredient.method,
  property, requisite type, and step ID completions
- Add hover_test.go: tests for markdown generation, word extraction,
  and edge cases
- Add diagnostics_test.go: tests for nil recipe, parse errors, empty
  ingredient, requisite validation, range helpers
- Update all dependencies to latest versions
- Add target/ to .gitignore (stale Rust build artifacts)
This commit is contained in:
2026-03-08 12:52:04 +00:00
parent d5a9585c58
commit 96be450cb2
6 changed files with 658 additions and 68 deletions

View File

@@ -0,0 +1,304 @@
package lsp
import (
"testing"
"go.lsp.dev/protocol"
"github.com/gogrlx/grlx-lsp/internal/schema"
)
func TestCompleteTopLevel(t *testing.T) {
h := NewHandler(schema.DefaultRegistry())
src := ``
h.updateDocument("file:///test.grlx", src)
items := h.completeTopLevel("")
if len(items) != len(schema.TopLevelKeys) {
t.Errorf("expected %d top-level items, got %d", len(schema.TopLevelKeys), len(items))
}
labelSet := make(map[string]bool)
for _, item := range items {
labelSet[item.Label] = true
}
for _, key := range schema.TopLevelKeys {
if !labelSet[key] {
t.Errorf("missing top-level completion: %s", key)
}
}
}
func TestCompleteIngredientMethod(t *testing.T) {
h := NewHandler(schema.DefaultRegistry())
// Without dot prefix — should return all ingredient.method combos
items := h.completeIngredientMethod("")
if len(items) == 0 {
t.Fatal("expected completion items for all ingredient.method combos")
}
labelSet := make(map[string]bool)
for _, item := range items {
labelSet[item.Label] = true
}
for _, name := range []string{"file.managed", "cmd.run", "pkg.installed", "service.running"} {
if !labelSet[name] {
t.Errorf("missing completion: %s", name)
}
}
// With dot prefix — should complete methods for that ingredient
items = h.completeIngredientMethod("file.")
if len(items) == 0 {
t.Fatal("expected method completions for file ingredient")
}
for _, item := range items {
if item.Kind != protocol.CompletionItemKindFunction {
t.Errorf("expected Function kind, got %v", item.Kind)
}
}
// Unknown ingredient with dot
items = h.completeIngredientMethod("bogus.")
if len(items) != 0 {
t.Errorf("expected no completions for unknown ingredient, got %d", len(items))
}
}
func TestCompleteProperties(t *testing.T) {
h := NewHandler(schema.DefaultRegistry())
src := `steps:
install nginx:
pkg.installed:
- name: nginx
- `
h.updateDocument("file:///test.grlx", src)
doc := h.getDocument("file:///test.grlx")
// Line 4 is inside pkg.installed properties, "name" is already used
items := h.completeProperties(doc, 4)
// Should offer "version" but NOT "name" (already used)
for _, item := range items {
if item.Label == "- name: " {
t.Error("should not offer already-used property 'name'")
}
}
// Should offer requisites
foundRequisites := false
for _, item := range items {
if item.Label == "- requisites:" {
foundRequisites = true
}
}
if !foundRequisites {
t.Error("expected requisites in property completions")
}
}
func TestCompletePropertiesNoStep(t *testing.T) {
h := NewHandler(schema.DefaultRegistry())
src := `steps:`
h.updateDocument("file:///test.grlx", src)
doc := h.getDocument("file:///test.grlx")
items := h.completeProperties(doc, 0)
if len(items) != 0 {
t.Errorf("expected no completions when no step found, got %d", len(items))
}
}
func TestCompleteRequisiteTypes(t *testing.T) {
h := NewHandler(schema.DefaultRegistry())
items := h.completeRequisiteTypes("")
if len(items) != len(schema.AllRequisiteTypes) {
t.Errorf("expected %d requisite types, got %d", len(schema.AllRequisiteTypes), len(items))
}
for _, item := range items {
if item.Kind != protocol.CompletionItemKindEnum {
t.Errorf("expected Enum kind for requisite, got %v", item.Kind)
}
}
}
func TestCompleteStepIDs(t *testing.T) {
h := NewHandler(schema.DefaultRegistry())
src := `steps:
install nginx:
pkg.installed:
- name: nginx
start nginx:
service.running:
- name: nginx`
h.updateDocument("file:///test.grlx", src)
doc := h.getDocument("file:///test.grlx")
items := h.completeStepIDs(doc)
if len(items) != 2 {
t.Errorf("expected 2 step IDs, got %d", len(items))
}
labelSet := make(map[string]bool)
for _, item := range items {
labelSet[item.Label] = true
}
if !labelSet["install nginx"] || !labelSet["start nginx"] {
t.Errorf("missing expected step IDs, got: %v", labelSet)
}
}
func TestCompleteStepIDsNilRecipe(t *testing.T) {
h := NewHandler(schema.DefaultRegistry())
doc := &document{content: "", recipe: nil}
items := h.completeStepIDs(doc)
if len(items) != 0 {
t.Errorf("expected no step IDs for nil recipe, got %d", len(items))
}
}
func TestIsTopLevel(t *testing.T) {
content := "steps:\n install:\n pkg.installed:"
if !isTopLevel(content, 0) {
t.Error("line 0 should be top-level")
}
if isTopLevel(content, 1) {
t.Error("line 1 should not be top-level (indented)")
}
if isTopLevel(content, 99) {
t.Error("out-of-bounds line should not be top-level")
}
if isTopLevel(content, -1) {
t.Error("negative line should not be top-level")
}
}
func TestIsInRequisites(t *testing.T) {
tests := []struct {
line string
want bool
}{
{" - require: step1", true},
{" - onchanges: step1", true},
{" - onfail: step1", true},
{" - name: foo", false},
{"steps:", false},
}
for _, tt := range tests {
got := isInRequisites(tt.line)
if got != tt.want {
t.Errorf("isInRequisites(%q) = %v, want %v", tt.line, got, tt.want)
}
}
}
func TestIsInRequisiteValue(t *testing.T) {
content := `steps:
first:
file.exists:
- name: /tmp/a
- requisites:
- require:
- first`
if !isInRequisiteValue(content, 6) {
t.Error("line 6 should be in requisite value context")
}
if isInRequisiteValue(content, 0) {
t.Error("line 0 should not be in requisite value context")
}
}
func TestIsPropertyPosition(t *testing.T) {
tests := []struct {
line string
want bool
}{
{" - name: foo", true},
{" - ", true},
{" ", true},
{"steps:", false},
{" - name: foo", false}, // only 2 spaces indent
}
for _, tt := range tests {
got := isPropertyPosition(tt.line)
if got != tt.want {
t.Errorf("isPropertyPosition(%q) = %v, want %v", tt.line, got, tt.want)
}
}
}
func TestBuildMethodDoc(t *testing.T) {
ing := &schema.Ingredient{Name: "file", Description: "Manage files"}
m := &schema.Method{
Name: "managed",
Description: "Download and manage a file",
Properties: []schema.Property{
{Key: "name", Type: "string", Required: true, Description: "The file path"},
{Key: "source", Type: "string", Required: true, Description: "Source URL"},
{Key: "mode", Type: "string", Required: false},
},
}
doc := buildMethodDoc(ing, m)
if doc == "" {
t.Fatal("expected non-empty doc string")
}
// Required properties should be marked with *
if !contains(doc, "* name") {
t.Error("expected required marker for 'name'")
}
if !contains(doc, "* source") {
t.Error("expected required marker for 'source'")
}
}
func TestFindStepForLine(t *testing.T) {
h := NewHandler(schema.DefaultRegistry())
src := `steps:
install nginx:
pkg.installed:
- name: nginx
start nginx:
service.running:
- name: nginx`
h.updateDocument("file:///test.grlx", src)
doc := h.getDocument("file:///test.grlx")
// Line 3 is inside "install nginx" step
step := h.findStepForLine(doc, 3)
if step == nil {
t.Fatal("expected to find step for line 3")
}
if step.Ingredient != "pkg" || step.Method != "installed" {
t.Errorf("expected pkg.installed, got %s.%s", step.Ingredient, step.Method)
}
// Line 6 is inside "start nginx" step
step = h.findStepForLine(doc, 6)
if step == nil {
t.Fatal("expected to find step for line 6")
}
if step.Ingredient != "service" || step.Method != "running" {
t.Errorf("expected service.running, got %s.%s", step.Ingredient, step.Method)
}
// Nil recipe
step = h.findStepForLine(&document{content: "", recipe: nil}, 0)
if step != nil {
t.Error("expected nil for nil recipe")
}
}
func contains(s, substr string) bool {
return len(s) >= len(substr) && (s == substr || len(s) > 0 && containsHelper(s, substr))
}
func containsHelper(s, substr string) bool {
for i := 0; i+len(substr) <= len(s); i++ {
if s[i:i+len(substr)] == substr {
return true
}
}
return false
}