Files
gastown/internal/daemon/backoff_test.go
Steve Yegge 1554380228 feat(deacon): improve timing and add heartbeat command
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>
2025-12-20 02:12:21 -08:00

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)
}
}
}