- Parse each child's own "Today" day widget for the timetable (FR-16); drop the school-wide week calendar's lesson grid and clean the leaked routine rows. - Add the editable codes map (FR-17): subjects, rooms, teachers and period labels, seeded, an unknown code falls back to the raw code; `school codes` CLI. - Brief: Finn's block first, then Harry's, four sections always present (FR-9); periods decoded into plain English; the undated tail is one counted line (FR-18). - Calendar: write periods + work due + work overdue + major events, per child, on every pass. Identity is the stable per-item key, so a re-run never re-inserts (FR-8). morning-brief.sh now runs scan -> calendar push -> notify. - Day-widget labels are 12-hour: 1-6 read as PM, fixing afternoon periods. Proven live on .13: image 0.6.2-phase6b7b; both children's briefs decoded and separate; 35 events in calendar.school; runs 2 and 3 of the scan added nothing. Co-Authored-By: Paperclip <noreply@paperclip.ing>
149 lines
4.9 KiB
Go
149 lines
4.9 KiB
Go
package store
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// TestCalendarCandidatesSelectsImportantFutureEventsOnly proves the Phase 6
|
|
// selection rule: only important, future Compass events that are not in the
|
|
// calendar yet are candidates.
|
|
func TestCalendarCandidatesSelectsImportantFutureEventsOnly(t *testing.T) {
|
|
ctx := context.Background()
|
|
st := newTestStore(t)
|
|
harry, _ := st.ChildByName(ctx, "Harry")
|
|
finn, _ := st.ChildByName(ctx, "Finn")
|
|
|
|
now := time.Date(2026, 10, 15, 9, 0, 0, 0, time.UTC)
|
|
future := now.Add(4 * time.Hour)
|
|
past := now.Add(-4 * time.Hour)
|
|
|
|
items := []Item{
|
|
{Source: "compass", Type: "event", Title: "Concert", DueAt: &future, Importance: "high", ExternalRef: "e1"},
|
|
{Source: "compass", Type: "event", Title: "Lesson", DueAt: &future, Importance: "normal", ExternalRef: "e2"},
|
|
{Source: "compass", Type: "event", Title: "Yesterday", DueAt: &past, Importance: "high", ExternalRef: "e3"},
|
|
{Source: "compass", Type: "task", Title: "Essay", DueAt: &future, Importance: "high", ExternalRef: "e4"},
|
|
{Source: "teams", Type: "event", Title: "Teams thing", DueAt: &future, Importance: "high", ExternalRef: "e5"},
|
|
}
|
|
if _, _, err := st.UpsertItems(ctx, harry.ID, items); err != nil {
|
|
t.Fatalf("upsert: %v", err)
|
|
}
|
|
if _, _, err := st.UpsertItems(ctx, finn.ID, []Item{
|
|
{Source: "compass", Type: "event", Title: "Finn event", DueAt: &future, Importance: "high", ExternalRef: "f1"},
|
|
}); err != nil {
|
|
t.Fatalf("upsert finn: %v", err)
|
|
}
|
|
|
|
all, err := st.CalendarCandidates(ctx, nil, nil, now, 0)
|
|
if err != nil {
|
|
t.Fatalf("candidates: %v", err)
|
|
}
|
|
if len(all) != 2 {
|
|
t.Fatalf("want 2 candidates (one per child), got %d: %+v", len(all), all)
|
|
}
|
|
if all[0].Title != "Concert" || all[1].Title != "Finn event" {
|
|
t.Errorf("unexpected candidate order: %+v", all)
|
|
}
|
|
|
|
onlyHarry, err := st.CalendarCandidates(ctx, &harry.ID, nil, now, 0)
|
|
if err != nil {
|
|
t.Fatalf("candidates: %v", err)
|
|
}
|
|
if len(onlyHarry) != 1 || onlyHarry[0].Child != "Harry" {
|
|
t.Fatalf("per-child filter failed: %+v", onlyHarry)
|
|
}
|
|
|
|
// Mark the concert as synced; it must leave the candidate set.
|
|
if err := st.MarkItemCalendarSynced(ctx, all[0].ID, "uid-1", now); err != nil {
|
|
t.Fatalf("mark: %v", err)
|
|
}
|
|
after, err := st.CalendarCandidates(ctx, nil, nil, now, 0)
|
|
if err != nil {
|
|
t.Fatalf("candidates: %v", err)
|
|
}
|
|
if len(after) != 1 || after[0].Title != "Finn event" {
|
|
t.Fatalf("synced item is still a candidate: %+v", after)
|
|
}
|
|
ci, err := st.CalendarItemByID(ctx, all[0].ID)
|
|
if err != nil || ci == nil {
|
|
t.Fatalf("item: %v %v", ci, err)
|
|
}
|
|
if ci.HAEventUID != "uid-1" || ci.HAPushedAt == nil {
|
|
t.Errorf("mark did not stick: %+v", ci)
|
|
}
|
|
}
|
|
|
|
// TestCalendarPushSummaryIsAppendOnly proves a failed write stays on record.
|
|
func TestCalendarPushSummaryIsAppendOnly(t *testing.T) {
|
|
ctx := context.Background()
|
|
st := newTestStore(t)
|
|
harry, _ := st.ChildByName(ctx, "Harry")
|
|
due := time.Now().Add(time.Hour).UTC()
|
|
if _, _, err := st.UpsertItems(ctx, harry.ID, []Item{
|
|
{Source: "compass", Type: "event", Title: "Concert", DueAt: &due, Importance: "high", ExternalRef: "e1"},
|
|
}); err != nil {
|
|
t.Fatalf("upsert: %v", err)
|
|
}
|
|
items, _ := st.ItemsForChild(ctx, harry.ID)
|
|
|
|
empty, err := st.CalendarPushSummary(ctx)
|
|
if err != nil {
|
|
t.Fatalf("summary: %v", err)
|
|
}
|
|
if empty.EverRun || empty.Total != 0 || empty.Last != nil {
|
|
t.Fatalf("fresh summary = %+v", empty)
|
|
}
|
|
|
|
if _, err := st.RecordCalendarPush(ctx, items[0].ID, "calendar.school", "", false, "boom"); err != nil {
|
|
t.Fatalf("record: %v", err)
|
|
}
|
|
if _, err := st.RecordCalendarPush(ctx, items[0].ID, "calendar.school", "uid-1", true, ""); err != nil {
|
|
t.Fatalf("record: %v", err)
|
|
}
|
|
|
|
summary, err := st.CalendarPushSummary(ctx)
|
|
if err != nil {
|
|
t.Fatalf("summary: %v", err)
|
|
}
|
|
if summary.Total != 2 || summary.Failed != 1 || !summary.EverRun {
|
|
t.Fatalf("summary = %+v", summary)
|
|
}
|
|
if summary.Last == nil || !summary.Last.OK || summary.Last.UID != "uid-1" {
|
|
t.Fatalf("last = %+v", summary.Last)
|
|
}
|
|
}
|
|
|
|
// TestCodesSeedAndEdit proves the decode map is seeded, editable and that a
|
|
// correction survives a re-seed (FR-17).
|
|
func TestCodesSeedAndEdit(t *testing.T) {
|
|
ctx := context.Background()
|
|
st := newTestStore(t)
|
|
|
|
codes, err := st.Codes(ctx)
|
|
if err != nil {
|
|
t.Fatalf("codes: %v", err)
|
|
}
|
|
if codes.Subject["7SCF"] != "Year 7 Science" {
|
|
t.Errorf("7SCF = %q, want the seeded Year 7 Science", codes.Subject["7SCF"])
|
|
}
|
|
if codes.Subject["NOPE"] != "" {
|
|
t.Errorf("unknown code should be missing, got %q", codes.Subject["NOPE"])
|
|
}
|
|
|
|
if _, err := st.SetCode(ctx, "teacher", "KNI", "Mr Smith"); err != nil {
|
|
t.Fatalf("set: %v", err)
|
|
}
|
|
again, err := st.Codes(ctx)
|
|
if err != nil {
|
|
t.Fatalf("codes again: %v", err)
|
|
}
|
|
if again.Teacher["KNI"] != "Mr Smith" {
|
|
t.Errorf("KNI = %q, want the correction to survive the re-seed", again.Teacher["KNI"])
|
|
}
|
|
|
|
if _, err := st.SetCode(ctx, "bogus", "X", "Y"); err == nil {
|
|
t.Error("an unknown kind should be rejected")
|
|
}
|
|
}
|