Layer 1: Implements gt agent state command for managing agent bead labels: - gt agent state <bead> - Get all state labels - gt agent state <bead> --set idle=0 - Set label value - gt agent state <bead> --incr idle - Increment numeric label - gt agent state <bead> --del idle - Delete label Layer 2: Fixes await-signal iteration tracking: - Adds --agent-bead flag to read/write idle:N label - Implements exponential backoff: base * mult^idle_cycles - Auto-increments idle counter on timeout - Returns idle_cycles in result for caller to reset on signal This enables patrol agents to back off during quiet periods while staying responsive to signals. Part of epic gt-srm3y. (gt-srm3y) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
126 lines
2.7 KiB
Go
126 lines
2.7 KiB
Go
package cmd
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestCalculateEffectiveTimeout(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
timeout string
|
|
backoffBase string
|
|
backoffMult int
|
|
backoffMax string
|
|
idleCycles int
|
|
want time.Duration
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "simple timeout 60s",
|
|
timeout: "60s",
|
|
want: 60 * time.Second,
|
|
},
|
|
{
|
|
name: "simple timeout 5m",
|
|
timeout: "5m",
|
|
want: 5 * time.Minute,
|
|
},
|
|
{
|
|
name: "backoff base only, idle=0",
|
|
timeout: "60s",
|
|
backoffBase: "30s",
|
|
idleCycles: 0,
|
|
want: 30 * time.Second,
|
|
},
|
|
{
|
|
name: "backoff with idle=1, mult=2",
|
|
timeout: "60s",
|
|
backoffBase: "30s",
|
|
backoffMult: 2,
|
|
idleCycles: 1,
|
|
want: 60 * time.Second,
|
|
},
|
|
{
|
|
name: "backoff with idle=2, mult=2",
|
|
timeout: "60s",
|
|
backoffBase: "30s",
|
|
backoffMult: 2,
|
|
idleCycles: 2,
|
|
want: 2 * time.Minute,
|
|
},
|
|
{
|
|
name: "backoff with max cap",
|
|
timeout: "60s",
|
|
backoffBase: "30s",
|
|
backoffMult: 2,
|
|
backoffMax: "5m",
|
|
idleCycles: 10, // Would be 30s * 2^10 = ~8.5h but capped at 5m
|
|
want: 5 * time.Minute,
|
|
},
|
|
{
|
|
name: "backoff base exceeds max",
|
|
timeout: "60s",
|
|
backoffBase: "15m",
|
|
backoffMax: "10m",
|
|
want: 10 * time.Minute,
|
|
},
|
|
{
|
|
name: "invalid timeout",
|
|
timeout: "invalid",
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "invalid backoff base",
|
|
timeout: "60s",
|
|
backoffBase: "invalid",
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "invalid backoff max",
|
|
timeout: "60s",
|
|
backoffBase: "30s",
|
|
backoffMax: "invalid",
|
|
wantErr: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Set package-level variables
|
|
awaitSignalTimeout = tt.timeout
|
|
awaitSignalBackoffBase = tt.backoffBase
|
|
awaitSignalBackoffMult = tt.backoffMult
|
|
if tt.backoffMult == 0 {
|
|
awaitSignalBackoffMult = 2 // default
|
|
}
|
|
awaitSignalBackoffMax = tt.backoffMax
|
|
|
|
got, err := calculateEffectiveTimeout(tt.idleCycles)
|
|
if (err != nil) != tt.wantErr {
|
|
t.Errorf("calculateEffectiveTimeout() error = %v, wantErr %v", err, tt.wantErr)
|
|
return
|
|
}
|
|
if !tt.wantErr && got != tt.want {
|
|
t.Errorf("calculateEffectiveTimeout() = %v, want %v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAwaitSignalResult(t *testing.T) {
|
|
// Test that result struct marshals correctly
|
|
result := AwaitSignalResult{
|
|
Reason: "signal",
|
|
Elapsed: 5 * time.Second,
|
|
Signal: "[12:34:56] + gt-abc created · New issue",
|
|
}
|
|
|
|
if result.Reason != "signal" {
|
|
t.Errorf("expected reason 'signal', got %q", result.Reason)
|
|
}
|
|
if result.Signal == "" {
|
|
t.Error("expected signal to be set")
|
|
}
|
|
}
|