mirror of
https://github.com/YspCoder/clawgo.git
synced 2026-05-16 14:47:30 +08:00
169 lines
5.0 KiB
Go
169 lines
5.0 KiB
Go
package sentinel
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/YspCoder/clawgo/pkg/config"
|
|
)
|
|
|
|
func TestCheckConfigReportsMissingAndCorruptConfig(t *testing.T) {
|
|
s := NewService(filepath.Join(t.TempDir(), "missing.json"), t.TempDir(), 60, false, nil)
|
|
issues := s.checkConfig()
|
|
if len(issues) != 1 || !strings.Contains(issues[0], "config file missing") {
|
|
t.Fatalf("missing config issues = %+v", issues)
|
|
}
|
|
|
|
cfgPath := filepath.Join(t.TempDir(), "config.json")
|
|
if err := os.WriteFile(cfgPath, []byte("{bad-json"), 0644); err != nil {
|
|
t.Fatalf("write corrupt config: %v", err)
|
|
}
|
|
s = NewService(cfgPath, t.TempDir(), 60, false, nil)
|
|
issues = s.checkConfig()
|
|
if len(issues) != 1 || !strings.Contains(issues[0], "config parse failed") {
|
|
t.Fatalf("corrupt config issues = %+v", issues)
|
|
}
|
|
}
|
|
|
|
func TestCheckMemoryReportsMissingAndAutoHeals(t *testing.T) {
|
|
workspace := t.TempDir()
|
|
s := NewService(validConfigFile(t, t.TempDir()), workspace, 60, false, nil)
|
|
|
|
issues := s.checkMemory()
|
|
if len(issues) != 1 || !strings.Contains(issues[0], "memory dir missing") {
|
|
t.Fatalf("missing memory issues = %+v", issues)
|
|
}
|
|
|
|
s = NewService(validConfigFile(t, t.TempDir()), workspace, 60, true, nil)
|
|
issues = s.checkMemory()
|
|
if len(issues) != 1 || !strings.Contains(issues[0], "auto-healed") {
|
|
t.Fatalf("auto-heal memory issues = %+v", issues)
|
|
}
|
|
if _, err := os.Stat(filepath.Join(workspace, "memory")); err != nil {
|
|
t.Fatalf("memory dir was not created: %v", err)
|
|
}
|
|
|
|
issues = s.checkMemory()
|
|
if len(issues) != 1 || !strings.Contains(issues[0], "MEMORY.md missing, auto-healed") {
|
|
t.Fatalf("auto-heal MEMORY.md issues = %+v", issues)
|
|
}
|
|
if _, err := os.Stat(filepath.Join(workspace, "memory", "MEMORY.md")); err != nil {
|
|
t.Fatalf("MEMORY.md was not created: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestCheckLogsReportsMissingLogDirAndAutoHeals(t *testing.T) {
|
|
root := t.TempDir()
|
|
logDir := filepath.Join(root, "logs")
|
|
cfgPath := validConfigFileWithLogDir(t, t.TempDir(), logDir)
|
|
|
|
s := NewService(cfgPath, t.TempDir(), 60, false, nil)
|
|
issues := s.checkLogs()
|
|
if len(issues) != 1 || !strings.Contains(issues[0], "log dir missing") {
|
|
t.Fatalf("missing log dir issues = %+v", issues)
|
|
}
|
|
|
|
s = NewService(cfgPath, t.TempDir(), 60, true, nil)
|
|
issues = s.checkLogs()
|
|
if len(issues) != 1 || !strings.Contains(issues[0], "auto-healed") {
|
|
t.Fatalf("auto-heal log dir issues = %+v", issues)
|
|
}
|
|
if _, err := os.Stat(logDir); err != nil {
|
|
t.Fatalf("log dir was not created: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestCheckChannelsReportsHealthFailuresAndRestartsWhenAutoHealEnabled(t *testing.T) {
|
|
mgr := &fakeHealthManager{health: map[string]error{"telegram": errors.New("offline")}}
|
|
s := NewService(validConfigFile(t, t.TempDir()), t.TempDir(), 60, true, nil)
|
|
s.mgr = mgr
|
|
|
|
issues := s.checkChannels()
|
|
if len(issues) != 1 || !strings.Contains(issues[0], "telegram health check failed") {
|
|
t.Fatalf("channel issues = %+v", issues)
|
|
}
|
|
waitForRestarts(t, &mgr.restarts, 1)
|
|
}
|
|
|
|
func TestRunChecksCallsAlertCallbackAndSuppressesDuplicates(t *testing.T) {
|
|
cfgPath := filepath.Join(t.TempDir(), "missing.json")
|
|
workspace := t.TempDir()
|
|
var alerts int64
|
|
s := NewService(cfgPath, workspace, 60, false, func(msg string) {
|
|
atomic.AddInt64(&alerts, 1)
|
|
})
|
|
|
|
s.runChecks()
|
|
if got := atomic.LoadInt64(&alerts); got == 0 {
|
|
t.Fatal("runChecks did not call alert callback")
|
|
}
|
|
first := atomic.LoadInt64(&alerts)
|
|
s.runChecks()
|
|
if got := atomic.LoadInt64(&alerts); got != first {
|
|
t.Fatalf("duplicate alerts = %d, want suppressed at %d", got, first)
|
|
}
|
|
}
|
|
|
|
func TestStartStopLifecycle(t *testing.T) {
|
|
s := NewService(validConfigFile(t, t.TempDir()), t.TempDir(), 3600, false, nil)
|
|
s.Start()
|
|
if !s.runner.Running() {
|
|
t.Fatal("Start did not mark service running")
|
|
}
|
|
s.Stop()
|
|
if s.runner.Running() {
|
|
t.Fatal("Stop left service running")
|
|
}
|
|
}
|
|
|
|
type fakeHealthManager struct {
|
|
health map[string]error
|
|
restarts int64
|
|
}
|
|
|
|
func (m *fakeHealthManager) CheckHealth(ctx context.Context) map[string]error {
|
|
return m.health
|
|
}
|
|
|
|
func (m *fakeHealthManager) RestartChannel(ctx context.Context, name string) error {
|
|
atomic.AddInt64(&m.restarts, 1)
|
|
return nil
|
|
}
|
|
|
|
func waitForRestarts(t *testing.T, restarts *int64, want int64) {
|
|
t.Helper()
|
|
deadline := time.Now().Add(200 * time.Millisecond)
|
|
for time.Now().Before(deadline) {
|
|
if atomic.LoadInt64(restarts) >= want {
|
|
return
|
|
}
|
|
time.Sleep(time.Millisecond)
|
|
}
|
|
t.Fatalf("restarts = %d, want at least %d", atomic.LoadInt64(restarts), want)
|
|
}
|
|
|
|
func validConfigFile(t *testing.T, dir string) string {
|
|
t.Helper()
|
|
return validConfigFileWithLogDir(t, dir, filepath.Join(dir, "logs"))
|
|
}
|
|
|
|
func validConfigFileWithLogDir(t *testing.T, dir, logDir string) string {
|
|
t.Helper()
|
|
cfg := config.DefaultConfig()
|
|
cfg.Logging.Enabled = true
|
|
cfg.Logging.Dir = logDir
|
|
cfg.Logging.Filename = "clawgo.log"
|
|
cfg.Agents.Defaults.Workspace = filepath.Join(dir, "workspace")
|
|
cfgPath := filepath.Join(dir, "config.json")
|
|
if err := config.SaveConfig(cfgPath, cfg); err != nil {
|
|
t.Fatalf("SaveConfig: %v", err)
|
|
}
|
|
return cfgPath
|
|
}
|