Skip to content

fix(downloads): iOS image-model download stuck failed — durable staging + retry re-download#557

Closed
alichherawalla wants to merge 7 commits into
mainfrom
fix/ios-download-durable-staging
Closed

fix(downloads): iOS image-model download stuck failed — durable staging + retry re-download#557
alichherawalla wants to merge 7 commits into
mainfrom
fix/ios-download-durable-staging

Conversation

@alichherawalla

@alichherawalla alichherawalla commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator

The bug (device, production build)

SDXL (Core ML) image download showed 2.8 GB / 2.8 GB then failed with "…coreml_apple_…xl-base-ios couldn't be opened because there is no such file", and Retry did nothing. The bytes downloaded fine — it died in finalize (unzip couldn't find the zip), and retry kept re-running the same dead finalize.

CoreML is iOS-only, so the recent Android download-durability work didn't touch CoreML directly — it exposed a latent bug in the shared download-finalize plumbing underneath it.

Root cause (native) + fix

ios/DownloadManagerModule.swift staged a completed single-file download in NSTemporaryDirectory(), which iOS reaps across relaunch / under memory pressure. The staged path is persisted (localUri) and finalize can now run after a relaunch (the queued-survival rework) — when the temp file is already gone. A multi-GB zip is the worst case.

Fix: stage completed downloads in a durable Documents/.download-staging dir (survives relaunch + memory pressure, excluded from iCloud backup).

Retry dead-end (JS) + fix

retryImageDownloadtryResumeImageFinalization re-ran moveCompletedDownload on the stale id → same "no such file" every tap, and never fell through to the fresh-download path. Fix: when neither a valid zip nor an extracted dir survives, resumeZipDownload re-downloads from scratch instead of rethrowing.

Android safety

Zero Android-native files changed. Swift is iOS-only; the one shared JS file (imageDownloadResume.ts) is a strictly-additive graceful fallback (recover-vs-dead-end), which improves Android's zip resume too. Verified: tsc clean, download+image suites 174 passed, Android JS bundle builds.

Test

iosImageStagingPurgedRedownloads.redflow.test.ts — drives the real retry-finalize path over a faked native boundary (moveCompletedDownload rejects, no artifact survives); asserts a fresh native download is issued + the row goes active. Red-verified (reverted fallback → stuck failed, no new row) → green. Blast radius: 243 passed.

Verification

  • JS retry-recovery: verified (jest red→green + blast radius).
  • Native durable staging: device-verify in progress (npm run ios:device on a connected iPhone).

Summary by CodeRabbit

  • New Features
    • Enhanced recovery for interrupted or unrecoverable image downloads, including zip resume scenarios via saved metadata.
    • Provides a clearer failure state when metadata is missing or re-download can’t be started.
  • Bug Fixes
    • Prevented completed download artifacts from being lost after relaunch by storing them in durable staging instead of temporary storage.
  • Tests
    • Added iOS/Android integration and rendered test coverage for scenarios where native finalize can’t access purged staged bytes.
  • Refactor
    • Standardized retry logic inputs from saved metadata.
  • Chores
    • Added Slack notifications for releases and beta announcements.

Also folded in: Slack release announcements (was PR #556)

Merged chore/slack-release-notify into this branch (disjoint file-set — only scripts/uat.sh + scripts/release.sh, zero overlap with the download fix, clean merge, no conflicts):

  • uat.sh (beta) and release.sh (prod) now post release notes to Slack via notify-slack-release.mjs after the GitHub release is cut. Fail-soft (|| true).
  • Webhook read from the gitignored .env.keygen (scoped sed, one key). No-ops if unset.
  • No double-post: CI's Slack step only fires on manual workflow_dispatch, not the tag these local scripts cut.

#556 is now subsumed by this PR and can be closed once this merges.

…se scripts

uat.sh (beta) and release.sh (production/local) now post the release notes
to Slack via scripts/notify-slack-release.mjs after the GitHub release is cut,
before the notes tempfile is removed. Fail-soft (|| true) so a chat post can
never fail a shipped build.

The webhook is read from the gitignored .env.keygen (the local secret store
that already holds the RevenueCat/Resend/Keygen/Cloudflare tokens), mirroring
the SLACK_WEBHOOK_URL repo secret the CI release workflows use. Extracted with
a scoped sed for that one key, not a full source, so the other secrets in
.env.keygen stay out of the script env. notify-slack-release.mjs no-ops when
the webhook is unset, so this is a no-op on machines without it.
release.sh omitted CHANNEL_LABEL, so prod announcements rendered with no
channel tag while uat.sh betas showed `beta`. Pass CHANNEL_LABEL=stable
for parity — the notify script's documented values are beta | stable.
…STemporaryDirectory

Root cause of the SDXL (Core ML) image-model download failing on iOS with
"...coreml_apple_...xl-base-ios couldn't be opened because there is no such
file": a completed single-file download was staged in NSTemporaryDirectory(),
which iOS reaps across app relaunches and under memory pressure. The staged
zip's path is persisted (localUri) and finalize (moveCompletedDownload -> unzip)
can now run AFTER a relaunch (the queued-survival rework), by which point the OS
has already deleted the temp file -> unzip opens a zip that no longer exists.

A multi-GB image zip is the worst case: it commonly spans a backgrounding/
relaunch before its unzip finishes. Since CoreML is iOS-only, the shared
download-infra change surfaced it only on iOS.

Stage completed downloads (both the URLSession delegate hand-off copy and the
handleSingleFileCompletion move) in a DURABLE Documents/.download-staging dir
that survives relaunch + memory pressure, excluded from iCloud backup (dir flag
is not inherited, so the staged file is excluded per-file too).

Native change — device-verified separately (jest fakes the native module).
…ble on retry

When an image zip download completed but its staged bytes are gone at finalize
(iOS temp-purge on pre-durable-staging builds, or the user cleared storage),
resumeZipDownload rethrew moveCompletedDownload's "no such file" and the row
stuck at 'failed'. Retry re-ran the same finalize on the same dead download, so
tapping Retry did nothing (retryImageDownload's tryResumeImageFinalization
handles it and never falls through to the fresh-download path).

Now, when neither a valid zip nor an extracted dir survives, resumeZipDownload
re-downloads from scratch via proceedWithDownload (descriptor reconstructed from
the entry's persisted metadata) instead of dead-ending. The user recovers by
re-downloading rather than being stuck on a permanently-failed card.

Red-flow test drives the real retry-finalize path over a faked native boundary
where moveCompletedDownload rejects (staged source purged) and no artifact
survives; asserts a fresh native download is issued + the row goes active again.
Verified red (reverted fallback -> stuck 'failed', no new row) then green.
@qodo-code-review

Copy link
Copy Markdown

ⓘ Qodo reviews are paused because your trial has ended. Ask your workspace admin to add credits to resume reviews. Manage billing

@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Completed iOS downloads now use durable Documents-based staging. Resume handling reconstructs image descriptors from persisted metadata and starts a fresh download when staged ZIP bytes are unavailable. Integration tests cover recovery, while release scripts add fail-soft Slack announcements.

Changes

iOS Download Recovery

Layer / File(s) Summary
Durable completed-download staging
ios/DownloadManagerModule.swift
Completed single-file downloads are copied into a Documents-based staging directory, with staged files excluded from iCloud backup before completion handling.
Metadata-based resume recovery
src/screens/ModelsScreen/imageDescriptor.ts, src/screens/DownloadManagerScreen/retryHandlers.ts, src/screens/ModelsScreen/imageDownloadResume.ts
Retry and resume flows reconstruct image descriptors from persisted metadata; unrecoverable ZIP artifacts trigger a fresh download, or mark the entry failed when its URL is unavailable.
Purged-staging integration coverage
__tests__/integration/downloads/iosImageStagingPurgedRedownloads.*
Tests simulate missing staged bytes and verify that retry creates an active download and clears the failed state.

Release Notifications

Layer / File(s) Summary
Fail-soft Slack release announcements
scripts/release.sh, scripts/uat.sh
Release and beta build scripts resolve the Slack webhook, send release metadata to the notification script, and ignore notification failures.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant DownloadManagerScreen
  participant resumeImageDownload
  participant moveCompletedDownload
  participant proceedWithDownload
  participant NativeDownloadManager
  DownloadManagerScreen->>resumeImageDownload: retry failed image download
  resumeImageDownload->>moveCompletedDownload: move completed staged artifact
  moveCompletedDownload-->>resumeImageDownload: return missing-file error
  resumeImageDownload->>proceedWithDownload: reconstruct descriptor from metadata
  proceedWithDownload->>NativeDownloadManager: start fresh native download
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title is concise and accurately summarizes the main iOS download recovery fix.
Description check ✅ Passed The description clearly explains the bug, fix, testing, and verification, though it does not follow the template sections exactly.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/ios-download-durable-staging

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@ios/DownloadManagerModule.swift`:
- Around line 1332-1337: Clean up staged .tmp files in the completion and
startup flows: update handleSingleFileCompletion and
handleMultiFileTaskCompletion to remove location when moving fails or
before/after the multi-file copy fallback, and initialize cleanup in or
immediately after setupSession() to delete orphaned .tmp files from
completedStagingDirectory().

In `@src/screens/ModelsScreen/imageDownloadResume.ts`:
- Around line 92-94: Update the failure message in the resume-download flow near
setStatus to replace the em dash with a period, preserving the existing meaning
as two separate sentences.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a472f777-63d6-4ab6-be5e-e636679b8e48

📥 Commits

Reviewing files that changed from the base of the PR and between f916294 and 4fe2e81.

📒 Files selected for processing (3)
  • __tests__/integration/downloads/iosImageStagingPurgedRedownloads.redflow.test.ts
  • ios/DownloadManagerModule.swift
  • src/screens/ModelsScreen/imageDownloadResume.ts

Comment on lines +1332 to +1337
// We must copy it to a safe location SYNCHRONOUSLY before returning. That location must be
// DURABLE (not NSTemporaryDirectory) — if the app is killed between here and handleCompletion,
// a temp copy would be reaped and the completed bytes lost. See completedStagingDirectory().
let fileManager = FileManager.default
let safeTmp = NSTemporaryDirectory() + "dl_task_\(downloadTask.taskIdentifier)_\(UUID().uuidString).tmp"
let stagingDir = DownloadManagerModule.completedStagingDirectory()
let safeTmp = "\(stagingDir)/dl_task_\(downloadTask.taskIdentifier)_\(UUID().uuidString).tmp"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Resource leak: .tmp files are not cleaned up on failure or app termination.

Because .download-staging is a durable directory (unlike NSTemporaryDirectory), any .tmp files left here will persist indefinitely. These are multi-GB model artifacts, so leaking them will quickly exhaust device storage.

Currently, safeTmp files leak in three scenarios:

  1. Single-file error: If moveItem fails in handleSingleFileCompletion, location (the .tmp file) is left behind.
  2. Multi-file fallback: If moveItem fails in handleMultiFileTaskCompletion and falls back to copyItem, location is left behind regardless of whether the copy succeeds or fails.
  3. App termination: If the app is killed between didFinishDownloadingTo and the async handleCompletion block, the .tmp file is orphaned. Since URLSession foreground tasks do not survive restart, the file will never be processed.

To fix this, clean up location in the error/fallback paths of the completion handlers, and clear any orphaned .tmp files when initializing the session.

♻️ Proposed fixes to prevent `.tmp` file leaks

1. Clean up in handleSingleFileCompletion (around line 1238):

     } catch {
       NSLog("[DownloadManager] Failed to move single file: %@", error.localizedDescription)
+      try? fileManager.removeItem(at: location)
       info.status = "failed"
       taskToDownloadId.removeValue(forKey: taskId)

2. Clean up in handleMultiFileTaskCompletion (around line 1151):

       } catch {
         NSLog("[DownloadManager] Failed to save file %@: %@",
               fileTask.relativePath, error.localizedDescription)
       }
+      try? fileManager.removeItem(at: location)
     }

     fileTask.completed = true

3. Clean up orphaned .tmp files on startup (add inside or immediately after setupSession()):

queue.async(flags: .barrier) {
  let stagingDir = DownloadManagerModule.completedStagingDirectory()
  if let files = try? FileManager.default.contentsOfDirectory(atPath: stagingDir) {
    for file in files where file.hasSuffix(".tmp") {
      try? FileManager.default.removeItem(atPath: "\(stagingDir)/\(file)")
    }
  }
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ios/DownloadManagerModule.swift` around lines 1332 - 1337, Clean up staged
.tmp files in the completion and startup flows: update
handleSingleFileCompletion and handleMultiFileTaskCompletion to remove location
when moving fails or before/after the multi-file copy fallback, and initialize
cleanup in or immediately after setupSession() to delete orphaned .tmp files
from completedStagingDirectory().

Comment on lines +92 to +94
useDownloadStore.getState().setStatus(ctx.entry.downloadId, 'failed', {
message: 'Download expired and could not be re-downloaded — remove and download again.',
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Avoid using em dashes in UI strings.

As per coding guidelines, UI strings and content must avoid em dashes. Consider using a period to separate the clauses into two distinct sentences.

📝 Proposed fix
-      message: 'Download expired and could not be re-downloaded — remove and download again.',
+      message: 'Download expired and could not be re-downloaded. Remove and download again.',
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useDownloadStore.getState().setStatus(ctx.entry.downloadId, 'failed', {
message: 'Download expired and could not be re-downloaded — remove and download again.',
});
useDownloadStore.getState().setStatus(ctx.entry.downloadId, 'failed', {
message: 'Download expired and could not be re-downloaded. Remove and download again.',
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/screens/ModelsScreen/imageDownloadResume.ts` around lines 92 - 94, Update
the failure message in the resume-download flow near setStatus to replace the em
dash with a period, preserving the existing meaning as two separate sentences.

Source: Coding guidelines

…uard on both platforms

Addresses hygiene/tests self-audit on the retry-recovery fix:
- DRY: extract imageDescriptorFromMetadata into a pure zero-IO module (imageDescriptor.ts),
  used by BOTH the iOS retry path (retryHandlers) and resume's re-download fallback, instead of
  two hand-built descriptors that could drift.
- Copy: drop the em dash from the user-facing re-download failure message (brand guide).
- Tests: resumeImageDownload is shared (iOS retry + Android app-open resume), so the red-flow now
  runs the purged-artifact recovery on BOTH ios and android with a device-faithful fixture each
  (CoreML zip / MNN zip). Red-verified on both (reverted fallback -> stuck 'failed', no new row).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/screens/ModelsScreen/imageDescriptor.ts`:
- Around line 8-23: Update imageDescriptorFromMetadata so the required
ImageModelDescriptor name field uses an empty-string fallback when
meta.imageModelName is nullish, matching the existing handling of other string
properties.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e40fd5af-4383-474c-ad85-ffd65e3138e5

📥 Commits

Reviewing files that changed from the base of the PR and between 4fe2e81 and 7fb6b06.

📒 Files selected for processing (4)
  • __tests__/integration/downloads/iosImageStagingPurgedRedownloads.redflow.test.ts
  • src/screens/DownloadManagerScreen/retryHandlers.ts
  • src/screens/ModelsScreen/imageDescriptor.ts
  • src/screens/ModelsScreen/imageDownloadResume.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/integration/downloads/iosImageStagingPurgedRedownloads.redflow.test.ts
  • src/screens/ModelsScreen/imageDownloadResume.ts

Comment on lines +8 to +23
export function imageDescriptorFromMetadata(modelId: string, meta: Record<string, any>): ImageModelDescriptor {
return {
id: modelId,
name: meta.imageModelName,
description: meta.imageModelDescription ?? '',
downloadUrl: meta.imageModelDownloadUrl ?? '',
size: meta.imageModelSize ?? 0,
style: meta.imageModelStyle ?? '',
backend: meta.imageModelBackend ?? 'coreml',
attentionVariant: meta.imageModelAttentionVariant,
huggingFaceRepo: meta.imageModelRepo,
huggingFaceFiles: meta.imageModelHuggingFaceFiles,
coremlFiles: meta.imageModelCoremlFiles,
repo: meta.imageModelRepo,
};
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Provide a fallback for the required name property.

The name property is required by the ImageModelDescriptor interface, but meta.imageModelName is mapped without a default. If it is undefined in the persisted metadata, it could violate the downstream contract. Consider adding a fallback (e.g., ?? '') for consistency with the other string properties.

🐛 Proposed fix
-    name: meta.imageModelName,
+    name: meta.imageModelName ?? '',
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function imageDescriptorFromMetadata(modelId: string, meta: Record<string, any>): ImageModelDescriptor {
return {
id: modelId,
name: meta.imageModelName,
description: meta.imageModelDescription ?? '',
downloadUrl: meta.imageModelDownloadUrl ?? '',
size: meta.imageModelSize ?? 0,
style: meta.imageModelStyle ?? '',
backend: meta.imageModelBackend ?? 'coreml',
attentionVariant: meta.imageModelAttentionVariant,
huggingFaceRepo: meta.imageModelRepo,
huggingFaceFiles: meta.imageModelHuggingFaceFiles,
coremlFiles: meta.imageModelCoremlFiles,
repo: meta.imageModelRepo,
};
}
export function imageDescriptorFromMetadata(modelId: string, meta: Record<string, any>): ImageModelDescriptor {
return {
id: modelId,
name: meta.imageModelName ?? '',
description: meta.imageModelDescription ?? '',
downloadUrl: meta.imageModelDownloadUrl ?? '',
size: meta.imageModelSize ?? 0,
style: meta.imageModelStyle ?? '',
backend: meta.imageModelBackend ?? 'coreml',
attentionVariant: meta.imageModelAttentionVariant,
huggingFaceRepo: meta.imageModelRepo,
huggingFaceFiles: meta.imageModelHuggingFaceFiles,
coremlFiles: meta.imageModelCoremlFiles,
repo: meta.imageModelRepo,
};
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/screens/ModelsScreen/imageDescriptor.ts` around lines 8 - 23, Update
imageDescriptorFromMetadata so the required ImageModelDescriptor name field uses
an empty-string fallback when meta.imageModelName is nullish, matching the
existing handling of other string properties.

…rd, assert recovery

Mounts the real DownloadManagerScreen over the native+fs fakes, taps the actual
failed-retry-button on the failed SDXL card, and asserts the card recovers (row
no longer 'failed', Retry gone, a fresh native download in flight). Closes the
/tests gap: the sibling .redflow.test.ts drives the service directly; this drives
the real UI gesture through the full retry wiring.

Pins Platform.OS=ios (the device bug) so imageProvider.retry takes the iOS
re-download path, not Android's native-resume branch. Red-verified (reverted the
reDownloadFromMetadata fallback -> card stuck 'failed'). registerCoreDownloadProviders()
before mount, else the service silently refuses retry (no owning provider).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
__tests__/integration/downloads/iosImageStagingPurgedRedownloads.rendered.redflow.test.tsx (1)

42-42: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Reuse the modelKey variable.

Since makeImageModelKey(modelId) already constructs image:${modelId}, you can safely reuse the modelKey variable here to eliminate the duplicated string interpolation.

💡 Proposed refactor
-      modelKey, downloadId: 'dl-sdxl', modelId: `image:${modelId}`, fileName,
+      modelKey, downloadId: 'dl-sdxl', modelId: modelKey, fileName,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@__tests__/integration/downloads/iosImageStagingPurgedRedownloads.rendered.redflow.test.tsx`
at line 42, Update the download object in the test to reuse the existing
modelKey variable for modelId instead of reconstructing image:${modelId} with a
template string. Keep the surrounding downloadId and fileName assignments
unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@__tests__/integration/downloads/iosImageStagingPurgedRedownloads.rendered.redflow.test.tsx`:
- Around line 77-82: Move the failed-retry-button and boundary active-row
assertions into the existing waitFor callback alongside the download status
assertion. Keep retrying until the store status, rendered UI, and boundary rows
all reflect successful recovery, while preserving the current assertion
conditions.

---

Nitpick comments:
In
`@__tests__/integration/downloads/iosImageStagingPurgedRedownloads.rendered.redflow.test.tsx`:
- Line 42: Update the download object in the test to reuse the existing modelKey
variable for modelId instead of reconstructing image:${modelId} with a template
string. Keep the surrounding downloadId and fileName assignments unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 2c05c7c5-7860-4e80-9c2d-88e165bc251b

📥 Commits

Reviewing files that changed from the base of the PR and between 7fb6b06 and 42c76c6.

📒 Files selected for processing (4)
  • __tests__/integration/downloads/iosImageStagingPurgedRedownloads.rendered.redflow.test.tsx
  • scripts/release.sh
  • scripts/uat.sh
  • src/screens/ModelsScreen/imageDownloadResume.ts
💤 Files with no reviewable changes (1)
  • src/screens/ModelsScreen/imageDownloadResume.ts

Comment on lines +77 to +82
await waitFor(() => {
expect(useDownloadStore.getState().downloads[modelKey].status).not.toBe('failed');
}, { timeout: 5000 });
expect(view.queryByTestId('failed-retry-button')).toBeNull();
const rows = boundary.download!.active();
expect(rows.some(r => r.modelId === `image:${modelId}` || r.fileName === fileName)).toBe(true);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Move all recovery assertions inside waitFor to prevent test flakiness.

React state updates and renders can be batched and asynchronous. Waiting only for the Zustand store to update does not guarantee that React has finished committing the corresponding UI changes. Asserting the DOM synchronously immediately after the store update can lead to a race condition where the UI still reflects the old state.

Moving the UI and boundary assertions into the waitFor block ensures both the state and the DOM are fully updated and synchronized before proceeding. As per coding guidelines, integration tests should "assert what the user sees", and asserting the UI directly inside the asynchronous wait properly aligns with this principle.

💚 Proposed fix to group assertions
     await waitFor(() => {
       expect(useDownloadStore.getState().downloads[modelKey].status).not.toBe('failed');
-    }, { timeout: 5000 });
-    expect(view.queryByTestId('failed-retry-button')).toBeNull();
-    const rows = boundary.download!.active();
-    expect(rows.some(r => r.modelId === `image:${modelId}` || r.fileName === fileName)).toBe(true);
+      expect(view.queryByTestId('failed-retry-button')).toBeNull();
+      const rows = boundary.download!.active();
+      expect(rows.some(r => r.modelId === `image:${modelId}` || r.fileName === fileName)).toBe(true);
+    }, { timeout: 5000 });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
await waitFor(() => {
expect(useDownloadStore.getState().downloads[modelKey].status).not.toBe('failed');
}, { timeout: 5000 });
expect(view.queryByTestId('failed-retry-button')).toBeNull();
const rows = boundary.download!.active();
expect(rows.some(r => r.modelId === `image:${modelId}` || r.fileName === fileName)).toBe(true);
await waitFor(() => {
expect(useDownloadStore.getState().downloads[modelKey].status).not.toBe('failed');
expect(view.queryByTestId('failed-retry-button')).toBeNull();
const rows = boundary.download!.active();
expect(rows.some(r => r.modelId === `image:${modelId}` || r.fileName === fileName)).toBe(true);
}, { timeout: 5000 });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@__tests__/integration/downloads/iosImageStagingPurgedRedownloads.rendered.redflow.test.tsx`
around lines 77 - 82, Move the failed-retry-button and boundary active-row
assertions into the existing waitFor callback alongside the download status
assertion. Keep retrying until the store status, rendered UI, and boundary rows
all reflect successful recovery, while preserving the current assertion
conditions.

Source: Coding guidelines

@sonarqubecloud

Copy link
Copy Markdown

@alichherawalla

Copy link
Copy Markdown
Collaborator Author

Consolidated into #558 — the single release PR. The iOS download durable-staging + retry fix and the Slack release-notify scripts were merged into fix/510-load-anyway-memory, which #558 tracks. Closing to keep one release PR.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant