Phase 5a slice B. Replace web/timeline.go's hand-rolled fan-out + day
grouping with calls into the aggregator package.
- web/timeline.go: collectTimelineTodos + collectTimelineEvents +
in-line day grouping deleted. buildTimeline now calls
aggregator.Todos/Events/Docs/Creations, decorates each typed row
with the template-friendly TimelineRow shape (PER, StartLabel,
DurationHint), then hands rows to aggregate.BuildTimelineDays for
sorting + sticky-pill markers + far-future fade.
- web/timeline.go: TimelineRow / TimelineDay are now type aliases for
the aggregate package's versions (Phase 5a slice A introduced them
with the same flat-field layout the templates already address).
- web/server.go: new Server.Aggregator() factory builds a fresh
*aggregate.Aggregator wired to the server's current CalDAV/Gitea
deps (so main.go can install those after web.New without a re-init
hook).
- web/{gitea,dashboard,gitea_writeback,gitea_test}.go: issueCache
methods capitalised (Get/Set/Invalidate) so the aggregator's
IssueCache interface accepts *web.issueCache directly. No behaviour
change.
All web/timeline_*test.go pass unmodified — the refactor preserves
output shape and template field paths.
Task: t-projax-5a-aggregator
67 lines
1.6 KiB
Go
67 lines
1.6 KiB
Go
package web
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/m/projax/gitea"
|
|
)
|
|
|
|
func TestIssueCacheTTL(t *testing.T) {
|
|
c := newIssueCache(50 * time.Millisecond)
|
|
c.Set("k", []gitea.Issue{{Number: 1, Title: "a"}})
|
|
if got, ok := c.Get("k"); !ok || len(got) != 1 {
|
|
t.Fatalf("immediate get: ok=%v got=%v", ok, got)
|
|
}
|
|
time.Sleep(80 * time.Millisecond)
|
|
if _, ok := c.Get("k"); ok {
|
|
t.Errorf("expected miss after TTL, got hit")
|
|
}
|
|
}
|
|
|
|
func TestRelativeTime(t *testing.T) {
|
|
now := time.Date(2026, 5, 15, 12, 0, 0, 0, time.UTC)
|
|
for _, tc := range []struct {
|
|
offset time.Duration
|
|
want string
|
|
}{
|
|
{-30 * time.Second, "just now"},
|
|
{-5 * time.Minute, "5m ago"},
|
|
{-3 * time.Hour, "3h ago"},
|
|
{-30 * time.Hour, "yesterday"},
|
|
{-5 * 24 * time.Hour, "5d ago"},
|
|
} {
|
|
got := relativeTime(now, now.Add(tc.offset))
|
|
if got != tc.want {
|
|
t.Errorf("offset=%v got %q want %q", tc.offset, got, tc.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestToViewPreservesFields(t *testing.T) {
|
|
in := []gitea.Issue{
|
|
{
|
|
Number: 7,
|
|
Title: "x",
|
|
State: "open",
|
|
Labels: []string{"bug"},
|
|
Assignees: []string{"a"},
|
|
Milestone: "M",
|
|
HTMLURL: "https://example/7",
|
|
UpdatedAt: time.Date(2026, 5, 14, 12, 0, 0, 0, time.UTC),
|
|
},
|
|
}
|
|
now := time.Date(2026, 5, 15, 12, 0, 0, 0, time.UTC)
|
|
got := toView(in, now)
|
|
if len(got) != 1 {
|
|
t.Fatalf("expected 1, got %d", len(got))
|
|
}
|
|
v := got[0]
|
|
if v.Number != 7 || v.Title != "x" || v.HTMLURL != "https://example/7" {
|
|
t.Errorf("field mismatch: %+v", v)
|
|
}
|
|
if v.UpdatedRel != "yesterday" {
|
|
t.Errorf("UpdatedRel = %q, want yesterday", v.UpdatedRel)
|
|
}
|
|
}
|