Vectrum
Sign In
182 lines · 6.1 KB · Go
package tools

import (
	"context"
	"os"
	"path/filepath"
	"strings"
	"sync"
	"testing"
	"time"

	"codey/internal/diag"
)

// stubDiagnoser records what it was asked about and answers with a fixed
// block, so the tests are about the tools' handling and not about any linter.
type stubDiagnoser struct {
	mu    sync.Mutex
	calls []string
	roots []string
	reply string
}

func (s *stubDiagnoser) Report(_ context.Context, root, path string) string {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.calls = append(s.calls, path)
	s.roots = append(s.roots, root)
	return s.reply
}

const block = "diagnostics (gopls): 1 problem\n  sample.go:6:2: undefined: fmt.Printl"

func TestEditAppendsDiagnosticsToASuccessfulResult(t *testing.T) {
	root, track, _ := editFixture(t, sample)
	stub := &stubDiagnoser{reply: block}
	tool := NewEdit(root, track, WithDiagnostics(stub))

	out, err := edit(t, tool, EditArgs{Path: "sample.go", OldStr: `fmt.Println("hello", name)`, NewStr: `fmt.Printl("hello", name)`})
	if err != nil {
		t.Fatalf("edit: %v", err)
	}
	if !strings.HasSuffix(out, "\n\n"+block) {
		t.Errorf("diagnostics not appended:\n%s", out)
	}
	if !strings.Contains(out, "-fmt.Println") {
		t.Errorf("the diff should still come first:\n%s", out)
	}
	if len(stub.calls) != 1 || stub.calls[0] != filepath.Join(root, "sample.go") || stub.roots[0] != root {
		t.Errorf("Report called with %v in %v", stub.calls, stub.roots)
	}
}

func TestEditSilentDiagnoserLeavesTheResultAlone(t *testing.T) {
	root, track, _ := editFixture(t, sample)
	plain, err := edit(t, NewEdit(root, track), EditArgs{Path: "sample.go", OldStr: `"world"`, NewStr: `"there"`})
	if err != nil {
		t.Fatal(err)
	}
	// Reset the file and the tracker, then do the same edit with a silent
	// diagnoser: the result must be byte-identical.
	path := filepath.Join(root, "sample.go")
	if err := os.WriteFile(path, []byte(sample), 0o644); err != nil {
		t.Fatal(err)
	}
	info, _ := os.Stat(path)
	track.MarkRead(path, info.ModTime())
	stub := &stubDiagnoser{}
	withDiag, err := edit(t, NewEdit(root, track, WithDiagnostics(stub)), EditArgs{Path: "sample.go", OldStr: `"world"`, NewStr: `"there"`})
	if err != nil {
		t.Fatal(err)
	}
	if withDiag != plain {
		t.Errorf("silent diagnoser changed the result:\n%q\nvs\n%q", withDiag, plain)
	}
	if len(stub.calls) != 1 {
		t.Errorf("Report called %d times", len(stub.calls))
	}
}

func TestEditFailureDoesNotConsultDiagnostics(t *testing.T) {
	root, track, _ := editFixture(t, sample)
	stub := &stubDiagnoser{reply: block}
	tool := NewEdit(root, track, WithDiagnostics(stub))

	_, err := edit(t, tool, EditArgs{Path: "sample.go", OldStr: "not in the file", NewStr: "x"})
	if err == nil {
		t.Fatal("expected a miss")
	}
	if strings.Contains(err.Error(), "diagnostics") {
		t.Errorf("diagnostics leaked into an error: %v", err)
	}
	if len(stub.calls) != 0 {
		t.Errorf("Report called on a failed edit: %v", stub.calls)
	}
}

func TestWriteAppendsDiagnostics(t *testing.T) {
	root := t.TempDir()
	stub := &stubDiagnoser{reply: block}
	tool := NewWrite(root, newTracker(), WithDiagnostics(stub))

	out, err := run(t, tool, map[string]any{"path": "new/sample.go", "content": "package main\n"})
	if err != nil {
		t.Fatalf("write: %v", err)
	}
	want := "Created new/sample.go (13 bytes, 1 lines).\n\n" + block
	if out != want {
		t.Errorf("got %q, want %q", out, want)
	}
	if len(stub.calls) != 1 || stub.calls[0] != filepath.Join(root, "new", "sample.go") {
		t.Errorf("Report called with %v", stub.calls)
	}

	// A refused write never gets that far: a file that predates the session
	// and has never been read through the tracker may not be overwritten.
	stub.calls = nil
	unread := filepath.Join(root, "unread.go")
	if err := os.WriteFile(unread, []byte("package x\n"), 0o644); err != nil {
		t.Fatal(err)
	}
	past := time.Now().Add(-time.Hour)
	os.Chtimes(unread, past, past)
	if _, err := run(t, tool, map[string]any{"path": "unread.go", "content": "again\n"}); err == nil {
		t.Fatal("overwrite of a file never read was allowed")
	}
	if len(stub.calls) != 0 {
		t.Errorf("Report called for a refused write: %v", stub.calls)
	}
}

func TestDescriptionMentionsDiagnosticsOnlyWhenWired(t *testing.T) {
	root := t.TempDir()
	if strings.Contains(NewEdit(root, nil).Description(), "diagnostics") {
		t.Error("edit_file promises diagnostics it has no way to produce")
	}
	if !strings.Contains(NewEdit(root, nil, WithDiagnostics(&stubDiagnoser{})).Description(), "diagnostics") {
		t.Error("edit_file with a Diagnoser does not tell the model to expect one")
	}
	if strings.Contains(NewWrite(root, nil).Description(), "diagnostics") {
		t.Error("write_file promises diagnostics it has no way to produce")
	}
}

// The wiring main.go does: a *diag.Runner that may be nil when the config
// says off. A nil pointer inside a non-nil interface must still be harmless.
func TestNilRunnerThroughTheOptionIsHarmless(t *testing.T) {
	var runner *diag.Runner
	root, track, _ := editFixture(t, sample)
	tool := NewEdit(root, track, WithDiagnostics(runner))
	out, err := edit(t, tool, EditArgs{Path: "sample.go", OldStr: `"world"`, NewStr: `"there"`})
	if err != nil {
		t.Fatal(err)
	}
	if strings.Contains(out, "diagnostics") {
		t.Errorf("nil runner produced diagnostics: %s", out)
	}
}

// With a real Runner and the Go toolchain, an edit that breaks the build says
// so in the tool result. This is the feature, end to end, minus the model.
func TestEditWithRealRunnerReportsTheCompilerError(t *testing.T) {
	if testing.Short() {
		t.Skip("runs the go tool")
	}
	root, track, _ := editFixture(t, sample)
	if err := os.WriteFile(filepath.Join(root, "go.mod"), []byte("module sample\n\ngo 1.22\n"), 0o644); err != nil {
		t.Fatal(err)
	}
	runner := diag.NewRunner()
	defer runner.Close()
	tool := NewEdit(root, track, WithDiagnostics(runner))

	out, err := edit(t, tool, EditArgs{Path: "sample.go", OldStr: `fmt.Println("hello", name)`, NewStr: `fmt.Printl("hello", name)`})
	if err != nil {
		t.Fatalf("edit: %v", err)
	}
	if !strings.Contains(out, "diagnostics (") || !strings.Contains(out, "sample.go:6:6: undefined: fmt.Printl") {
		// gopls, if installed, says the same thing from a different source.
		t.Errorf("compiler error not reported:\n%s", out)
	}
}