summaryrefslogtreecommitdiff
path: root/internal/cli/throttle_test.go
blob: 57ed8ba82c338a7f72b4ff9314ee49653f84b8f8 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
package cli

import (
	"strings"
	"testing"
	"time"

	"codeberg.org/snonux/gitsyncer/internal/state"
)

func TestEvaluateSyncPolicy_SkipsRepoSyncedWithinDay(t *testing.T) {
	st := &state.State{}
	st.SetLastRepoSync("repo", time.Now().Add(-23*time.Hour))

	decision := evaluateSyncPolicy("repo", st, false, false, false)

	if !decision.Skip {
		t.Fatal("expected repo synced within 24 hours to be skipped")
	}
	if !strings.Contains(decision.Message, "Use --force to override.") {
		t.Fatalf("expected force override hint, got %q", decision.Message)
	}
}

func TestEvaluateSyncPolicy_AllowsRepoAfterDailyWindow(t *testing.T) {
	st := &state.State{}
	st.SetLastRepoSync("repo", time.Now().Add(-25*time.Hour))

	decision := evaluateSyncPolicy("repo", st, false, false, false)

	if decision.Skip {
		t.Fatalf("expected repo synced more than 24 hours ago to proceed, got %q", decision.Message)
	}
}

func TestEvaluateSyncPolicy_ForceBypassesDailyAndThrottleLimits(t *testing.T) {
	t.Setenv("HOME", t.TempDir())

	st := &state.State{}
	now := time.Now()
	st.SetRepoSync("repo", now.Add(-1*time.Hour), now.Add(30*24*time.Hour))

	decision := evaluateSyncPolicy("repo", st, false, true, true)

	if decision.Skip {
		t.Fatalf("expected --force to bypass sync limits, got %q", decision.Message)
	}
	if decision.SetNextAllowed {
		t.Fatal("did not expect --force to request throttle-window persistence")
	}
}

func TestEvaluateSyncPolicy_ThrottleSetsWindowWhenRepoIsIdle(t *testing.T) {
	t.Setenv("HOME", t.TempDir())

	start := time.Now()
	decision := evaluateSyncPolicy("repo", &state.State{}, false, false, true)
	end := time.Now()

	if !decision.Skip {
		t.Fatal("expected idle repo to be skipped when throttle is enabled")
	}
	if !decision.SetNextAllowed {
		t.Fatal("expected throttle evaluation to request a persisted next-allowed time")
	}

	minAllowed := start.Add(throttleMinDays * 24 * time.Hour)
	maxAllowed := end.Add(throttleMaxDays*24*time.Hour + time.Minute)
	if decision.NextAllowed.Before(minAllowed) || decision.NextAllowed.After(maxAllowed) {
		t.Fatalf("expected throttle window between %s and %s, got %s", minAllowed, maxAllowed, decision.NextAllowed)
	}
}

func TestRecordRepoSync_ClearsThrottleWindowWhenThrottleDisabled(t *testing.T) {
	st := &state.State{}
	st.SetRepoSync("repo", time.Now().Add(-72*time.Hour), time.Now().Add(72*time.Hour))

	recordRepoSync("repo", st, false)

	if st.GetLastRepoSync("repo").IsZero() {
		t.Fatal("expected last sync time to be recorded")
	}
	if !st.GetNextRepoSyncAllowed("repo").IsZero() {
		t.Fatal("expected throttle window to be cleared when throttle is disabled")
	}
}

func TestRecordRepoSync_SetsThrottleWindowWhenThrottleEnabled(t *testing.T) {
	st := &state.State{}

	recordRepoSync("repo", st, true)

	lastSync := st.GetLastRepoSync("repo")
	if lastSync.IsZero() {
		t.Fatal("expected last sync time to be recorded")
	}

	nextAllowed := st.GetNextRepoSyncAllowed("repo")
	if nextAllowed.IsZero() {
		t.Fatal("expected throttle window to be recorded")
	}

	minAllowed := lastSync.Add(throttleMinDays * 24 * time.Hour)
	maxAllowed := lastSync.Add(throttleMaxDays*24*time.Hour + time.Minute)
	if nextAllowed.Before(minAllowed) || nextAllowed.After(maxAllowed) {
		t.Fatalf("expected throttle window between %s and %s, got %s", minAllowed, maxAllowed, nextAllowed)
	}
}

func TestRandomThrottleDuration_WithinBoundsAndNotDegenerate(t *testing.T) {
	const draws = 200

	seen := map[time.Duration]struct{}{}
	minDuration := time.Duration(throttleMinDays) * 24 * time.Hour
	maxDuration := time.Duration(throttleMaxDays) * 24 * time.Hour

	for i := 0; i < draws; i++ {
		d := randomThrottleDuration()
		if d < minDuration || d > maxDuration {
			t.Fatalf("expected duration between %s and %s, got %s", minDuration, maxDuration, d)
		}
		if d%(24*time.Hour) != 0 {
			t.Fatalf("expected whole-day duration, got %s", d)
		}
		seen[d] = struct{}{}
	}

	if len(seen) < 2 {
		t.Fatalf("expected at least 2 distinct durations across %d draws, got %d", draws, len(seen))
	}
}