Files
clawgo/pkg/sentinel/service_test.go
2026-05-10 17:27:06 +08:00

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
}