192 lines
7.1 KiB
Go
192 lines
7.1 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func entryAt(id int64, mark Mark, date time.Time, due time.Time, group int64) Entry {
|
|
return Entry{ID: id, Mark: mark, Date: date, DueDate: due, Group: group, Judged: date}
|
|
}
|
|
|
|
// 1521/AC1: entries newest first, except a mail that matters comes first, nearest due date first among
|
|
// those (none last).
|
|
func TestSortEntries(t *testing.T) {
|
|
d := func(day int) time.Time { return time.Date(2026, 10, day, 0, 0, 0, 0, time.UTC) }
|
|
entries := []Entry{
|
|
entryAt(1, MarkNeither, d(10), time.Time{}, 0),
|
|
entryAt(2, MarkMatters, d(5), d(20), 0), // matters, due the 20th
|
|
entryAt(3, MarkMatters, d(1), time.Time{}, 0), // matters, no due date
|
|
entryAt(4, MarkNeither, d(15), time.Time{}, 0),
|
|
entryAt(5, MarkMatters, d(8), d(12), 0), // matters, due the 12th (nearest)
|
|
}
|
|
sorted := SortEntries(entries)
|
|
var order []int64
|
|
for _, e := range sorted {
|
|
order = append(order, e.ID)
|
|
}
|
|
want := []int64{5, 2, 3, 4, 1} // matters by nearest due date (5 then 2), matters-no-due-date last among matters (3), then non-matters newest first (4, 1)
|
|
if len(order) != len(want) {
|
|
t.Fatalf("order = %v, want %v", order, want)
|
|
}
|
|
for i := range want {
|
|
if order[i] != want[i] {
|
|
t.Fatalf("order = %v, want %v", order, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 1521/AC1, AC4: filtered by mail group or by mark when asked.
|
|
func TestFilterEntries(t *testing.T) {
|
|
entries := []Entry{
|
|
{ID: 1, Group: 10, Mark: MarkMatters},
|
|
{ID: 2, Group: 10, Mark: MarkNeither},
|
|
{ID: 3, Group: 20, Mark: MarkMatters},
|
|
}
|
|
if got := FilterEntries(entries, 10, ""); len(got) != 2 {
|
|
t.Fatalf("filter by group 10: %v, want 2", got)
|
|
}
|
|
if got := FilterEntries(entries, 0, MarkMatters); len(got) != 2 {
|
|
t.Fatalf("filter by mark matters: %v, want 2", got)
|
|
}
|
|
if got := FilterEntries(entries, 10, MarkMatters); len(got) != 1 || got[0].ID != 1 {
|
|
t.Fatalf("filter by group 10 and mark matters: %v, want just entry 1", got)
|
|
}
|
|
}
|
|
|
|
// 1521/AC2: groups largest first.
|
|
func TestSortGroups(t *testing.T) {
|
|
groups := []Group{{ID: 1, Label: "small", Count: 1}, {ID: 2, Label: "big", Count: 100}, {ID: 3, Label: "medium", Count: 10}}
|
|
sorted := SortGroups(groups)
|
|
if sorted[0].Label != "big" || sorted[1].Label != "medium" || sorted[2].Label != "small" {
|
|
t.Fatalf("sorted = %+v, want big, medium, small", sorted)
|
|
}
|
|
}
|
|
|
|
// 1521/AC3: reading lessons per 100 read mails for each of the last 8 weeks; a week with no mail read has
|
|
// no rate.
|
|
func TestWeeklyMeasure(t *testing.T) {
|
|
now := time.Date(2026, 10, 21, 12, 0, 0, 0, time.UTC) // a Wednesday
|
|
thisWeek := weekStart(now)
|
|
lastWeek := thisWeek.AddDate(0, 0, -7)
|
|
|
|
entries := []Entry{
|
|
{ID: 1, Judged: thisWeek.Add(time.Hour)},
|
|
{ID: 2, Judged: thisWeek.Add(2 * time.Hour)},
|
|
{ID: 3, Judged: lastWeek.Add(time.Hour)},
|
|
}
|
|
lessons := []Lesson{
|
|
{ID: 1, Made: thisWeek.Add(3 * time.Hour)},
|
|
}
|
|
weeks := WeeklyMeasure(entries, lessons, now)
|
|
if len(weeks) != WeeksCovered {
|
|
t.Fatalf("len(weeks) = %d, want %d", len(weeks), WeeksCovered)
|
|
}
|
|
last := weeks[len(weeks)-1]
|
|
if !last.WeekStart.Equal(thisWeek) {
|
|
t.Fatalf("last week's start = %v, want %v", last.WeekStart, thisWeek)
|
|
}
|
|
if last.MailsRead != 2 || last.Lessons != 1 || last.Rate == nil || *last.Rate != 50 {
|
|
t.Fatalf("this week = %+v, want 2 read, 1 lesson, rate 50", last)
|
|
}
|
|
secondLast := weeks[len(weeks)-2]
|
|
if secondLast.MailsRead != 1 || secondLast.Lessons != 0 || secondLast.Rate == nil || *secondLast.Rate != 0 {
|
|
t.Fatalf("last week = %+v, want 1 read, 0 lessons, rate 0", secondLast)
|
|
}
|
|
// Every other week (further back, with no mail read) carries no rate.
|
|
for i := 0; i < len(weeks)-2; i++ {
|
|
if weeks[i].Rate != nil {
|
|
t.Fatalf("week %d = %+v, want no rate (no mail read)", i, weeks[i])
|
|
}
|
|
}
|
|
}
|
|
|
|
// 1521/AC1: the entries verb answers the mail entries newest first, filtered by group or mark, with the
|
|
// required fields.
|
|
func TestEntriesToolAnswersFilteredAndSorted(t *testing.T) {
|
|
ctx := context.Background()
|
|
store := newMemStore()
|
|
g, err := store.FindOrCreateGroup(ctx, "newsletters")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
store.AddEntry(ctx, Entry{Account: "bob", Subject: "a", Mark: MarkNeither, Group: g.ID, Date: time.Date(2026, 10, 1, 0, 0, 0, 0, time.UTC)})
|
|
store.AddEntry(ctx, Entry{Account: "bob", Subject: "b", Mark: MarkMatters, DueDate: time.Date(2026, 11, 1, 0, 0, 0, 0, time.UTC), Date: time.Date(2026, 10, 2, 0, 0, 0, 0, time.UTC)})
|
|
|
|
b := &Bundle{
|
|
Caller: func(map[string]any) string { return "mesh-mail-web" },
|
|
callersOverride: func() (Callers, error) { return Callers{"mesh-mail-web"}, nil },
|
|
Store: func(context.Context) (Store, error) { return store, nil },
|
|
}
|
|
out, err := b.entriesTool(map[string]any{"group": "newsletters"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
m := out.(map[string]any)
|
|
entries := m["entries"].([]map[string]any)
|
|
if len(entries) != 1 || entries[0]["subject"] != "a" || entries[0]["group"] != "newsletters" {
|
|
t.Fatalf("filtered by group: %v", entries)
|
|
}
|
|
|
|
out, err = b.entriesTool(map[string]any{"mark": "matters"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
entries = out.(map[string]any)["entries"].([]map[string]any)
|
|
if len(entries) != 1 || entries[0]["subject"] != "b" || entries[0]["due-date"] != "2026-11-01" {
|
|
t.Fatalf("filtered by mark: %v", entries)
|
|
}
|
|
}
|
|
|
|
// 1521/AC2: the groups verb answers each mail group with how many mails it holds, largest first.
|
|
func TestGroupsToolAnswersLargestFirst(t *testing.T) {
|
|
ctx := context.Background()
|
|
store := newMemStore()
|
|
small, _ := store.FindOrCreateGroup(ctx, "small")
|
|
big, _ := store.FindOrCreateGroup(ctx, "big")
|
|
store.AddEntry(ctx, Entry{Group: small.ID})
|
|
store.AddEntry(ctx, Entry{Group: big.ID})
|
|
store.AddEntry(ctx, Entry{Group: big.ID})
|
|
|
|
b := &Bundle{
|
|
Caller: func(map[string]any) string { return "mesh-mail-web" },
|
|
callersOverride: func() (Callers, error) { return Callers{"mesh-mail-web"}, nil },
|
|
Store: func(context.Context) (Store, error) { return store, nil },
|
|
}
|
|
out, err := b.groupsTool(map[string]any{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
groups := out.(map[string]any)["groups"].([]map[string]any)
|
|
if len(groups) != 2 || groups[0]["label"] != "big" || groups[0]["count"] != 2 || groups[1]["label"] != "small" || groups[1]["count"] != 1 {
|
|
t.Fatalf("groups = %v", groups)
|
|
}
|
|
}
|
|
|
|
// 1521/AC3: status answers healthy or not, the cause and since when, and the weekly measure.
|
|
func TestStatusToolAnswersHealthAndWeeklyMeasure(t *testing.T) {
|
|
ctx := context.Background()
|
|
store := newMemStore()
|
|
since := time.Date(2026, 10, 20, 9, 30, 0, 0, time.UTC)
|
|
store.SetReaderHealth(ctx, ReaderHealth{Healthy: false, Cause: OutcomeRefused, Why: "holds no licence", Since: since})
|
|
|
|
b := &Bundle{
|
|
Caller: func(map[string]any) string { return "mesh-mail-web" },
|
|
callersOverride: func() (Callers, error) { return Callers{"mesh-mail-web"}, nil },
|
|
Store: func(context.Context) (Store, error) { return store, nil },
|
|
}
|
|
out, err := b.statusTool(map[string]any{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
m := out.(map[string]any)
|
|
if m["healthy"] != false || m["cause"] != "refused" || m["why"] != "holds no licence" {
|
|
t.Fatalf("status = %v", m)
|
|
}
|
|
weeks := m["weeks"].([]map[string]any)
|
|
if len(weeks) != WeeksCovered {
|
|
t.Fatalf("len(weeks) = %d, want %d", len(weeks), WeeksCovered)
|
|
}
|
|
}
|