Phase 6b/7b: per-child decoded periods, separate brief blocks, idempotent calendar (AUD-119)
- 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>
This commit is contained in:
@@ -330,3 +330,85 @@ func TestSyncWithoutEventIDsStillNeverDuplicates(t *testing.T) {
|
||||
t.Fatalf("re-run duplicated the event (%d create calls)", ha.created())
|
||||
}
|
||||
}
|
||||
|
||||
// seedCalendarItem stores one item and returns it.
|
||||
func seedCalendarItem(t *testing.T, st *store.Store, childID int64, item store.Item) store.Item {
|
||||
t.Helper()
|
||||
if _, _, err := st.UpsertItems(context.Background(), childID, []store.Item{item}); err != nil {
|
||||
t.Fatalf("upsert: %v", err)
|
||||
}
|
||||
items, err := st.ItemsForChild(context.Background(), childID)
|
||||
if err != nil {
|
||||
t.Fatalf("items: %v", err)
|
||||
}
|
||||
for _, it := range items {
|
||||
if it.ExternalRef == item.ExternalRef {
|
||||
return it
|
||||
}
|
||||
}
|
||||
t.Fatalf("seeded item %q not found", item.ExternalRef)
|
||||
return store.Item{}
|
||||
}
|
||||
|
||||
// TestSyncWritesPeriodsWorkAndOverdueOnce proves FR-8: a child's own periods,
|
||||
// the work due in the future and the overdue work all reach the calendar, and a
|
||||
// re-run adds nothing. Submitted work is never written.
|
||||
func TestSyncWritesPeriodsWorkAndOverdueOnce(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
st, childID := testStore(t)
|
||||
loc := time.Local
|
||||
now := StartOfDay(time.Now().In(loc)).Add(6 * time.Hour)
|
||||
period := now.Add(2 * time.Hour).Truncate(time.Minute)
|
||||
overdue := now.AddDate(0, 0, -3).Truncate(time.Minute)
|
||||
future := now.AddDate(0, 0, 3).Truncate(time.Minute)
|
||||
|
||||
seedCalendarItem(t, st, childID, store.Item{
|
||||
Source: "compass", Type: "event", Subject: "7SCF",
|
||||
Title: "1 - 7SCF - H14LAB - KNI", DueAt: &period,
|
||||
SourcePage: "child-profile", Importance: "normal", ExternalRef: "period-1",
|
||||
})
|
||||
seedCalendarItem(t, st, childID, store.Item{
|
||||
Source: "compass", Type: "task", Subject: "Science", Title: "Lab report",
|
||||
Body: "Status: Overdue", DueAt: &overdue, Importance: "high", ExternalRef: "overdue-1",
|
||||
})
|
||||
seedCalendarItem(t, st, childID, store.Item{
|
||||
Source: "compass", Type: "task", Subject: "Maths", Title: "Exercise 4",
|
||||
Body: "Status: Pending", DueAt: &future, Importance: "normal", ExternalRef: "due-1",
|
||||
})
|
||||
seedCalendarItem(t, st, childID, store.Item{
|
||||
Source: "compass", Type: "task", Subject: "English", Title: "Done essay",
|
||||
Body: "Status: On Time", DueAt: &overdue, Importance: "normal", ExternalRef: "submitted-1",
|
||||
})
|
||||
|
||||
ha := newFakeHA(t, "calendar.school")
|
||||
c := testClient(t, ha.URL, "calendar.school")
|
||||
|
||||
res, err := Sync(ctx, st, c, Options{Now: now}, silentLogger())
|
||||
if err != nil {
|
||||
t.Fatalf("first Sync: %v", err)
|
||||
}
|
||||
if res.Created != 3 || res.Failed != 0 {
|
||||
t.Fatalf("first result = %+v", res)
|
||||
}
|
||||
if ha.created() != 3 {
|
||||
t.Fatalf("first Sync made %d create calls, want 3", ha.created())
|
||||
}
|
||||
kinds := map[Kind]int{}
|
||||
for _, ev := range res.Events {
|
||||
kinds[ev.Kind]++
|
||||
}
|
||||
if kinds[KindPeriod] != 1 || kinds[KindOverdue] != 1 || kinds[KindDue] != 1 {
|
||||
t.Errorf("kinds = %+v, want one period, one overdue and one due", kinds)
|
||||
}
|
||||
|
||||
second, err := Sync(ctx, st, c, Options{Now: now}, silentLogger())
|
||||
if err != nil {
|
||||
t.Fatalf("second Sync: %v", err)
|
||||
}
|
||||
if second.Created != 0 || second.Candidates != 0 {
|
||||
t.Fatalf("second result = %+v, want nothing new", second)
|
||||
}
|
||||
if ha.created() != 3 {
|
||||
t.Fatalf("re-run duplicated events (%d create calls)", ha.created())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user