Vectrum
Sign In
179 lines · 4.8 KB · Go
package tools

import (
	"context"
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"testing"
	"time"
)

type fakeTracker struct {
	path string
	mod  time.Time
	n    int
}

func (f *fakeTracker) MarkRead(path string, mod time.Time) {
	f.path, f.mod, f.n = path, mod, f.n+1
}

func writeFile(t *testing.T, dir, name, content string) string {
	t.Helper()
	p := filepath.Join(dir, name)
	if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(p, []byte(content), 0o644); err != nil {
		t.Fatal(err)
	}
	return p
}

func readCall(t *testing.T, tool Tool, args any) (string, error) {
	t.Helper()
	raw, err := json.Marshal(args)
	if err != nil {
		t.Fatal(err)
	}
	return tool.Execute(context.Background(), raw)
}

func TestReadFileNumbersLines(t *testing.T) {
	dir := t.TempDir()
	writeFile(t, dir, "main.go", "package main\n\nfunc main() {}\n")
	out, err := readCall(t, NewRead(dir, nil), ReadArgs{Path: "main.go"})
	if err != nil {
		t.Fatalf("read: %v", err)
	}
	want := "     1\tpackage main\n     2\t\n     3\tfunc main() {}\n"
	if out != want {
		t.Errorf("got:\n%q\nwant:\n%q", out, want)
	}
}

func TestReadFileOffsetAndLimit(t *testing.T) {
	dir := t.TempDir()
	var sb strings.Builder
	for i := 1; i <= 10; i++ {
		fmt.Fprintf(&sb, "line %d\n", i)
	}
	writeFile(t, dir, "ten.txt", sb.String())
	tool := NewRead(dir, nil)

	out, err := readCall(t, tool, ReadArgs{Path: "ten.txt", Offset: 4, Limit: 2})
	if err != nil {
		t.Fatalf("read: %v", err)
	}
	if !strings.Contains(out, "     4\tline 4") || !strings.Contains(out, "     5\tline 5") {
		t.Errorf("window should be lines 4-5, got:\n%s", out)
	}
	if strings.Contains(out, "line 6") {
		t.Errorf("limit not respected:\n%s", out)
	}
	if !strings.Contains(out, "offset=6") {
		t.Errorf("a partial read should tell the model how to continue, got:\n%s", out)
	}
}

func TestReadFileErrorsCarryEnoughToRecover(t *testing.T) {
	dir := t.TempDir()
	writeFile(t, dir, "sub/a.go", "x\n")
	tool := NewRead(dir, nil)

	tests := []struct {
		name string
		args ReadArgs
		want string
	}{
		{name: "missing file names the path", args: ReadArgs{Path: "nope.go"}, want: "no such file: nope.go"},
		{name: "directory suggests another tool", args: ReadArgs{Path: "sub"}, want: "is a directory"},
		{name: "offset past EOF reports the real length", args: ReadArgs{Path: "sub/a.go", Offset: 99}, want: "which has 1 lines"},
		{name: "empty path", args: ReadArgs{}, want: "path is required"},
	}
	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			_, err := readCall(t, tool, tc.args)
			if err == nil {
				t.Fatal("want an error")
			}
			if !strings.Contains(err.Error(), tc.want) {
				t.Errorf("error = %q, want it to contain %q", err, tc.want)
			}
		})
	}
}

func TestReadFileMarksTheReadWithFileMtime(t *testing.T) {
	dir := t.TempDir()
	path := writeFile(t, dir, "tracked.go", "a\n")
	info, err := os.Stat(path)
	if err != nil {
		t.Fatal(err)
	}
	tr := &fakeTracker{}
	if _, err := readCall(t, NewRead(dir, tr), ReadArgs{Path: "tracked.go"}); err != nil {
		t.Fatalf("read: %v", err)
	}
	if tr.n != 1 {
		t.Fatalf("MarkRead called %d times, want 1", tr.n)
	}
	if tr.path != path {
		t.Errorf("tracked path = %q, want the resolved path %q", tr.path, path)
	}
	if !tr.mod.Equal(info.ModTime()) {
		t.Errorf("tracked mtime = %v, want the file's %v", tr.mod, info.ModTime())
	}
}

func TestReadFileDoesNotTrackFailedReads(t *testing.T) {
	tr := &fakeTracker{}
	if _, err := readCall(t, NewRead(t.TempDir(), tr), ReadArgs{Path: "ghost.go"}); err == nil {
		t.Fatal("want an error")
	}
	if tr.n != 0 {
		t.Error("a failed read must not count as having read the file")
	}
}

func TestReadFileEmptyFile(t *testing.T) {
	dir := t.TempDir()
	abs := writeFile(t, dir, "empty.txt", "")
	out, err := readCall(t, NewRead(dir, nil), ReadArgs{Path: abs})
	if err != nil {
		t.Fatalf("an absolute path inside the root should work: %v", err)
	}
	if !strings.Contains(out, "is empty") {
		t.Errorf("out = %q, want an explicit empty-file note rather than a blank result", out)
	}
}

func TestReadFileCapsRunawayLines(t *testing.T) {
	dir := t.TempDir()
	writeFile(t, dir, "min.js", strings.Repeat("x", maxLineRunes+500)+"\n")
	out, err := readCall(t, NewRead(dir, nil), ReadArgs{Path: "min.js"})
	if err != nil {
		t.Fatalf("read: %v", err)
	}
	if !strings.Contains(out, "500 more characters on this line") {
		t.Errorf("long line should be capped with a count, got %d bytes", len(out))
	}
}

func TestHeadTailKeepsTheEnd(t *testing.T) {
	var lines []string
	for i := 1; i <= 50; i++ {
		lines = append(lines, fmt.Sprintf("line %d", i))
	}
	got := headTail(strings.Join(lines, "\n"), 2, 2)
	want := "line 1\nline 2\n… 46 lines elided …\nline 49\nline 50"
	if got != want {
		t.Errorf("got:\n%s\nwant:\n%s", got, want)
	}
	if short := headTail("a\nb\nc", 2, 2); short != "a\nb\nc" {
		t.Errorf("short input should pass through unchanged, got %q", short)
	}
}