Timing changes for more relaxed poke intervals: - Daemon heartbeat: 60s → 5 minutes - Backoff base: 60s → 5 minutes - Backoff max: 10m → 30 minutes - Fresh threshold: <2min → <5min - Stale threshold: 2-5min → 5-15min - Very stale threshold: >5min → >15min New command: - `gt deacon heartbeat [action]` - Touch heartbeat file easily Template rewrite: - Clearer wake/sleep model - Documents wake sources (daemon poke, mail, timer callbacks) - Simpler rounds with `gt deacon heartbeat` instead of bash echo - Mentions plugins as optional maintenance tasks - Explains timer callbacks pattern 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
291 lines
7.6 KiB
Go
291 lines
7.6 KiB
Go
package daemon
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestDefaultBackoffConfig(t *testing.T) {
|
|
config := DefaultBackoffConfig()
|
|
|
|
if config.Strategy != StrategyGeometric {
|
|
t.Errorf("expected strategy Geometric, got %v", config.Strategy)
|
|
}
|
|
if config.BaseInterval != 5*time.Minute {
|
|
t.Errorf("expected base interval 5m, got %v", config.BaseInterval)
|
|
}
|
|
if config.MaxInterval != 30*time.Minute {
|
|
t.Errorf("expected max interval 30m, got %v", config.MaxInterval)
|
|
}
|
|
if config.Factor != 1.5 {
|
|
t.Errorf("expected factor 1.5, got %v", config.Factor)
|
|
}
|
|
}
|
|
|
|
func TestNewAgentBackoff(t *testing.T) {
|
|
config := DefaultBackoffConfig()
|
|
ab := NewAgentBackoff("test-agent", config)
|
|
|
|
if ab.AgentID != "test-agent" {
|
|
t.Errorf("expected agent ID 'test-agent', got %s", ab.AgentID)
|
|
}
|
|
if ab.BaseInterval != 5*time.Minute {
|
|
t.Errorf("expected base interval 5m, got %v", ab.BaseInterval)
|
|
}
|
|
if ab.CurrentInterval != 5*time.Minute {
|
|
t.Errorf("expected current interval 5m, got %v", ab.CurrentInterval)
|
|
}
|
|
if ab.ConsecutiveMiss != 0 {
|
|
t.Errorf("expected consecutive miss 0, got %d", ab.ConsecutiveMiss)
|
|
}
|
|
}
|
|
|
|
func TestAgentBackoff_ShouldPoke(t *testing.T) {
|
|
config := &BackoffConfig{
|
|
Strategy: StrategyGeometric,
|
|
BaseInterval: 100 * time.Millisecond, // Short for testing
|
|
MaxInterval: 1 * time.Second,
|
|
Factor: 1.5,
|
|
}
|
|
ab := NewAgentBackoff("test", config)
|
|
|
|
// Should poke immediately (never poked)
|
|
if !ab.ShouldPoke() {
|
|
t.Error("expected ShouldPoke=true for new agent")
|
|
}
|
|
|
|
// Record a poke
|
|
ab.RecordPoke()
|
|
|
|
// Should not poke immediately after
|
|
if ab.ShouldPoke() {
|
|
t.Error("expected ShouldPoke=false immediately after poke")
|
|
}
|
|
|
|
// Wait for interval
|
|
time.Sleep(110 * time.Millisecond)
|
|
|
|
// Now should poke again
|
|
if !ab.ShouldPoke() {
|
|
t.Error("expected ShouldPoke=true after interval elapsed")
|
|
}
|
|
}
|
|
|
|
func TestAgentBackoff_GeometricBackoff(t *testing.T) {
|
|
config := &BackoffConfig{
|
|
Strategy: StrategyGeometric,
|
|
BaseInterval: 100 * time.Millisecond,
|
|
MaxInterval: 1 * time.Second,
|
|
Factor: 1.5,
|
|
}
|
|
ab := NewAgentBackoff("test", config)
|
|
|
|
// Initial interval
|
|
if ab.CurrentInterval != 100*time.Millisecond {
|
|
t.Errorf("expected initial interval 100ms, got %v", ab.CurrentInterval)
|
|
}
|
|
|
|
// First miss: 100ms * 1.5 = 150ms
|
|
ab.RecordMiss(config)
|
|
if ab.CurrentInterval != 150*time.Millisecond {
|
|
t.Errorf("expected interval 150ms after 1 miss, got %v", ab.CurrentInterval)
|
|
}
|
|
if ab.ConsecutiveMiss != 1 {
|
|
t.Errorf("expected consecutive miss 1, got %d", ab.ConsecutiveMiss)
|
|
}
|
|
|
|
// Second miss: 150ms * 1.5 = 225ms
|
|
ab.RecordMiss(config)
|
|
if ab.CurrentInterval != 225*time.Millisecond {
|
|
t.Errorf("expected interval 225ms after 2 misses, got %v", ab.CurrentInterval)
|
|
}
|
|
|
|
// Third miss: 225ms * 1.5 = 337.5ms
|
|
ab.RecordMiss(config)
|
|
expected := time.Duration(337500000) // 337.5ms in nanoseconds
|
|
if ab.CurrentInterval != expected {
|
|
t.Errorf("expected interval ~337.5ms after 3 misses, got %v", ab.CurrentInterval)
|
|
}
|
|
}
|
|
|
|
func TestAgentBackoff_ExponentialBackoff(t *testing.T) {
|
|
config := &BackoffConfig{
|
|
Strategy: StrategyExponential,
|
|
BaseInterval: 100 * time.Millisecond,
|
|
MaxInterval: 1 * time.Second,
|
|
Factor: 2.0, // Ignored for exponential
|
|
}
|
|
ab := NewAgentBackoff("test", config)
|
|
|
|
// First miss: 100ms * 2 = 200ms
|
|
ab.RecordMiss(config)
|
|
if ab.CurrentInterval != 200*time.Millisecond {
|
|
t.Errorf("expected interval 200ms after 1 miss, got %v", ab.CurrentInterval)
|
|
}
|
|
|
|
// Second miss: 200ms * 2 = 400ms
|
|
ab.RecordMiss(config)
|
|
if ab.CurrentInterval != 400*time.Millisecond {
|
|
t.Errorf("expected interval 400ms after 2 misses, got %v", ab.CurrentInterval)
|
|
}
|
|
|
|
// Third miss: 400ms * 2 = 800ms
|
|
ab.RecordMiss(config)
|
|
if ab.CurrentInterval != 800*time.Millisecond {
|
|
t.Errorf("expected interval 800ms after 3 misses, got %v", ab.CurrentInterval)
|
|
}
|
|
}
|
|
|
|
func TestAgentBackoff_FixedStrategy(t *testing.T) {
|
|
config := &BackoffConfig{
|
|
Strategy: StrategyFixed,
|
|
BaseInterval: 100 * time.Millisecond,
|
|
MaxInterval: 1 * time.Second,
|
|
Factor: 1.5,
|
|
}
|
|
ab := NewAgentBackoff("test", config)
|
|
|
|
// Multiple misses should not change interval
|
|
ab.RecordMiss(config)
|
|
ab.RecordMiss(config)
|
|
ab.RecordMiss(config)
|
|
|
|
if ab.CurrentInterval != 100*time.Millisecond {
|
|
t.Errorf("expected interval to stay at 100ms with fixed strategy, got %v", ab.CurrentInterval)
|
|
}
|
|
if ab.ConsecutiveMiss != 3 {
|
|
t.Errorf("expected consecutive miss 3, got %d", ab.ConsecutiveMiss)
|
|
}
|
|
}
|
|
|
|
func TestAgentBackoff_MaxInterval(t *testing.T) {
|
|
config := &BackoffConfig{
|
|
Strategy: StrategyExponential,
|
|
BaseInterval: 100 * time.Millisecond,
|
|
MaxInterval: 500 * time.Millisecond,
|
|
Factor: 2.0,
|
|
}
|
|
ab := NewAgentBackoff("test", config)
|
|
|
|
// Keep missing until we hit the cap
|
|
for i := 0; i < 10; i++ {
|
|
ab.RecordMiss(config)
|
|
}
|
|
|
|
if ab.CurrentInterval != 500*time.Millisecond {
|
|
t.Errorf("expected interval capped at 500ms, got %v", ab.CurrentInterval)
|
|
}
|
|
}
|
|
|
|
func TestAgentBackoff_RecordActivity(t *testing.T) {
|
|
config := &BackoffConfig{
|
|
Strategy: StrategyGeometric,
|
|
BaseInterval: 100 * time.Millisecond,
|
|
MaxInterval: 1 * time.Second,
|
|
Factor: 1.5,
|
|
}
|
|
ab := NewAgentBackoff("test", config)
|
|
|
|
// Build up some backoff
|
|
ab.RecordMiss(config)
|
|
ab.RecordMiss(config)
|
|
ab.RecordMiss(config)
|
|
|
|
if ab.CurrentInterval == 100*time.Millisecond {
|
|
t.Error("expected interval to have increased")
|
|
}
|
|
if ab.ConsecutiveMiss != 3 {
|
|
t.Errorf("expected consecutive miss 3, got %d", ab.ConsecutiveMiss)
|
|
}
|
|
|
|
// Record activity - should reset
|
|
ab.RecordActivity()
|
|
|
|
if ab.CurrentInterval != 100*time.Millisecond {
|
|
t.Errorf("expected interval reset to 100ms, got %v", ab.CurrentInterval)
|
|
}
|
|
if ab.ConsecutiveMiss != 0 {
|
|
t.Errorf("expected consecutive miss reset to 0, got %d", ab.ConsecutiveMiss)
|
|
}
|
|
if ab.LastActivity.IsZero() {
|
|
t.Error("expected LastActivity to be set")
|
|
}
|
|
}
|
|
|
|
func TestBackoffManager_GetOrCreate(t *testing.T) {
|
|
bm := NewBackoffManager(DefaultBackoffConfig())
|
|
|
|
// First call creates
|
|
ab1 := bm.GetOrCreate("agent1")
|
|
if ab1 == nil {
|
|
t.Fatal("expected agent backoff to be created")
|
|
}
|
|
if ab1.AgentID != "agent1" {
|
|
t.Errorf("expected agent ID 'agent1', got %s", ab1.AgentID)
|
|
}
|
|
|
|
// Second call returns same instance
|
|
ab2 := bm.GetOrCreate("agent1")
|
|
if ab1 != ab2 {
|
|
t.Error("expected same instance on second call")
|
|
}
|
|
|
|
// Different agent creates new instance
|
|
ab3 := bm.GetOrCreate("agent2")
|
|
if ab1 == ab3 {
|
|
t.Error("expected different instance for different agent")
|
|
}
|
|
}
|
|
|
|
func TestBackoffManager_Stats(t *testing.T) {
|
|
config := &BackoffConfig{
|
|
Strategy: StrategyGeometric,
|
|
BaseInterval: 100 * time.Millisecond,
|
|
MaxInterval: 1 * time.Second,
|
|
Factor: 1.5,
|
|
}
|
|
bm := NewBackoffManager(config)
|
|
|
|
// Create some agents with different backoff states
|
|
bm.RecordPoke("agent1")
|
|
bm.RecordMiss("agent1")
|
|
|
|
bm.RecordPoke("agent2")
|
|
bm.RecordMiss("agent2")
|
|
bm.RecordMiss("agent2")
|
|
|
|
stats := bm.Stats()
|
|
|
|
if len(stats) != 2 {
|
|
t.Errorf("expected 2 agents in stats, got %d", len(stats))
|
|
}
|
|
|
|
// agent1: 100ms * 1.5 = 150ms
|
|
if stats["agent1"] != 150*time.Millisecond {
|
|
t.Errorf("expected agent1 interval 150ms, got %v", stats["agent1"])
|
|
}
|
|
|
|
// agent2: 100ms * 1.5 * 1.5 = 225ms
|
|
if stats["agent2"] != 225*time.Millisecond {
|
|
t.Errorf("expected agent2 interval 225ms, got %v", stats["agent2"])
|
|
}
|
|
}
|
|
|
|
func TestExtractRigName(t *testing.T) {
|
|
tests := []struct {
|
|
session string
|
|
expected string
|
|
}{
|
|
{"gt-gastown-witness", "gastown"},
|
|
{"gt-myrig-witness", "myrig"},
|
|
{"gt-my-rig-name-witness", "my-rig-name"},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
result := extractRigName(tc.session)
|
|
if result != tc.expected {
|
|
t.Errorf("extractRigName(%q) = %q, expected %q", tc.session, result, tc.expected)
|
|
}
|
|
}
|
|
}
|