package activitypool import ( "context" "errors" "testing" "time" healthDomain "proxy-pool/internal/domain/health" proxyDomain "proxy-pool/internal/domain/proxy" ) func TestMemoryPoolAppliesGlobalObservationAtomically(t *testing.T) { now := time.Date(2026, 7, 31, 11, 0, 0, 0, time.UTC) pool := seededHealthPool(t, now) if _, err := pool.ApplyHealth(context.Background(), HealthUpdate{ ProxyID: "proxy-a", CheckedAt: now.Add(time.Second), NextState: proxyDomain.StateChecking, }); err != nil { t.Fatalf("ApplyHealth(checking): %v", err) } failed, err := pool.ApplyGlobalObservation(context.Background(), GlobalHealthCommand{ Observation: healthObservation("task-1", false, now.Add(2*time.Second)), MaxConsecutiveFailures: 3, }) if err != nil || failed.State != proxyDomain.StateUnhealthy || failed.GlobalHealth.ConsecutiveFailures != 1 { t.Fatalf("ApplyGlobalObservation(initial failure) = %+v, %v", failed, err) } if _, err := pool.ApplyHealth(context.Background(), HealthUpdate{ ProxyID: "proxy-a", CheckedAt: now.Add(3 * time.Second), NextState: proxyDomain.StateChecking, }); err != nil { t.Fatalf("ApplyHealth(recheck): %v", err) } available, err := pool.ApplyGlobalObservation(context.Background(), GlobalHealthCommand{ Observation: healthObservation("task-2", true, now.Add(4*time.Second)), MaxConsecutiveFailures: 3, }) if err != nil || available.State != proxyDomain.StateAvailable || available.GlobalHealth.ConsecutiveFailures != 0 || available.Proxy.LastSuccessAt == nil || !available.Proxy.LastSuccessAt.Equal(now.Add(4*time.Second)) { t.Fatalf("ApplyGlobalObservation(recovery) = %+v, %v", available, err) } } func TestMemoryPoolGlobalObservationReplayDoesNotIncreaseFailureStreak(t *testing.T) { now := time.Date(2026, 7, 31, 11, 0, 0, 0, time.UTC) pool := seededHealthPool(t, now) if _, err := pool.ApplyHealth(context.Background(), HealthUpdate{ ProxyID: "proxy-a", CheckedAt: now.Add(time.Second), NextState: proxyDomain.StateChecking, }); err != nil { t.Fatalf("ApplyHealth(checking): %v", err) } command := GlobalHealthCommand{Observation: healthObservation("task-1", false, now.Add(2*time.Second)), MaxConsecutiveFailures: 3} first, err := pool.ApplyGlobalObservation(context.Background(), command) if err != nil { t.Fatalf("ApplyGlobalObservation(first): %v", err) } replayed, err := pool.ApplyGlobalObservation(context.Background(), command) if err != nil || replayed.GlobalHealth != first.GlobalHealth { t.Fatalf("ApplyGlobalObservation(replay) = %+v, %v", replayed, err) } conflicting := command conflicting.Observation.Success = true conflicting.Observation.FailureClass = "" if _, err := pool.ApplyGlobalObservation(context.Background(), conflicting); !errors.Is(err, healthDomain.ErrConflictingObservation) { t.Fatalf("ApplyGlobalObservation(conflict) error = %v, want ErrConflictingObservation", err) } } func TestMemoryPoolRejectsTargetObservationFromGlobalStore(t *testing.T) { now := time.Date(2026, 7, 31, 11, 0, 0, 0, time.UTC) pool := seededHealthPool(t, now) _, err := pool.ApplyGlobalObservation(context.Background(), GlobalHealthCommand{Observation: healthDomain.Observation{ TaskID: "task-target", ProxyID: "proxy-a", Level: healthDomain.LevelTarget, RoutingName: "route-a", TargetURL: "https://target.example/", Success: false, FailureClass: "target_403", ObservedAt: now.Add(time.Second), }, MaxConsecutiveFailures: 2}) if !errors.Is(err, ErrInvalidHealthUpdate) { t.Fatalf("ApplyGlobalObservation(target) error = %v, want ErrInvalidHealthUpdate", err) } } func seededHealthPool(t *testing.T, now time.Time) *MemoryPool { t.Helper() pool := NewMemoryPool() _, err := pool.UpsertFetched(context.Background(), "provider-a", FetchedBatch{ ObservedAt: now, ConfiguredTTL: time.Minute, MaxSize: 1, Proxies: []proxyDomain.Proxy{{ ID: "proxy-a", Scheme: proxyDomain.SchemeHTTP, Host: "192.0.2.10", Port: 8080, }}, }) if err != nil { t.Fatalf("UpsertFetched(): %v", err) } return pool } func healthObservation(taskID string, success bool, observedAt time.Time) healthDomain.Observation { observation := healthDomain.Observation{ TaskID: taskID, ProxyID: "proxy-a", Level: healthDomain.LevelBasic, Success: success, Latency: 10 * time.Millisecond, ObservedAt: observedAt, } if !success { observation.FailureClass = "timeout" } return observation }