Files
ollama/cmd/tui/chat/input_test.go
2026-07-09 17:27:31 -07:00

763 lines
23 KiB
Go

package chat
import (
"context"
"os"
"path/filepath"
"strings"
"testing"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
coreagent "github.com/ollama/ollama/agent"
"github.com/ollama/ollama/api"
)
func TestChatHelpCommandShowsV1Commands(t *testing.T) {
m := chatModel{input: []rune("/help")}
updated, cmd := m.handleSubmit()
if cmd != nil {
t.Fatal("help command should not return a command")
}
fm := updated.(chatModel)
if len(fm.entries) != 1 {
t.Fatalf("entries = %d, want 1", len(fm.entries))
}
for _, want := range []string{
"**Commands**",
"- `/model`: switch models",
"- `/think`: set thinking mode",
"- `/compact`: summarize older context",
"- `/help`: show commands",
"- `/bye`: exit",
"- `/prompt`: show full prompt, tools, and messages",
"- `/save <filename>`: save request JSON; saved as <filename>.json",
"**Shortcuts**",
"- `shift+enter`: insert a newline",
"- `shift+tab`: toggle permission mode",
} {
if !strings.Contains(fm.entries[0].content, want) {
t.Fatalf("help output missing %q:\n%s", want, fm.entries[0].content)
}
}
for _, removed := range []string{"/history", "/load", "/raw", "/resume", "/set", "/show", "/skills", "/verbose"} {
if strings.Contains(fm.entries[0].content, removed) {
t.Fatalf("removed command %q should stay hidden from help:\n%s", removed, fm.entries[0].content)
}
}
}
func TestChatNewCommandRepaintsFromTop(t *testing.T) {
m := chatModel{
input: []rune("/new"),
flowPrintedLines: 4,
entries: []chatEntry{{role: "assistant", content: "old transcript"}},
messages: []api.Message{{Role: "user", Content: "old prompt"}},
approvalState: testApprovalState(true, map[string]bool{"edit": true}),
opts: Options{AllowAllTools: true},
permissionNotice: "full access enabled",
}
updated, cmd := m.handleSubmit()
if cmd == nil {
t.Fatal("/new should return a repaint command")
}
m = updated.(chatModel)
if m.status != "new chat" {
t.Fatalf("status = %q, want new chat", m.status)
}
if len(m.entries) != 0 || len(m.messages) != 0 {
t.Fatalf("chat was not reset: entries=%#v messages=%#v", m.entries, m.messages)
}
if m.flowPrintedLines != 0 {
t.Fatalf("flowPrintedLines = %d, want 0", m.flowPrintedLines)
}
if m.approvalState.AllowAll() || m.opts.AllowAllTools || m.approvalState.Allows("edit") || m.permissionNotice != "" {
t.Fatalf("permissions were not reset: allowAll=%v opts=%v editAllowed=%v notice=%q", m.approvalState.AllowAll(), m.opts.AllowAllTools, m.approvalState.Allows("edit"), m.permissionNotice)
}
if msg := cmd(); msg == nil {
t.Fatal("repaint command returned nil")
}
}
func TestChatNewCommandPreservesLaunchFullAccessDefault(t *testing.T) {
m := chatModel{
input: []rune("/new"),
defaultAllowAll: true,
approvalState: testApprovalState(false, map[string]bool{"edit": true}),
}
updated, _ := m.handleSubmit()
fm := updated.(chatModel)
if !fm.approvalState.AllowAll() || !fm.opts.AllowAllTools {
t.Fatalf("full access default was not restored: allowAll=%v opts=%v", fm.approvalState.AllowAll(), fm.opts.AllowAllTools)
}
fm.approvalState.SetAllowAll(false)
if fm.approvalState.Allows("edit") {
t.Fatal("edit scope should be cleared")
}
}
func TestChatSaveCommandRequiresFilename(t *testing.T) {
m := chatModel{
input: []rune("/save"),
opts: Options{Model: "llama3.2"},
}
updated, cmd := m.handleSubmit()
if cmd != nil {
t.Fatal("/save should not start a command")
}
fm := updated.(chatModel)
if fm.status != "error" {
t.Fatalf("status = %q, want error", fm.status)
}
if len(fm.entries) != 1 || !strings.Contains(fm.entries[0].content, "usage: /save <filename>") {
t.Fatalf("entries = %#v, want usage error", fm.entries)
}
}
func TestChatSaveCommandWritesRequestJSON(t *testing.T) {
dir := t.TempDir()
m := chatModel{
input: []rune("/save request"),
workingDir: dir,
opts: Options{
Model: "llama3.2",
SystemPrompt: "You are Ollama.",
},
messages: []api.Message{{Role: "user", Content: "hello"}},
}
updated, cmd := m.handleSubmit()
if cmd != nil {
t.Fatal("/save redirect should not start a command")
}
fm := updated.(chatModel)
if fm.status != "saved" {
t.Fatalf("status = %q, want saved", fm.status)
}
data, err := os.ReadFile(filepath.Join(dir, "request.json"))
if err != nil {
t.Fatal(err)
}
raw := string(data)
for _, want := range []string{
`"model": "llama3.2"`,
`"role": "system"`,
`"content": "You are Ollama."`,
`"role": "user"`,
`"content": "hello"`,
} {
if !strings.Contains(raw, want) {
t.Fatalf("saved request missing %q:\n%s", want, raw)
}
}
if got := fm.entries[len(fm.entries)-1].content; got != "saved as request.json" {
t.Fatalf("save entry = %q, want saved filename", got)
}
}
func TestChatSaveCommandRejectsPath(t *testing.T) {
dir := t.TempDir()
m := chatModel{
input: []rune("/save ../request"),
workingDir: dir,
opts: Options{Model: "llama3.2"},
}
updated, cmd := m.handleSubmit()
if cmd != nil {
t.Fatal("invalid /save should not start a command")
}
fm := updated.(chatModel)
if fm.status != "error" {
t.Fatalf("status = %q, want error", fm.status)
}
if _, err := os.Stat(filepath.Join(filepath.Dir(dir), "request.json")); !os.IsNotExist(err) {
t.Fatalf("/save wrote outside working dir, stat err = %v", err)
}
}
func TestChatPromptCommandOpensPromptDebugScreen(t *testing.T) {
registry := &coreagent.Registry{}
registry.Register(chatTestTool{})
toolArgs := api.NewToolCallFunctionArguments()
toolArgs.Set("query", "show me everything")
m := chatModel{
input: []rune("/prompt"),
width: 100,
height: 20,
opts: Options{
Model: "llama3.2",
SystemPrompt: "You are Ollama.",
Tools: registry,
ContextWindowTokens: 1024,
},
messages: []api.Message{
{Role: "user", Content: "hello\nsecond line"},
{
Role: "assistant",
Content: "I'll call a tool.",
Thinking: "I should call the fake tool first.",
ToolCalls: []api.ToolCall{{
ID: "call-1",
Function: api.ToolCallFunction{
Name: "fake_tool",
Arguments: toolArgs,
},
}},
},
{Role: "tool", ToolName: "fake_tool", ToolCallID: "call-1", Content: "tool result line 1\ntool result line 2"},
},
}
updated, cmd := m.handleSubmit()
if cmd == nil {
t.Fatal("/prompt should enable managed prompt screen")
}
fm := updated.(chatModel)
if fm.promptDebug == nil {
t.Fatal("/prompt should open prompt debug screen")
}
if len(fm.entries) != 0 {
t.Fatalf("/prompt should not append a transcript entry: %#v", fm.entries)
}
out := stripANSI(fm.View())
body := stripANSI(strings.Join(fm.promptDebugLines(160), "\n"))
for _, want := range []string{
"Prompt",
"full request preview",
} {
if !strings.Contains(out, want) {
t.Fatalf("/prompt view missing %q:\n%s", want, out)
}
}
for _, want := range []string{
"model: llama3.2",
"estimated prompt:",
"/ 1024 tokens",
"messages: 4",
"tools: 1",
"Tools",
"1. fake_tool",
"description:",
"does test work",
"parameters: object",
"1. system",
"You are Ollama.",
"2. user",
"hello",
"second line",
"3. assistant",
"thinking:",
"I should call the fake tool first.",
"content:",
"I'll call a tool.",
"tool call 1: fake_tool",
"id: call-1",
"arguments:",
"query: show me everything",
"4. tool:fake_tool",
"tool_name: fake_tool",
"tool_call_id: call-1",
"tool result",
"tool result line 1",
"tool result line 2",
} {
if !strings.Contains(body, want) {
t.Fatalf("/prompt output missing %q:\n%s", want, body)
}
}
assistantStart := strings.Index(body, "3. assistant")
if assistantStart < 0 {
t.Fatalf("/prompt output missing assistant message:\n%s", body)
}
assistantBody := body[assistantStart:]
thinkingIndex := strings.Index(assistantBody, "thinking:")
contentIndex := strings.Index(assistantBody, "content:")
if thinkingIndex < 0 || contentIndex < 0 {
t.Fatalf("/prompt assistant output missing thinking/content labels:\n%s", body)
}
if thinkingIndex > contentIndex {
t.Fatalf("/prompt assistant thinking should render before content:\n%s", body)
}
for _, unwanted := range []string{`"query":`, `"name": "fake_tool"`} {
if strings.Contains(body, unwanted) {
t.Fatalf("/prompt output should be rendered, not raw JSON; found %q:\n%s", unwanted, body)
}
}
messagesIndex := strings.Index(body, "Messages")
toolsIndex := strings.Index(body, "Tools")
if messagesIndex < 0 || toolsIndex < 0 || toolsIndex < messagesIndex {
t.Fatalf("/prompt should render tools after messages:\n%s", body)
}
updated, cmd = fm.Update(tea.KeyMsg{Type: tea.KeyEsc})
if cmd == nil {
t.Fatal("closing prompt debug should disable prompt mouse mode")
}
fm = updated.(chatModel)
if fm.promptDebug != nil {
t.Fatal("esc should close prompt debug screen")
}
}
func TestChatPromptDebugCapsToolResultPreview(t *testing.T) {
longResult := strings.Repeat("x", maxPromptDebugToolResultRunes+25) + "tail-marker"
m := chatModel{
width: 120,
height: 20,
promptDebug: &chatPromptDebug{
request: api.ChatRequest{
Model: "llama3.2",
Messages: []api.Message{{
Role: "tool",
ToolName: "bash",
ToolCallID: "call-1",
Content: longResult,
}},
},
},
}
body := stripANSI(strings.Join(m.promptDebugLines(120), "\n"))
if strings.Contains(body, "tail-marker") {
t.Fatalf("/prompt should cap rendered tool results:\n%s", body)
}
if !strings.Contains(body, "...") {
t.Fatalf("/prompt capped tool result should show ellipsis:\n%s", body)
}
if got := strings.Count(body, "x"); got != maxPromptDebugToolResultRunes-3 {
t.Fatalf("rendered tool result x count = %d, want %d:\n%s", got, maxPromptDebugToolResultRunes-3, body)
}
if got := m.promptDebug.request.Messages[0].Content; got != longResult {
t.Fatal("/prompt rendering should not mutate the request")
}
}
func TestChatPromptDebugMouseWheelScrolls(t *testing.T) {
m := chatModel{
width: 80,
height: 8,
opts: Options{Model: "llama3.2", ContextWindowTokens: 1024},
promptDebug: &chatPromptDebug{
request: api.ChatRequest{Model: "llama3.2"},
tokens: 10,
},
}
for range 30 {
m.promptDebug.request.Messages = append(m.promptDebug.request.Messages, api.Message{Role: "user", Content: "line"})
}
if m.promptDebugMaxScroll() == 0 {
t.Fatal("test setup should produce a scrollable prompt debug screen")
}
updated, _ := m.Update(tea.MouseMsg{Type: tea.MouseWheelDown})
m = updated.(chatModel)
if m.promptDebug.scroll == 0 {
t.Fatal("mouse wheel down should scroll prompt debug screen")
}
updated, _ = m.Update(tea.MouseMsg{Type: tea.MouseWheelUp})
m = updated.(chatModel)
if m.promptDebug.scroll != 0 {
t.Fatalf("mouse wheel up should return prompt debug screen to top, got scroll %d", m.promptDebug.scroll)
}
}
func TestTruncateInputLineUsesDisplayWidth(t *testing.T) {
line := truncateInputLine(strings.Repeat("界", 10), 10)
if got := lipgloss.Width(line); got > 10 {
t.Fatalf("line %q width = %d, want <= 10", line, got)
}
}
func TestRenderInputBoxTruncationUsesSingleContinuationMarker(t *testing.T) {
lines := renderInputBoxLines("one two three four five six seven", len("one two three four five six seven"), 16, 1, "")
rendered := strings.Join(lines, "\n")
if strings.Contains(rendered, "... ...") {
t.Fatalf("input rendered duplicate continuation marker: %q", rendered)
}
if strings.Contains(rendered, "one two") {
t.Fatalf("input should keep the latest truncated line: %q", rendered)
}
}
type shiftEnterCSITestMsg string
func (m shiftEnterCSITestMsg) String() string {
return string(m)
}
func TestChatInputHandlesShiftEnterCSIMessage(t *testing.T) {
m := chatModel{input: []rune("line one")}
updated, _ := m.Update(shiftEnterCSITestMsg("?CSI[49 51 59 50 117]?"))
m = updated.(chatModel)
if got := string(m.input); got != "line one\n" {
t.Fatalf("input = %q, want newline inserted", got)
}
}
func TestChatInputAcceptsSpace(t *testing.T) {
m := chatModel{}
updated, _ := m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("hello")})
m = updated.(chatModel)
updated, _ = m.Update(tea.KeyMsg{Type: tea.KeySpace, Runes: []rune(" ")})
m = updated.(chatModel)
updated, _ = m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("world")})
m = updated.(chatModel)
if got := string(m.input); got != "hello world" {
t.Fatalf("input = %q, want hello world", got)
}
}
func TestChatLargePasteUsesPlaceholderAndExpandsOnSubmit(t *testing.T) {
pasted := strings.Repeat("line\n", pastedTextPlaceholderMinLines-1) + "line"
m := chatModel{
ctx: context.Background(),
opts: Options{
Model: "test",
Client: chatTestClient{},
},
}
updated, _ := m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune(pasted), Paste: true})
m = updated.(chatModel)
if got, want := string(m.input), "[Pasted text #1 +8 lines]"; got != want {
t.Fatalf("input = %q, want %q", got, want)
}
updated, cmd := m.Update(tea.KeyMsg{Type: tea.KeyEnter})
m = updated.(chatModel)
if cmd == nil {
t.Fatal("enter should start a run")
}
done := waitForRunDone(t, m.events)
if done.err != nil {
t.Fatal(done.err)
}
if done.result == nil || len(done.result.Messages) < 1 || done.result.Messages[0].Content != pasted {
t.Fatalf("messages = %#v, want expanded pasted text", done.result)
}
}
func TestChatBackspaceDeletesWholePastedTextPlaceholder(t *testing.T) {
m := chatModel{
input: []rune("use [Pasted text #1 +8 lines]"),
inputPastedTexts: []chatInputPastedText{{
placeholder: "[Pasted text #1 +8 lines]",
content: "hidden",
}},
}
updated, _ := m.Update(tea.KeyMsg{Type: tea.KeyBackspace})
m = updated.(chatModel)
if got := string(m.input); got != "use " {
t.Fatalf("input after backspace = %q, want pasted text placeholder removed", got)
}
if got := len(m.inputPastedTexts); got != 0 {
t.Fatalf("pasted texts after backspace = %d, want 0", got)
}
}
func TestInitialPromptHistoryLoadsFromMessages(t *testing.T) {
history := initialPromptHistory(context.Background(), Options{
Messages: []api.Message{
{Role: "user", Content: "old prompt"},
{Role: "assistant", Content: "answer"},
{Role: "user", Content: "new prompt"},
},
})
if got, want := strings.Join(history, "|"), "old prompt|new prompt"; got != want {
t.Fatalf("history = %#v, want %s", history, want)
}
}
func TestChatDeletedSlashCommandsAreUnknown(t *testing.T) {
for _, command := range []string{"/copy", "/copy-all", "/launch", "/system", "/history", "/load", "/raw", "/resume", "/set", "/show", "/skills", "/verbose"} {
t.Run(command, func(t *testing.T) {
m := chatModel{input: []rune(command)}
updated, cmd := m.handleSubmit()
if cmd != nil {
t.Fatal("deleted slash command should not return a command")
}
m = updated.(chatModel)
if len(m.entries) != 1 || m.entries[0].role != "error" || !strings.Contains(m.entries[0].content, "Unknown command") {
t.Fatalf("entries = %#v, want unknown command error", m.entries)
}
})
}
}
func TestChatViewRendersSlashCommandSuggestions(t *testing.T) {
m := chatModel{
input: []rune("/"),
width: 80,
height: 18,
}
view := stripANSI(m.View())
for _, want := range []string{"/clear", "/model", "/new", "/think", "/tools"} {
if !strings.Contains(view, want) {
t.Fatalf("view missing %s suggestion: %q", want, view)
}
}
for _, removed := range []string{"/copy", "/copy-all", "/history", "/load", "/raw", "/resume", "/set", "/show", "/skills", "/verbose"} {
if strings.Contains(view, removed) {
t.Fatalf("bare slash should hide removed command %s: %q", removed, view)
}
}
if got := len(m.slashCommandLines(80)); got != maxSlashCompletions {
t.Fatalf("slash suggestions = %d, want %d", got, maxSlashCompletions)
}
}
func TestChatToolsCommandTogglesToolRegistry(t *testing.T) {
registry := &coreagent.Registry{}
registry.Register(chatTestTool{})
calls := 0
m := chatModel{
ctx: context.Background(),
input: []rune("/tools"),
opts: Options{
Model: "llama3.2",
Tools: registry,
ToolRegistryForModel: func(context.Context, string) *coreagent.Registry {
calls++
return registry
},
},
}
updated, cmd := m.handleSubmit()
if cmd != nil {
t.Fatal("/tools should not start a command")
}
m = updated.(chatModel)
if !m.opts.ToolsDisabled || m.opts.Tools == nil {
t.Fatalf("tools off state = disabled:%v tools:%#v", m.opts.ToolsDisabled, m.opts.Tools)
}
if len(m.entries) != 0 {
t.Fatalf("/tools should only update action space, got entries:%#v", m.entries)
}
req, _ := m.requestPreview()
if got := len(req.Tools); got != 0 {
t.Fatalf("request preview tools = %d, want 0", got)
}
m.input = []rune("/tools")
updated, cmd = m.handleSubmit()
if cmd != nil {
t.Fatal("/tools should not start a command")
}
m = updated.(chatModel)
if m.opts.ToolsDisabled || m.opts.Tools == nil {
t.Fatalf("tools on state = disabled:%v tools:%#v", m.opts.ToolsDisabled, m.opts.Tools)
}
if calls != 1 {
t.Fatalf("tool registry calls = %d, want 1", calls)
}
if len(m.entries) != 0 {
t.Fatalf("/tools should only update action space, got entries:%#v", m.entries)
}
req, _ = m.requestPreview()
if got := len(req.Tools); got != 1 {
t.Fatalf("request preview tools = %d, want 1", got)
}
}
func TestChatToolsCommandUsage(t *testing.T) {
m := chatModel{input: []rune("/tools off")}
updated, cmd := m.handleSubmit()
if cmd != nil {
t.Fatal("invalid /tools should not start a command")
}
m = updated.(chatModel)
if m.status != "error" || len(m.entries) != 1 || !strings.Contains(m.entries[0].content, "usage: /tools") {
t.Fatalf("invalid /tools result = status:%q entries:%#v", m.status, m.entries)
}
}
func TestChatSlashCommandSuggestionsIncludePromptAndSave(t *testing.T) {
for _, tt := range []struct {
input string
command string
description string
}{
{input: "/pr", command: "/prompt", description: "show full prompt, tools, and messages"},
{input: "/sa", command: "/save", description: "save request JSON; saved as <filename>.json"},
} {
t.Run(tt.command, func(t *testing.T) {
m := chatModel{input: []rune(tt.input)}
lines := stripANSI(strings.Join(m.slashCommandLines(80), "\n"))
if !strings.Contains(lines, tt.command) || !strings.Contains(lines, tt.description) {
t.Fatalf("suggestions missing %s: %q", tt.command, lines)
}
})
}
}
func TestChatSlashCommandSuggestionsIncludeThink(t *testing.T) {
m := chatModel{input: []rune("/th")}
lines := stripANSI(strings.Join(m.slashCommandLines(80), "\n"))
if !strings.Contains(lines, "/think") || !strings.Contains(lines, "set thinking mode") {
t.Fatalf("suggestions missing /think: %q", lines)
}
}
func TestChatEnterAcceptsSelectedSlashCommand(t *testing.T) {
m := chatModel{input: []rune("/th")}
updated, cmd := m.Update(tea.KeyMsg{Type: tea.KeyEnter})
m = updated.(chatModel)
if cmd != nil {
t.Fatal("think command should not return a command")
}
if m.thinkPicker == nil {
t.Fatal("selected /think command should open picker")
}
}
func TestChatSlashCommandsRunWhileModelResponds(t *testing.T) {
m := chatModel{running: true, input: []rune("/help")}
updated, cmd := m.handleSubmit()
if cmd != nil {
t.Fatal("help command should not return a command")
}
m = updated.(chatModel)
if len(m.entries) != 1 || m.entries[0].role != "slash" {
t.Fatalf("entries = %#v, want immediate slash output", m.entries)
}
}
func TestChatMessageSubmitWhileRunningPreservesDraft(t *testing.T) {
attachment := chatInputAttachment{placeholder: "[image 1]", kind: "image"}
pasted := chatInputPastedText{placeholder: "[pasted 1]", content: "long paste"}
m := chatModel{
running: true,
input: []rune("next prompt [image 1] [pasted 1]"),
inputCursor: 4,
inputCursorSet: true,
inputAttachments: []chatInputAttachment{attachment},
inputPastedTexts: []chatInputPastedText{pasted},
}
updated, cmd := m.handleSubmit()
if cmd != nil {
t.Fatal("busy submit should not start a command")
}
m = updated.(chatModel)
if got := string(m.input); got != "next prompt [image 1] [pasted 1]" {
t.Fatalf("input = %q, want draft preserved", got)
}
if m.inputCursor != 4 || !m.inputCursorSet {
t.Fatalf("cursor not preserved: cursor=%d set=%v", m.inputCursor, m.inputCursorSet)
}
if len(m.inputAttachments) != 1 || m.inputAttachments[0].placeholder != attachment.placeholder || m.inputAttachments[0].kind != attachment.kind {
t.Fatalf("attachments not preserved: %#v", m.inputAttachments)
}
if len(m.inputPastedTexts) != 1 || m.inputPastedTexts[0] != pasted {
t.Fatalf("pasted text not preserved: %#v", m.inputPastedTexts)
}
if len(m.entries) != 0 {
t.Fatalf("busy submit should not add entries: %#v", m.entries)
}
}
func TestChatThinkCommandOpensPicker(t *testing.T) {
m := chatModel{input: []rune("/think")}
updated, cmd := m.handleSubmit()
if cmd != nil {
t.Fatal("think command should not return a command")
}
m = updated.(chatModel)
if m.thinkPicker == nil {
t.Fatal("think picker should open")
}
if view := stripANSI(m.renderThinkPicker(80)); !strings.Contains(view, "Thinking mode") || !strings.Contains(view, "high") {
t.Fatalf("think picker view missing options: %q", view)
}
}
func TestChatThinkCommandSetsModes(t *testing.T) {
for _, tt := range []struct {
input string
want any
}{
{input: "/think on", want: true},
{input: "/think off", want: false},
{input: "/think high", want: "high"},
} {
t.Run(tt.input, func(t *testing.T) {
m := chatModel{input: []rune(tt.input)}
updated, cmd := m.handleSubmit()
if cmd != nil {
t.Fatal("think command should not return a command")
}
m = updated.(chatModel)
if m.opts.Think == nil || m.opts.Think.Value != tt.want {
t.Fatalf("think = %#v, want %#v", m.opts.Think, tt.want)
}
})
}
}
func TestChatViewRendersFileMentionSuggestions(t *testing.T) {
dir := t.TempDir()
if err := os.Mkdir(filepath.Join(dir, "cmd"), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, "README.md"), []byte("hi"), 0o644); err != nil {
t.Fatal(err)
}
m := chatModel{
workingDir: dir,
input: []rune("open @"),
width: 80,
height: 18,
}
view := stripANSI(m.View())
if !strings.Contains(view, "@cmd/") || !strings.Contains(view, "@README.md") {
t.Fatalf("file mention suggestions missing: %q", view)
}
}
func TestChatFileMentionSuggestionsFilterAndComplete(t *testing.T) {
dir := t.TempDir()
if err := os.Mkdir(filepath.Join(dir, "cmd"), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, "README.md"), []byte("hi"), 0o644); err != nil {
t.Fatal(err)
}
m := chatModel{
workingDir: dir,
input: []rune("open @REA"),
}
lines := stripANSI(strings.Join(m.completionLines(80), "\n"))
if !strings.Contains(lines, "@README.md") || strings.Contains(lines, "@cmd/") {
t.Fatalf("filtered file suggestions = %q", lines)
}
m.applyCompletion()
if got := string(m.input); got != "open @README.md " {
t.Fatalf("completed input = %q", got)
}
}