Fix crash: enrichment held array indices across awaits
CI / build-and-deploy (pull_request) Successful in 30s

`startEnrichment` snapshotted `bookmarks.indices`, then awaited a network
call per bookmark. `bookmarks` is replaced wholesale by loads, searches
and the unread filter, so by the time the loop read `bookmarks[i]` the
index could be out of range — EXC_BREAKPOINT in
Array._checkSubscript, straight from the read. The write path already
guarded with `i < bookmarks.count`; the read did not.

`enrichAll()` had the same shape and the same exposure.

Both now track bookmark ids and re-resolve the position after each await,
via a shared `apply(summary:tags:toId:)` that skips a bookmark that is no
longer loaded rather than writing to whatever now sits at that index —
which is the other half of the bug: a stale-but-in-range index would have
silently attached one bookmark's summary to another.

Found while investigating leftover Spotlight "translation error ...
Code=1 (null)" entries after the indexing fix. Those turned out to be a
symptom, not a separate bug: the crash tore the process down mid-donation
and the in-flight items failed to translate. With the crash fixed they
are gone, and the suite went from crashing (0 tests executed) to green
across three consecutive runs.

Adds a source round-trip to SpotlightRetrievalTests, so both donation
paths — app entities and raw searchable items — are covered.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JgLHztZGaEHvS3KNeGQmRM
This commit is contained in:
Krishna Kumar
2026-07-29 23:46:07 -05:00
co-authored by Claude Opus 5
parent 1995b38b52
commit 2e1b454c09
2 changed files with 64 additions and 26 deletions
+19
View File
@@ -37,4 +37,23 @@ struct SpotlightRetrievalTests {
"url did not survive the round trip: \(hit.url)")
#expect(hit.host == "github.com", "host was \(hit.host)")
}
@Test func indexedSourceIsRetrievable() async throws {
let source = IngestedSource(
kind: .text,
title: "Wqvbnmtest meeting notes",
bodyText: "A distinctive probe document about wqvbnmtest.",
tags: ["wqvbnmtest"]
)
await SourceSpotlightIndexer.index([source])
var hits: [RetrievedBookmark] = []
for _ in 0..<20 {
try? await Task.sleep(for: .milliseconds(700))
hits = await SpotlightBookmarkSearch.run(query: "wqvbnmtest", limit: 5)
if !hits.isEmpty { break }
}
await SourceSpotlightIndexer.remove(ids: [source.id])
#expect(!hits.isEmpty, "Spotlight returned no hits for an indexed source")
#expect(hits.first?.title.contains("Wqvbnmtest") == true)
}
}