main c21f374

policy_test.go RawHistoryFolder

1package core
2
3import (
4	"fmt"
5	"github.com/bnema/neferafk/internal/ports"
6	"slices"
7	"testing"
8	"time"
9)
10
11func handle(t *testing.T, p *Policy, now time.Time, e ports.PolicyEvent) []ports.PolicyEffect {
12	t.Helper()
13	out, err := p.Handle(now, e)
14	if err != nil {
15		t.Fatal(err)
16	}
17	return out
18}
19func actions(effects []ports.PolicyEffect) []ports.Action {
20	var a []ports.Action
21	for _, e := range effects {
22		switch e.(type) {
23		case ports.AcquireLock:
24			a = append(a, ports.Lock)
25		case ports.SetFade:
26			a = append(a, ports.Fade)
27		case ports.SetOutputPower:
28			a = append(a, ports.OutputOff)
29		case ports.Suspend:
30			a = append(a, ports.Sleep)
31		}
32	}
33	return a
34}
35func config(ds [4]time.Duration) ports.Config {
36	c := ports.Defaults()
37	c.LockAfter, c.FadeAfter, c.OffAfter, c.SleepAfter = ds[0], ds[1], ds[2], ds[3]
38	return c
39}
40
41func TestAllSixteenSubsetsTies(t *testing.T) {
42	for bits := 0; bits < 16; bits++ {
43		t.Run(fmt.Sprint(bits), func(t *testing.T) {
44			var ds [4]time.Duration
45			var want []ports.Action
46			for i := range 4 {
47				if bits&(1<<i) != 0 {
48					ds[i] = time.Second
49					want = append(want, ports.Action(i))
50				}
51			}
52			now := time.Unix(100, 0)
53			p, armed, err := New(config(ds), now)
54			if err != nil {
55				t.Fatal(err)
56			}
57			arm := armed[0].(ports.ArmIdle)
58			if len(arm.Notifications) != min(bits, 1) {
59				t.Fatal(arm)
60			}
61			got := handle(t, p, now.Add(time.Second), ports.IdleDue{Generation: arm.Generation, After: time.Second})
62			if bits&1 != 0 {
63				got = append(got, handle(t, p, now.Add(time.Second), ports.LockConfirmed{Generation: arm.Generation})...)
64			}
65			if !slices.Equal(actions(got), want) {
66				t.Fatalf("got %v want %v", actions(got), want)
67			}
68		})
69	}
70}
71
72func TestAllTwentyFourDeadlineOrders(t *testing.T) {
73	var visit func([]int, []int)
74	visit = func(prefix, remaining []int) {
75		if len(remaining) > 0 {
76			for i, v := range remaining {
77				visit(append(slices.Clone(prefix), v), append(slices.Clone(remaining[:i]), remaining[i+1:]...))
78			}
79			return
80		}
81		t.Run(fmt.Sprint(prefix), func(t *testing.T) {
82			var ds [4]time.Duration
83			for rank, a := range prefix {
84				ds[a] = time.Duration(rank+1) * time.Second
85			}
86			now := time.Unix(100, 0)
87			p, _, _ := New(config(ds), now)
88			var got []ports.Action
89			for rank := range 4 {
90				d := time.Duration(rank+1) * time.Second
91				out := handle(t, p, now.Add(d), ports.IdleDue{Generation: 1, After: d})
92				got = append(got, actions(out)...)
93				if p.acquiring {
94					got = append(got, actions(handle(t, p, now.Add(d), ports.LockConfirmed{Generation: p.lockGeneration}))...)
95				}
96			}
97			var want []ports.Action
98			for _, a := range prefix {
99				want = append(want, ports.Action(a))
100			}
101			if !slices.Equal(got, want) {
102				t.Fatalf("got %v want %v", got, want)
103			}
104		})
105	}
106	visit(nil, []int{0, 1, 2, 3})
107}
108
109func TestAcquiringHoldsOnlyDueOffSleepActivityNeverUnlocks(t *testing.T) {
110	now := time.Unix(100, 0)
111	p, _, _ := New(config([4]time.Duration{time.Second, 2 * time.Second, 3 * time.Second, 4 * time.Second}), now)
112	handle(t, p, now, ports.IdleDue{Generation: 1, After: time.Second})
113	if len(handle(t, p, now, ports.IdleDue{Generation: 1, After: 3 * time.Second})) != 0 {
114		t.Fatal("off before lock confirmation")
115	}
116	if got := actions(handle(t, p, now, ports.IdleDue{Generation: 1, After: 2 * time.Second})); !slices.Equal(got, []ports.Action{ports.Fade}) {
117		t.Fatal(got)
118	}
119	handle(t, p, now, ports.IdleDue{Generation: 1, After: 4 * time.Second})
120	if got := actions(handle(t, p, now, ports.LockConfirmed{Generation: 1})); !slices.Equal(got, []ports.Action{ports.OutputOff, ports.Sleep}) {
121		t.Fatal(got)
122	}
123	out := handle(t, p, now, ports.Activity{})
124	if !p.protected || p.off || p.faded {
125		t.Fatal(p.State())
126	}
127	if len(out) != 3 {
128		t.Fatal(out)
129	}
130	if len(handle(t, p, now, ports.IdleDue{Generation: 1, After: time.Second})) != 0 {
131		t.Fatal("old proxy event ran")
132	}
133	handle(t, p, now, ports.LockReleased{Generation: 99})
134	if !p.protected {
135		t.Fatal("stale unlock")
136	}
137	handle(t, p, now, ports.LockReleased{Generation: 1})
138	if p.protected {
139		t.Fatal("valid visual release ignored")
140	}
141}
142
143func TestReloadRebasesOnlyChangedPendingAndKeepsProtection(t *testing.T) {
144	now := time.Unix(100, 0)
145	cfg := ports.Defaults()
146	p, _, _ := New(cfg, now)
147	next := cfg
148	next.FadeAfter = 8 * time.Minute
149	out := handle(t, p, now.Add(time.Minute), ports.Reload{Config: next})
150	arm := out[0].(ports.ArmIdle)
151	want := []ports.IdleNotification{{ID: 1, After: 5 * time.Minute}, {ID: 3, After: 7 * time.Minute}, {ID: 5, After: 8 * time.Minute}, {ID: 4, After: 20 * time.Minute}}
152	if !slices.Equal(arm.Notifications, want) {
153		t.Fatalf("arm %+v", arm)
154	}
155	handle(t, p, now, ports.IdleDue{Generation: 1, Notification: 1, After: 5 * time.Minute})
156	next.LockAfter = 0
157	handle(t, p, now, ports.Reload{Config: next})
158	if !p.acquiring {
159		t.Fatal("reload cancelled acquisition")
160	}
161	handle(t, p, now, ports.LockConfirmed{Generation: 2})
162	if !p.protected {
163		t.Fatal("reload lost lock generation")
164	}
165	bad := next
166	bad.SleepAfter = -1
167	if _, err := p.Handle(now, ports.Reload{Config: bad}); err == nil {
168		t.Fatal("bad config committed")
169	}
170	if p.cfg != next {
171		t.Fatal("partial reload")
172	}
173	handle(t, p, now, ports.Activity{})
174	if !p.protected {
175		t.Fatal("activity unlock")
176	}
177}
178
179func TestBeforeSleepHonorsLockDisabled(t *testing.T) {
180	now := time.Unix(100, 0)
181	for _, enabled := range []bool{false, true} {
182		cfg := ports.Defaults()
183		if !enabled {
184			cfg.LockAfter = 0
185		}
186		p, _, _ := New(cfg, now)
187		out := handle(t, p, now, ports.BeforeSleep{})
188		if !enabled {
189			if _, ok := out[0].(ports.SleepReady); !ok {
190				t.Fatal(out)
191			}
192			if p.acquiring {
193				t.Fatal("forced disabled lock")
194			}
195		} else {
196			if _, ok := out[0].(ports.AcquireLock); !ok {
197				t.Fatal(out)
198			}
199			out = handle(t, p, now, ports.LockConfirmed{Generation: 1})
200			if _, ok := out[len(out)-1].(ports.SleepReady); !ok {
201				t.Fatal(out)
202			}
203		}
204	}
205}
206
207func TestReloadRetainsUnchangedProxyRejectsReplacedProxyAndDoneFlags(t *testing.T) {
208	now := time.Unix(100, 0)
209	cfg := ports.Defaults()
210	p, initial, _ := New(cfg, now)
211	old := initial[0].(ports.ArmIdle)
212	fadeID := old.Notifications[1].ID
213	next := cfg
214	next.FadeAfter = 8 * time.Minute
215	out := handle(t, p, now.Add(time.Minute), ports.Reload{Config: next})
216	if len(handle(t, p, now, ports.IdleDue{Generation: 1, Notification: fadeID, After: 6 * time.Minute})) != 0 {
217		t.Fatal("replaced proxy delivered")
218	}
219	var newFade ports.IdleNotification
220	for _, n := range out[0].(ports.ArmIdle).Notifications {
221		if n.After == 8*time.Minute {
222			newFade = n
223		}
224	}
225	if got := actions(handle(t, p, now, ports.IdleDue{Generation: 2, Notification: newFade.ID, After: newFade.After})); !slices.Equal(got, []ports.Action{ports.Fade}) {
226		t.Fatal(got)
227	}
228	next.FadeAfter = time.Minute
229	handle(t, p, now, ports.Reload{Config: next})
230	if !p.deadlines[ports.Fade].done {
231		t.Fatal("reload reset completed fade")
232	}
233	if len(handle(t, p, now, ports.IdleDue{Generation: 2, Notification: newFade.ID, After: newFade.After})) != 0 {
234		t.Fatal("done fade replayed")
235	}
236}
237
238func TestCompletedFadeDisableReenableRetiresOldNotification(t *testing.T) {
239	now := time.Unix(100, 0)
240	cfg := config([4]time.Duration{0, time.Second, 0, 0})
241	p, initial, _ := New(cfg, now)
242	proxy := initial[0].(ports.ArmIdle).Notifications[0]
243	handle(t, p, now, ports.IdleDue{Generation: 1, Notification: proxy.ID, After: proxy.After})
244	cfg.FadeAfter = 0
245	handle(t, p, now, ports.Reload{Config: cfg})
246	d := p.deadlines[ports.Fade]
247	if d.after != 0 || !d.done || d.due || d.notification != 0 {
248		t.Fatalf("disable metadata %+v", d)
249	}
250	cfg.FadeAfter = 2 * time.Second
251	out := handle(t, p, now, ports.Reload{Config: cfg})
252	if len(out[0].(ports.ArmIdle).Notifications) != 0 {
253		t.Fatal("completed action rearmed")
254	}
255	if len(handle(t, p, now, ports.IdleDue{Generation: 1, Notification: proxy.ID, After: proxy.After})) != 0 {
256		t.Fatal("old event replayed")
257	}
258	handle(t, p, now, ports.Activity{})
259	if p.deadlines[ports.Fade].after != 2*time.Second || p.deadlines[ports.Fade].done {
260		t.Fatal("next cycle ignored latest enablement")
261	}
262}
263
264func TestEqualAfterReloadKeepsOriginalProxySeparateFromRebasedDeadline(t *testing.T) {
265	now := time.Unix(100, 0)
266	cfg := config([4]time.Duration{time.Minute, 2 * time.Minute, 0, 0})
267	p, initial, _ := New(cfg, now)
268	old := initial[0].(ports.ArmIdle).Notifications[0]
269	cfg.FadeAfter = time.Minute
270	arm := handle(t, p, now.Add(30*time.Second), ports.Reload{Config: cfg})[0].(ports.ArmIdle)
271	if len(arm.Notifications) != 2 || arm.Notifications[0].ID == arm.Notifications[1].ID {
272		t.Fatal("unrelated clock origins incorrectly coalesced")
273	}
274	// Equal timeout values do not imply equal due instants: fade was rearmed
275	// at reload while lock's existing notification stayed registered.
276	if got := actions(handle(t, p, now, ports.IdleDue{Generation: 1, Notification: old.ID, After: old.After})); !slices.Equal(got, []ports.Action{ports.Lock}) {
277		t.Fatal(got)
278	}
279	if got := actions(handle(t, p, now, ports.IdleDue{Generation: 2, Notification: arm.Notifications[1].ID, After: time.Minute})); !slices.Equal(got, []ports.Action{ports.Fade}) {
280		t.Fatal(got)
281	}
282}
283
284func TestLockRequestedAcquiresOnceAndNeverUnlocks(t *testing.T) {
285	now := time.Unix(100, 0)
286	p, _, _ := New(config([4]time.Duration{0, 0, time.Minute, 0}), now)
287	out := handle(t, p, now, ports.LockRequested{})
288	if len(out) != 1 || out[0] != (ports.AcquireLock{Generation: 1}) {
289		t.Fatalf("lock request %v", out)
290	}
291	if got := handle(t, p, now, ports.LockRequested{}); len(got) != 0 {
292		t.Fatalf("duplicate acquisition %v", got)
293	}
294	handle(t, p, now, ports.LockConfirmed{Generation: 1})
295	if got := handle(t, p, now, ports.LockRequested{}); len(got) != 0 || !p.State().Protected {
296		t.Fatalf("protected lock re-requested %v", got)
297	}
298	handle(t, p, now, ports.Activity{})
299	if !p.State().Protected {
300		t.Fatal("activity released the lock")
301	}
302}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 2

Tags 9

↑↓ moveenter openesc close