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)
}
}
|