fix: reload var state when emhttp writes temp files#1950
Conversation
- ensure the state directory watcher treats var.ini.new as the same logical file as var.ini - keep polled files like disks.ini and shares.ini on their existing polling path - avoid ignoring the watched root directory when chokidar calls the ignore callback without stats - add unit coverage for .new file handling and root-directory filtering - add an integration test that uses a real chokidar watcher against a temp state directory - add a small watcher cleanup method so integration tests can close live file watchers cleanly
WalkthroughThis PR adds comprehensive testing and filename normalization logic for handling temporary state files ( Changes
Sequence DiagramsequenceDiagram
participant FileWatcher as File Watcher
participant StateManager as StateManager
participant Normalizer as getNormalizedStateFileName()
participant Filter as shouldIgnoreStatePath()
participant KeyResolver as getStateFileKeyFromPath()
participant Store as Store
FileWatcher->>StateManager: File event (var.ini.new)
StateManager->>Normalizer: Normalize filename
Normalizer-->>StateManager: "var.ini"
StateManager->>Filter: Check if should ignore
Filter->>Normalizer: Get normalized name
Normalizer-->>Filter: "var.ini"
Filter-->>StateManager: false (not ignored)
StateManager->>KeyResolver: Get StateFileKey
KeyResolver->>Normalizer: Normalize for key lookup
Normalizer-->>KeyResolver: "var.ini"
KeyResolver-->>StateManager: StateFileKey.var
StateManager->>Store: dispatch(loadSingleStateFile(var))
StateManager->>Store: dispatch(loadRegistrationKey())
Store-->>StateManager: Acknowledged
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #1950 +/- ##
==========================================
+ Coverage 51.74% 51.77% +0.02%
==========================================
Files 1026 1026
Lines 70725 70748 +23
Branches 7894 7903 +9
==========================================
+ Hits 36599 36628 +29
+ Misses 34003 33997 -6
Partials 123 123 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
|
This plugin has been deployed to Cloudflare R2 and is available for testing. |
There was a problem hiding this comment.
🧹 Nitpick comments (2)
api/src/__test__/store/watch/state-watch.integration.test.ts (2)
100-107: Acceptable approach for negative test, but consider documenting the delay.For negative tests (asserting something did NOT happen), a fixed delay is often necessary. The 250ms delay is reasonable, though a brief comment explaining the timing choice would help maintainability.
💡 Optional comment addition
it('does not route disks.ini.new through the directory watcher', async () => { const { store } = await import('@app/store/index.js'); await writeFile(join(statesDirectory, 'disks.ini.new'), '[disk1]\nname=disk1\n'); + // Allow sufficient time for the watcher to process the file event await new Promise((resolve) => setTimeout(resolve, 250)); expect(store.dispatch).not.toHaveBeenCalled(); });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@api/src/__test__/store/watch/state-watch.integration.test.ts` around lines 100 - 107, Add a brief comment explaining the 250ms delay in the test "does not route disks.ini.new through the directory watcher" so future readers understand it’s a conservative wait for the file-watcher debounce/async processing; locate the test block containing the setTimeout call (the async it(...) that writes 'disks.ini.new' and awaits new Promise((resolve) => setTimeout(resolve, 250))) and insert a one-line comment above the await describing why 250ms was chosen (e.g., to allow the watcher’s debounce/async loop to run) — no code behavior changes required.
73-73: Consider usingvi.waitForinstead of fixed delay.The 500ms fixed delay for watcher readiness could cause flakiness in CI environments with varying I/O performance. Consider using
vi.waitForwith a condition that confirms the watcher is ready, similar to the pattern used in the first test (lines 94-97).💡 Suggested approach
If chokidar emits a 'ready' event, you could wait for that signal instead of a fixed timeout. Alternatively, if no reliable signal exists, the current approach may be acceptable but consider increasing the timeout or adding a comment explaining why 500ms was chosen.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@api/src/__test__/store/watch/state-watch.integration.test.ts` at line 73, Replace the fixed 500ms delay (the await new Promise(setTimeout) call) with a deterministic wait using vi.waitFor that verifies the watcher is ready; for example, wait for the chokidar watcher to emit its 'ready' event or for a boolean/spy indicating readiness (reference the watcher instance used in this test and the vi.waitFor helper), falling back to increasing the timeout only if no readiness signal is available.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@api/src/__test__/store/watch/state-watch.integration.test.ts`:
- Around line 100-107: Add a brief comment explaining the 250ms delay in the
test "does not route disks.ini.new through the directory watcher" so future
readers understand it’s a conservative wait for the file-watcher debounce/async
processing; locate the test block containing the setTimeout call (the async
it(...) that writes 'disks.ini.new' and awaits new Promise((resolve) =>
setTimeout(resolve, 250))) and insert a one-line comment above the await
describing why 250ms was chosen (e.g., to allow the watcher’s debounce/async
loop to run) — no code behavior changes required.
- Line 73: Replace the fixed 500ms delay (the await new Promise(setTimeout)
call) with a deterministic wait using vi.waitFor that verifies the watcher is
ready; for example, wait for the chokidar watcher to emit its 'ready' event or
for a boolean/spy indicating readiness (reference the watcher instance used in
this test and the vi.waitFor helper), falling back to increasing the timeout
only if no readiness signal is available.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 37780d66-536e-4c38-baab-98491f5125a4
📒 Files selected for processing (3)
api/src/__test__/store/watch/state-watch.integration.test.tsapi/src/__test__/store/watch/state-watch.test.tsapi/src/store/watch/state-watch.ts
🤖 I have created a release *beep* *boop* --- ## [4.31.1](v4.31.0...v4.31.1) (2026-03-23) ### Bug Fixes * **onboarding:** separate apply and completion flows ([#1948](#1948)) ([5be53a4](5be53a4)) * reload var state when emhttp writes temp files ([#1950](#1950)) ([7265105](7265105)) * **state-watch:** watch canonical ini files atomically ([#1953](#1953)) ([6471b3f](6471b3f)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Summary
*.ini.newstate-file events back to their underlying state file key in the state watcherWhy
On the Unraid server, emhttp writes temp files like
var.ini.newand swaps them in. The existing watcher only recognized exact*.inibasenames, sovar.ini.newupdates were ignored and the API store could stay stale even when disk state was correct.Testing
pnpm --filter ./api test src/__test__/store/watch/state-watch.test.ts src/__test__/store/watch/state-watch.integration.test.ts src/unraid-api/graph/resolvers/servers/server.service.spec.tspnpm build192.168.1.176withpnpm unraid:deploy 192.168.1.176Gamer5after restartGamer5 -> Gamer5tmp -> Gamer5and saw GraphQL follow both changes correctlySummary by CodeRabbit
Release Notes
New Features
Tests