diff --git a/cmd/server/tests/search_test.go b/cmd/server/tests/search_test.go index 0e381ad5..36c01655 100644 --- a/cmd/server/tests/search_test.go +++ b/cmd/server/tests/search_test.go @@ -413,10 +413,15 @@ func TestCollectionSearchLibraryFilter(t *testing.T) { shouldContain: book2ID, }, { - name: "invalid library_id", - query: "Harry", - libraryID: "00000000-0000-0000-0000-000000000000", - expectedStatus: http.StatusNotFound, + name: "invalid library_id", + query: "Harry", + libraryID: "00000000-0000-0000-0000-000000000000", + // Family-lens standing rule (AGENTS.md): an explicit + // library_id outside the visible set is a 403, never a + // silent pass — and a nonexistent id is out-of-set too, + // so existence is not enumerable. Behavior shipped in + // 7e586df4 (Phase 1); this pin update was missed then. + expectedStatus: http.StatusForbidden, }, } diff --git a/cmd/server/tests/websocket_test.go b/cmd/server/tests/websocket_test.go index 2b6d1eae..451c4d8c 100644 --- a/cmd/server/tests/websocket_test.go +++ b/cmd/server/tests/websocket_test.go @@ -157,37 +157,14 @@ func TestWebSocketProgressBroadcast(t *testing.T) { assert.Equal(t, "web", sourceDevice["type"]) } -// TestWebSocketPingPong tests that ping/pong messages work correctly -func TestWebSocketPingPong(t *testing.T) { - setup := setupTestServer(t) - - token := setup.Token - - wsURL := strings.Replace(setup.Server.URL, "http", "ws", 1) + "/ws/sync?token=" + token - ws, _, err := websocket.DefaultDialer.Dial(wsURL, nil) - require.NoError(t, err) - defer ws.Close() - - // Read initial message - ws.SetReadDeadline(time.Now().Add(5 * time.Second)) - _, _, _ = ws.ReadMessage() - - // Send a ping message (as a text message for testing) - err = ws.WriteMessage(websocket.TextMessage, []byte(`{"type":"ping"}`)) - require.NoError(t, err) - - // Server should respond with pong - ws.SetReadDeadline(time.Now().Add(2 * time.Second)) - _, msg, err := ws.ReadMessage() - if err == nil { - var pongMsg map[string]interface{} - err = json.Unmarshal(msg, &pongMsg) - if err == nil { - // Server might respond with pong - assert.Equal(t, "pong", pongMsg["type"]) - } - } -} +// TestWebSocketPingPong was deleted (2026-10-20, Phase 6 full-suite +// repair): it asserted a TEXT-level {"type":"ping"} → {"type":"pong"} +// exchange that no server code has ever implemented and no client +// (web/src or Android) has ever sent — it passed vacuously through its +// read-timeout path until interleaved scan_progress broadcasts started +// failing the assert. Keepalive is the standard protocol-level ping the +// hub already sends (websocket.go's ticker); if a text-level app ping +// is ever wanted, build it server-side first, then re-pin it. // TestWebSocketConnectionLimit tests that the server handles multiple connections func TestWebSocketConnectionLimit(t *testing.T) { diff --git a/docs/contract_pins.md b/docs/contract_pins.md index db7575f2..2f4e85a4 100644 --- a/docs/contract_pins.md +++ b/docs/contract_pins.md @@ -237,3 +237,23 @@ cover_image_path, has_conflict, format_group, badge`. - Watcher: a failed capability probe (one `Add` on a root folder) disables the watcher with ONE startup line instead of per-directory warning spam. + +## 2026-10-20 — Stale pin repair (behavior shipped in 7e586df4) + +- `GET /api/media-items/search` with an explicit `library_id` outside + the requesting user's visible set — INCLUDING a nonexistent id — + answers **403** (`library not accessible`), not 404. The family-lens + standing rule (AGENTS.md) mandates 403 for every out-of-set id so + library existence is not enumerable; Phase 1 (7e586df4) shipped it + and documented it here, but the `invalid library_id` subtest in + search_test.go still pinned the pre-family-lens 404. Pin widened to + the shipped 403; no server change (discovered during the Phase 6 + full-suite run; the Phase 6 diff touches no handler code). +- `TestWebSocketPingPong` DELETED: it asserted a text-level + `{"type":"ping"}` → `{"type":"pong"}` exchange that no server code + has ever implemented and no client (web/src, Android) has ever sent. + It passed vacuously through its read-timeout path (deadline error + skipped the asserts) until interleaved `scan_progress` broadcasts + from the live server's scans started reaching the socket first. + Keepalive is the standard protocol-level ping the hub already sends. + Never-built behavior is not a pinnable contract.