Download
124 lines 3.7 KiBGoModified 2026-10-31 raw
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124
package agent

import (
	"context"
	"errors"
	"os"
	"path/filepath"
	"strings"
	"syscall"
	"testing"
	"time"

	"blinker/internal/llm"
	"blinker/internal/sandbox"
)

func TestInstructions(t *testing.T) {
	a, events := withCalls(t, &llm.Client{}, 0, 0)
	home, _ := os.LookupEnv("HOME")
	a.ConfigDir = filepath.Join(home, ".config", "blinker")
	write := func(path, text string) {
		t.Helper()
		if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
			t.Fatal(err)
		}
		if err := os.WriteFile(path, []byte(text), 0o600); err != nil {
			t.Fatal(err)
		}
	}
	project := filepath.Join(a.Tools.SB.Project(), "AGENTS.md")
	notices := func() string {
		var b strings.Builder
		for _, ev := range *events {
			if n, ok := ev.(Notice); ok {
				b.WriteString(n.Text + "\n")
			}
		}
		*events = nil
		return b.String()
	}

	// Neither file: nothing to say.
	a.loadInstructions(context.Background())
	if n := notices(); n != "" || strings.Contains(a.systemPrompt(), "instructions, ") {
		t.Errorf("with no files: notices %q", n)
	}

	write(filepath.Join(a.ConfigDir, "AGENTS.md"), "Answer in French.\n")
	write(project, "Use tabs.\nRun make test.\n")
	a.loadInstructions(context.Background())
	sys := a.systemPrompt()
	if !strings.Contains(sys, "own instructions, for every project:\n\nAnswer in French.") ||
		!strings.Contains(sys, "Use tabs.\nRun make test.") ||
		strings.Index(sys, "Answer in French.") > strings.Index(sys, "Use tabs.") {
		t.Errorf("system prompt:\n%s", sys)
	}
	if n := notices(); strings.Count(n, "Loaded the instructions") != 2 || !strings.Contains(n, "(2 lines)") {
		t.Errorf("notices %q", n)
	}

	// A project's AGENTS.md that leads to a credential is read in the
	// sandbox, which refuses it.
	write(filepath.Join(home, ".aws", "credentials"), "aws_secret_access_key = SECRET\n")
	if err := os.Remove(project); err != nil {
		t.Fatal(err)
	}
	if err := os.Symlink(filepath.Join(home, ".aws", "credentials"), project); err != nil {
		t.Fatal(err)
	}
	a.loadInstructions(context.Background())
	if strings.Contains(a.systemPrompt(), "SECRET") {
		t.Error("the credential reached the system prompt")
	}
	if n := notices(); !strings.Contains(n, "Did not load the instructions in "+project) {
		t.Errorf("notices %q", n)
	}

	// Too large, or not text: refused.
	if err := os.Remove(project); err != nil {
		t.Fatal(err)
	}
	write(project, strings.Repeat("x", MaxInstructions+1))
	write(filepath.Join(a.ConfigDir, "AGENTS.md"), "a\x00b")
	a.loadInstructions(context.Background())
	if a.userInstructions != "" || a.projectInstructions != "" {
		t.Error("a bad file was loaded")
	}
	if n := notices(); !strings.Contains(n, "over 32 KB") || !strings.Contains(n, "not UTF-8 text") {
		t.Errorf("notices %q", n)
	}
}

// The user's instructions are read only from a regular file, up to the
// limit: a FIFO would block, a device may never end.
func TestReadLimited(t *testing.T) {
	dir := t.TempDir()
	fifo := filepath.Join(dir, "fifo")
	if err := syscall.Mkfifo(fifo, 0o600); err != nil {
		t.Fatal(err)
	}
	done := make(chan error, 1)
	go func() { _, err := ReadLimited(fifo, 10); done <- err }()
	select {
	case err := <-done:
		if err == nil {
			t.Error("read a FIFO")
		}
	case <-time.After(5 * time.Second):
		t.Fatal("reading a FIFO blocked")
	}
	if _, err := ReadLimited("/dev/zero", 10); err == nil {
		t.Error("read /dev/zero")
	}
	big := filepath.Join(dir, "big")
	if err := os.WriteFile(big, []byte("12345678901"), 0o600); err != nil {
		t.Fatal(err)
	}
	if _, err := ReadLimited(big, 10); !errors.Is(err, sandbox.ErrTooLarge) {
		t.Errorf("an 11-byte file with a limit of 10: %v", err)
	}
	if data, err := ReadLimited(big, 11); err != nil || string(data) != "12345678901" {
		t.Errorf("ReadLimited = %q, %v", data, err)
	}
}