Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions internal/tui/core/app/input_features.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ const (
)

var workspaceCommandExecutor = defaultWorkspaceCommandExecutor
var readClipboardImage = tuiinfra.ReadClipboardImage
var saveClipboardImageToTempFile = tuiinfra.SaveImageToTempFile
var detectImageMimeType = tuiinfra.DetectImageMimeType

func isWorkspaceCommandInput(input string) bool {
return strings.HasPrefix(strings.TrimSpace(input), workspaceCommandPrefix)
Expand Down Expand Up @@ -296,7 +299,7 @@ func (a *App) addImageAttachment(path string) error {
return fmt.Errorf("image size exceeds %d MB limit", imageMaxSizeBytes/(1024*1024))
}

mimeType := tuiinfra.DetectImageMimeType(absPath)
mimeType := detectImageMimeType(absPath)
if mimeType == "" {
return fmt.Errorf("unsupported image format")
}
Expand Down Expand Up @@ -359,7 +362,7 @@ func (a *App) addImageFromClipboard() error {
return fmt.Errorf("maximum %d image attachments allowed", maxImageAttachments)
}

data, err := tuiinfra.ReadClipboardImage()
data, err := readClipboardImage()
if err != nil {
return fmt.Errorf("failed to read clipboard image: %w", err)
}
Expand All @@ -372,12 +375,12 @@ func (a *App) addImageFromClipboard() error {
return fmt.Errorf("image size exceeds %d MB limit", imageMaxSizeBytes/(1024*1024))
}

tmpPath, err := tuiinfra.SaveImageToTempFile(data, "paste")
tmpPath, err := saveClipboardImageToTempFile(data, "paste")
if err != nil {
return fmt.Errorf("failed to save clipboard image: %w", err)
}

mimeType := tuiinfra.DetectImageMimeType(tmpPath)
mimeType := detectImageMimeType(tmpPath)
if mimeType == "" {
return fmt.Errorf("unsupported image format from clipboard")
}
Expand Down
240 changes: 240 additions & 0 deletions internal/tui/core/app/input_features_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package tui

import (
"context"
"errors"
"fmt"
"os"
"path/filepath"
Expand All @@ -14,6 +16,15 @@ import (
providertypes "neo-code/internal/provider/types"
)

type snapshotErrProviderService struct {
stubProviderService
err error
}

func (s snapshotErrProviderService) ListModelsSnapshot(ctx context.Context) ([]providertypes.ModelDescriptor, error) {
return nil, s.err
}

func TestTokenAndReferenceParsing(t *testing.T) {
start, end, token, ok := tokenRange(" @file/path", tokenSelectorFirst)
if !ok || start != 2 || end != len(" @file/path") || token != "@file/path" {
Expand Down Expand Up @@ -71,6 +82,34 @@ func TestApplyFileReference(t *testing.T) {
}
}

func TestApplyFileReferenceBranches(t *testing.T) {
app, _ := newTestApp(t)
if err := app.applyFileReference(" "); err == nil {
t.Fatalf("expected empty file path error")
}

root := t.TempDir()
app.state.CurrentWorkdir = filepath.Join(root, "workdir")
inside := filepath.Join(app.state.CurrentWorkdir, "a.txt")
outside := filepath.Join(root, "outside.txt")
if err := os.MkdirAll(filepath.Dir(inside), 0o755); err != nil {
t.Fatalf("mkdir inside: %v", err)
}
if err := os.WriteFile(inside, []byte("a"), 0o644); err != nil {
t.Fatalf("write inside: %v", err)
}
if err := os.WriteFile(outside, []byte("b"), 0o644); err != nil {
t.Fatalf("write outside: %v", err)
}

if err := app.applyFileReference(outside); err != nil {
t.Fatalf("apply outside reference error: %v", err)
}
if !strings.Contains(app.input.Value(), "@") {
t.Fatalf("expected file reference token to be inserted")
}
}

func TestImageAttachmentLifecycle(t *testing.T) {
app, _ := newTestApp(t)
root := t.TempDir()
Expand Down Expand Up @@ -176,6 +215,207 @@ func TestComposeMessageWithImageAttachments(t *testing.T) {
}
}

func TestComposeMessageWithImageAttachmentsNoAttachments(t *testing.T) {
app, _ := newTestApp(t)
got := app.composeMessageWithImageAttachments(" hello ")
if got != "hello" {
t.Fatalf("expected trimmed content without attachment block, got %q", got)
}
}

func TestApplyImageReference(t *testing.T) {
app, _ := newTestApp(t)
root := t.TempDir()
imagePath := filepath.Join(root, "ok.png")
if err := os.WriteFile(imagePath, []byte("png"), 0o644); err != nil {
t.Fatalf("write image: %v", err)
}
if err := app.applyImageReference("@image:" + imagePath); err != nil {
t.Fatalf("applyImageReference() error = %v", err)
}
if app.getImageAttachmentCount() != 1 {
t.Fatalf("expected one attachment after applyImageReference")
}
if err := app.applyImageReference("not-an-image-reference"); err == nil {
t.Fatalf("expected invalid image reference error")
}
}

func TestGetAndClearImageAttachments(t *testing.T) {
app, _ := newTestApp(t)
app.pendingImageAttachments = []pendingImageAttachment{
{Name: "a.png", Path: "/tmp/a.png", MimeType: "image/png", Size: 1},
}
if len(app.getImageAttachments()) != 1 {
t.Fatalf("expected one attachment from getter")
}
app.clearImageAttachments()
if len(app.getImageAttachments()) != 0 {
t.Fatalf("expected no attachments after clear")
}
}

func TestLoadImageAttachmentDataInvalidIndex(t *testing.T) {
app, _ := newTestApp(t)
if _, err := app.loadImageAttachmentData(0); err == nil {
t.Fatalf("expected invalid attachment index error")
}
}

func TestAddImageFromClipboardUnsupported(t *testing.T) {
app, _ := newTestApp(t)
if err := app.addImageFromClipboard(); err == nil {
t.Fatalf("expected unsupported clipboard image error")
}
}

func TestAddImageFromClipboardSuccess(t *testing.T) {
app, _ := newTestApp(t)
originalRead := readClipboardImage
originalSave := saveClipboardImageToTempFile
originalDetect := detectImageMimeType
readClipboardImage = func() ([]byte, error) {
return []byte("image-bytes"), nil
}
saveClipboardImageToTempFile = func(data []byte, prefix string) (string, error) {
path := filepath.Join(t.TempDir(), "clipboard.png")
if err := os.WriteFile(path, data, 0o644); err != nil {
t.Fatalf("write temp clipboard image: %v", err)
}
return path, nil
}
detectImageMimeType = func(path string) string { return "image/png" }
defer func() {
readClipboardImage = originalRead
saveClipboardImageToTempFile = originalSave
detectImageMimeType = originalDetect
}()

if err := app.addImageFromClipboard(); err != nil {
t.Fatalf("addImageFromClipboard() error = %v", err)
}
if app.getImageAttachmentCount() != 1 {
t.Fatalf("expected one clipboard image attachment")
}
}

func TestAddImageFromClipboardBranches(t *testing.T) {
app, _ := newTestApp(t)
originalRead := readClipboardImage
originalSave := saveClipboardImageToTempFile
originalDetect := detectImageMimeType
defer func() {
readClipboardImage = originalRead
saveClipboardImageToTempFile = originalSave
detectImageMimeType = originalDetect
}()

readClipboardImage = func() ([]byte, error) { return nil, nil }
if err := app.addImageFromClipboard(); err == nil {
t.Fatalf("expected no image in clipboard error")
}

readClipboardImage = func() ([]byte, error) { return make([]byte, imageMaxSizeBytes+1), nil }
if err := app.addImageFromClipboard(); err == nil {
t.Fatalf("expected image size limit error")
}

readClipboardImage = func() ([]byte, error) { return []byte("x"), nil }
saveClipboardImageToTempFile = func(data []byte, prefix string) (string, error) {
return filepath.Join(t.TempDir(), "clipboard.bin"), nil
}
detectImageMimeType = func(path string) string { return "" }
if err := app.addImageFromClipboard(); err == nil {
t.Fatalf("expected unsupported image format error")
}

readClipboardImage = func() ([]byte, error) { return []byte("x"), nil }
saveClipboardImageToTempFile = func(data []byte, prefix string) (string, error) {
return "", errors.New("save failed")
}
if err := app.addImageFromClipboard(); err == nil {
t.Fatalf("expected save failure error")
}
}

func TestCheckModelImageSupportErrorAndModelNotFound(t *testing.T) {
app, _ := newTestApp(t)
app.providerSvc = snapshotErrProviderService{
stubProviderService: stubProviderService{},
err: errors.New("boom"),
}
if app.checkModelImageSupport() {
t.Fatalf("expected false when provider snapshot fails")
}
if !app.currentModelCapabilities.checked {
t.Fatalf("expected capability cache to be marked checked after failure")
}

app.currentModelCapabilities = modelCapabilityState{}
app.providerSvc = stubProviderService{
providers: []configstate.ProviderOption{{ID: app.state.CurrentProvider, Name: app.state.CurrentProvider}},
models: []providertypes.ModelDescriptor{{
ID: "other-model",
}},
}
if app.checkModelImageSupport() {
t.Fatalf("expected false when current model is missing from snapshot")
}
}

func TestExecuteWorkspaceCommand(t *testing.T) {
app, _ := newTestApp(t)
original := workspaceCommandExecutor
workspaceCommandExecutor = func(ctx context.Context, cfg config.Config, workdir string, command string) (string, error) {
if command != "echo hi" {
t.Fatalf("unexpected command: %q", command)
}
return "ok", nil
}
defer func() { workspaceCommandExecutor = original }()

command, output, err := executeWorkspaceCommand(context.Background(), app.configManager, app.state.CurrentWorkdir, "& echo hi")
if err != nil {
t.Fatalf("executeWorkspaceCommand() error = %v", err)
}
if command != "echo hi" || output != "ok" {
t.Fatalf("unexpected execute result command=%q output=%q", command, output)
}

if _, _, err := executeWorkspaceCommand(context.Background(), app.configManager, app.state.CurrentWorkdir, "& "); err == nil {
t.Fatalf("expected invalid workspace command error")
}
}

func TestDefaultWorkspaceCommandExecutor(t *testing.T) {
cfg := config.Config{Workdir: t.TempDir(), Shell: "bash", ToolTimeoutSec: 1}
if _, err := defaultWorkspaceCommandExecutor(context.Background(), cfg, cfg.Workdir, ""); err == nil {
t.Fatalf("expected empty command to fail")
}
}

func TestRunWorkspaceCommandCmd(t *testing.T) {
app, _ := newTestApp(t)
original := workspaceCommandExecutor
workspaceCommandExecutor = func(ctx context.Context, cfg config.Config, workdir string, command string) (string, error) {
return "done", nil
}
defer func() { workspaceCommandExecutor = original }()

cmd := runWorkspaceCommand(app.configManager, app.state.CurrentWorkdir, "& echo hi")
if cmd == nil {
t.Fatalf("expected workspace command cmd")
}
msg := cmd()
result, ok := msg.(workspaceCommandResultMsg)
if !ok {
t.Fatalf("expected workspaceCommandResultMsg, got %T", msg)
}
if result.Command != "echo hi" || result.Output != "done" || result.Err != nil {
t.Fatalf("unexpected workspace result: %+v", result)
}
}

func TestUpdateSendWithImageAttachmentsComposesRuntimeInput(t *testing.T) {
app, runtime := newTestApp(t)
root := t.TempDir()
Expand Down
Loading
Loading