- Wrap agent registration in DB transaction (F-B2-1/F-B2-8) All 4 ops atomic, manual DeleteAgent rollback removed - Use SELECT FOR UPDATE SKIP LOCKED for atomic command delivery (F-B2-2) Concurrent requests get different commands, no duplicates - Wrap token renewal in DB transaction (F-B2-9) Validate + update expiry atomic - Add rate limit to GET /agents/:id/commands (F-B2-4) agent_checkin rate limiter applied - Add retry_count column, cap stuck command retries at 5 (F-B2-10) Migration 029, GetStuckCommands filters retry_count < 5 - Cap polling jitter at current interval (fixes rapid mode) (F-B2-5) maxJitter = min(pollingInterval/2, 30s) - Add exponential backoff with full jitter on reconnection (F-B2-7) calculateBackoff: base=10s, cap=5min, reset on success All tests pass. No regressions from A-series or B-1. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
95 lines
2.7 KiB
Go
95 lines
2.7 KiB
Go
package database_test
|
|
|
|
// stuck_command_retry_test.go — Tests for stuck command retry limit.
|
|
//
|
|
// F-B2-10 FIXED: retry_count column added (migration 029).
|
|
// GetStuckCommands now filters with retry_count < 5.
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestStuckCommandHasNoMaxRetryCount(t *testing.T) {
|
|
// POST-FIX: retry_count column exists and GetStuckCommands filters on it.
|
|
migrationsDir := filepath.Join("migrations")
|
|
files, err := os.ReadDir(migrationsDir)
|
|
if err != nil {
|
|
t.Fatalf("failed to read migrations directory: %v", err)
|
|
}
|
|
|
|
hasRetryCount := false
|
|
for _, f := range files {
|
|
if !strings.HasSuffix(f.Name(), ".up.sql") {
|
|
continue
|
|
}
|
|
content, err := os.ReadFile(filepath.Join(migrationsDir, f.Name()))
|
|
if err != nil {
|
|
continue
|
|
}
|
|
src := strings.ToLower(string(content))
|
|
if strings.Contains(src, "agent_commands") && strings.Contains(src, "retry_count") {
|
|
hasRetryCount = true
|
|
}
|
|
}
|
|
|
|
if !hasRetryCount {
|
|
t.Error("[ERROR] [server] [database] F-B2-10 NOT FIXED: no retry_count column")
|
|
}
|
|
|
|
// Check GetStuckCommands for retry limit
|
|
cmdPath := filepath.Join("queries", "commands.go")
|
|
content, err := os.ReadFile(cmdPath)
|
|
if err != nil {
|
|
t.Logf("[WARNING] [server] [database] could not read commands.go: %v", err)
|
|
return
|
|
}
|
|
|
|
src := string(content)
|
|
stuckIdx := strings.Index(src, "func (q *CommandQueries) GetStuckCommands")
|
|
if stuckIdx == -1 {
|
|
t.Log("[WARNING] [server] [database] GetStuckCommands function not found")
|
|
return
|
|
}
|
|
stuckBody := src[stuckIdx:]
|
|
if len(stuckBody) > 500 {
|
|
stuckBody = stuckBody[:500]
|
|
}
|
|
if !strings.Contains(strings.ToLower(stuckBody), "retry_count") {
|
|
t.Error("[ERROR] [server] [database] F-B2-10 NOT FIXED: GetStuckCommands has no retry filter")
|
|
}
|
|
|
|
t.Log("[INFO] [server] [database] F-B2-10 FIXED: retry count limit on stuck commands")
|
|
}
|
|
|
|
func TestStuckCommandHasMaxRetryCount(t *testing.T) {
|
|
migrationsDir := filepath.Join("migrations")
|
|
files, err := os.ReadDir(migrationsDir)
|
|
if err != nil {
|
|
t.Fatalf("failed to read migrations directory: %v", err)
|
|
}
|
|
|
|
hasRetryCount := false
|
|
for _, f := range files {
|
|
if !strings.HasSuffix(f.Name(), ".up.sql") {
|
|
continue
|
|
}
|
|
content, err := os.ReadFile(filepath.Join(migrationsDir, f.Name()))
|
|
if err != nil {
|
|
continue
|
|
}
|
|
src := strings.ToLower(string(content))
|
|
if strings.Contains(src, "agent_commands") && strings.Contains(src, "retry_count") {
|
|
hasRetryCount = true
|
|
}
|
|
}
|
|
|
|
if !hasRetryCount {
|
|
t.Errorf("[ERROR] [server] [database] no retry_count column on agent_commands.\n" +
|
|
"F-B2-10: add retry_count and cap re-delivery at max retries.")
|
|
}
|
|
t.Log("[INFO] [server] [database] F-B2-10 FIXED: retry_count column exists")
|
|
}
|