Skip to content

test: cover highlight repository failures#127

Open
davidfedor wants to merge 1 commit into
mainfrom
codex/move-preview-member-to-extension
Open

test: cover highlight repository failures#127
davidfedor wants to merge 1 commit into
mainfrom
codex/move-preview-member-to-extension

Conversation

@davidfedor

@davidfedor davidfedor commented May 18, 2026

Copy link
Copy Markdown
Member

Motivation

  • Increase code coverage by exercising previously untested failure paths in the highlights repository (parsing malformed passage IDs and API failure / error branches).
  • Ensure repository queue and retry behavior is validated for remove/update failures and when saveOperations throws.

Description

  • Added test-only helpers and a throwing test subclass in Tests/YouVersionPlatformCoreTests/BibleHighlightsRepositoryTests.swift to simplify creating common BibleReference/PendingHighlightOperation fixtures and simulate a saveOperations failure.
  • Marked the test MockBibleHighlightsAPI as final and kept it as the mock implementation for API calls used in tests.
  • Added tests that cover malformed passageId values so convertToBibleHighlight() failure paths are hit via highlights(for:).
  • Added tests for saveOperations and processQueue covering remove/update failure results, thrown API errors, and the re-queue/retry behavior when operations fail or saveOperations throws.

Testing

  • Ran swift test and the full test suite completed successfully with all tests passing.
  • Ran SwiftLint via LINUX_SOURCEKIT_LIB_PATH=/root/.local/share/swiftly/toolchains/6.1.3/usr/lib /tmp/swiftlint-install/swiftlint --strict and it reported no violations.

Codex Task

Greptile Summary

This PR expands test coverage for BibleHighlightsRepository by exercising previously untested failure paths: malformed passageId parsing in highlights(for:), false-return and error-throw branches for remove/update in saveOperations, and the re-queue/retry behavior in processQueue when operations fail or saveOperations itself throws.

  • Adds a ThrowingSaveOperationsBibleHighlightsRepository test subclass that always throws from saveOperations, alongside reference() and operation() factory helpers that reduce boilerplate in the new tests.
  • Marks MockBibleHighlightsAPI as final, which is a clean improvement that lets the compiler dispatch calls statically.
  • Adds 8 focused new @Test functions covering the remove/update failure and error results for both saveOperations and processQueue.

Confidence Score: 5/5

Test-only additions with no changes to production code; safe to merge.

All changes are confined to the test file. The new tests correctly isolate each failure branch, use a fresh mock per test, and assert both the queue state and individual operation results. No production logic is modified.

No files require special attention.

Important Files Changed

Filename Overview
Tests/YouVersionPlatformCoreTests/BibleHighlightsRepositoryTests.swift Adds 8 new tests covering malformed passageId parsing, remove/update failure and error paths in saveOperations, and processQueue re-queue/retry behavior when saveOperations throws; introduces clean helper factories and a test-only subclass.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[processQueue called] --> B{operations in queue?}
    B -- No --> Z[return]
    B -- Yes --> C[call saveOperations]
    C -- success map returned --> D{any result == false?}
    D -- No --> E[mark all operations complete]
    E --> Z
    D -- Yes --> F[re-queue failed ops\nincrement retryCount]
    F --> G[sleep 2s then recursive call]
    G --> H{guard: already processing?}
    H -- Yes --> Z
    H -- No --> B
    C -- throws --> I[catch error\nmark ops failed\nre-queue with retryCount+1]
    I --> G
Loading

Fix All in Claude Code Fix All in Cursor

Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
Tests/YouVersionPlatformCoreTests/BibleHighlightsRepositoryTests.swift:490-499
The two new `processQueue` requeue tests verify `result?.error != nil` for the case where the API returns `false` (no throw), but neither checks `result?.retryCount`. Since `testProcessQueueRequeuesOperationWhenSaveOperationsThrows` does assert `retryCount == 1`, it would be worth adding the same assertion here so all three re-queue tests verify the same set of invariants.

```suggestion
        let result = repository.getOperationResult(for: operation.id)
        #expect(mockAPI.deleteHighlightCallCount >= 1)
        #expect(repository.pendingOperationCount == 1)
        #expect(repository.failedOperationCount == 1)
        #expect(result?.success == false)
        #expect(result?.retryCount == 1)
        #expect(result?.error != nil)
    }

    @Test
    func testProcessQueueRequeuesFailedUpdateOperation() async {
```

### Issue 2 of 2
Tests/YouVersionPlatformCoreTests/BibleHighlightsRepositoryTests.swift:499-514
Same gap as the remove requeue test — adding `#expect(result?.retryCount == 1)` here would make all three re-queue tests consistent and provide a stronger assertion about the retry state after a single failed `processQueue` call.

```suggestion
    func testProcessQueueRequeuesFailedUpdateOperation() async {
        let mockAPI = setUp()
        let repository = BibleHighlightsRepository(api: mockAPI)
        mockAPI.mockUpdateHighlightResult = false
        let operation = operation(operationType: .update)

        repository.queueOperation(operation)
        await repository.processQueue()

        let result = repository.getOperationResult(for: operation.id)
        #expect(mockAPI.updateHighlightCallCount >= 1)
        #expect(repository.pendingOperationCount == 1)
        #expect(repository.failedOperationCount == 1)
        #expect(result?.success == false)
        #expect(result?.retryCount == 1)
        #expect(result?.error != nil)
    }
```

Reviews (2): Last reviewed commit: "test: cover highlight repository failure..." | Re-trigger Greptile

@github-actions

Copy link
Copy Markdown
Contributor

✅ Commit Lint: passed

All commit messages in this PR conform to Conventional Commits.

📦 Release preview: no release

These commits would not trigger a new release on main.

semantic-release dry-run output
[6:40:18 PM] [semantic-release] › ℹ  Running semantic-release version 22.0.12
[6:40:19 PM] [semantic-release] › ✔  Loaded plugin "verifyConditions" from "@semantic-release/changelog"
[6:40:19 PM] [semantic-release] › ✔  Loaded plugin "verifyConditions" from "@semantic-release/exec"
[6:40:19 PM] [semantic-release] › ✔  Loaded plugin "verifyConditions" from "@semantic-release/github"
[6:40:19 PM] [semantic-release] › ✔  Loaded plugin "verifyConditions" from "@semantic-release/git"
[6:40:19 PM] [semantic-release] › ✔  Loaded plugin "analyzeCommits" from "@semantic-release/commit-analyzer"
[6:40:19 PM] [semantic-release] › ✔  Loaded plugin "analyzeCommits" from "@semantic-release/exec"
[6:40:19 PM] [semantic-release] › ✔  Loaded plugin "verifyRelease" from "@semantic-release/exec"
[6:40:19 PM] [semantic-release] › ✔  Loaded plugin "generateNotes" from "@semantic-release/release-notes-generator"
[6:40:19 PM] [semantic-release] › ✔  Loaded plugin "generateNotes" from "@semantic-release/exec"
[6:40:19 PM] [semantic-release] › ✔  Loaded plugin "prepare" from "@semantic-release/changelog"
[6:40:19 PM] [semantic-release] › ✔  Loaded plugin "prepare" from "@semantic-release/exec"
[6:40:19 PM] [semantic-release] › ✔  Loaded plugin "prepare" from "@semantic-release/git"
[6:40:19 PM] [semantic-release] › ✔  Loaded plugin "publish" from "@semantic-release/exec"
[6:40:19 PM] [semantic-release] › ✔  Loaded plugin "publish" from "@semantic-release/github"
[6:40:19 PM] [semantic-release] › ✔  Loaded plugin "addChannel" from "@semantic-release/exec"
[6:40:19 PM] [semantic-release] › ✔  Loaded plugin "addChannel" from "@semantic-release/github"
[6:40:19 PM] [semantic-release] › ✔  Loaded plugin "success" from "@semantic-release/exec"
[6:40:19 PM] [semantic-release] › ✔  Loaded plugin "success" from "@semantic-release/github"
[6:40:19 PM] [semantic-release] › ✔  Loaded plugin "fail" from "@semantic-release/exec"
[6:40:19 PM] [semantic-release] › ✔  Loaded plugin "fail" from "@semantic-release/github"
[6:40:20 PM] [semantic-release] › ℹ  This test run was triggered on the branch undefined, while semantic-release is configured to only publish from codex/move-preview-member-to-extension, therefore a new version won’t be published.

@github-actions

Copy link
Copy Markdown
Contributor

Code Coverage Report

Coverage after merging codex/move-preview-member-to-extension into main will be
36.86%
Coverage Report
FileStmtsBranchesFuncsLinesUncovered Lines
/Users/runner/work/platform-sdk-swift/platform-sdk-swift/DerivedData/Build/Intermediates.noindex/YouVersionPlatform.build/Debug-iphonesimulator/YouVersionPlatformReader.build/DerivedSources
   resource_bundle_accessor.swift87.50%100%75%88.89%19, 44–45
/Users/runner/work/platform-sdk-swift/platform-sdk-swift/DerivedData/Build/Intermediates.noindex/YouVersionPlatform.build/Debug-iphonesimulator/YouVersionPlatformUI.build/DerivedSources
   resource_bundle_accessor.swift87.50%100%75%88.89%19, 44–45
/Users/runner/work/platform-sdk-swift/platform-sdk-swift/Sources/YouVersionPlatformCore
   YouVersionPlatformConfiguration.swift100%100%100%100%
/Users/runner/work/platform-sdk-swift/platform-sdk-swift/Sources/YouVersionPlatformCore/APIs
   URLBuilder.swift85.99%100%88.89%85.61%111, 12–15, 156, 16–19, 90–99
   URLRequest+YouVersion.swift100%100%100%100%
   YouVersionAPI.swift59.79%100%70%58.62%14–37, 73, 81, 83
/Users/runner/work/platform-sdk-swift/platform-sdk-swift/Sources/YouVersionPlatformCore/APIs/Bible
   BibleVersionAPI.swift71.79%100%60%73.53%118–132, 37–39, 58, 65–67, 75, 87
   BibleVersions.swift68.06%100%55.56%69.84%32, 54, 60–61, 77–88
   Highlights.swift78.48%100%64.71%80.14%113, 125–132, 156, 159, 175, 202, 205, 213, 234–236, 31, 34, 50, 79, 83, 95
/Users/runner/work/platform-sdk-swift/platform-sdk-swift/Sources/YouVersionPlatformCore/APIs/Languages
   Languages.swift89.86%100%71.43%91.94%47–49, 79
/Users/runner/work/platform-sdk-swift/platform-sdk-swift/Sources/YouVersionPlatformCore/APIs/Organizations
   Organizations.swift37.04%100%33.33%37.50%29–39, 41–43, 51
/Users/runner/work/platform-sdk-swift/platform-sdk-swift/Sources/YouVersionPlatformCore/APIs/Users
   SignInWithYouVersionPKCEAuthorizationRequest.swift94.39%100%92.31%94.68%80–81, 87, 98
   SignInWithYouVersionPermission.swift0%100%0%0%8
   SignInWithYouVersionResult.swift86.21%100%50%92%
   Users.swift71.37%100%58.33%72.86%112, 144–150, 155, 16, 165, 17–20, 202, 205, 21, 222, 228, 245–247, 265–267, 269–271, 273–275, 277–279, 284–286, 31–40, 44, 61, 67–71, 85
   YouVersionUserInfo.swift70.59%100%50%73.33%18, 29–31
/Users/runner/work/platform-sdk-swift/platform-sdk-swift/Sources/YouVersionPlatformCore/APIs/VOTD
   VOTD.swift94.12%100%100%93.33%22
   YouVersionVerseOfTheDay.swift0%100%0%0%15–18, 20–25
/Users/runner/work/platform-sdk-swift/platform-sdk-swift/Sources/YouVersionPlatformCore/Bible
   BibleBook.swift100%100%100%100%
   BibleChapterRepository.swift76.92%100%75.86%77.17%109, 111–113, 125–130, 42, 49, 54–57, 65–68, 83, 96–99
   BibleContentStorage.swift99.18%100%100%99.07%16
   BibleHighlight.swift55.56%100%50%57.14%12–14
   BibleHighlightsCache.swift83.92%100%80%84.96%103–105, 117–118, 135–136, 62, 65–67, 72
   BibleHighlightsRepository.swift93.55%100%81.40%95.14%19–21, 23–25, 27–29, 31–33
   BibleHighlightsViewModel.swift95.35%100%93.75%95.58%45, 48, 52, 57
   BibleReference.swift84.15%100%67.24%86.79%131, 226–227, 251, 255, 288, 301, 311, 321, 331, 333–344, 53–57, 83, 85, 87
   BibleTextNode.swift92.86%100%75%95.83%
   BibleTextNodeParser.swift86.43%100%100%85.16%101, 116–119, 12, 120–126, 136, 24–27, 96
   BibleVersion.swift69.07%100%86.67%65.85%102, 107–126, 61, 71–74, 80
   BibleVersionRepository.swift82.53%100%77.36%83.68%125–128, 159–161, 171–173, 212–217, 243, 248, 276–278, 310–312, 71–74, 82–85
   DeprecatedBibleAPI.swift0%100%0%0%100, 102–104, 106–108, 110–112, 114–116, 119–121, 126, 128, 131–133, 138, 140, 142–144, 147–149, 27, 29–31, 36, 38, 40–42, 44–46, 48–50, 52–54, 56–58, 65–67, 69–71, 73–75, 77–79, 81–83, 85–87, 94–96, 98–99
/Users/runner/work/platform-sdk-swift/platform-sdk-swift/Sources/YouVersionPlatformCore/Logging
   YouVersionPlatformLogger.swift55.36%100%41.67%59.09%100, 49–51, 56–58, 77–79, 90, 92, 94, 98
/Users/runner/work/platform-sdk-swift/platform-sdk-swift/Sources/YouVersionPlatformCore/Utilities
   AbbreviationSplitting.swift100%100%100%100%
/Users/runner/work/platform-sdk-swift/platform-sdk-swift/Sources/YouVersionPlatformReader
   BibleReaderView.swift0%100%0%0%100–155, 158–177, 179–196, 198–217, 219–224, 226–251, 253–301, 305, 307–309, 31, 310–312, 316, 32, 322–326, 328–329, 33, 330–332, 334–339, 34, 340, 35–39, 54–57, 59–76, 82, 88–91, 93–99
/Users/runner/work/platform-sdk-swift/platform-sdk-swift/Sources/YouVersionPlatformReader/Components
   BibleReaderBookAndChapterPickerView.swift0%100%0%0%100–102, 104–128, 130–138, 17–18,

@davidfedor
davidfedor marked this pull request as ready for review May 18, 2026 19:13
Comment on lines +481 to +532
func testProcessQueueRequeuesFailedRemoveOperation() async {
let mockAPI = setUp()
let repository = BibleHighlightsRepository(api: mockAPI)
mockAPI.mockDeleteHighlightResult = false
let operation = operation(color: nil, operationType: .remove)

repository.queueOperation(operation)
await repository.processQueue()

let result = repository.getOperationResult(for: operation.id)
#expect(mockAPI.deleteHighlightCallCount >= 1)
#expect(repository.pendingOperationCount == 1)
#expect(repository.failedOperationCount == 1)
#expect(result?.success == false)
#expect(result?.error != nil)
}

@Test
func testProcessQueueRequeuesFailedUpdateOperation() async {
let mockAPI = setUp()
let repository = BibleHighlightsRepository(api: mockAPI)
mockAPI.mockUpdateHighlightResult = false
let operation = operation(operationType: .update)

repository.queueOperation(operation)
await repository.processQueue()

let result = repository.getOperationResult(for: operation.id)
#expect(mockAPI.updateHighlightCallCount >= 1)
#expect(repository.pendingOperationCount == 1)
#expect(repository.failedOperationCount == 1)
#expect(result?.success == false)
#expect(result?.error != nil)
}

@Test
func testProcessQueueRequeuesOperationWhenSaveOperationsThrows() async {
let mockAPI = setUp()
let error = NSError(domain: "TestError", code: 1)
let repository = ThrowingSaveOperationsBibleHighlightsRepository(api: mockAPI, saveOperationsError: error)
let operation = operation()

repository.queueOperation(operation)
await repository.processQueue()

let result = repository.getOperationResult(for: operation.id)
#expect(repository.pendingOperationCount == 1)
#expect(repository.failedOperationCount == 1)
#expect(result?.success == false)
#expect(result?.retryCount == 1)
#expect(result?.error != nil)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Retry sleep makes these tests ~2 seconds each

Three of the new tests — testProcessQueueRequeuesFailedRemoveOperation (line 481), testProcessQueueRequeuesFailedUpdateOperation (line 499), and testProcessQueueRequeuesOperationWhenSaveOperationsThrows (line 517) — each call await repository.processQueue() with a mock that always fails. The production processQueue re-queues the failing operations and then does try await Task.sleep(nanoseconds: 2_000_000_000) before the recursive call. Because the recursive call returns immediately (the guard !processingQueue short-circuits), the function does complete, but only after the 2-second sleep. That adds at least ~6 seconds wall-clock time to the test suite per run. The pre-existing tests testProcessQueueHandlesFailure and testProcessQueueHandlesError have the same characteristic, so this is not a new problem you introduced — it's just worth noting so the team can decide whether to abstract the retry delay in the repository (e.g., inject a clock or delay strategy) in a follow-up.

Prompt To Fix With AI
This is a comment left during a code review.
Path: Tests/YouVersionPlatformCoreTests/BibleHighlightsRepositoryTests.swift
Line: 481-532

Comment:
**Retry sleep makes these tests ~2 seconds each**

Three of the new tests — `testProcessQueueRequeuesFailedRemoveOperation` (line 481), `testProcessQueueRequeuesFailedUpdateOperation` (line 499), and `testProcessQueueRequeuesOperationWhenSaveOperationsThrows` (line 517) — each call `await repository.processQueue()` with a mock that always fails. The production `processQueue` re-queues the failing operations and then does `try await Task.sleep(nanoseconds: 2_000_000_000)` before the recursive call. Because the recursive call returns immediately (the `guard !processingQueue` short-circuits), the function does complete, but only after the 2-second sleep. That adds at least ~6 seconds wall-clock time to the test suite per run. The pre-existing tests `testProcessQueueHandlesFailure` and `testProcessQueueHandlesError` have the same characteristic, so this is not a new problem you introduced — it's just worth noting so the team can decide whether to abstract the retry delay in the repository (e.g., inject a clock or delay strategy) in a follow-up.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Claude Code Fix in Cursor

Comment on lines +104 to +110
func operation(
color: String? = "#FF0000",
operationType: HighlightOperationType = .add,
verse: Int = 1
) -> PendingHighlightOperation {
PendingHighlightOperation(references: [reference(verse: verse)], color: color, operationType: operationType)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Local variable operation shadows the operation() helper method

In several tests (e.g. lines 331, 344, 357, 370, 485, 503) a let operation = operation(...) declaration is used, where the local variable name is identical to the helper method being called. Swift resolves this correctly because the call appears on the right-hand side before the binding takes effect, but the shadowing makes the code harder to scan — especially since operation is the same identifier in both positions. Renaming the helper to something like makeOperation() (consistent with the common Swift make… factory convention) or renaming the local bindings to op would remove the ambiguity entirely.

Prompt To Fix With AI
This is a comment left during a code review.
Path: Tests/YouVersionPlatformCoreTests/BibleHighlightsRepositoryTests.swift
Line: 104-110

Comment:
**Local variable `operation` shadows the `operation()` helper method**

In several tests (e.g. lines 331, 344, 357, 370, 485, 503) a `let operation = operation(...)` declaration is used, where the local variable name is identical to the helper method being called. Swift resolves this correctly because the call appears on the right-hand side before the binding takes effect, but the shadowing makes the code harder to scan — especially since `operation` is the same identifier in both positions. Renaming the helper to something like `makeOperation()` (consistent with the common Swift `make…` factory convention) or renaming the local bindings to `op` would remove the ambiguity entirely.

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Claude Code Fix in Cursor

@davidfedor

Copy link
Copy Markdown
Member Author

@greptile please re-review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant