Skip to content

fix(media): watch the background path refresh instead of firing it into the void - #255

Open
EtienneLescot wants to merge 1 commit into
mainfrom
fix/media-links-write-queue
Open

fix(media): watch the background path refresh instead of firing it into the void#255
EtienneLescot wants to merge 1 commit into
mainfrom
fix/media-links-write-queue

Conversation

@EtienneLescot

@EtienneLescot EtienneLescot commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

Summary

The CI Test job is intermittently red under a green summary — 137 files passed, 1628 tests passed, then the job exits non-zero. Run 30935779600 is the example this came from:

Vitest caught 1 unhandled error during the test run.
Unhandled Rejection
Error: ENOENT: no such file or directory, mkdir '/tmp/openscreen-media-links-N91r50'
 ❯ writeRegistry electron/media/mediaLinksRegistry.ts:149:2
This error originated in "electron/media/mediaLinksRegistry.test.ts"

The cause is not in the test. findMediaLinksByFingerprint is a read that writes: when a file has moved, it refreshes lastKnownPath so the next lookup can take a cheaper path. Not awaiting that write is the right call — a lookup should not pay for it — but void promise is not fire-and-forget, it is fire-and-crash. Nothing was watching the promise, so any failure became a process-level unhandledRejection.

Vitest reports an unhandled rejection from outside every test and fails the run, which is why the job could be red with nothing failing. In the packaged app the same rejection lands in the main process, where the recovery is a good deal worse than the missed refresh it came from.

The refresh stays unawaited and now logs what it could not do — the same answer readRegistry already gives a registry it cannot read.

While in there, withWriteLock drops its queue entry once the chain drains, guarded by an identity check so a writer that queued behind us is not stranded. The map is keyed by an arbitrary directory path and grew for the life of the process. This is the pattern DocumentService.writeProject already uses, so the two write queues now read the same.

Related issue

Refs #217 — found while reviewing that PR's CI, but unrelated to it and fixed separately here.

Type of change

  • Bug fix

Release impact

  • Patch

Desktop impact

  • Not platform-specific

Testing

Two cases, and only the first is a detector — that is stated in the test itself rather than left for a reader to discover.

logs a refresh it cannot write, and still answers the lookup makes the directory read-only, so the read still works and only the write fails, deterministically. On the old code it fails with expected [ …(1) ] to deeply equal [] — one unhandled rejection captured. Skipped out loud when running as root, where the permission bit does not bite.

survives the directory disappearing while the refresh is queued reproduces the CI shape — remove the directory while the refresh is in the queue — and asserts only that no rejection escapes, since either side of that race is a legitimate outcome.

Verified:

  • npm test three consecutive full runs: 137 files, 1627 passed, no unhandled rejection.
  • electron/media/mediaLinksRegistry.test.ts five consecutive runs: 12 passed each time.
  • Removing the .catch() turns the first case red and leaves the other eleven green — the test fails for the reason it claims to.
  • npx tsc --noEmit, npx tsc -p tsconfig.test.json --noEmit, and biome all clean.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Improved media link lookups so they continue returning results when background registry updates fail.
    • Prevented asynchronous refresh errors from causing unhandled promise rejections.
    • Improved reliability when registry directories are removed or changed during queued updates.

…to the void

`findMediaLinksByFingerprint` is a read that writes: when the file has moved, it
refreshes `lastKnownPath` so the next lookup is cheaper. Not awaiting that write
is right — a lookup should not pay for it — but `void promise` is not
fire-and-forget, it is fire-and-crash. Nothing was watching, so any failure
surfaced as a process-level `unhandledRejection`.

That is what made the CI Test job intermittent: vitest reports an unhandled
rejection from OUTSIDE every test and fails the run, so the job went red under a
green summary — 1628 passing tests and a stack pointing at a temp directory a
finished suite had already removed (run 30935779600). In the packaged app the
same rejection lands in the main process, where the recovery is worse than a
missed refresh ever was.

The refresh stays unawaited and now logs what it could not do, which is the same
answer `readRegistry` already gives a registry it cannot read.

While in there, `withWriteLock` drops its queue entry once the chain drains,
guarded by an identity check so a writer that queued behind us is not stranded.
The map is keyed by an arbitrary directory path and grew for the life of the
process. This is the pattern `DocumentService.writeProject` already uses, so the
two write queues now read the same.

Two cases cover it. The one that fails on the old code makes the directory
read-only, so the write fails deterministically while the read still works, and
asserts that nothing escapes and the lookup still answers. The second reproduces
the CI shape — remove the directory while the refresh is queued — and asserts
only that no rejection escapes, since either side of that race is acceptable.
@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: cc173d9a-c2d9-4719-accd-30c14281e7d4

📥 Commits

Reviewing files that changed from the base of the PR and between 1749d84 and 306fcad.

📒 Files selected for processing (2)
  • electron/media/mediaLinksRegistry.test.ts
  • electron/media/mediaLinksRegistry.ts

📝 Walkthrough

Walkthrough

The registry now cleans drained write queues safely and catches asynchronous path-refresh failures. Tests cover failed writes, warning behavior, unhandled rejection prevention, and directory removal races.

Changes

Registry refresh handling

Layer / File(s) Summary
Write queue cleanup
electron/media/mediaLinksRegistry.ts
withWriteLock uses a non-rejecting queue tail and removes the queue entry only when no newer writer exists.
Refresh failure handling and coverage
electron/media/mediaLinksRegistry.ts, electron/media/mediaLinksRegistry.test.ts
findMediaLinksByFingerprint logs asynchronous refresh failures. Tests verify successful lookups, warning behavior, unhandled rejection prevention, and directory removal handling.

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

Suggested reviewers: arhxam

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the primary fix for unhandled background refresh failures.
Description check ✅ Passed The description covers the summary, issue reference, change type, release impact, platform impact, testing, and relevant regression cases.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
✨ 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/media-links-write-queue

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.

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