Files
gastown/internal/cmd/molecule_await_signal_test.go
gastown/polecats/nux a6ae2c6116 feat(backoff): Add gt agent state command and await-signal idle tracking
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>
2026-01-01 18:58:56 -08:00

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