From 2bec4e60bd120285aa6f6c43c9d421a00c312616 Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Mon, 2 Nov 2026 18:29:54 -0500 Subject: [PATCH] test(pins): annotation lifecycles pin the restore GET, not just writes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Answering the owner's round-trip question (highlight/note/progress survives to the DB and restores on reopen) surfaced a gap: the annotation lifecycle pins covered POST/PUT/DELETE but never the GET list — the exact call web/src/reader/reader.ts makes when a book is reopened. Every lifecycle now pins 'GET list restores the annotation': the created row must come back by id with the update's fields applied (highlights assert color, notes content, bookmarks title/cfi), proving the DB write and the restore read through the same endpoint the UI loads. Ran green in the rebuilt tests image. Workflow note surfaced by this change: the tests service builds an image (no source mount), so test edits need 'docker compose ... build tests' (or run --build) before the filtered runner sees them — the first run of the new subtest silently executed the stale image. --- cmd/server/tests/contract_pins_test.go | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/cmd/server/tests/contract_pins_test.go b/cmd/server/tests/contract_pins_test.go index e839aa37..83732b81 100644 --- a/cmd/server/tests/contract_pins_test.go +++ b/cmd/server/tests/contract_pins_test.go @@ -1214,6 +1214,30 @@ func pinAnnotationLifecycle(t *testing.T, setup *TestServerSetup, fx contractFix require.NoError(t, json.Unmarshal(raw, &body2)) assertResponseShape(t, "annotation "+kind+" update", body2, rowKeys, rowKeys) + // The restore half of the round trip: what the reader fetches + // when the book is reopened must return the persisted annotation + // with the update applied — written to the DB, read back through + // the same list endpoint the UI loads on open. + t.Run(kind+" GET list restores the annotation", func(t *testing.T) { + resp3 := doReq(t, "GET", annotationURL(setup.Server.URL, fx.itemID, kind, ""), nil, setup.RegularToken) + defer resp3.Body.Close() + require.Equal(t, http.StatusOK, resp3.StatusCode, "list must stay 200") + var list []map[string]interface{} + require.NoError(t, json.NewDecoder(resp3.Body).Decode(&list)) + var found map[string]interface{} + for _, row := range list { + if row["id"] == id { + found = row + break + } + } + require.NotNil(t, found, "created %s must come back in the list that restores a reopened book", kind) + updates, _ := updateBody.(map[string]interface{}) + for key, want := range updates { + assert.Equal(t, want, found[key], "%s field must round-trip from the DB on restore", key) + } + }) + t.Run(kind+" DELETE response", func(t *testing.T) { resp3 := doReq(t, "DELETE", annotationURL(setup.Server.URL, fx.itemID, kind, id), nil, setup.RegularToken) defer resp3.Body.Close()