Files
linkding-ios/MarksTests/SpotlightRetrievalTests.swift
T
Krishna KumarandClaude Opus 5 2e1b454c09
CI / build-and-deploy (pull_request) Successful in 30s
Fix crash: enrichment held array indices across awaits
`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
2026-07-29 23:46:07 -05:00

60 lines
2.5 KiB
Swift

import Testing
import CoreSpotlight
@testable import Marks
/// End-to-end guard for the whole Spotlight path: index a bookmark, then
/// retrieve it the way the on-device assistant does.
///
/// Worth keeping. The bug this was written for made every item fail to
/// translate into the index while `indexAppEntities` still reported success,
/// so nothing short of a round trip would have caught it.
struct SpotlightRetrievalTests {
@Test func indexedBookmarkIsRetrievable() async throws {
let b = Bookmark(
id: 999_001,
url: "https://github.com/example/zqxjkltest",
title: "Zqxjkltest concurrency notes",
description: "A distinctive probe document about zqxjkltest.",
tagNames: ["zqxjkltest"],
dateAdded: Date(), dateModified: Date(),
isArchived: false, unread: false, shared: false,
websiteTitle: nil, websiteDescription: nil,
faviconUrl: nil, previewImageUrl: nil,
aiSummary: nil, aiTags: nil
)
await SpotlightIndexer.index([b])
var hits: [RetrievedBookmark] = []
for _ in 0..<20 {
try? await Task.sleep(for: .milliseconds(700))
hits = await SpotlightBookmarkSearch.run(query: "zqxjkltest", limit: 5)
if !hits.isEmpty { break }
}
await SpotlightIndexer.remove(ids: [999_001])
#expect(!hits.isEmpty, "Spotlight returned no hits for an indexed bookmark")
let hit = try #require(hits.first)
#expect(hit.title.contains("Zqxjkltest"))
#expect(hit.url == "https://github.com/example/zqxjkltest",
"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)
}
}