Phase 6: fix the live Home Assistant write (Local Calendar, no event id)
The first live run against the new calendar.school entity failed loudly twice, and both causes are fixed here. - The Local Calendar rejects `return_response` on calendar.create_event. The client retries the call without it and ignores the changed-states array that Home Assistant returns instead. - Home Assistant then reports no event id. The item keeps the description marker ([compass-nhs:<id>]) as its reference, so it leaves the candidate set and is never written twice. The live retry proved it: both events already in the calendar were adopted by their markers, with no duplicate. - calendar.get_events reports `start` / `end` (a date-time or a bare date), not `start_date_time`; the reader now accepts all three shapes. A model must no longer classify events. The calendar write depends on the school-event rule, and the LLM labelled every event "normal"; events now keep the deterministic rule under both classifiers. Also fix the Phase 5 screenshot script: the calendar banner shares the `.scan` class, so select each banner by its labelled section. Docs record the accepted decision: Sam chose a Home Assistant Local Calendar, so the target is calendar.school. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -18,15 +18,16 @@ import (
|
||||
type fakeHA struct {
|
||||
*httptest.Server
|
||||
|
||||
mu sync.Mutex
|
||||
entity string
|
||||
events []map[string]any
|
||||
createCalls int
|
||||
getCalls int
|
||||
failCreate bool
|
||||
returnUID bool
|
||||
lastCreate map[string]any
|
||||
lastGet map[string]any
|
||||
mu sync.Mutex
|
||||
entity string
|
||||
events []map[string]any
|
||||
createCalls int
|
||||
getCalls int
|
||||
failCreate bool
|
||||
returnUID bool
|
||||
rejectReturnResponse bool
|
||||
lastCreate map[string]any
|
||||
lastGet map[string]any
|
||||
}
|
||||
|
||||
func newFakeHA(t *testing.T, entity string) *fakeHA {
|
||||
@@ -57,6 +58,14 @@ func newFakeHA(t *testing.T, entity string) *fakeHA {
|
||||
if !f.authOK(w, r) {
|
||||
return
|
||||
}
|
||||
if f.rejectReturnResponse && r.URL.Query().Has("return_response") {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
writeJSON(w, map[string]any{"error": map[string]any{
|
||||
"code": "unknown_error",
|
||||
"message": "Service does not support responses. Remove return_response from request.",
|
||||
}})
|
||||
return
|
||||
}
|
||||
var body map[string]any
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
http.Error(w, "bad json", http.StatusBadRequest)
|
||||
@@ -76,12 +85,17 @@ func newFakeHA(t *testing.T, entity string) *fakeHA {
|
||||
uid = "uid-" + str(body["summary"])
|
||||
}
|
||||
f.events = append(f.events, map[string]any{
|
||||
"uid": uid,
|
||||
"summary": body["summary"],
|
||||
"description": body["description"],
|
||||
"start_date_time": body["start_date_time"],
|
||||
"end_date_time": body["end_date_time"],
|
||||
"uid": uid,
|
||||
"summary": body["summary"],
|
||||
"description": body["description"],
|
||||
"start": body["start_date_time"],
|
||||
"end": body["end_date_time"],
|
||||
})
|
||||
if !r.URL.Query().Has("return_response") {
|
||||
// Home Assistant replies with the changed states, a JSON array.
|
||||
writeJSON(w, []map[string]any{})
|
||||
return
|
||||
}
|
||||
writeJSON(w, map[string]any{
|
||||
"changed_entities": map[string]any{},
|
||||
"service_response": map[string]any{f.entity: map[string]any{"uid": uid}},
|
||||
@@ -309,3 +323,52 @@ func TestFindMarker(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestCreateEventFallsBackWhenTheCalendarHasNoResponses proves the Local
|
||||
// Calendar integration is usable: it rejects `return_response`, which is not a
|
||||
// write failure.
|
||||
func TestCreateEventFallsBackWhenTheCalendarHasNoResponses(t *testing.T) {
|
||||
ha := newFakeHA(t, "calendar.school")
|
||||
ha.rejectReturnResponse = true
|
||||
c := testClient(t, ha.URL, "calendar.school")
|
||||
|
||||
start := time.Date(2026, 10, 15, 18, 0, 0, 0, time.UTC)
|
||||
uid, err := c.CreateEvent(context.Background(), Event{
|
||||
Summary: "Concert", Start: &start, End: ptrTime(start.Add(time.Hour)),
|
||||
Description: "source: " + Marker(29),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("CreateEvent: %v", err)
|
||||
}
|
||||
if uid != "" {
|
||||
t.Errorf("want no uid, got %q", uid)
|
||||
}
|
||||
if ha.created() != 1 {
|
||||
t.Fatalf("event was not written (%d create calls)", ha.created())
|
||||
}
|
||||
}
|
||||
|
||||
// TestParseHATime covers the shapes calendar.get_events reports.
|
||||
func TestParseHATime(t *testing.T) {
|
||||
timed := map[string]any{"start": "2026-10-15T15:05:00+11:00"}
|
||||
got, raw := parseHATime(timed, "start")
|
||||
if got == nil || raw != "2026-10-15T15:05:00+11:00" {
|
||||
t.Fatalf("timed = %v %q", got, raw)
|
||||
}
|
||||
if got.Hour() != 15 || got.Minute() != 5 {
|
||||
t.Errorf("timed = %v", got)
|
||||
}
|
||||
|
||||
allDay := map[string]any{"start": "2026-10-15"}
|
||||
got, raw = parseHATime(allDay, "start")
|
||||
if got == nil || raw != "2026-10-15" {
|
||||
t.Fatalf("all-day = %v %q", got, raw)
|
||||
}
|
||||
if y, m, d := got.Date(); y != 2026 || m != time.October || d != 15 {
|
||||
t.Errorf("all-day = %v", got)
|
||||
}
|
||||
|
||||
if got, raw := parseHATime(map[string]any{}, "start"); got != nil || raw != "" {
|
||||
t.Errorf("missing = %v %q", got, raw)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user