summaryrefslogtreecommitdiff
path: root/internal/hexailsp/run_more_test.go
blob: d0f17b56f6556e3d640de36c2f922119c61f28b5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
package hexailsp

import (
	"bytes"
	"context"
	"io"
	"log"
	"testing"

	"codeberg.org/snonux/hexai/internal/appconfig"
	"codeberg.org/snonux/hexai/internal/llm"
	"codeberg.org/snonux/hexai/internal/lsp"
	"codeberg.org/snonux/hexai/internal/runtimeconfig"
)

type recRunner struct{ ran bool }

func (r *recRunner) Run() error { r.ran = true; return nil }

type applyRunner struct{ opts []lsp.ServerOptions }

func (r *applyRunner) Run() error                          { return nil }
func (r *applyRunner) ApplyOptions(opts lsp.ServerOptions) { r.opts = append(r.opts, opts) }

type stubClient struct{}

func (stubClient) Chat(context.Context, []llm.Message, ...llm.RequestOption) (string, error) {
	return "", nil
}
func (stubClient) Name() string         { return "stub" }
func (stubClient) DefaultModel() string { return "stub-model" }

type recordingStatusSink struct{}

func (recordingStatusSink) SetLLMStart(string, string) error { return nil }
func (recordingStatusSink) SetGlobal(lsp.GlobalStatus) error { return nil }

func TestRunWithFactory_BuildsOptionsAndClient(t *testing.T) {
	var captured lsp.ServerOptions
	factory := func(r io.Reader, w io.Writer, logger *log.Logger, opts lsp.ServerOptions) ServerRunner {
		captured = opts
		return &recRunner{}
	}
	var in, out bytes.Buffer
	logger := log.New(&out, "", 0)
	cfg := appconfig.Load(logger)
	// Use ollama to avoid API keys
	cfg.Provider = "ollama"
	cfg.MaxTokens = 123
	cfg.PromptCodeActionRewriteSystem = "RSYS"
	cfg.PromptCodeActionRewriteUser = "RUSER"
	if err := RunWithFactory("", "", &in, &out, logger, cfg, nil, factory); err != nil {
		t.Fatalf("RunWithFactory error: %v", err)
	}
	if captured.Config == nil {
		t.Fatalf("expected Config to be set in ServerOptions")
	}
	if captured.Config.MaxTokens != 123 {
		t.Fatalf("opts not applied: %+v", captured)
	}
	if captured.Config.PromptCodeActionRewriteSystem != "RSYS" || captured.Config.PromptCodeActionRewriteUser != "RUSER" {
		t.Fatalf("prompts not mapped: %+v", captured)
	}
	if captured.Client == nil {
		t.Fatalf("expected client to be constructed")
	}
}

func TestRunWithFactory_SubscriptionAppliesUpdates(t *testing.T) {
	var in, out bytes.Buffer
	logger := log.New(io.Discard, "", 0)
	runner := &applyRunner{}
	var capturedStore *runtimeconfig.Store
	factory := func(r io.Reader, w io.Writer, logger *log.Logger, opts lsp.ServerOptions) ServerRunner {
		capturedStore = opts.ConfigStore
		runner.opts = append(runner.opts, opts)
		return runner
	}
	cfg := appconfig.Load(nil)
	cfg.StatsWindowMinutes = 0
	cfg.ContextMode = " WINDOW "
	if err := RunWithFactory("", "", &in, &out, logger, cfg, stubClient{}, factory); err != nil {
		t.Fatalf("RunWithFactory error: %v", err)
	}
	if capturedStore == nil {
		t.Fatal("expected config store to be passed to factory")
	}
	if len(runner.opts) == 0 {
		t.Fatal("expected initial options to be recorded")
	}
	updated := cfg
	updated.MaxTokens = cfg.MaxTokens + 10
	updated.ContextMode = "always-full"
	capturedStore.Set(updated)
	if len(runner.opts) < 2 {
		t.Fatalf("expected ApplyOptions to be invoked on config update, got %d calls", len(runner.opts))
	}
	latest := runner.opts[len(runner.opts)-1]
	if latest.Config == nil {
		t.Fatalf("expected Config on latest options")
	}
	if latest.Config.MaxTokens != updated.MaxTokens {
		t.Fatalf("expected updated max tokens, got %+v", latest)
	}
	if latest.Config.ContextMode != "always-full" {
		t.Fatalf("expected normalized context mode, got %+v", latest)
	}
}

func TestRunWithDependencies_UsesInjectedClientBuilderAndStatusSink(t *testing.T) {
	var captured lsp.ServerOptions
	sink := &recordingStatusSink{}
	buildCalls := 0
	factory := func(r io.Reader, w io.Writer, logger *log.Logger, opts lsp.ServerOptions) ServerRunner {
		captured = opts
		return &recRunner{}
	}
	cfg := appconfig.Load(nil)
	if err := runWithDependencies("", "", bytes.NewBuffer(nil), bytes.NewBuffer(nil), log.New(io.Discard, "", 0), cfg, nil, factory, runDependencies{
		buildClient: func(appconfig.App, llm.Client) llm.Client {
			buildCalls++
			return stubClient{}
		},
		newConfigStore:   runtimeconfig.New,
		newIgnoreChecker: defaultIgnoreCheckerFactory,
		statusSink:       sink,
	}); err != nil {
		t.Fatalf("runWithDependencies error: %v", err)
	}
	if buildCalls != 1 {
		t.Fatalf("expected one client build, got %d", buildCalls)
	}
	if captured.Client == nil {
		t.Fatal("expected injected client to be passed through")
	}
	if captured.StatusSink != sink {
		t.Fatal("expected injected status sink to be passed through")
	}
}