Download
163 lines 5.1 KiBGoModified 2026-10-31 raw
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
package config

import (
	"os"
	"path/filepath"
	"strings"
	"testing"
	"time"
)

func TestLoadSave(t *testing.T) {
	dir := filepath.Join(t.TempDir(), "blinker")
	c, err := Load(dir)
	if err != nil || c.CommandTimeout != Duration(2*time.Minute) || c.Configured() || len(c.Sandbox.ProxyPorts) != 2 {
		t.Fatalf("missing files: %+v, %v", c, err)
	}
	c.Model, c.APIKey, c.Sandbox.ScratchRoot = "m", "sk-1", "/scratch"
	if err := c.Save(dir); err != nil {
		t.Fatal(err)
	}
	fi, err := os.Stat(filepath.Join(dir, AgentFile))
	if err != nil || fi.Mode().Perm() != 0o600 {
		t.Errorf("mode = %v, %v", fi.Mode(), err)
	}
	got, err := Load(dir)
	if err != nil || got.Model != "m" || got.APIKey != "sk-1" || !got.Configured() || got.Sandbox.ScratchRoot != "/scratch" {
		t.Errorf("round trip: %+v, %v", got, err)
	}
	// The sandbox's file holds only its settings: no key.
	data, err := os.ReadFile(filepath.Join(dir, SandboxFile))
	if err != nil || !strings.Contains(string(data), "/scratch") || strings.Contains(string(data), "sk-1") || strings.Contains(string(data), "model") {
		t.Errorf("%s: %s, %v", SandboxFile, data, err)
	}
	if s, err := LoadSandbox(dir); err != nil || s.ScratchRoot != "/scratch" {
		t.Errorf("sandbox settings: %+v, %v", s, err)
	}
}

func TestLoadRejects(t *testing.T) {
	for name, text := range map[string]string{
		"unknown":  `{"modle": "typo"}`,
		"duration": `{"command_timeout": 5}`,
		"param":    `{"thinking_param": "yes"}`,
		"sandbox":  `{"proxy_ports": [443]}`, // in the sandbox's file
		"diff":     `{"diff_command": [""]}`,
		"url":      `{"base_url": "localhost:8080/v1"}`,
		"scheme":   `{"base_url": "file:///etc/passwd"}`,
		"temp":     `{"temperature": 3}`,
		"top_p":    `{"top_p": 1.5}`,
		"top_k":    `{"top_k": -1}`,
		"tokens":   `{"max_tokens": -1}`,
		"ca_cert":  `{"ca_cert": "cert.pem"}`,
		"log_file": `{"log_file": "log.txt"}`,
	} {
		dir := t.TempDir()
		if err := os.WriteFile(filepath.Join(dir, AgentFile), []byte(text), 0o600); err != nil {
			t.Fatal(err)
		}
		if _, err := Load(dir); err == nil {
			t.Errorf("%s: loaded %s", name, text)
		}
	}
	for name, text := range map[string]string{
		"unknown": `{"model": "m"}`, // in the agent's file
		"floor":   `{"extra_blocklist": ["relative/path"]}`,
		"port":    `{"proxy_ports": [70000]}`,
		"scratch": `{"scratch_root": "tmp"}`,
	} {
		dir := t.TempDir()
		if err := os.WriteFile(filepath.Join(dir, SandboxFile), []byte(text), 0o600); err != nil {
			t.Fatal(err)
		}
		if _, err := LoadSandbox(dir); err == nil {
			t.Errorf("sandbox %s: loaded %s", name, text)
		}
		if _, err := Load(dir); err == nil {
			t.Errorf("agent with sandbox %s: loaded %s", name, text)
		}
	}
}

func TestProjectName(t *testing.T) {
	a, b := projectName("/home/u/my proj", "/home/u"), projectName("/home/u/my-proj", "/home/u")
	if a == b || !strings.HasPrefix(a, "my-proj-") || len(a) != len("my-proj-")+8 {
		t.Errorf("names %q, %q", a, b)
	}
	// Outside home, the whole path; the hash tells apart paths that look
	// the same.
	for _, c := range []struct{ project, prefix string }{
		{"/opt/x", "opt-x-"},
		{"/home/u2/x", "home-u2-x-"},
		{"/home/u", "home-"},
		{"/", "root-"},
	} {
		if n := projectName(c.project, "/home/u"); !strings.HasPrefix(n, c.prefix) {
			t.Errorf("%s: %q", c.project, n)
		}
	}
	if projectName("/home/u/opt/x", "/home/u") == projectName("/opt/x", "/home/u") {
		t.Error("~/opt/x and /opt/x share a name")
	}
	if long := projectName("/"+strings.Repeat("x", 200), "/home/u"); len(long) > 90 {
		t.Errorf("long name %q", long)
	}
}

// Anthropic's key goes to Anthropic's host only, not to any URL that
// mentions it.
func TestKeyHost(t *testing.T) {
	t.Setenv("ANTHROPIC_API_KEY", "sk-ant")
	t.Setenv("OPENAI_API_KEY", "sk-oai")
	for url, want := range map[string]string{
		"https://api.anthropic.com/v1":                "sk-ant",
		"https://api.anthropic.com.evil.example/v1":   "sk-oai",
		"https://evil.example/api.anthropic.com/v1":   "sk-oai",
		"https://evil.example/v1?x=api.anthropic.com": "sk-oai",
		"http://api.anthropic.com/v1":                 "sk-oai",
	} {
		if got := (Config{BaseURL: url}).Key(); got != want {
			t.Errorf("%s: key %q, want %q", url, got, want)
		}
	}
}

func TestValidateModelName(t *testing.T) {
	c := Defaults()
	for _, m := range []string{"qwen\x1b]52;c;eA==\x07", "a\u202eb", "x\ny"} {
		c.Model = m
		if c.Validate() == nil {
			t.Errorf("model %q passed", m)
		}
	}
}

// A config holding a key that others can read is refused.
func TestLoadRefusesReadableKey(t *testing.T) {
	dir := t.TempDir()
	path := filepath.Join(dir, AgentFile)
	c := Defaults()
	c.APIKey = "sk-1"
	if err := c.Save(dir); err != nil {
		t.Fatal(err)
	}
	if _, err := Load(dir); err != nil {
		t.Fatalf("a private config: %v", err)
	}
	if err := os.Chmod(path, 0o644); err != nil {
		t.Fatal(err)
	}
	if _, err := Load(dir); err == nil || !strings.Contains(err.Error(), "chmod 600") {
		t.Errorf("a readable key: %v", err)
	}
	c.APIKey = ""
	if err := c.Save(dir); err != nil {
		t.Fatal(err)
	}
	if err := os.Chmod(path, 0o644); err != nil {
		t.Fatal(err)
	}
	if _, err := Load(dir); err != nil {
		t.Errorf("a readable config without a key: %v", err)
	}
}