package event import ( "strings" "sync" "testing" "time" ) var testNow = time.Date(2026, 8, 1, 9, 30, 0, 0, time.UTC) func TestNormalizeFillsDefaults(t *testing.T) { got := Event{Source: "poll:zenmoney", Title: " spent today "}.Normalize(testNow) if got.Title != "spent today" { t.Errorf("title = %q, want collapsed whitespace", got.Title) } if got.Priority != PriorityNormal { t.Errorf("priority = %q, want %q", got.Priority, PriorityNormal) } if got.Kind != KindFact { t.Errorf("kind = %q, want %q", got.Kind, KindFact) } if !got.OccurredAt.Equal(testNow) { t.Errorf("occurred_at = %v, want %v", got.OccurredAt, testNow) } } func TestNormalizeKeepsRealOccurredAt(t *testing.T) { // A wg handshake carries the handshake instant, not "now". Flattening that // would make every intake look like it happened at notice time. real := testNow.Add(-3 * time.Hour) got := Event{Source: "infer:wg", Title: "wg_handshake", OccurredAt: real}.Normalize(testNow) if !got.OccurredAt.Equal(real) { t.Errorf("occurred_at = %v, want the supplied %v", got.OccurredAt, real) } } func TestNormalizeTruncatesOnRuneBoundary(t *testing.T) { long := strings.Repeat("я", TitleMaxRunes+50) got := Event{Source: "rss:x", Title: long}.Normalize(testNow) r := []rune(got.Title) if len(r) != TitleMaxRunes+1 { // +1 for the ellipsis marker t.Fatalf("title runes = %d, want %d", len(r), TitleMaxRunes+1) } if r[len(r)-1] != '…' { t.Errorf("truncated title does not mark the cut: %q", string(r[len(r)-3:])) } for _, c := range r[:TitleMaxRunes] { if c != 'я' { t.Fatalf("truncation broke a rune: got %q", c) } } } func TestNormalizeRejectsUnknownPriority(t *testing.T) { got := Event{Source: "s", Title: "t", Priority: "URGENT!!"}.Normalize(testNow) if got.Priority != PriorityNormal { t.Errorf("priority = %q, want %q", got.Priority, PriorityNormal) } } func TestValid(t *testing.T) { base := Event{Source: "rss:tech", Kind: KindNote, Title: "заголовок", OccurredAt: testNow} if !base.Valid() { t.Fatal("well-formed event reported invalid") } for name, mut := range map[string]func(Event) Event{ "no source": func(e Event) Event { e.Source = ""; return e }, "no title": func(e Event) Event { e.Title = ""; return e }, "no time": func(e Event) Event { e.OccurredAt = time.Time{}; return e }, "bad kind": func(e Event) Event { e.Kind = "whatever"; return e }, } { if mut(base).Valid() { t.Errorf("%s: reported valid", name) } } } func TestSourceKind(t *testing.T) { cases := map[string]string{ "rss:tech": KindNote, "crawl:kernel": KindNote, // No email rule: a WriteFact under an email source is a fact, not a // captured task. The mail path builds its own task envelope. "email:inbox": KindFact, "probe:netdata": KindHealth, "ambient:notif": KindFact, "tap:voice": KindFact, } for src, want := range cases { if got := SourceKind(src, KindFact); got != want { t.Errorf("SourceKind(%q) = %q, want %q", src, got, want) } } } func TestBusNilIsANoOp(t *testing.T) { // The whole adoption story depends on this: an intake path calls Publish // unconditionally, and a daemon with no bus behaves as it did before. var b *Bus b.Publish(Event{Source: "s", Kind: KindFact, Title: "t"}, testNow) b.Subscribe(func(Event) { t.Error("nil bus delivered to a subscriber") }) if got := b.Recent(10); got != nil { t.Errorf("Recent on nil bus = %v, want nil", got) } if got := b.Len(); got != 0 { t.Errorf("Len on nil bus = %d, want 0", got) } } func TestBusRecentIsNewestFirst(t *testing.T) { b := NewBus(8) for _, title := range []string{"one", "two", "three"} { b.Publish(Event{Source: "rss:t", Kind: KindNote, Title: title}, testNow) } got := b.Recent(0) if len(got) != 3 { t.Fatalf("len = %d, want 3", len(got)) } want := []string{"three", "two", "one"} for i, w := range want { if got[i].Title != w { t.Errorf("Recent()[%d] = %q, want %q", i, got[i].Title, w) } } if lim := b.Recent(2); len(lim) != 2 || lim[0].Title != "three" { t.Errorf("Recent(2) = %v, want the two newest", lim) } } func TestBusRingEvicts(t *testing.T) { b := NewBus(3) for _, title := range []string{"a", "b", "c", "d", "e"} { b.Publish(Event{Source: "s", Kind: KindFact, Title: title}, testNow) } if b.Len() != 3 { t.Fatalf("Len = %d, want the capacity 3", b.Len()) } got := b.Recent(0) want := []string{"e", "d", "c"} for i, w := range want { if got[i].Title != w { t.Errorf("Recent()[%d] = %q, want %q", i, got[i].Title, w) } } } func TestBusDropsInvalid(t *testing.T) { b := NewBus(4) b.Publish(Event{Kind: KindFact, Title: "no source"}, testNow) b.Publish(Event{Source: "s", Kind: KindFact}, testNow) if b.Len() != 0 { t.Errorf("Len = %d, want 0 — an envelope with no provenance must not be kept", b.Len()) } } func TestBusSubscriberPanicDoesNotBreakIntake(t *testing.T) { b := NewBus(4) var seen int b.Subscribe(func(Event) { panic("observer is broken") }) b.Subscribe(func(Event) { seen++ }) b.Publish(Event{Source: "s", Kind: KindFact, Title: "t"}, testNow) if seen != 1 { t.Errorf("healthy subscriber called %d times, want 1", seen) } if b.Len() != 1 { t.Errorf("event not recorded despite a panicking subscriber") } } func TestBusConcurrentPublish(t *testing.T) { b := NewBus(256) var wg sync.WaitGroup for i := 0; i < 16; i++ { wg.Add(1) go func() { defer wg.Done() for j := 0; j < 10; j++ { b.Publish(Event{Source: "s", Kind: KindFact, Title: "t"}, testNow) } }() } wg.Wait() if b.Len() != 160 { t.Errorf("Len = %d, want 160", b.Len()) } } // The ring is insertion-ordered and the page calls itself newest first, so the // two only agree if the column is notice time. A cold feed read publishes a // week of items in feed order, which used to make the OccurredAt column walk // forwards and backwards on the same page. func TestRecentIsOrderedByNoticeTimeNotByWhenThingsHappened(t *testing.T) { b := NewBus(8) // Published in feed order, six days old first, and a mark stamped now. for i, e := range []Event{ {Source: "rss:t", Kind: KindNote, Title: "six days ago", OccurredAt: testNow.Add(-6 * 24 * time.Hour)}, {Source: "rss:t", Kind: KindNote, Title: "two days ago", OccurredAt: testNow.Add(-2 * 24 * time.Hour)}, {Source: "ambient:notif", Kind: KindFact, Title: "a meeting at six", OccurredAt: testNow.Add(9 * time.Hour)}, } { b.Publish(e, testNow.Add(time.Duration(i)*time.Second)) } got := b.Recent(0) want := []string{"a meeting at six", "two days ago", "six days ago"} for i, w := range want { if got[i].Title != w { t.Errorf("Recent()[%d] = %q, want %q (notice order)", i, got[i].Title, w) } } // Notice time is monotone down the page even where occurrence time is not. for i := 1; i < len(got); i++ { if got[i].NoticedAt.After(got[i-1].NoticedAt) { t.Errorf("NoticedAt is not descending at %d", i) } } if got[0].OccurredAt.Before(got[1].OccurredAt) { t.Fatal("this fixture is supposed to have occurrence time out of order") } // A caller does not get to claim when Maven noticed something. forged := Event{Source: "s", Kind: KindFact, Title: "t", NoticedAt: testNow.Add(100 * time.Hour)} b.Publish(forged, testNow) if n := b.Recent(1)[0].NoticedAt; !n.Equal(testNow) { t.Errorf("NoticedAt = %v, want the publish instant %v", n, testNow) } }