9e383eb751
The ring is insertion-ordered and the page called itself newest first while printing OccurredAt, which is when the thing happened. A cold feed read publishes a week of items in feed order and the ambient relay stamps a 09:00 notification with an 18:00 meeting, so the timestamp column ran forwards and backwards on the same page. Events now carry NoticedAt, filled by the bus and not by the caller, and the page sorts and labels by it while still showing when the thing itself happened. Four writers on that page had not arrived from anywhere: the feed watermark, the crawl hash, the praxis trace of an act she performed and a quiet-hours toggle he pressed. On a cold start with a few feeds they could evict real intake out of a 512-entry ring. The decorator now skips Maven's own bookkeeping. Priority was the only surviving trace of confidence, and it inverts: a relayed meeting at 0.6 read as low while an rss watermark at 1.0 read as normal. The fact's own kind, its confidence and the id it voids now travel in Payload, which was unused. A retraction is marked as one and scored low, instead of publishing an envelope indistinguishable from a fresh reading of the same key. Smaller: SourceKind no longer maps every email source to a task, so a future fact under an email prefix is not journalled as one; newEventBus is quiet when it is handed no config at all; and morningTmpl has its own doc comment back. Found in review of #78. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TrVSBKe3RFDF4fGYKWYQnX
226 lines
7.2 KiB
Go
226 lines
7.2 KiB
Go
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)
|
|
}
|
|
}
|