package analytics import ( "context" "encoding/json" "strings" "testing" "github.com/carlosframework/platform/internal/release" ) func delta(box string, seq int64, pv int64) Delta { h := &HLL{} h.Add(h64(box + itoa(int(seq)))) p := NewTopK() p.Add("/", pv) return Delta{Date: "2026-09-07", Box: box, Seq: seq, Hours: map[string]HourRow{"09": {PV: pv, Bytes: pv * 100}}, Visitors: h, Pages: p, Referers: NewTopK(), Events: NewTopK()} } func TestDayMergeIsSeqIdempotent(t *testing.T) { var d Day if ok, err := d.Merge(delta("box-a", 5, 10)); !ok || err != nil { t.Fatalf("first merge: %v %v", ok, err) } if ok, _ := d.Merge(delta("box-a", 5, 10)); ok { t.Fatal("replaying the same seq must be a no-op") } if ok, _ := d.Merge(delta("box-a", 4, 10)); ok { t.Fatal("an older seq must be a no-op") } if ok, _ := d.Merge(delta("box-b", 1, 7)); !ok { t.Fatal("another box's seq is independent") } if d.Hours["09"].PV != 17 || d.Pages["/"] != 17 || d.Seq["box-a"] != 5 || d.Seq["box-b"] != 1 || d.V != 1 || d.Date != "2026-09-07" { t.Fatalf("merged = %+v", d) } vis, _ := DecodeHLL(d.Visitors) if c := vis.Count(); c < 1.5 || c > 2.5 { t.Fatalf("visitors ≈ 2, got %.2f", c) } b, _ := json.Marshal(d) for _, want := range []string{`"v":1`, `"hours"`, `"visitors"`, `"pages"`, `"referers"`, `"events"`, `"seq"`, `"dropped"`, `"countries"`} { if !contains(string(b), want) { t.Errorf("wire missing %s in %s", want, b) } } } func TestDayMergeReplacesCorruptSketch(t *testing.T) { d := Day{Visitors: "garbage!"} ok, err := d.Merge(delta("box-a", 1, 5)) if !ok || err != nil { t.Fatalf("merge with corrupt sketch: %v %v", ok, err) } if d.Hours["09"].PV != 5 || d.Pages["/"] != 5 { t.Fatalf("day = %+v", d) } vis, err := DecodeHLL(d.Visitors) if err != nil { t.Fatalf("decode replaced sketch: %v", err) } if c := vis.Count(); c < 0.5 { t.Fatalf("expected at least one visitor recorded, got %.2f", c) } } func TestUpsertDayCASAcrossBoxes(t *testing.T) { ctx := context.Background() s := release.NewDirStore(t.TempDir()) if err := UpsertDay(ctx, s, "bab", "web", delta("box-a", 1, 3)); err != nil { t.Fatal(err) } if err := UpsertDay(ctx, s, "bab", "web", delta("box-a", 2, 4)); err != nil { t.Fatal(err) } if err := UpsertDay(ctx, s, "bab", "web", delta("box-a", 2, 4)); err != nil { // retry of an ambiguous put t.Fatal(err) } raw, _, err := s.Get(ctx, must(t)(DayKey("bab", "web", "box-a", "2026-09-07"))) if err != nil { t.Fatal(err) } var d Day json.Unmarshal(raw, &d) if d.Hours["09"].PV != 7 || d.Seq["box-a"] != 2 { t.Fatalf("day = %+v", d) } if k := must(t)(DayKey("bab", "web", "box-a", "2026-09-07")); k != "apps/bab/web/analytics/box-a/days/2026-09-07.json" { t.Fatal(k) } } func contains(s, sub string) bool { return strings.Contains(s, sub) } // must unwraps a key builder's (key, error) for fixtures whose names are // known good: must(t)(ChunkKey(...)). func must(t testing.TB) func(string, error) string { return func(k string, err error) string { t.Helper() if err != nil { t.Fatal(err) } return k } }