Files
meta-core/internal/reports/reports_test.go
T
2026-11-04 11:43:01 +00:00

228 lines
6.7 KiB
Go

package reports
import (
"encoding/json"
"path/filepath"
"strings"
"testing"
"time"
)
func open(t *testing.T, maxRows int, maxBytes int64) *Store {
t.Helper()
s, err := Open(filepath.Join(t.TempDir(), "db", "reports.db"), maxRows, maxBytes)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = s.Close() })
return s
}
func envelope(t *testing.T, v map[string]any) map[string]json.RawMessage {
t.Helper()
b, _ := json.Marshal(v)
var out map[string]json.RawMessage
if err := json.Unmarshal(b, &out); err != nil {
t.Fatal(err)
}
return out
}
func file(t *testing.T, s *Store, v map[string]any) int64 {
t.Helper()
r, err := Normalize(envelope(t, v), time.Now())
if err != nil {
t.Fatal(err)
}
id, err := s.Insert(r)
if err != nil {
t.Fatal(err)
}
return id
}
func TestNormalizeRequiresApp(t *testing.T) {
if _, err := Normalize(envelope(t, map[string]any{"note": "x"}), time.Now()); err == nil {
t.Fatal("a report without app must be refused")
}
}
func TestNormalizeClampsAndBounds(t *testing.T) {
log := make([]string, 250)
for i := range log {
log[i] = strings.Repeat("é", 700)
}
now := time.Now()
r, err := Normalize(envelope(t, map[string]any{
"app": "meta-read",
"note": strings.Repeat("n", 1500),
"title": strings.Repeat("t", 300),
"log": log,
"context": map[string]string{"big": strings.Repeat("x", MaxBlockBytes)},
"reader": map[string]int{"page": 4},
"device": nil,
"filedAt": now.Add(-time.Hour).UnixMilli(),
}), now)
if err != nil {
t.Fatal(err)
}
if len(r.Note) != maxNote || len(r.Title) != maxTitle {
t.Fatalf("note/title not clamped: %d %d", len(r.Note), len(r.Title))
}
if r.TS != now.Add(-time.Hour).UnixMilli() || r.Received != now.UnixMilli() {
t.Fatalf("filedAt should set ts, received stays now: %d %d", r.TS, r.Received)
}
var body map[string]json.RawMessage
_ = json.Unmarshal(r.Body, &body)
var lines []string
_ = json.Unmarshal(body["log"], &lines)
if len(lines) != MaxLogLines || r.LogLines != MaxLogLines {
t.Fatalf("log tail: %d lines (col %d)", len(lines), r.LogLines)
}
if n := len([]rune(lines[0])); n != MaxLogLineChars {
t.Fatalf("log line clamped by rune: %d", n)
}
if !strings.Contains(string(body["context"]), `"truncated":true`) {
t.Fatalf("oversized block not replaced by a marker: %.80s", body["context"])
}
if string(body["reader"]) != `{"page":4}` {
t.Fatalf("app block not kept verbatim: %s", body["reader"])
}
if _, ok := body["device"]; ok {
t.Fatal("null block should be dropped")
}
if _, ok := body["filedAt"]; ok {
t.Fatal("filedAt belongs in ts, not the body")
}
}
func TestNormalizeIgnoresImplausibleFiledAt(t *testing.T) {
now := time.Now()
for _, at := range []time.Time{now.Add(-60 * 24 * time.Hour), now.Add(time.Hour)} {
r, _ := Normalize(envelope(t, map[string]any{"app": "a", "filedAt": at.UnixMilli()}), now)
if r.TS != now.UnixMilli() {
t.Fatalf("filedAt %v should be ignored", at)
}
}
}
func TestInsertGetListFilters(t *testing.T) {
s := open(t, 0, 0)
a := file(t, s, map[string]any{"app": "meta-watch", "note": "stall at 3%", "cid": "bafyA"})
b := file(t, s, map[string]any{"app": "meta-read", "note": "blank page", "uid": "u1"})
c := file(t, s, map[string]any{"app": "meta-read", "note": "wrong cover 100%"})
got, err := s.Get(a)
if err != nil || got.App != "meta-watch" || got.CID != "bafyA" || got.Status != StatusOpen {
t.Fatalf("get: %+v %v", got.Summary, err)
}
if _, err := s.Get(999); err != ErrNotFound {
t.Fatalf("missing id: %v", err)
}
p, err := s.List(Filter{})
if err != nil || len(p.Reports) != 3 || p.Reports[0].ID != c {
t.Fatalf("list newest first: %+v %v", p.Reports, err)
}
if p.Apps["meta-read"] != 2 || p.Apps["meta-watch"] != 1 || p.Counts[StatusOpen] != 3 {
t.Fatalf("counts: %+v %+v", p.Apps, p.Counts)
}
p, _ = s.List(Filter{App: "meta-read"})
if len(p.Reports) != 2 || p.Counts[StatusOpen] != 2 || p.Apps["meta-watch"] != 1 {
t.Fatalf("app filter: %+v counts %+v apps %+v", p.Reports, p.Counts, p.Apps)
}
p, _ = s.List(Filter{Q: "100%"})
if len(p.Reports) != 1 || p.Reports[0].ID != c {
t.Fatalf("q is a literal substring (%% escaped): %+v", p.Reports)
}
p, _ = s.List(Filter{Q: "u1"})
if len(p.Reports) != 1 || p.Reports[0].ID != b {
t.Fatalf("q matches uid: %+v", p.Reports)
}
p, _ = s.List(Filter{Limit: 2})
if len(p.Reports) != 2 || !p.More {
t.Fatalf("page 1: %d more=%v", len(p.Reports), p.More)
}
p, _ = s.List(Filter{Limit: 2, Before: p.Reports[1].ID})
if len(p.Reports) != 1 || p.More || p.Reports[0].ID != a {
t.Fatalf("page 2: %+v more=%v", p.Reports, p.More)
}
}
func TestUpdateAndDelete(t *testing.T) {
s := open(t, 0, 0)
id := file(t, s, map[string]any{"app": "a"})
st, note := StatusTriaged, " Deck #900 "
r, err := s.Update(id, &st, &note)
if err != nil || r.Status != StatusTriaged || r.TriageNote != "Deck #900" || r.Updated == 0 {
t.Fatalf("update: %+v %v", r.Summary, err)
}
bad := "done"
if _, err := s.Update(id, &bad, nil); err == nil {
t.Fatal("invalid status accepted")
}
if _, err := s.Update(42, &st, nil); err != ErrNotFound {
t.Fatalf("update missing: %v", err)
}
p, _ := s.List(Filter{Status: StatusTriaged})
if len(p.Reports) != 1 {
t.Fatalf("status filter: %+v", p.Reports)
}
if err := s.Delete(id); err != nil {
t.Fatal(err)
}
if err := s.Delete(id); err != ErrNotFound {
t.Fatalf("second delete: %v", err)
}
}
func TestPruneEvictsClosedThenOldest(t *testing.T) {
s := open(t, 3, 0)
ids := []int64{
file(t, s, map[string]any{"app": "a", "note": "1"}),
file(t, s, map[string]any{"app": "a", "note": "2"}),
file(t, s, map[string]any{"app": "a", "note": "3"}),
}
closed := StatusClosed
if _, err := s.Update(ids[2], &closed, nil); err != nil {
t.Fatal(err)
}
// Fourth report: the closed one (newest of the three) goes first.
file(t, s, map[string]any{"app": "a", "note": "4"})
if _, err := s.Get(ids[2]); err != ErrNotFound {
t.Fatal("closed report should be evicted before older open ones")
}
// Fifth: no closed left, so the oldest open goes.
file(t, s, map[string]any{"app": "a", "note": "5"})
if _, err := s.Get(ids[0]); err != ErrNotFound {
t.Fatal("oldest open report should be evicted next")
}
p, _ := s.List(Filter{})
if len(p.Reports) != 3 {
t.Fatalf("store over its row bound: %d", len(p.Reports))
}
}
func TestPruneByBytes(t *testing.T) {
s := open(t, 0, 3000)
for i := 0; i < 5; i++ {
file(t, s, map[string]any{"app": "a", "note": strings.Repeat("x", 900)})
}
p, _ := s.List(Filter{})
var total int64
for _, r := range p.Reports {
total += r.Bytes
}
if total > 3000 || len(p.Reports) == 0 {
t.Fatalf("byte bound not held: %d bytes in %d reports", total, len(p.Reports))
}
}
func TestClampKeepsRunes(t *testing.T) {
if got := clamp("héllo", 2); got != "hé" {
t.Fatalf("clamp: %q", got)
}
}