diff options
| author | Paul Buetow <paul@buetow.org> | 2025-09-03 16:00:26 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2025-09-03 16:00:26 +0300 |
| commit | ffe9ed5531b6e62706ea555c48964ea0e560b780 (patch) | |
| tree | 81974f771543827f4c0743f5f1d66f5fbd06a2bd /internal/lsp/debounce_throttle_test.go | |
| parent | 71f0d04bd558433cebf1b05845c9fa0e2957eba8 (diff) | |
Phase 2: add configurable completion debounce\n\n- App config: completion_debounce_ms (default 200)\n- Server: wait until no input for debounce before LLM calls\n- Applies to chat and provider-native completion paths\n- Tests: add debounce and adjust to verify behavior\n\nAll unit tests pass.
Diffstat (limited to 'internal/lsp/debounce_throttle_test.go')
| -rw-r--r-- | internal/lsp/debounce_throttle_test.go | 84 |
1 files changed, 84 insertions, 0 deletions
diff --git a/internal/lsp/debounce_throttle_test.go b/internal/lsp/debounce_throttle_test.go new file mode 100644 index 0000000..012ec68 --- /dev/null +++ b/internal/lsp/debounce_throttle_test.go @@ -0,0 +1,84 @@ +package lsp + +import ( + "context" + "encoding/json" + "testing" + "time" + "codeberg.org/snonux/hexai/internal/llm" +) + +// timeLLM records the time when Chat is invoked. +type timeLLM struct{ t time.Time } + +func (t *timeLLM) Chat(ctx context.Context, _ []llm.Message, _ ...llm.RequestOption) (string, error) { + t.t = time.Now() + return "ok", nil +} +func (t *timeLLM) Name() string { return "fake" } +func (t *timeLLM) DefaultModel() string { return "m" } + +func TestCompletionDebounce_WaitsUntilQuiet(t *testing.T) { + s := newTestServer() + s.compCache = make(map[string]string) + s.triggerChars = []string{".", ":", "/", "_"} + s.maxTokens = 32 + s.completionDebounce = 30 * time.Millisecond + s.markActivity() // simulate recent input + + f := &timeLLM{} + s.llmClient = f + + line := "func f(i int) " + p := CompletionParams{Position: Position{Line: 0, Character: len(line)}, TextDocument: TextDocumentIdentifier{URI: "file://debounce.go"}} + p.Context = json.RawMessage([]byte(`{"triggerKind":1}`)) + + start := time.Now() + _, ok := s.tryLLMCompletion(p, "", line, "", "", "", false, "") + if !ok { + t.Fatalf("expected ok=true") + } + if f.t.IsZero() { + t.Fatalf("expected LLM to be called") + } + if f.t.Sub(start) < 25*time.Millisecond { // allow minor timing noise + t.Fatalf("expected debounce delay, got %s", f.t.Sub(start)) + } +} + +func TestCompletionThrottle_SerializesCalls(t *testing.T) { + s := newTestServer() + s.compCache = make(map[string]string) + s.triggerChars = []string{".", ":", "/", "_"} + s.maxTokens = 32 + s.throttleInterval = 25 * time.Millisecond + + // first call uses timeLLM to record time + f1 := &timeLLM{} + s.llmClient = f1 + line := "func f(i int) " + p := CompletionParams{Position: Position{Line: 0, Character: len(line)}, TextDocument: TextDocumentIdentifier{URI: "file://throttle.go"}} + p.Context = json.RawMessage([]byte(`{"triggerKind":1}`)) + start := time.Now() + if _, ok := s.tryLLMCompletion(p, "", line, "", "", "", false, ""); !ok { + t.Fatalf("first call expected ok=true") + } + if f1.t.IsZero() { + t.Fatalf("expected first call time recorded") + } + + // second call immediately after; should be delayed by ~interval. + // Clear cache to ensure we actually call the LLM again. + s.compCache = make(map[string]string) + f2 := &timeLLM{} + s.llmClient = f2 + if _, ok := s.tryLLMCompletion(p, "", line, "", "", "", false, ""); !ok { + t.Fatalf("second call expected ok=true") + } + if f2.t.IsZero() { + t.Fatalf("expected second call time recorded") + } + if f2.t.Sub(start) < s.throttleInterval { + t.Fatalf("expected throttle spacing >= %s, got %s", s.throttleInterval, f2.t.Sub(start)) + } +} |
