Main reverted G's half merge (#189, reverted by8afc3ebin #191) and then took the reader release's S1-S5, which copied G's code. G's form holds where they differ (process_bus.go, seats.go, state.go and their tests); the release's kept names stay so main's callers keep building: SeatViewPrefix beside SeatReadPrefix, and QueueInvoke's empty strings on not-ok (with its test TestQueueInvoke). The tree differs from G's headf30dee54only by those and the release's two tests (objects cap option, seat states and stream kinds). Builds: design 47 G (the milestone's sdk part, as atf30dee54) Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
68 lines
2.8 KiB
Go
68 lines
2.8 KiB
Go
package process
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/nats-io/nats.go/jetstream"
|
|
)
|
|
|
|
type ledgerMsg struct {
|
|
jetstream.Msg
|
|
subject string
|
|
seq uint64
|
|
pending uint64
|
|
stored time.Time
|
|
times uint64
|
|
}
|
|
|
|
func (m ledgerMsg) Subject() string { return m.subject }
|
|
func (m ledgerMsg) Metadata() (*jetstream.MsgMetadata, error) {
|
|
return &jetstream.MsgMetadata{Sequence: jetstream.SequencePair{Stream: m.seq, Consumer: m.seq}, NumPending: m.pending, Timestamp: m.stored, NumDelivered: max(m.times, 1)}, nil
|
|
}
|
|
|
|
func TestConsumerLedgerKeepsTheFactsAfterEachMessage(t *testing.T) {
|
|
kept := map[string][]byte{}
|
|
now := time.Date(2026, 11, 12, 9, 0, 0, 0, time.UTC)
|
|
l := NewConsumerLedger("EVENTS", "n1_m", func(_ context.Context, key string, value []byte) error { kept[key] = value; return nil }, func() time.Time { return now }, nil)
|
|
read := func() ConsumerFacts {
|
|
var f ConsumerFacts
|
|
if err := json.Unmarshal(kept["n1_m"], &f); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return f
|
|
}
|
|
a, b := ledgerMsg{subject: "mesh.seat.s.event.a", seq: 7, pending: 1}, ledgerMsg{subject: "mesh.seat.s.event.b", seq: 8, pending: 0}
|
|
l.Delivered(a)
|
|
now = now.Add(time.Minute)
|
|
l.Delivered(b)
|
|
if f := read(); f.Pending != 0 || !f.OldestUnacknowledged.Equal(now.Add(-time.Minute)) {
|
|
t.Errorf("after two deliveries: %+v, want pending 0 and the first one's delivery as the oldest unacknowledged", f)
|
|
}
|
|
// A message delivered again keeps its first delivery's time (A-187): asked again in this run, and after a restart, where the
|
|
// new ledger knows none, the time the bus stored it.
|
|
now = now.Add(time.Minute)
|
|
l.Delivered(ledgerMsg{subject: a.subject, seq: 7, pending: 0, times: 2})
|
|
if f := read(); !f.OldestUnacknowledged.Equal(now.Add(-2 * time.Minute)) {
|
|
t.Errorf("after a delivery again: %+v, want the first delivery kept as the oldest unacknowledged", f)
|
|
}
|
|
stored := now.Add(-time.Hour)
|
|
again := NewConsumerLedger("EVENTS", "n1_m", func(_ context.Context, key string, value []byte) error { kept[key] = value; return nil }, func() time.Time { return now }, nil)
|
|
again.Delivered(ledgerMsg{subject: a.subject, seq: 7, stored: stored, times: 3})
|
|
if f := read(); !f.OldestUnacknowledged.Equal(stored) {
|
|
t.Errorf("after a restart's delivery again: %+v, want the time the bus stored it", f)
|
|
}
|
|
now = now.Add(-time.Minute)
|
|
at := now.Add(-time.Hour)
|
|
l.Applied(a, at)
|
|
if f := read(); f.Acknowledged != 7 || f.LastEvent != "mesh.seat.s.event.a" || !f.LastAt.Equal(at) || !f.OldestUnacknowledged.Equal(now) {
|
|
t.Errorf("after the first applied: %+v, want sequence 7, its event and at, and the second one's delivery as the oldest", f)
|
|
}
|
|
l.Applied(b, time.Time{})
|
|
if f := read(); f.Acknowledged != 8 || !f.OldestUnacknowledged.IsZero() {
|
|
t.Errorf("after both applied: %+v, want sequence 8 and none unacknowledged", f)
|
|
}
|
|
}
|