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

import (
	"io"
	"testing"
)

func TestStripTrailingTrigger(t *testing.T) {
	s := newTestServer()
	if got := s.chatSvc().stripTrailingTrigger("what?"); got != "what" {
		t.Fatalf("should remove trailing ?")
	}
	if got := s.chatSvc().stripTrailingTrigger("what?>"); got != "what?" {
		t.Fatalf("should drop trailing > when preceded by ?")
	}
	if got := s.chatSvc().stripTrailingTrigger("ok!>"); got != "ok!" {
		t.Fatalf("should drop > after !")
	}
	if got := s.chatSvc().stripTrailingTrigger("note:>"); got != "note:" {
		t.Fatalf("should drop > after :")
	}
	if got := s.chatSvc().stripTrailingTrigger("go;>"); got != "go;" {
		t.Fatalf("should drop > after ;")
	}
}

func TestBuildChatHistory_OrderAndLimit(t *testing.T) {
	s := newTestServer()
	uri := "file:///chat.txt"
	// Conversation: q1, > a1, blank, q2, > a2 lines, then current prompt
	doc := "q1\n> a1\n\nq2\n> a2\n\n"
	s.setDocument(uri, doc)
	msgs := s.chatSvc().buildChatHistory(uri, 5, "q3")
	// Expect: user q1, assistant a1, user q2, assistant a2, user q3
	if len(msgs) != 5 || msgs[0].Role != "user" || msgs[1].Role != "assistant" || msgs[2].Role != "user" || msgs[3].Role != "assistant" || msgs[4].Role != "user" {
		t.Fatalf("unexpected roles: %+v", msgs)
	}
	if msgs[0].Content != "q1" || msgs[1].Content != "a1" || msgs[2].Content != "q2" || msgs[3].Content != "a2" || msgs[4].Content != "q3" {
		t.Fatalf("unexpected contents: %+v", msgs)
	}
}

// TestChatEdits_StaleLineIndexAfterShrink is a regression test for the LSP
// server panic that occurred when an async chat response was applied after a
// concurrent didChange shrank the document. The chat prompt was detected at a
// line index that is now past the end of the (smaller) document, so the stale
// lineIdx exceeds len(d.lines). Before the fix, both applyChatEdits and
// buildChatHistory indexed d.lines without an upper-bound check and panicked
// with index-out-of-range. The fix makes applyChatEdits skip the stale edit and
// buildChatHistory clamp its starting index, so neither must panic.
func TestChatEdits_StaleLineIndexAfterShrink(t *testing.T) {
	s := newTestServer()
	// io.Discard avoids a nil-writer panic on the (valid-index) clientApplyEdit
	// path; the stale-index path bails out before reaching it.
	s.out = io.Discard
	uri := "file:///shrink.txt"
	// Originally the prompt sat on line 8, but the document has since shrunk to
	// just three lines. lineIdx=8 is now well past len(d.lines).
	s.setDocument(uri, "line0\nline1\nline2\n")
	staleLineIdx := 8

	// applyChatEdits must not panic on a stale (out-of-range) line index.
	s.chatSvc().applyChatEdits(uri, staleLineIdx, 12, 1, "> reply")

	// buildChatHistory must not panic and must still return the current prompt.
	msgs := s.chatSvc().buildChatHistory(uri, staleLineIdx, "current?")
	if len(msgs) == 0 || msgs[len(msgs)-1].Content != "current?" {
		t.Fatalf("expected history to end with current prompt, got: %+v", msgs)
	}

	// Boundary case: lineIdx exactly equals len(d.lines) (one past the last line).
	s.chatSvc().applyChatEdits(uri, 3, 4, 1, "> reply")
	if msgs := s.chatSvc().buildChatHistory(uri, 3, "q"); len(msgs) == 0 {
		t.Fatalf("expected non-empty history at boundary index")
	}

	// Negative index must also be handled gracefully.
	s.chatSvc().applyChatEdits(uri, -1, 0, 1, "> reply")
}