mirror of
https://github.com/patriceckhart/zot.git
synced 2026-06-28 14:23:41 +02:00
The read tool used to prefix every line with '%6d\t' (6-digit line number + tab), which added ~7 bytes / ~2 tokens per line to every read. On typical source files that's 15-20% of the read's token budget, repeated on every subsequent turn as the tool result stays in context. The line numbers weren't earning their keep: the model edits via exact-match text replacement, never line ranges, and the tui has always been capable of drawing its own gutter. Now it does: - Tool output is raw file bytes, no prefix. - A 'start_line' detail is attached to the ToolResult so the tui knows where to start counting. - The tui renders a synthetic gutter over the raw content using the existing renderNumberedFile path (new: renderRawFile), so on-screen it still looks exactly like cat -n. The old numbered format is still recognised for legacy transcripts saved before this commit (looksLikeNumberedFile guard stays). Measured: sample.ts (388 lines) used to cost 14957 bytes to send, now costs 12291 bytes (raw file). Saves ~670 tokens per read of a medium file; the same fraction applies to larger files too. Tests: TestReadOffsetLimit rewritten to assert raw output + start_line detail. TUI renderToolText signature grew one int (startLine) plumbed through renderToolResultContent.
186 lines
4.9 KiB
Go
186 lines
4.9 KiB
Go
package tools
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/patriceckhart/zot/internal/provider"
|
|
)
|
|
|
|
func mustJSON(t *testing.T, v any) json.RawMessage {
|
|
t.Helper()
|
|
b, err := json.Marshal(v)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return b
|
|
}
|
|
|
|
func TestReadText(t *testing.T) {
|
|
dir := t.TempDir()
|
|
p := filepath.Join(dir, "a.txt")
|
|
if err := os.WriteFile(p, []byte("hello\nworld\n"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
tool := &ReadTool{CWD: dir}
|
|
res, err := tool.Execute(context.Background(), mustJSON(t, map[string]any{"path": "a.txt"}), nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got := res.Content[0].(provider.TextBlock).Text
|
|
if !strings.Contains(got, "hello") || !strings.Contains(got, "world") {
|
|
t.Fatalf("got %q", got)
|
|
}
|
|
}
|
|
|
|
func TestReadOffsetLimit(t *testing.T) {
|
|
dir := t.TempDir()
|
|
p := filepath.Join(dir, "a.txt")
|
|
os.WriteFile(p, []byte("1\n2\n3\n4\n5\n"), 0o644)
|
|
tool := &ReadTool{CWD: dir}
|
|
res, _ := tool.Execute(context.Background(), mustJSON(t, map[string]any{"path": "a.txt", "offset": 2, "limit": 2}), nil)
|
|
got := res.Content[0].(provider.TextBlock).Text
|
|
// Current output format is raw bytes (no embedded line numbers):
|
|
// the tui draws its own gutter from the `start_line` detail.
|
|
if got != "2\n3\n" {
|
|
t.Fatalf("want \"2\\n3\\n\", got %q", got)
|
|
}
|
|
if start, ok := res.Details.(map[string]any)["start_line"]; !ok || start != 2 {
|
|
t.Errorf("start_line detail want 2, got %v", start)
|
|
}
|
|
}
|
|
|
|
func TestReadBinaryRejected(t *testing.T) {
|
|
dir := t.TempDir()
|
|
p := filepath.Join(dir, "b.bin")
|
|
os.WriteFile(p, []byte{0x00, 0x01, 0x02}, 0o644)
|
|
tool := &ReadTool{CWD: dir}
|
|
if _, err := tool.Execute(context.Background(), mustJSON(t, map[string]any{"path": "b.bin"}), nil); err == nil {
|
|
t.Fatal("want binary rejection")
|
|
}
|
|
}
|
|
|
|
func TestWriteCreatesDirs(t *testing.T) {
|
|
dir := t.TempDir()
|
|
tool := &WriteTool{CWD: dir}
|
|
_, err := tool.Execute(context.Background(), mustJSON(t, map[string]any{"path": "sub/a.txt", "content": "hi"}), nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
b, err := os.ReadFile(filepath.Join(dir, "sub", "a.txt"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(b) != "hi" {
|
|
t.Fatalf("got %q", string(b))
|
|
}
|
|
}
|
|
|
|
func TestEditSingle(t *testing.T) {
|
|
dir := t.TempDir()
|
|
p := filepath.Join(dir, "a.txt")
|
|
os.WriteFile(p, []byte("hello world\n"), 0o644)
|
|
tool := &EditTool{CWD: dir}
|
|
_, err := tool.Execute(context.Background(), mustJSON(t, map[string]any{
|
|
"path": "a.txt",
|
|
"edits": []map[string]any{{"oldText": "world", "newText": "gopher"}},
|
|
}), nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
b, _ := os.ReadFile(p)
|
|
if string(b) != "hello gopher\n" {
|
|
t.Fatalf("got %q", string(b))
|
|
}
|
|
}
|
|
|
|
func TestEditMultiple(t *testing.T) {
|
|
dir := t.TempDir()
|
|
p := filepath.Join(dir, "a.txt")
|
|
os.WriteFile(p, []byte("a\nb\nc\n"), 0o644)
|
|
tool := &EditTool{CWD: dir}
|
|
_, err := tool.Execute(context.Background(), mustJSON(t, map[string]any{
|
|
"path": "a.txt",
|
|
"edits": []map[string]any{
|
|
{"oldText": "a", "newText": "A"},
|
|
{"oldText": "c", "newText": "C"},
|
|
},
|
|
}), nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
b, _ := os.ReadFile(p)
|
|
if string(b) != "A\nb\nC\n" {
|
|
t.Fatalf("got %q", string(b))
|
|
}
|
|
}
|
|
|
|
func TestEditAmbiguous(t *testing.T) {
|
|
dir := t.TempDir()
|
|
p := filepath.Join(dir, "a.txt")
|
|
os.WriteFile(p, []byte("x\nx\n"), 0o644)
|
|
tool := &EditTool{CWD: dir}
|
|
_, err := tool.Execute(context.Background(), mustJSON(t, map[string]any{
|
|
"path": "a.txt",
|
|
"edits": []map[string]any{{"oldText": "x", "newText": "y"}},
|
|
}), nil)
|
|
if err == nil {
|
|
t.Fatal("want ambiguous error")
|
|
}
|
|
}
|
|
|
|
func TestEditPreservesCRLF(t *testing.T) {
|
|
dir := t.TempDir()
|
|
p := filepath.Join(dir, "a.txt")
|
|
os.WriteFile(p, []byte("hello\r\nworld\r\n"), 0o644)
|
|
tool := &EditTool{CWD: dir}
|
|
_, err := tool.Execute(context.Background(), mustJSON(t, map[string]any{
|
|
"path": "a.txt",
|
|
"edits": []map[string]any{{"oldText": "world", "newText": "gopher"}},
|
|
}), nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
b, _ := os.ReadFile(p)
|
|
if string(b) != "hello\r\ngopher\r\n" {
|
|
t.Fatalf("got %q", string(b))
|
|
}
|
|
}
|
|
|
|
func TestBashSuccess(t *testing.T) {
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("posix shell only")
|
|
}
|
|
tool := &BashTool{CWD: t.TempDir()}
|
|
res, err := tool.Execute(context.Background(), mustJSON(t, map[string]any{"command": "echo hi"}), nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got := res.Content[0].(provider.TextBlock).Text
|
|
if !strings.Contains(got, "hi") || !strings.Contains(got, "[exit 0]") {
|
|
t.Fatalf("got %q", got)
|
|
}
|
|
if res.IsError {
|
|
t.Fatal("unexpected error flag")
|
|
}
|
|
}
|
|
|
|
func TestBashFailure(t *testing.T) {
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("posix shell only")
|
|
}
|
|
tool := &BashTool{CWD: t.TempDir()}
|
|
res, _ := tool.Execute(context.Background(), mustJSON(t, map[string]any{"command": "false"}), nil)
|
|
if !res.IsError {
|
|
t.Fatal("want error")
|
|
}
|
|
got := res.Content[0].(provider.TextBlock).Text
|
|
if !strings.Contains(got, "[exit 1]") {
|
|
t.Fatalf("got %q", got)
|
|
}
|
|
}
|