package lsp import ( "encoding/json" "testing" tut "codeberg.org/snonux/hexai/internal/testutil" ) func TestStripDuplicateGeneralPrefix_ExactOverlap(t *testing.T) { prefix := "func New " sugg := "func New() *CustData" got := stripDuplicateGeneralPrefix(prefix, sugg) // We expect the already typed prefix to be removed from the suggestion. if got == sugg { t.Fatalf("expected duplicate prefix to be stripped; got unchanged: %q", got) } if got != "() *CustData" { t.Fatalf("got %q want %q", got, "() *CustData") } } func TestStripDuplicateGeneralPrefix_TokenBoundarySuffix(t *testing.T) { prefix := "db." sugg := "db.Query()" got := stripDuplicateGeneralPrefix(prefix, sugg) if got != "Query()" { t.Fatalf("got %q want %q", got, "Query()") } } func TestStripDuplicateAssignmentPrefix_AssignAndWalrus(t *testing.T) { // walrus if out := stripDuplicateAssignmentPrefix("name := ", "name := compute()"); out != "compute()" { t.Fatalf(":= expected compute(), got %q", out) } // equals if out := stripDuplicateAssignmentPrefix("x = ", "x = y+1"); out != "y+1" { t.Fatalf("= expected y+1, got %q", out) } } func TestTryLLMCompletion_ManualInvokeAfterWhitespace_Allows(t *testing.T) { s := &Server{maxTokens: 32, triggerChars: []string{".", ":", "/", "_"}, compCache: make(map[string]string)} initServerDefaults(s) s.llmClient = fakeLLM{resp: tut.MultilineFunctionSuggestion()} line := "func fib(i int) " // cursor after space p := CompletionParams{Position: Position{Line: 0, Character: len(line)}, TextDocument: TextDocumentIdentifier{URI: "file://x.go"}} // Simulate manual user invocation (TriggerKind=1) p.Context = json.RawMessage([]byte(`{"triggerKind":1}`)) items, ok := s.tryLLMCompletion(p, "", line, "", "", "", false, "") if !ok { t.Fatalf("expected ok=true for manual invoke after whitespace") } if len(items) == 0 { t.Fatalf("expected at least one completion item") } } func TestTryLLMCompletion_InlinePromptAlwaysTriggers(t *testing.T) { s := &Server{maxTokens: 32, triggerChars: []string{".", ":", "/", "_"}, compCache: make(map[string]string)} initServerDefaults(s) s.llmClient = fakeLLM{resp: "replacement"} line := "prefix >do something> suffix" // No trigger char immediately before cursor; place cursor at end p := CompletionParams{Position: Position{Line: 0, Character: len(line)}, TextDocument: TextDocumentIdentifier{URI: "file://inline.go"}} items, ok := s.tryLLMCompletion(p, "", line, "", "", "", false, "") if !ok || len(items) == 0 { t.Fatalf("expected completion to trigger on inline >text> prompt") } } func TestTryLLMCompletion_DoubleOpenEmpty_DoesNotAutoTrigger(t *testing.T) { s := &Server{ maxTokens: 32, triggerChars: []string{".", ":", "/", "_"}, compCache: make(map[string]string), inlineOpen: ">", inlineClose: ">", inlineOpenChar: '>', inlineCloseChar: '>', } initServerDefaults(s) initServerDefaults(s) fake := &countingLLM{} s.llmClient = fake line := ">> " // empty content after double-open should not force-trigger p := CompletionParams{Position: Position{Line: 0, Character: len(line)}, TextDocument: TextDocumentIdentifier{URI: "file://empty-inline.go"}} items, ok := s.tryLLMCompletion(p, "", line, "", "", "", false, "") if !ok { t.Fatalf("expected ok=true for non-trigger path") } if len(items) != 0 { t.Fatalf("expected no items when inline double-open is empty") } if fake.calls != 0 { t.Fatalf("LLM should not be called; calls=%d", fake.calls) } } func TestHasDoubleSemicolonTrigger_Variants(t *testing.T) { if hasDoubleOpenTrigger(">>", '>', '>') { t.Fatalf("bare double-open should not trigger") } if hasDoubleOpenTrigger(">> ", '>', '>') { t.Fatalf("double-open followed by space should not trigger") } if hasDoubleOpenTrigger(">>>", '>', '>') { t.Fatalf("';;;' should not trigger (no content)") } if !hasDoubleOpenTrigger(">>x>", '>', '>') { t.Fatalf("expected trigger for ';;x;' pattern") } } func TestBareDoubleOpenPreventsAutoTriggerEvenWithOtherTriggers(t *testing.T) { s := &Server{ maxTokens: 32, triggerChars: []string{".", ":", "/", "_"}, compCache: make(map[string]string), inlineOpen: ">", inlineClose: ">", inlineOpenChar: '>', inlineCloseChar: '>', } fake := &countingLLM{} s.llmClient = fake // Place a '.' earlier but also include bare double-open at end; should not auto-trigger line := "obj. call >>" p := CompletionParams{Position: Position{Line: 0, Character: len(line)}, TextDocument: TextDocumentIdentifier{URI: "file://bare-ds.go"}} items, ok := s.tryLLMCompletion(p, "", line, "", "", "", false, "") if !ok { t.Fatalf("expected ok=true (handled), but not auto-triggering") } if len(items) != 0 { t.Fatalf("expected no items due to bare double-open") } if fake.calls != 0 { t.Fatalf("LLM should not be called; calls=%d", fake.calls) } } func TestBareDoubleOpenOnNextLine_PreventsAutoTrigger(t *testing.T) { s := &Server{maxTokens: 32, triggerChars: []string{".", ":", "/", "_"}, compCache: make(map[string]string)} initServerDefaults(s) fake := &countingLLM{} s.llmClient = fake current := "expression := flag.String(\"expression\", \"\", \"Expression to evaluate\")" below := ">>" p := CompletionParams{Position: Position{Line: 0, Character: len(current)}, TextDocument: TextDocumentIdentifier{URI: "file://nextline.go"}} items, ok := s.tryLLMCompletion(p, "", current, below, "", "", false, "") if !ok { t.Fatalf("expected ok=true handled") } if len(items) != 0 { t.Fatalf("expected no items due to bare double-open on next line") } if fake.calls != 0 { t.Fatalf("LLM should not be called; calls=%d", fake.calls) } } func TestBareDoubleOpenPreventsManualInvoke(t *testing.T) { s := &Server{maxTokens: 32, triggerChars: []string{".", ":", "/", "_"}, compCache: make(map[string]string)} initServerDefaults(s) fake := &countingLLM{} s.llmClient = fake line := ">>" p := CompletionParams{Position: Position{Line: 0, Character: len(line)}, TextDocument: TextDocumentIdentifier{URI: "file://bare-ds-manual.go"}} // Simulate manual invoke p.Context = json.RawMessage([]byte(`{"triggerKind":1}`)) items, ok := s.tryLLMCompletion(p, "", line, "", "", "", false, "") if !ok { t.Fatalf("expected ok=true (handled)") } if len(items) != 0 { t.Fatalf("expected no items for bare double-open even with manual invoke") } if fake.calls != 0 { t.Fatalf("LLM should not be called; calls=%d", fake.calls) } }