mirror of
https://github.com/YspCoder/clawgo.git
synced 2026-05-17 13:14:44 +08:00
parallel optimization groundwork
This commit is contained in:
@@ -16,6 +16,11 @@ import (
|
||||
|
||||
type AlertFunc func(msg string)
|
||||
|
||||
type channelHealthManager interface {
|
||||
CheckHealth(ctx context.Context) map[string]error
|
||||
RestartChannel(ctx context.Context, name string) error
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
cfgPath string
|
||||
workspace string
|
||||
@@ -25,7 +30,7 @@ type Service struct {
|
||||
runner *lifecycle.LoopRunner
|
||||
mu sync.RWMutex
|
||||
lastAlerts map[string]time.Time
|
||||
mgr *channels.Manager
|
||||
mgr channelHealthManager
|
||||
healingChannels map[string]bool
|
||||
}
|
||||
|
||||
|
||||
168
pkg/sentinel/service_test.go
Normal file
168
pkg/sentinel/service_test.go
Normal file
@@ -0,0 +1,168 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user