Flux source URL, flux-git rotator verify repo, review-bot REPOS, pipeline API, clone and push URLs, the heating-can source label and the review-bot and kuma-sync Go module paths. Merge right after the transfer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
100 lines
3.6 KiB
Go
100 lines
3.6 KiB
Go
package main
|
|
|
|
import (
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func env(m map[string]string) func(string) string { return func(k string) string { return m[k] } }
|
|
|
|
func noFiles(string) ([]byte, error) { return nil, errors.New("no such file") }
|
|
|
|
func baseEnv() map[string]string {
|
|
return map[string]string{
|
|
"REVIEW_TOKEN": "t", "WEBHOOK_SECRET": "s", "MISTRAL_API_KEY": "k",
|
|
"CONNECTOR_ID": "c1", "REPOS": "konairius/homelab, konairius/shelly-manager",
|
|
}
|
|
}
|
|
|
|
func TestLoadConfigDefaults(t *testing.T) {
|
|
c, err := loadConfig(env(baseEnv()), noFiles)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if c.Listen != ":8080" || c.GiteaURL != "https://gitea.renner.uno" || c.BotLogin != "review-bot" ||
|
|
c.Workers != 1 || c.Sweep != 30*time.Minute || c.RateLimitRetries != 3 || c.RateLimitWait != time.Minute ||
|
|
c.Selector != "" || c.MCPURL != "https://gitea-mcp.renner.uno/mcp" || c.AnthropicEffort != "high" {
|
|
t.Errorf("unexpected defaults: %+v", c)
|
|
}
|
|
if len(c.Models) != 1 || c.Models[0].ID() != "mistral:mistral-large-4-0" {
|
|
t.Errorf("models: %+v", c.Models)
|
|
}
|
|
if len(c.Repos) != 2 || c.Repos[1] != "konairius/shelly-manager" {
|
|
t.Errorf("repos: %v", c.Repos)
|
|
}
|
|
}
|
|
|
|
func TestLoadConfigSecretFromFile(t *testing.T) {
|
|
e := baseEnv()
|
|
delete(e, "REVIEW_TOKEN")
|
|
e["REVIEW_TOKEN_FILE"] = "/run/secrets/review-token"
|
|
c, err := loadConfig(env(e), func(p string) ([]byte, error) { return []byte("from-file\n"), nil })
|
|
if err != nil || c.ReviewToken != "from-file" {
|
|
t.Errorf("got %q, %v", c.ReviewToken, err)
|
|
}
|
|
}
|
|
|
|
func TestLoadConfigErrors(t *testing.T) {
|
|
_, err := loadConfig(env(map[string]string{"WORKERS": "0", "REPOS": "nonsense", "SWEEP_INTERVAL": "soon",
|
|
"SELECTOR": "dice"}), noFiles)
|
|
if err == nil {
|
|
t.Fatal("expected errors")
|
|
}
|
|
for _, want := range []string{"REVIEW_TOKEN is not set", "WEBHOOK_SECRET is not set", "no model of MODELS is usable",
|
|
"MISTRAL_API_KEY is not set", "WORKERS must be", "SWEEP_INTERVAL must be", `REPOS entry "nonsense"`, "SELECTOR must be"} {
|
|
if !strings.Contains(err.Error(), want) {
|
|
t.Errorf("missing %q in: %v", want, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// A backend without its credentials drops its models with a warning instead of stopping the bot.
|
|
func TestLoadConfigDropsBackendsWithoutCredentials(t *testing.T) {
|
|
e := baseEnv()
|
|
e["MODELS"] = "anthropic:claude-opus-5-5 = strong\nmistral:mistral-large-4-0\n# comment\n\nanthropic:claude-sonnet-5-5"
|
|
e["SELECTOR"] = "typesafe"
|
|
c, err := loadConfig(env(e), noFiles)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(c.Models) != 1 || c.Models[0].ID() != "mistral:mistral-large-4-0" {
|
|
t.Errorf("models: %+v", c.Models)
|
|
}
|
|
if c.Selector != "" || len(c.Warnings) != 3 || !strings.Contains(strings.Join(c.Warnings, "\n"), "ANTHROPIC_API_KEY is not set") {
|
|
t.Errorf("selector %q, warnings %v", c.Selector, c.Warnings)
|
|
}
|
|
|
|
e["ANTHROPIC_API_KEY"], e["TYPESAFE_API_KEY"] = "a", "ts"
|
|
c, _ = loadConfig(env(e), noFiles)
|
|
if len(c.Models) != 1 || !strings.Contains(strings.Join(c.Warnings, "\n"), "MCP_TOKEN is not set") {
|
|
t.Errorf("anthropic without MCP_TOKEN must stay off: %+v %v", c.Models, c.Warnings)
|
|
}
|
|
|
|
e["MCP_TOKEN"] = "m"
|
|
c, err = loadConfig(env(e), noFiles)
|
|
if err != nil || len(c.Models) != 3 || c.Models[0].ID() != "anthropic:claude-opus-5-5" ||
|
|
c.Models[0].Description != "strong" || c.Selector != "typesafe" || len(c.Warnings) != 0 {
|
|
t.Errorf("models %+v, selector %q, warnings %v, err %v", c.Models, c.Selector, c.Warnings, err)
|
|
}
|
|
}
|
|
|
|
func TestParseModelsErrors(t *testing.T) {
|
|
for _, bad := range []string{"claude-opus-5-5", "openai:gpt = x", "mistral:", "mistral:a\nmistral:a"} {
|
|
if _, err := parseModels(bad); err == nil {
|
|
t.Errorf("%q: want an error", bad)
|
|
}
|
|
}
|
|
}
|