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
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// runCLI executes the root command with args and returns combined stdout.
|
||||
func runCLI(t *testing.T, args ...string) (string, error) {
|
||||
t.Helper()
|
||||
root := newRootCmd()
|
||||
var out bytes.Buffer
|
||||
root.SetOut(&out)
|
||||
root.SetErr(&out)
|
||||
root.SetArgs(args)
|
||||
err := root.Execute()
|
||||
return out.String(), err
|
||||
}
|
||||
|
||||
func TestConfigInitWritesAndRefusesOverwrite(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "config.yaml")
|
||||
|
||||
out, err := runCLI(t, "config", "init", "--config", path)
|
||||
if err != nil {
|
||||
t.Fatalf("config init: %v", err)
|
||||
}
|
||||
if !strings.Contains(out, "wrote example config") {
|
||||
t.Errorf("unexpected output: %q", out)
|
||||
}
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("reading written config: %v", err)
|
||||
}
|
||||
if !strings.Contains(string(data), "personalities:") {
|
||||
t.Error("written config missing personalities section")
|
||||
}
|
||||
|
||||
// Second init without --force must fail.
|
||||
if _, err := runCLI(t, "config", "init", "--config", path); err == nil {
|
||||
t.Error("expected error re-initialising existing config without --force")
|
||||
}
|
||||
// With --force it should succeed.
|
||||
if _, err := runCLI(t, "config", "init", "--config", path, "--force"); err != nil {
|
||||
t.Errorf("config init --force: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigShowValidatesAndPrints(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
tea := filepath.Join(dir, "tea.yml")
|
||||
if err := os.WriteFile(tea, []byte("logins:\n - name: b\n url: https://git.unkin.net\n token: tok\n default: true\n user: botuser\n"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cfgPath := filepath.Join(dir, "config.yaml")
|
||||
body := "gitea_url: https://git.unkin.net\nrepos: [unkin/teabot]\npersonalities:\n" +
|
||||
" - {name: solo, tea_config: " + tea + ", role: both, git_name: S, git_email: s@x}\n"
|
||||
if err := os.WriteFile(cfgPath, []byte(body), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
out, err := runCLI(t, "config", "show", "--config", cfgPath)
|
||||
if err != nil {
|
||||
t.Fatalf("config show: %v", err)
|
||||
}
|
||||
for _, want := range []string{"unkin/teabot", "solo", "botuser", "agent-dev"} {
|
||||
if !strings.Contains(out, want) {
|
||||
t.Errorf("config show output missing %q\n%s", want, out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigShowReportsInvalidConfig(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
cfgPath := filepath.Join(dir, "config.yaml")
|
||||
// No repos, no personalities -> validation error.
|
||||
if err := os.WriteFile(cfgPath, []byte("gitea_url: https://git.unkin.net\n"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := runCLI(t, "config", "show", "--config", cfgPath); err == nil {
|
||||
t.Error("expected validation error for empty config")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewLoggerLevels(t *testing.T) {
|
||||
for _, lvl := range []string{"debug", "info", "warn", "error", "unknown"} {
|
||||
if l := newLogger(false, lvl); l == nil {
|
||||
t.Errorf("newLogger(%q) returned nil", lvl)
|
||||
}
|
||||
}
|
||||
if l := newLogger(true, "info"); l == nil {
|
||||
t.Error("JSON logger nil")
|
||||
}
|
||||
// Sanity: debug logger actually enables debug level.
|
||||
if !newLogger(false, "debug").Enabled(nil, slog.LevelDebug) { //nolint:staticcheck
|
||||
t.Error("debug logger should enable debug level")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user