detached 08a23ac

test(daemon): assert ring/pending-queue contents, not just lengths

TestNoticeCenterRingEviction only checked len(history())==200, which
can't tell "kept newest" from "kept arbitrary." TestNoticeCenterPendingQueue
never exercised the 32-entry cap-overflow branch. Both now assert
identity and ordering, and cover overflow plus dedup-at-cap.
commit
08a23acb3f2caffad772490db33a7a341cbd78e1
author
brice · · 2026-07-20 12:13 UTC
parent
dbdedb4
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
1 file · +72 -16
M internal/usecase/daemon/notify_test.go +72 -16
@@ -1,6 +1,7 @@
11 package daemon
22
33 import (
4+ "fmt"
45 "testing"
56 "time"
67
@@ -14,12 +15,26 @@ func notice(code domain.NoticeCode, msg string, sid domain.SessionID) domain.Not
1415 func TestNoticeCenterRingEviction(t *testing.T) {
1516 nc := newNoticeCenter()
1617 for i := 0; i < 205; i++ {
17- nc.record(notice(domain.NoticeInternal, "m", ""))
18+ nc.record(notice(domain.NoticeInternal, fmt.Sprintf("n%d", i), ""))
1819 }
1920 h := nc.history()
2021 if len(h) != 200 {
2122 t.Fatalf("history len = %d, want 200", len(h))
2223 }
24+ if h[0].Message != "n204" {
25+ t.Fatalf("history[0] = %q, want n204 (newest insert)", h[0].Message)
26+ }
27+ if h[199].Message != "n5" {
28+ t.Fatalf("history[199] = %q, want n5 (oldest retained after eviction)", h[199].Message)
29+ }
30+ // Every slot must hold exactly the expected message: strictly newest-first,
31+ // no gaps, and none of the evicted n0..n4 leaked back in.
32+ for i, n := range h {
33+ want := fmt.Sprintf("n%d", 204-i)
34+ if n.Message != want {
35+ t.Fatalf("history[%d] = %q, want %q", i, n.Message, want)
36+ }
37+ }
2338 }
2439
2540 func TestNoticeCenterHistoryNewestFirst(t *testing.T) {
@@ -37,19 +52,60 @@ func TestNoticeCenterHistoryNewestFirst(t *testing.T) {
3752 }
3853
3954 func TestNoticeCenterPendingQueue(t *testing.T) {
40- nc := newNoticeCenter()
41- for i := 0; i < 40; i++ {
42- nc.queueGlobal(notice(domain.NoticeSnapshotRestore, "restore failed", ""))
43- }
44- nc.queueGlobal(notice(domain.NoticePersistDisabled, "persistence disabled", ""))
45- pending := nc.drainPending()
46- if len(pending) != 2 {
47- t.Fatalf("pending len = %d, want 2 (deduped by code)", len(pending))
48- }
49- if pending[0].Count != 40 {
50- t.Fatalf("coalesced Count = %d, want 40", pending[0].Count)
51- }
52- if got := nc.drainPending(); len(got) != 0 {
53- t.Fatalf("second drain len = %d, want 0", len(got))
54- }
55+ t.Run("dedup coalesces by code without growing the queue", func(t *testing.T) {
56+ nc := newNoticeCenter()
57+ for i := 0; i < 40; i++ {
58+ nc.queueGlobal(notice(domain.NoticeSnapshotRestore, "restore failed", ""))
59+ }
60+ nc.queueGlobal(notice(domain.NoticePersistDisabled, "persistence disabled", ""))
61+ pending := nc.drainPending()
62+ if len(pending) != 2 {
63+ t.Fatalf("pending len = %d, want 2 (deduped by code)", len(pending))
64+ }
65+ if pending[0].Count != 40 {
66+ t.Fatalf("coalesced Count = %d, want 40", pending[0].Count)
67+ }
68+ if got := nc.drainPending(); len(got) != 0 {
69+ t.Fatalf("second drain len = %d, want 0", len(got))
70+ }
71+ })
72+
73+ t.Run("cap overflow drops the 33rd distinct code", func(t *testing.T) {
74+ nc := newNoticeCenter()
75+ for i := 0; i < 33; i++ {
76+ nc.queueGlobal(notice(domain.NoticeCode(i), fmt.Sprintf("code %d", i), ""))
77+ }
78+ pending := nc.drainPending()
79+ if len(pending) != 32 {
80+ t.Fatalf("pending len = %d, want 32 (bounded at cap)", len(pending))
81+ }
82+ for i, n := range pending {
83+ if n.Code != domain.NoticeCode(i) {
84+ t.Fatalf("pending[%d].Code = %v, want %v (first 32 distinct codes retained, in order)", i, n.Code, domain.NoticeCode(i))
85+ }
86+ }
87+ for _, n := range pending {
88+ if n.Code == domain.NoticeCode(32) {
89+ t.Fatalf("pending contains code %v, which should have been dropped on overflow", domain.NoticeCode(32))
90+ }
91+ }
92+ })
93+
94+ t.Run("dedup on an already-queued code still increments Count at the cap", func(t *testing.T) {
95+ nc := newNoticeCenter()
96+ for i := 0; i < 32; i++ {
97+ nc.queueGlobal(notice(domain.NoticeCode(i), fmt.Sprintf("code %d", i), ""))
98+ }
99+ // The queue is now full. Re-queuing an existing code must coalesce
100+ // in place, not hit the overflow-drop branch.
101+ nc.queueGlobal(notice(domain.NoticeCode(0), "code 0 again", ""))
102+ nc.queueGlobal(notice(domain.NoticeCode(0), "code 0 again", ""))
103+ pending := nc.drainPending()
104+ if len(pending) != 32 {
105+ t.Fatalf("pending len = %d, want 32 (dedup must not grow past cap)", len(pending))
106+ }
107+ if pending[0].Code != domain.NoticeCode(0) || pending[0].Count != 3 {
108+ t.Fatalf("pending[0] = {Code:%v Count:%d}, want {Code:%v Count:3}", pending[0].Code, pending[0].Count, domain.NoticeCode(0))
109+ }
110+ })
55111 }
@@ -1,6 +1,7 @@
1 package daemon1 package daemon
2 2
3 import (3 import (
4+ "fmt"
4 "testing"5 "testing"
5 "time"6 "time"
6 7
@@ -14,12 +15,26 @@ func notice(code domain.NoticeCode, msg string, sid domain.SessionID) domain.Not
14 func TestNoticeCenterRingEviction(t *testing.T) {15 func TestNoticeCenterRingEviction(t *testing.T) {
15 nc := newNoticeCenter()16 nc := newNoticeCenter()
16 for i := 0; i < 205; i++ {17 for i := 0; i < 205; i++ {
17- nc.record(notice(domain.NoticeInternal, "m", ""))18+ nc.record(notice(domain.NoticeInternal, fmt.Sprintf("n%d", i), ""))
18 }19 }
19 h := nc.history()20 h := nc.history()
20 if len(h) != 200 {21 if len(h) != 200 {
21 t.Fatalf("history len = %d, want 200", len(h))22 t.Fatalf("history len = %d, want 200", len(h))
22 }23 }
24+ if h[0].Message != "n204" {
25+ t.Fatalf("history[0] = %q, want n204 (newest insert)", h[0].Message)
26+ }
27+ if h[199].Message != "n5" {
28+ t.Fatalf("history[199] = %q, want n5 (oldest retained after eviction)", h[199].Message)
29+ }
30+ // Every slot must hold exactly the expected message: strictly newest-first,
31+ // no gaps, and none of the evicted n0..n4 leaked back in.
32+ for i, n := range h {
33+ want := fmt.Sprintf("n%d", 204-i)
34+ if n.Message != want {
35+ t.Fatalf("history[%d] = %q, want %q", i, n.Message, want)
36+ }
37+ }
23 }38 }
24 39
25 func TestNoticeCenterHistoryNewestFirst(t *testing.T) {40 func TestNoticeCenterHistoryNewestFirst(t *testing.T) {
@@ -37,19 +52,60 @@ func TestNoticeCenterHistoryNewestFirst(t *testing.T) {
37 }52 }
38 53
39 func TestNoticeCenterPendingQueue(t *testing.T) {54 func TestNoticeCenterPendingQueue(t *testing.T) {
40- nc := newNoticeCenter()55+ t.Run("dedup coalesces by code without growing the queue", func(t *testing.T) {
41- for i := 0; i < 40; i++ {56+ nc := newNoticeCenter()
42- nc.queueGlobal(notice(domain.NoticeSnapshotRestore, "restore failed", ""))57+ for i := 0; i < 40; i++ {
43- }58+ nc.queueGlobal(notice(domain.NoticeSnapshotRestore, "restore failed", ""))
44- nc.queueGlobal(notice(domain.NoticePersistDisabled, "persistence disabled", ""))59+ }
45- pending := nc.drainPending()60+ nc.queueGlobal(notice(domain.NoticePersistDisabled, "persistence disabled", ""))
46- if len(pending) != 2 {61+ pending := nc.drainPending()
47- t.Fatalf("pending len = %d, want 2 (deduped by code)", len(pending))62+ if len(pending) != 2 {
48- }63+ t.Fatalf("pending len = %d, want 2 (deduped by code)", len(pending))
49- if pending[0].Count != 40 {64+ }
50- t.Fatalf("coalesced Count = %d, want 40", pending[0].Count)65+ if pending[0].Count != 40 {
51- }66+ t.Fatalf("coalesced Count = %d, want 40", pending[0].Count)
52- if got := nc.drainPending(); len(got) != 0 {67+ }
53- t.Fatalf("second drain len = %d, want 0", len(got))68+ if got := nc.drainPending(); len(got) != 0 {
54- }69+ t.Fatalf("second drain len = %d, want 0", len(got))
70+ }
71+ })
72+
73+ t.Run("cap overflow drops the 33rd distinct code", func(t *testing.T) {
74+ nc := newNoticeCenter()
75+ for i := 0; i < 33; i++ {
76+ nc.queueGlobal(notice(domain.NoticeCode(i), fmt.Sprintf("code %d", i), ""))
77+ }
78+ pending := nc.drainPending()
79+ if len(pending) != 32 {
80+ t.Fatalf("pending len = %d, want 32 (bounded at cap)", len(pending))
81+ }
82+ for i, n := range pending {
83+ if n.Code != domain.NoticeCode(i) {
84+ t.Fatalf("pending[%d].Code = %v, want %v (first 32 distinct codes retained, in order)", i, n.Code, domain.NoticeCode(i))
85+ }
86+ }
87+ for _, n := range pending {
88+ if n.Code == domain.NoticeCode(32) {
89+ t.Fatalf("pending contains code %v, which should have been dropped on overflow", domain.NoticeCode(32))
90+ }
91+ }
92+ })
93+
94+ t.Run("dedup on an already-queued code still increments Count at the cap", func(t *testing.T) {
95+ nc := newNoticeCenter()
96+ for i := 0; i < 32; i++ {
97+ nc.queueGlobal(notice(domain.NoticeCode(i), fmt.Sprintf("code %d", i), ""))
98+ }
99+ // The queue is now full. Re-queuing an existing code must coalesce
100+ // in place, not hit the overflow-drop branch.
101+ nc.queueGlobal(notice(domain.NoticeCode(0), "code 0 again", ""))
102+ nc.queueGlobal(notice(domain.NoticeCode(0), "code 0 again", ""))
103+ pending := nc.drainPending()
104+ if len(pending) != 32 {
105+ t.Fatalf("pending len = %d, want 32 (dedup must not grow past cap)", len(pending))
106+ }
107+ if pending[0].Code != domain.NoticeCode(0) || pending[0].Count != 3 {
108+ t.Fatalf("pending[0] = {Code:%v Count:%d}, want {Code:%v Count:3}", pending[0].Code, pending[0].Count, domain.NoticeCode(0))
109+ }
110+ })
55 }111 }

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 12

Tags 11

↑↓ moveenter openesc close