package main import ( "context" "encoding/json" "errors" "testing" "time" stdio "git.novox.be/novox/mesh-sdk/go" ) func addedEventBody(t *testing.T, account, folder, messageID string, uid uint32) json.RawMessage { t.Helper() raw, err := json.Marshal(map[string]any{ "account": account, "folder": folder, "uid": uid, "uidvalidity": uint32(1), "message-id": messageID, "date": "2026-10-20T10:00:00Z", "from": []Person{{Name: "Alice", Address: "alice@example.org"}}, "subject": "hello", "size": int64(1234), "flags": []string{}, }) if err != nil { t.Fatal(err) } return raw } // 1518/AC2: an added event is recorded as a waiting mail before the handler returns success. func TestHearRecordsTheEventAsAWaitingMailBeforeSucceeding(t *testing.T) { store := newMemStore() now := func() time.Time { return time.Date(2026, 10, 20, 10, 0, 1, 0, time.UTC) } raw := addedEventBody(t, "bob", "INBOX", "", 7) if err := Hear(context.Background(), store, raw, now); err != nil { t.Fatal(err) } waiting, err := store.Waiting(context.Background()) if err != nil { t.Fatal(err) } if len(waiting) != 1 { t.Fatalf("waiting = %v, want one waiting mail", waiting) } w := waiting[0] if w.Account != "bob" || w.Folder != "INBOX" || w.MessageID != "" || w.UID != 7 || w.Subject != "hello" || w.Size != 1234 || len(w.From) != 1 || w.From[0].Address != "alice@example.org" { t.Fatalf("waiting mail: %+v", w) } } // 1518/AC2: a store failure is returned unchanged, so the event is redelivered rather than lost. func TestHearReturnsAStoreFailureSoTheEventIsRedelivered(t *testing.T) { store := newMemStore() store.failNext = errStoreFake raw := addedEventBody(t, "bob", "INBOX", "", 8) err := Hear(context.Background(), store, raw, time.Now) if err != errStoreFake { t.Fatalf("Hear with a failing store: %v, want %v", err, errStoreFake) } waiting, _ := store.Waiting(context.Background()) if len(waiting) != 0 { t.Fatalf("waiting = %v, want none (the failed add kept nothing)", waiting) } } // 1518/AC3: the same event heard twice gives one waiting mail. func TestHearTwiceGivesOneWaitingMail(t *testing.T) { store := newMemStore() raw := addedEventBody(t, "bob", "INBOX", "", 9) if err := Hear(context.Background(), store, raw, time.Now); err != nil { t.Fatal(err) } if err := Hear(context.Background(), store, raw, time.Now); err != nil { t.Fatal(err) } waiting, _ := store.Waiting(context.Background()) if len(waiting) != 1 { t.Fatalf("waiting = %v, want exactly one", waiting) } } // 1518/AC4: waiting mails are handed out in arrival order (the order they were heard), oldest first — not by // their own date, and regardless of whether reading was healthy when they arrived (Hear never consults // health; a mail heard while unhealthy is recorded exactly the same way). func TestHearKeepsArrivalOrder(t *testing.T) { store := newMemStore() // Heard out of date order: the third mail (by date) arrives first. first := addedEventBody(t, "bob", "INBOX", "", 1) second := addedEventBody(t, "bob", "INBOX", "", 2) third := addedEventBody(t, "bob", "INBOX", "", 3) for _, raw := range []json.RawMessage{first, second, third} { if err := Hear(context.Background(), store, raw, time.Now); err != nil { t.Fatal(err) } } waiting, _ := store.Waiting(context.Background()) if len(waiting) != 3 { t.Fatalf("waiting = %v, want three", waiting) } wantOrder := []string{"", "", ""} for i, w := range waiting { if w.MessageID != wantOrder[i] { t.Fatalf("waiting[%d].MessageID = %q, want %q (arrival order)", i, w.MessageID, wantOrder[i]) } } } // A malformed event is taken and dropped, never redelivered forever (mirrors mesh-mail-notifier's watchWith). func TestHearDropsAMalformedEvent(t *testing.T) { store := newMemStore() if err := Hear(context.Background(), store, json.RawMessage(`not json`), time.Now); err != nil { t.Fatalf("Hear on a malformed event: %v, want it taken and dropped", err) } waiting, _ := store.Waiting(context.Background()) if len(waiting) != 0 { t.Fatalf("waiting = %v, want none", waiting) } } // TestHearLoopRetriesStartingAt250msDoublingToAMinute (issue 1518, fixed per the lead's try feedback on // bb594652): a subscribe the runtime refuses is retried starting at hearFirstRetryWait (250ms), doubling // each time, capped at a minute — never mesh-mail-notifier's own 2s start, so a bundle hears within about a // second of stdio.ServeOn beginning to serve. storeFor is never called: every refusal is the subscribe's own. func TestHearLoopRetriesStartingAt250msDoublingToAMinute(t *testing.T) { var waits []time.Duration recordSleep := func(d time.Duration) { waits = append(waits, d) } calls := 0 subscribe := func(string, func(stdio.Envelope) error) error { calls++ if calls <= 4 { return errors.New("the runtime refused this subscribe") } return nil } storeFor := func(context.Context) (Store, error) { t.Fatal("hearLoop must not call storeFor merely to retry a subscribe refusal") return nil, nil } hearLoop(storeFor, subscribe, recordSleep) want := []time.Duration{250 * time.Millisecond, 500 * time.Millisecond, time.Second, 2 * time.Second} if len(waits) != len(want) { t.Fatalf("waited %v, want %v", waits, want) } for i, w := range want { if waits[i] != w { t.Fatalf("wait[%d] = %s, want %s", i, waits[i], w) } } if calls != 5 { t.Fatalf("subscribe was called %d time(s), want 5 (four refusals, then the one that succeeded)", calls) } }