Files
teabot/internal/config/config_test.go
T
unkinben 1b4448afb4
ci/woodpecker/pr/test Pipeline failed
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
Add teabot daemon implementation
teabot watches Gitea repos and dispatches one-shot Claude Code sessions in
Docker containers to work issues and review PRs, acting as configurable bot
personalities.

Claude-Session: https://claude.ai/code/session_015ur3i7D2azsMAWTSVABApv
2026-07-26 23:36:21 +10:00

281 lines
7.8 KiB
Go

package config
import (
"os"
"path/filepath"
"testing"
"time"
)
// writeTeaConfig writes a minimal tea config.yml and returns its path.
func writeTeaConfig(t *testing.T, dir, name, url, token, user string, isDefault bool) string {
t.Helper()
path := filepath.Join(dir, name+".yml")
content := "logins:\n" +
" - name: " + name + "\n" +
" url: " + url + "\n" +
" token: " + token + "\n" +
" default: " + boolStr(isDefault) + "\n" +
" user: " + user + "\n"
if err := os.WriteFile(path, []byte(content), 0o600); err != nil {
t.Fatalf("writing tea config: %v", err)
}
return path
}
func boolStr(b bool) string {
if b {
return "true"
}
return "false"
}
func writeConfig(t *testing.T, dir, body string) string {
t.Helper()
path := filepath.Join(dir, "config.yaml")
if err := os.WriteFile(path, []byte(body), 0o600); err != nil {
t.Fatalf("writing config: %v", err)
}
return path
}
func TestLoadAppliesDefaultsAndResolvesPersonalities(t *testing.T) {
dir := t.TempDir()
impl := writeTeaConfig(t, dir, "impl", "https://git.unkin.net", "tok-impl", "implbot", true)
rev := writeTeaConfig(t, dir, "rev", "https://git.unkin.net", "tok-rev", "revbot", false)
body := `repos:
- unkin/teabot
personalities:
- name: implementer
tea_config: ` + impl + `
role: implementer
git_name: Impl Bot
git_email: impl@unkin.net
- name: reviewer
tea_config: ` + rev + `
role: reviewer
git_name: Rev Bot
git_email: rev@unkin.net
`
cfg, err := Load(writeConfig(t, dir, body))
if err != nil {
t.Fatalf("Load: %v", err)
}
if cfg.GiteaURL != DefaultGiteaURL {
t.Errorf("GiteaURL default = %q, want %q", cfg.GiteaURL, DefaultGiteaURL)
}
if cfg.PollInterval != DefaultPollInterval {
t.Errorf("PollInterval default = %s, want %s", cfg.PollInterval, DefaultPollInterval)
}
if cfg.JobImage != DefaultJobImage {
t.Errorf("JobImage default = %q, want %q", cfg.JobImage, DefaultJobImage)
}
if cfg.MaxConcurrent != DefaultMaxConcurrent {
t.Errorf("MaxConcurrent default = %d, want %d", cfg.MaxConcurrent, DefaultMaxConcurrent)
}
// Personalities must be resolved from their tea configs.
if got := cfg.Personalities[0]; got.Login != "implbot" || got.Token != "tok-impl" {
t.Errorf("implementer resolved = login %q token %q", got.Login, got.Token)
}
if got := cfg.Personalities[1]; got.Login != "revbot" || got.Token != "tok-rev" {
t.Errorf("reviewer resolved = login %q token %q", got.Login, got.Token)
}
}
func TestLoadHonoursOverrides(t *testing.T) {
dir := t.TempDir()
tea := writeTeaConfig(t, dir, "both", "https://git.example.com", "tok", "bot", true)
body := `gitea_url: https://git.example.com/
poll_interval: 5s
job_timeout: 10m
max_concurrent: 7
job_image: example/img:1
repos:
- foo/bar
personalities:
- name: both
tea_config: ` + tea + `
role: both
git_name: Bot
git_email: bot@example.com
`
cfg, err := Load(writeConfig(t, dir, body))
if err != nil {
t.Fatalf("Load: %v", err)
}
if cfg.GiteaURL != "https://git.example.com" {
t.Errorf("GiteaURL = %q (trailing slash not trimmed?)", cfg.GiteaURL)
}
if cfg.PollInterval != 5*time.Second {
t.Errorf("PollInterval = %s", cfg.PollInterval)
}
if cfg.JobTimeout != 10*time.Minute {
t.Errorf("JobTimeout = %s", cfg.JobTimeout)
}
if cfg.MaxConcurrent != 7 {
t.Errorf("MaxConcurrent = %d", cfg.MaxConcurrent)
}
}
func TestValidateErrors(t *testing.T) {
dir := t.TempDir()
tea := writeTeaConfig(t, dir, "t", "https://git.unkin.net", "tok", "bot", true)
cases := map[string]string{
"no repos": `personalities:
- {name: a, tea_config: ` + tea + `, role: both}
`,
"bad repo form": `repos: [notaslash]
personalities:
- {name: a, tea_config: ` + tea + `, role: both}
`,
"no personalities": `repos: [a/b]
`,
"only implementer": `repos: [a/b]
personalities:
- {name: a, tea_config: ` + tea + `, role: implementer}
`,
"only reviewer": `repos: [a/b]
personalities:
- {name: a, tea_config: ` + tea + `, role: reviewer}
`,
"invalid role": `repos: [a/b]
personalities:
- {name: a, tea_config: ` + tea + `, role: bogus}
`,
}
for name, body := range cases {
t.Run(name, func(t *testing.T) {
_, err := Load(writeConfig(t, t.TempDir(), body))
if err == nil {
t.Fatalf("expected error for %q, got nil", name)
}
})
}
}
func TestDuplicatePersonalityNameRejected(t *testing.T) {
dir := t.TempDir()
tea := writeTeaConfig(t, dir, "t", "https://git.unkin.net", "tok", "bot", true)
body := `repos: [a/b]
personalities:
- {name: dup, tea_config: ` + tea + `, role: implementer}
- {name: dup, tea_config: ` + tea + `, role: reviewer}
`
if _, err := Load(writeConfig(t, dir, body)); err == nil {
t.Fatal("expected duplicate-name error")
}
}
func TestRoleDefaultsToBoth(t *testing.T) {
dir := t.TempDir()
tea := writeTeaConfig(t, dir, "t", "https://git.unkin.net", "tok", "bot", true)
body := `repos: [a/b]
personalities:
- name: solo
tea_config: ` + tea + `
git_name: X
git_email: x@y.z
`
cfg, err := Load(writeConfig(t, dir, body))
if err != nil {
t.Fatalf("Load: %v", err)
}
if cfg.Personalities[0].Role != RoleBoth {
t.Errorf("role = %q, want both", cfg.Personalities[0].Role)
}
if !cfg.Personalities[0].CanImplement() || !cfg.Personalities[0].CanReview() {
t.Error("both role should implement and review")
}
}
func TestBotLoginsAndSelectors(t *testing.T) {
cfg := &Config{Personalities: []Personality{
{Name: "i", Role: RoleImplementer, Login: "ibot"},
{Name: "r", Role: RoleReviewer, Login: "rbot"},
}}
logins := cfg.BotLogins()
if !logins["ibot"] || !logins["rbot"] || len(logins) != 2 {
t.Errorf("BotLogins = %v", logins)
}
if p := cfg.ImplementerFor(); p == nil || p.Name != "i" {
t.Errorf("ImplementerFor = %v", p)
}
if p := cfg.ReviewerFor(); p == nil || p.Name != "r" {
t.Errorf("ReviewerFor = %v", p)
}
}
func TestParseTeaConfigPrefersMatchingURL(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "multi.yml")
content := `logins:
- name: other
url: https://other.example.com
token: other-tok
default: true
user: otheruser
- name: target
url: https://git.unkin.net
token: target-tok
user: targetuser
`
if err := os.WriteFile(path, []byte(content), 0o600); err != nil {
t.Fatal(err)
}
login, err := ParseTeaConfig(path, "https://git.unkin.net")
if err != nil {
t.Fatalf("ParseTeaConfig: %v", err)
}
if login.User != "targetuser" || login.Token != "target-tok" {
t.Errorf("matched wrong login: %+v", login)
}
}
func TestParseTeaConfigFallsBackToDefault(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "d.yml")
content := `logins:
- name: a
url: https://a.example.com
token: a-tok
user: a
- name: b
url: https://b.example.com
token: b-tok
default: true
user: b
`
if err := os.WriteFile(path, []byte(content), 0o600); err != nil {
t.Fatal(err)
}
login, err := ParseTeaConfig(path, "https://nomatch.example.com")
if err != nil {
t.Fatalf("ParseTeaConfig: %v", err)
}
if login.User != "b" {
t.Errorf("expected default login b, got %q", login.User)
}
}
func TestExampleConfigIsValidWhenTeaConfigsExist(t *testing.T) {
// The shipped example references tea configs by ~/ path; here we just
// verify the example YAML parses into a Config with the expected shape by
// substituting resolvable tea configs.
dir := t.TempDir()
impl := writeTeaConfig(t, dir, "impl", "https://git.unkin.net", "tok", "implbot", true)
rev := writeTeaConfig(t, dir, "rev", "https://git.unkin.net", "tok2", "revbot", false)
body := `gitea_url: https://git.unkin.net
repos: [unkin/teabot]
personalities:
- {name: implementer, tea_config: ` + impl + `, role: implementer, git_name: I, git_email: i@x}
- {name: reviewer, tea_config: ` + rev + `, role: reviewer, git_name: R, git_email: r@x}
`
if _, err := Load(writeConfig(t, dir, body)); err != nil {
t.Fatalf("example-shaped config failed to load: %v", err)
}
}