Skip to content

fix(api): isUpdateAvailable returns null for all containers when cache has an undef entry#2030

Merged
elibosley merged 3 commits into
mainfrom
fix/docker-update-check-null
Jun 18, 2026
Merged

fix(api): isUpdateAvailable returns null for all containers when cache has an undef entry#2030
elibosley merged 3 commits into
mainfrom
fix/docker-update-check-null

Conversation

@elibosley

@elibosley elibosley commented Jun 18, 2026

Copy link
Copy Markdown
Member

Summary

docker.containers[].isUpdateAvailable resolves to null for every container, even though containerUpdateStatuses (a separate code path) reports the correct UP_TO_DATE / UPDATE_AVAILABLE / UNKNOWN values for the same containers.

Root cause

isUpdateAvailable reads /var/lib/docker/unraid-update-status.json, which the webgui (DockerClient.php) writes — not the API. For stopped or locally-built containers the webgui legitimately writes local/remote as null and status as the string 'undef' (surfaced elsewhere as UNKNOWN).

The API validated the whole file with z.record(...), which fails wholesale if any single value fails. The entry schema required non-null local/remote strings and status of exactly 'true' | 'false', so a single undef/null entry rejected the entire file → readCachedUpdateStatus() returned {} → every container reported null, including ones that actually had updates.

This matches the report exactly: the failing response contained several UNKNOWN (undef) entries, which poisoned the parse for all rows.

Fix

  • Loosen CachedStatusEntrySchema to match what the webgui actually writes (local/remote nullable+optional, status an optional nullable string so 'undef' is accepted).
  • Parse the cache per-entry, keeping valid entries and skipping only malformed ones (with a warning) instead of discarding the whole file.

Downstream logic in isUpdateAvailableCached already handled these shapes (it filters undef digests and treats any non-true/false status as unknown), so it just needed to actually receive the data.

Tests

Added docker-php.service.spec.ts covering the regression (a valid entry survives a malformed sibling), per-entry dropping of bad entries, and the manifest behavior. Full API suite + type-check pass.

Links

Fixes OS-381

Summary by CodeRabbit

  • Bug Fixes

    • Improved robustness of Docker update status caching—malformed cache entries no longer cause the entire cache to fail, allowing valid entries to be processed normally.
  • Tests

    • Added comprehensive test coverage for Docker cache validation edge cases.
    • Extended OIDC integration test timeout for improved reliability.

@coderabbitai

coderabbitai Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

CachedStatusEntrySchema in docker-php.service.ts is widened to make local, remote, and status optional and nullable. readCachedUpdateStatus is rewritten to validate each cache entry individually via safeParse, skipping invalid entries with a warning. A new spec file exercises this behavior. Separately, the OIDC integration test suite timeout is raised to 20 seconds.

Changes

Docker Cache Parsing Robustness

Layer / File(s) Summary
Schema widening and per-entry safeParse parsing
api/src/unraid-api/graph/resolvers/docker/docker-php.service.ts
CachedStatusEntrySchema now marks local, remote, and status as optional/nullable; readCachedUpdateStatus verifies the top-level JSON is an object and then uses safeParse per entry, skipping invalid entries with a warning instead of failing the whole parse.
Cache parsing and isUpdateAvailableCached tests
api/src/unraid-api/graph/resolvers/docker/docker-php.service.spec.ts
New spec mocks fs/promises.readFile and tests: valid entries surviving malformed siblings, wrong-shape entries being dropped, ENOENT returning {}, update detection with malformed siblings, null for undef-style entries, and false when digests match.

OIDC Integration Test Timeout

Layer / File(s) Summary
Increased describe-block timeout
api/src/unraid-api/graph/resolvers/sso/core/oidc.service.integration.test.ts
Top-level describe timeout raised to 20 seconds with a comment explaining the need to accommodate real outbound OIDC network calls in CI.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

A cache once brittle, now bends with grace,
Bad entries skipped — no panic, no disgrace.
Each digest checked, then filed away,
The undef lurks but cannot sway.
🐇 "Parse them gently," said the rabbit with cheer,
"And keep the good ones safely here!"

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and specifically describes the main fix: addressing the issue where isUpdateAvailable returns null for all containers when cache has an undef entry.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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/docker-update-check-null

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

isUpdateAvailable resolved to null for every container while
containerUpdateStatuses (a separate code path) reported correct values.

The webgui writes null digests and status:'undef' (surfaced as
UNKNOWN) for stopped or locally-built containers in
/var/lib/docker/unraid-update-status.json. The API validated the whole
file with z.record, so a single such entry failed validation for the
entire file, making readCachedUpdateStatus return {} and every
container's isUpdateAvailable resolve to null.

Loosen the entry schema to the shapes the webgui actually writes and
parse per-entry, skipping only malformed entries instead of discarding
the whole cache.

FeatureOS: https://product.unraid.net/p/docker-graphql-isupdateavailable-always-returns-null
@elibosley elibosley force-pushed the fix/docker-update-check-null branch from 29bda93 to f285873 Compare June 18, 2026 05:14
@elibosley elibosley changed the title fix(api): docker update check returns null for all containers when cache has an undef/stopped-container entry fix(api): isUpdateAvailable returns null for all containers when cache has an undef entry Jun 18, 2026
The test hits a real network endpoint (expired-cert.badssl.com) and can
exceed the 5s default in CI. Bump to 20s for headroom.
A different network-dependent test in the suite timed out at 5s in CI after the
per-test bump. These integration tests all make real outbound calls (discovery,
SSL/DNS probes), so raise the timeout for the whole suite instead of one test.

@coderabbitai coderabbitai Bot left a comment

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.

🧹 Nitpick comments (1)
api/src/unraid-api/graph/resolvers/docker/docker-php.service.spec.ts (1)

14-14: ⚡ Quick win

Remove unnecessary type assertion.

The as never cast suppresses all type checking. Since JSON.stringify(data) returns a string and mockResolvedValue accepts string | Buffer, the cast should be unnecessary. As per coding guidelines, avoid using casting whenever possible—prefer proper typing from the start.

♻️ Proposed fix
-const writeCache = (data: unknown) => mockReadFile.mockResolvedValue(JSON.stringify(data) as never);
+const writeCache = (data: unknown) => mockReadFile.mockResolvedValue(JSON.stringify(data));
🤖 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 `@api/src/unraid-api/graph/resolvers/docker/docker-php.service.spec.ts` at line
14, In the writeCache function, remove the `as never` type assertion from the
`mockReadFile.mockResolvedValue()` call. Since `JSON.stringify(data)` already
returns a string and `mockResolvedValue` is typed to accept string or Buffer,
the unnecessary cast is suppressing valid type checking. Delete the `as never`
portion so the function relies on proper typing without explicit type
assertions.

Source: Coding guidelines

🤖 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.

Nitpick comments:
In `@api/src/unraid-api/graph/resolvers/docker/docker-php.service.spec.ts`:
- Line 14: In the writeCache function, remove the `as never` type assertion from
the `mockReadFile.mockResolvedValue()` call. Since `JSON.stringify(data)`
already returns a string and `mockResolvedValue` is typed to accept string or
Buffer, the unnecessary cast is suppressing valid type checking. Delete the `as
never` portion so the function relies on proper typing without explicit type
assertions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: c157c0ff-c130-4f6b-83d1-3565e183da75

📥 Commits

Reviewing files that changed from the base of the PR and between 6f94aa1 and b33ea0d.

📒 Files selected for processing (3)
  • api/src/unraid-api/graph/resolvers/docker/docker-php.service.spec.ts
  • api/src/unraid-api/graph/resolvers/docker/docker-php.service.ts
  • api/src/unraid-api/graph/resolvers/sso/core/oidc.service.integration.test.ts

@codecov

codecov Bot commented Jun 18, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.90909% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 52.69%. Comparing base (6f94aa1) to head (b33ea0d).

Files with missing lines Patch % Lines
...d-api/graph/resolvers/docker/docker-php.service.ts 90.90% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2030      +/-   ##
==========================================
+ Coverage   52.63%   52.69%   +0.05%     
==========================================
  Files        1035     1035              
  Lines       72034    72044      +10     
  Branches     8248     8276      +28     
==========================================
+ Hits        37917    37965      +48     
+ Misses      33991    33953      -38     
  Partials      126      126              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions

Copy link
Copy Markdown
Contributor

This plugin has been deployed to Cloudflare R2 and is available for testing.
Download it at this URL:

https://preview.dl.unraid.net/unraid-api/tag/PR2030/dynamix.unraid.net.plg

@elibosley elibosley merged commit b166407 into main Jun 18, 2026
13 checks passed
@elibosley elibosley deleted the fix/docker-update-check-null branch June 18, 2026 15:16
@github-actions

Copy link
Copy Markdown
Contributor

🔄 PR Merged - Plugin Redirected to Staging

This PR has been merged and the preview plugin has been updated to redirect to the staging version.

For users testing this PR:

  • Your plugin will automatically update to the staging version on the next update check
  • The staging version includes all merged changes from this PR
  • No manual intervention required

Staging URL:

https://preview.dl.unraid.net/unraid-api/dynamix.unraid.net.plg

Thank you for testing! 🚀

elibosley pushed a commit that referenced this pull request Jun 18, 2026
🤖 I have created a release *beep* *boop*
---


## [4.35.1](v4.35.0...v4.35.1)
(2026-06-18)


### Bug Fixes

* **api:** isUpdateAvailable returns null for all containers when cache
has an undef entry ([#2030](#2030))
([b166407](b166407))
* **api:** keep plugin config loads side-effect free
([#2025](#2025))
([3c84326](3c84326))
* **plugin:** preserve Connect during install start
([#2027](#2027))
([6f94aa1](6f94aa1))

---
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>
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