Phase 6 follow-up: today-and-future events, one write per school-wide event

Two corrections found while preparing the first live run:

- A scan that runs late in the day still records today's school events; the
  candidate window now starts at the local start of day instead of "now".
- The Compass week calendar is school-wide, so one event is stored against each
  child. Write it once, with no child prefix in the title, and record the same
  Home Assistant event id on every child's item. A per-child event still keeps
  the child's name.

Also report pending events as events, not rows, in `school calendar status`.
Tests cover the window and the de-duplication.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
2026-10-15 18:05:33 +11:00
parent f6be17fe4f
commit f56dbb46ac
5 changed files with 244 additions and 80 deletions

View File

@@ -188,31 +188,35 @@ func TestSyncDryRunWritesNothing(t *testing.T) {
}
}
func TestSyncIgnoresRoutineLessonsAndPastEvents(t *testing.T) {
func TestSyncWindowIsTodayAndFuture(t *testing.T) {
ctx := context.Background()
st, childID := testStore(t)
now := time.Now()
day := StartOfDay(now)
seedEvent(t, st, childID, "5 - 11HLEGB5 - D8 - LHT", now.Add(3*time.Hour), "normal", "lesson")
seedEvent(t, st, childID, "Year 11 Assembly - HALL", now.Add(-2*time.Hour), "high", "past")
seedEvent(t, st, childID, "Yesterday excursion - CITY", day.Add(-time.Hour), "high", "yesterday")
seedEvent(t, st, childID, "Year 11 Assembly - HALL", day.Add(time.Minute), "high", "today")
ha := newFakeHA(t, "calendar.school")
c := testClient(t, ha.URL, "calendar.school")
candidates, err := st.CalendarCandidates(ctx, nil, nil, now, 0)
candidates, err := st.CalendarCandidates(ctx, nil, nil, day, 0)
if err != nil {
t.Fatalf("candidates: %v", err)
}
// The store selects on importance; the lesson and the past event are
// excluded because the parser never marks a lesson high and the past event
// is behind `now`.
if len(candidates) != 0 {
// A routine lesson is never high, and yesterday is behind the window. The
// event earlier today stays a candidate: a scan that runs late in the day
// must still record today's school events.
if len(candidates) != 1 || candidates[0].Title != "Year 11 Assembly - HALL" {
t.Fatalf("candidates = %+v", candidates)
}
res, err := Sync(ctx, st, c, Options{Now: now}, silentLogger())
if err != nil {
t.Fatalf("Sync: %v", err)
}
if res.Created != 0 || ha.created() != 0 {
t.Fatalf("result = %+v", res)
if res.Candidates != 1 || res.Created != 1 || ha.created() != 1 {
t.Fatalf("result = %+v (create calls %d)", res, ha.created())
}
}
@@ -236,3 +240,53 @@ func TestSyncReportsAReadFailureInsteadOfWritingBlind(t *testing.T) {
t.Errorf("error = %q", res.Errors[0])
}
}
// TestSyncWritesASchoolWideEventOnce proves the shared Compass week calendar is
// not duplicated: one event stored against both children is written once, and
// both children's items record the same Home Assistant event id.
func TestSyncWritesASchoolWideEventOnce(t *testing.T) {
ctx := context.Background()
st, harryID := testStore(t)
if err := st.SeedChildren(ctx, "Finn"); err != nil {
t.Fatalf("seed finn: %v", err)
}
finn, err := st.ChildByName(ctx, "Finn")
if err != nil || finn == nil {
t.Fatalf("finn: %v %v", finn, err)
}
due := time.Now().Add(4 * time.Hour).UTC().Truncate(time.Minute)
harryItem := seedEvent(t, st, harryID, "Year 11 Assembly - HALL - CMA", due, "high", "assembly")
finnItem := seedEvent(t, st, finn.ID, "Year 11 Assembly - HALL - CMA", due, "high", "assembly")
ha := newFakeHA(t, "calendar.school")
c := testClient(t, ha.URL, "calendar.school")
res, err := Sync(ctx, st, c, Options{Now: time.Now()}, silentLogger())
if err != nil {
t.Fatalf("Sync: %v", err)
}
if res.Candidates != 1 || res.Created != 1 {
t.Fatalf("result = %+v", res)
}
if ha.created() != 1 {
t.Fatalf("school-wide event was written %d times", ha.created())
}
// A school-wide event carries just its title, not a child's name.
if got := str(ha.lastCreate["summary"]); got != "Year 11 Assembly - HALL - CMA" {
t.Errorf("summary = %q", got)
}
uid := res.Events[0].UID
if uid == "" {
t.Fatal("event written without a Home Assistant id")
}
for _, id := range []int64{harryItem.ID, finnItem.ID} {
ci, err := st.CalendarItemByID(ctx, id)
if err != nil || ci == nil {
t.Fatalf("item %d: %v %v", id, ci, err)
}
if ci.HAEventUID != uid {
t.Errorf("item %d uid = %q, want %q", id, ci.HAEventUID, uid)
}
}
}