summaryrefslogtreecommitdiff
path: root/internal/lsp/codeaction_custom_test.go
blob: d7fe2834ce056b3e7e18b6ee24d10c26c8989222 (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
package lsp

import (
	"bytes"
	"encoding/json"
	"io"
	"log"
	"strings"
	"testing"

	"codeberg.org/snonux/hexai/internal/appconfig"
)

// local copy of captureResponse for this test file
func capResp(t *testing.T, buf *bytes.Buffer) Response {
	t.Helper()
	raw := buf.String()
	idx := strings.Index(raw, "\r\n\r\n")
	if idx < 0 {
		t.Fatalf("no header/body separator in %q", raw)
	}
	body := raw[idx+4:]
	var resp Response
	if err := json.Unmarshal([]byte(body), &resp); err != nil {
		t.Fatalf("unmarshal response: %v", err)
	}
	return resp
}

func TestHandleCodeAction_ListsCustomActions(t *testing.T) {
	var out bytes.Buffer
	cfg := appconfig.App{
		CoreConfig: appconfig.CoreConfig{
			InlineOpen:   ">!",
			InlineClose:  ">",
			ChatSuffix:   ">",
			ChatPrefixes: []string{"?", "!", ":", ";"},
		},
		PromptConfig: appconfig.PromptConfig{
			CustomActions: []appconfig.CustomAction{
				{ID: "extract", Title: "Extract function", Scope: "selection", Kind: "refactor.extract", Instruction: "Extract into function"},
				{ID: "fix", Title: "Fix diagnostics", Scope: "diagnostics", Kind: "quickfix", User: "Fix:\n{{diagnostics}}\n\n{{selection}}"},
			},
		},
	}
	s := &Server{
		logger: log.New(io.Discard, "", 0),
		docs:   make(map[string]*document),
		out:    &out,
		cfg:    cfg,
	}
	s.llmClient = fakeLLM{resp: "ok"}
	// Prepare document and params
	uri := "file:///t.go"
	s.setDocument(uri, "package x\n\nfunc f(){}\n")
	p := CodeActionParams{TextDocument: TextDocumentIdentifier{URI: uri}, Range: Range{Start: Position{Line: 2}, End: Position{Line: 2, Character: 5}}}
	// Include diagnostics context so diagnostics-scoped action appears
	ctx := CodeActionContext{Diagnostics: []Diagnostic{{Range: Range{Start: Position{Line: 2}}, Message: "warn"}}}
	raw, _ := json.Marshal(ctx)
	p.Context = json.RawMessage(raw)

	// Call handler
	req := Request{JSONRPC: "2.0", ID: json.RawMessage("1"), Method: "textDocument/codeAction"}
	req.Params, _ = json.Marshal(p)
	out.Reset()
	s.handleCodeAction(req)
	resp := capResp(t, &out)
	var actions []CodeAction
	rb, _ := json.Marshal(resp.Result)
	if err := json.Unmarshal(rb, &actions); err != nil {
		t.Fatalf("decode: %v", err)
	}
	var seenSel, seenDiag bool
	for _, a := range actions {
		if a.Title == "Hexai: Extract function" {
			seenSel = true
		}
		if a.Title == "Hexai: Fix diagnostics" {
			seenDiag = true
		}
	}
	if !seenSel || !seenDiag {
		t.Fatalf("expected both custom actions, got %+v", actions)
	}
}

func TestResolveCodeAction_CustomInstructionAndUser(t *testing.T) {
	s := newTestServer()
	s.llmClient = fakeLLM{resp: "REPLACED"}
	cfg := s.cfg
	cfg.CustomActions = []appconfig.CustomAction{
		{ID: "extract", Title: "Extract function", Scope: "selection", Kind: "refactor.extract", Instruction: "Extract into function"},
		{ID: "fix", Title: "Fix diagnostics", Scope: "diagnostics", Kind: "quickfix", User: "Fix: {{diagnostics}}\n{{selection}}"},
	}
	s.cfg = cfg
	uri := "file:///t.go"
	p := CodeActionParams{TextDocument: TextDocumentIdentifier{URI: uri}, Range: Range{Start: Position{Line: 1}, End: Position{Line: 1, Character: 3}}}

	// Build selection-scoped custom action payload
	selPayload := struct {
		Type      string `json:"type"`
		ID        string `json:"id"`
		URI       string `json:"uri"`
		Range     Range  `json:"range"`
		Selection string `json:"selection"`
	}{Type: "custom", ID: "extract", URI: uri, Range: p.Range, Selection: "abc"}
	raw1, _ := json.Marshal(selPayload)
	ca1 := CodeAction{Title: "Hexai: Extract function", Data: raw1}
	if resolved, ok := s.resolveCodeAction(ca1); !ok || resolved.Edit == nil {
		t.Fatalf("expected resolve for instruction-based custom action")
	}

	// Build diagnostics-scoped custom action payload
	diagPayload := struct {
		Type        string       `json:"type"`
		ID          string       `json:"id"`
		URI         string       `json:"uri"`
		Range       Range        `json:"range"`
		Selection   string       `json:"selection"`
		Diagnostics []Diagnostic `json:"diagnostics"`
	}{Type: "custom", ID: "fix", URI: uri, Range: p.Range, Selection: "abc", Diagnostics: []Diagnostic{{Message: "lint"}}}
	raw2, _ := json.Marshal(diagPayload)
	ca2 := CodeAction{Title: "Hexai: Fix diagnostics", Data: raw2}
	if resolved, ok := s.resolveCodeAction(ca2); !ok || resolved.Edit == nil {
		t.Fatalf("expected resolve for user-based custom action")
	}
}