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 Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
1 file · +72 -16
M internal/usecase/daemon/notify_test.go +72 -16
| @@ -1,6 +1,7 @@ | |||
| 1 | 1 | package daemon | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | + | "fmt" | |
| 4 | 5 | "testing" | |
| 5 | 6 | "time" | |
| 6 | 7 | ||
| @@ -14,12 +15,26 @@ func notice(code domain.NoticeCode, msg string, sid domain.SessionID) domain.Not | |||
| 14 | 15 | func TestNoticeCenterRingEviction(t *testing.T) { | |
| 15 | 16 | nc := newNoticeCenter() | |
| 16 | 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 | 20 | h := nc.history() | |
| 20 | 21 | if len(h) != 200 { | |
| 21 | 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 | 40 | func TestNoticeCenterHistoryNewestFirst(t *testing.T) { | |
| @@ -37,19 +52,60 @@ func TestNoticeCenterHistoryNewestFirst(t *testing.T) { | |||
| 37 | 52 | } | |
| 38 | 53 | ||
| 39 | 54 | 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 | + | }) | |
| 55 | 111 | } | |
| @@ -1,6 +1,7 @@ | |||||
| 1 | package daemon | 1 | 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 | } | ||