Skip to content

feat: share internal boot state#1921

Merged
elibosley merged 4 commits intomainfrom
codex/share-internal-boot-state
Mar 16, 2026
Merged

feat: share internal boot state#1921
elibosley merged 4 commits intomainfrom
codex/share-internal-boot-state

Conversation

@elibosley
Copy link
Member

@elibosley elibosley commented Mar 16, 2026

Summary by CodeRabbit

  • New Features

    • Server state now exposes a distinct-TPM indicator enabling TPM-based GUID replacement even without internal-boot setup.
  • Bug Fixes

    • Refined TPM license transfer button visibility to rely on the new TPM indicator.
  • Tests

    • Added comprehensive unit tests for internal boot-state behavior and enhanced onboarding/registration test coverage.
  • Chores

    • Improved cache-backed handling and invalidation for internal boot-device state to reduce redundant lookups and coalesce concurrent requests.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 16, 2026

Walkthrough

Adds cache-backed internal boot-device state with TTL and in-flight deduplication, a public cache invalidation method, and tests covering caching, concurrency, and invalidation. Onboarding invokes cache invalidation after boot configuration. UI/store and tests adjust TPM GUID/transfer visibility logic and test setup consolidation.

Changes

Cohort / File(s) Summary
Internal Boot State Service
api/src/unraid-api/graph/resolvers/disks/internal-boot-state.service.ts, api/src/unraid-api/graph/resolvers/disks/internal-boot-state.service.spec.ts
Introduces CACHE_MANAGER injection, cache-backed lookup with TTL, in-flight request deduplication, invalidateCachedInternalBootDeviceState() public method, and comprehensive tests for cache hits/misses, concurrent coalescing, and invalidation flows.
Onboarding Service
api/src/unraid-api/graph/resolvers/onboarding/onboarding-internal-boot.service.ts, api/src/unraid-api/graph/resolvers/onboarding/onboarding-internal-boot.service.spec.ts
Calls invalidateCachedInternalBootDeviceState() after successful boot configuration; tests added/updated to assert invocation and to handle a failing invalidation (logs warning, preserves success).
Web — Store / Component / Tests
web/src/store/server.ts, web/src/components/Registration.standalone.vue, web/__test__/components/Registration.test.ts, web/__test__/store/server.test.ts
Exposes hasDistinctTpmGuid from server store, updates Registration component to rely on the new TPM-guid computed value for transfer-button visibility, consolidates Registration test setup, and adds a store test validating TPM-based GUID replacement without internal-boot setup.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Service as InternalBootStateService
    participant Cache as Cache Manager
    participant Disks as DisksService

    Client->>Service: getHasInternalBootDevices()
    Service->>Cache: get("internal-boot-device-exists")
    Cache-->>Service: miss

    par concurrent callers
        Client->>Service: getHasInternalBootDevices()
        Service->>Service: sees in-flight promise -> await
    and single loader
        Service->>Disks: getDisks()/getArrayData()
        Disks-->>Service: disk list / boot info
        Service->>Service: compute boolean result
        Service->>Cache: set(key, result, TTL)
        Cache-->>Service: stored
        Service->>Service: resolve in-flight promise
    end

    Service-->>Client: result (cached / resolved)
    Client->>Service: invalidateCachedInternalBootDeviceState()
    Service->>Cache: del(key)
    Cache-->>Service: deleted
    Service->>Service: clear in-flight marker
    Service-->>Client: done
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I nibbled at cache keys in morning light,

Coalesced the hops of concurrent flight,
TTLs keep the burrow tidy and keen,
Invalidation clears the patch unseen,
TPM buttons gleam — a hop, a happy scene.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: introducing caching and sharing of internal boot state across the service layer with deduplication and cache invalidation.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch codex/share-internal-boot-state
📝 Coding Plan
  • Generate coding plan for human review comments

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

@elibosley elibosley force-pushed the codex/share-internal-boot-state branch from ba5a6bd to ba27610 Compare March 16, 2026 21:16
@chatgpt-codex-connector
Copy link

💡 Codex Review

if (!bootDisk || bootDisk.type !== ArrayDiskType.FLASH) {
return false;

P1 Badge Determine flash boot state from mount, not boot.type

This check can never report true in the target scenario because getArrayData() prefers ArrayDiskType.BOOT entries whenever internal boot slots exist (core/modules/array/get-array-data.ts lines 22-29), even if the machine is still physically booted from USB; therefore bootDisk.type !== FLASH short-circuits to false exactly when internal boot is configured while still on flash. That makes bootedFromFlashWithInternalBootSetup incorrect in vars(), so the new onboarding/registration behavior that depends on this shared state will be wrong in production for systems with configured BOOT slots.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

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 the current code and only fix it if needed.

Inline comments:
In `@api/src/unraid-api/graph/resolvers/disks/internal-boot-state.service.ts`:
- Around line 39-41: The invalidateCachedInternalBootDeviceState() currently
clears the cache key and nulls pendingHasInternalBootDevicesLookup but doesn't
prevent an in-flight loadHasInternalBootDevices() from later repopulating the
cache with stale data; fix by adding a generation token (e.g.,
this.internalBootLookupGeneration: number) that
invalidateCachedInternalBootDeviceState() increments, then have
loadHasInternalBootDevices() capture the current generation into a local
variable and only write to cache and clear pendingHasInternalBootDevicesLookup
if the captured generation still equals this.internalBootLookupGeneration;
update references to pendingHasInternalBootDevicesLookup and
INTERNAL_BOOT_DEVICE_SETUP_CACHE_KEY accordingly and add a regression test that
calls invalidateCachedInternalBootDeviceState() before the first lookup resolves
to ensure the stale result does not repopulate the cache.

In
`@api/src/unraid-api/graph/resolvers/onboarding/onboarding-internal-boot.service.ts`:
- Line 410: The call to
this.internalBootStateService.invalidateCachedInternalBootDeviceState() must be
treated as best-effort so its failure cannot flip a successfully completed
mkbootpool flow into an overall failure; wrap that await in its own try/catch,
catch any error and log it as a warning (do not rethrow or return a failure),
then continue returning the successful result from the surrounding handler
(i.e., keep cmdCreatePool, cmdAssignDisk and cmdMakeBootable outcomes as
authoritative). Locate the call to invalidateCachedInternalBootDeviceState in
the onboarding-internal-boot service method and replace the direct await with a
try { await
this.internalBootStateService.invalidateCachedInternalBootDeviceState(); } catch
(err) { /* log warning via existing logger/processLogger */ } so cache
invalidation errors are surfaced as warnings only.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: b90fac45-751c-4971-8719-839cf0e0f83d

📥 Commits

Reviewing files that changed from the base of the PR and between 540d6f9 and e0b9925.

📒 Files selected for processing (6)
  • api/src/unraid-api/graph/resolvers/disks/internal-boot-state.service.spec.ts
  • api/src/unraid-api/graph/resolvers/disks/internal-boot-state.service.ts
  • api/src/unraid-api/graph/resolvers/onboarding/onboarding-internal-boot.service.spec.ts
  • api/src/unraid-api/graph/resolvers/onboarding/onboarding-internal-boot.service.ts
  • web/__test__/components/Registration.test.ts
  • web/src/components/Registration.standalone.vue

@github-actions
Copy link
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/PR1921/dynamix.unraid.net.plg

@codecov
Copy link

codecov bot commented Mar 16, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 51.55%. Comparing base (540d6f9) to head (87a2ad9).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1921      +/-   ##
==========================================
+ Coverage   51.50%   51.55%   +0.04%     
==========================================
  Files        1024     1024              
  Lines       70790    70846      +56     
  Branches     7803     7814      +11     
==========================================
+ Hits        36459    36522      +63     
+ Misses      34209    34202       -7     
  Partials      122      122              

☔ View full report in Codecov by Sentry.
📢 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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

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 the current code and only fix it if needed.

Inline comments:
In
`@api/src/unraid-api/graph/resolvers/onboarding/onboarding-internal-boot.service.spec.ts`:
- Around line 227-251: The Logger.prototype.warn spy created as warnSpy in the
test leaks into other tests because it's not restored; update the test (the spec
containing the it('returns success when cache invalidation fails after setup and
logs a warning' ...) block) to restore the spy after assertions by calling
warnSpy.mockRestore() (or ensure restoration in a finally/afterEach cleanup) so
the Logger.prototype.warn implementation is returned to its original behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 0b9d9b31-0128-4735-b6be-201ca3bb1f64

📥 Commits

Reviewing files that changed from the base of the PR and between e0b9925 and 87a2ad9.

📒 Files selected for processing (7)
  • api/src/unraid-api/graph/resolvers/disks/internal-boot-state.service.spec.ts
  • api/src/unraid-api/graph/resolvers/disks/internal-boot-state.service.ts
  • api/src/unraid-api/graph/resolvers/onboarding/onboarding-internal-boot.service.spec.ts
  • api/src/unraid-api/graph/resolvers/onboarding/onboarding-internal-boot.service.ts
  • web/__test__/store/server.test.ts
  • web/src/components/Registration.standalone.vue
  • web/src/store/server.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • api/src/unraid-api/graph/resolvers/onboarding/onboarding-internal-boot.service.ts
  • api/src/unraid-api/graph/resolvers/disks/internal-boot-state.service.spec.ts

Comment on lines +227 to 251
it('returns success when cache invalidation fails after setup and logs a warning', async () => {
const warnSpy = vi.spyOn(Logger.prototype, 'warn').mockImplementation(() => undefined);
vi.mocked(emcmd).mockResolvedValue({ ok: true } as Awaited<ReturnType<typeof emcmd>>);
internalBootStateService.invalidateCachedInternalBootDeviceState.mockRejectedValue(
new Error('cache delete failed')
);
const service = createService();

const result = await service.createInternalBootPool({
poolName: 'cache',
devices: ['disk-1'],
bootSizeMiB: 16384,
updateBios: false,
});

expect(result.ok).toBe(true);
expect(result.code).toBe(0);
expect(vi.mocked(emcmd)).toHaveBeenCalledTimes(4);
expect(internalBootStateService.invalidateCachedInternalBootDeviceState).toHaveBeenCalledTimes(
1
);
expect(warnSpy).toHaveBeenCalledWith(
'Failed to invalidate cached internal boot device state after successful setup: cache delete failed'
);
});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

In Vitest v3, does vi.clearAllMocks() restore vi.spyOn() implementations on class prototypes, or should vi.restoreAllMocks() / spy.mockRestore() be used?

💡 Result:

In Vitest v3, vi.clearAllMocks() does not restore vi.spyOn() implementations (including spies placed on class prototypes)—it only clears call/instance history and keeps the mocked implementation in place. [1]

To put a spied prototype method back to its original implementation/descriptor, use vi.restoreAllMocks() (restores all spies) or spy.mockRestore() (restores one spy). [1][2]

Sources: [1] [2]


🏁 Script executed:

fd -t f "onboarding-internal-boot.service.spec.ts" -x cat -n {}

Repository: unraid/api

Length of output: 14868


Restore the Logger.prototype.warn spy after this test.

Line 228 creates a prototype spy but never restores it. vi.clearAllMocks() only clears call history—it does not restore spy implementations—so the mocked behavior leaks into subsequent tests.

🔧 Proposed fix
     it('returns success when cache invalidation fails after setup and logs a warning', async () => {
         const warnSpy = vi.spyOn(Logger.prototype, 'warn').mockImplementation(() => undefined);
-        vi.mocked(emcmd).mockResolvedValue({ ok: true } as Awaited<ReturnType<typeof emcmd>>);
-        internalBootStateService.invalidateCachedInternalBootDeviceState.mockRejectedValue(
-            new Error('cache delete failed')
-        );
-        const service = createService();
-
-        const result = await service.createInternalBootPool({
-            poolName: 'cache',
-            devices: ['disk-1'],
-            bootSizeMiB: 16384,
-            updateBios: false,
-        });
-
-        expect(result.ok).toBe(true);
-        expect(result.code).toBe(0);
-        expect(vi.mocked(emcmd)).toHaveBeenCalledTimes(4);
-        expect(internalBootStateService.invalidateCachedInternalBootDeviceState).toHaveBeenCalledTimes(
-            1
-        );
-        expect(warnSpy).toHaveBeenCalledWith(
-            'Failed to invalidate cached internal boot device state after successful setup: cache delete failed'
-        );
+        try {
+            vi.mocked(emcmd).mockResolvedValue({ ok: true } as Awaited<ReturnType<typeof emcmd>>);
+            internalBootStateService.invalidateCachedInternalBootDeviceState.mockRejectedValue(
+                new Error('cache delete failed')
+            );
+            const service = createService();
+
+            const result = await service.createInternalBootPool({
+                poolName: 'cache',
+                devices: ['disk-1'],
+                bootSizeMiB: 16384,
+                updateBios: false,
+            });
+
+            expect(result.ok).toBe(true);
+            expect(result.code).toBe(0);
+            expect(vi.mocked(emcmd)).toHaveBeenCalledTimes(4);
+            expect(
+                internalBootStateService.invalidateCachedInternalBootDeviceState
+            ).toHaveBeenCalledTimes(1);
+            expect(warnSpy).toHaveBeenCalledWith(
+                'Failed to invalidate cached internal boot device state after successful setup: cache delete failed'
+            );
+        } finally {
+            warnSpy.mockRestore();
+        }
     });
📝 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
it('returns success when cache invalidation fails after setup and logs a warning', async () => {
const warnSpy = vi.spyOn(Logger.prototype, 'warn').mockImplementation(() => undefined);
vi.mocked(emcmd).mockResolvedValue({ ok: true } as Awaited<ReturnType<typeof emcmd>>);
internalBootStateService.invalidateCachedInternalBootDeviceState.mockRejectedValue(
new Error('cache delete failed')
);
const service = createService();
const result = await service.createInternalBootPool({
poolName: 'cache',
devices: ['disk-1'],
bootSizeMiB: 16384,
updateBios: false,
});
expect(result.ok).toBe(true);
expect(result.code).toBe(0);
expect(vi.mocked(emcmd)).toHaveBeenCalledTimes(4);
expect(internalBootStateService.invalidateCachedInternalBootDeviceState).toHaveBeenCalledTimes(
1
);
expect(warnSpy).toHaveBeenCalledWith(
'Failed to invalidate cached internal boot device state after successful setup: cache delete failed'
);
});
it('returns success when cache invalidation fails after setup and logs a warning', async () => {
const warnSpy = vi.spyOn(Logger.prototype, 'warn').mockImplementation(() => undefined);
try {
vi.mocked(emcmd).mockResolvedValue({ ok: true } as Awaited<ReturnType<typeof emcmd>>);
internalBootStateService.invalidateCachedInternalBootDeviceState.mockRejectedValue(
new Error('cache delete failed')
);
const service = createService();
const result = await service.createInternalBootPool({
poolName: 'cache',
devices: ['disk-1'],
bootSizeMiB: 16384,
updateBios: false,
});
expect(result.ok).toBe(true);
expect(result.code).toBe(0);
expect(vi.mocked(emcmd)).toHaveBeenCalledTimes(4);
expect(
internalBootStateService.invalidateCachedInternalBootDeviceState
).toHaveBeenCalledTimes(1);
expect(warnSpy).toHaveBeenCalledWith(
'Failed to invalidate cached internal boot device state after successful setup: cache delete failed'
);
} finally {
warnSpy.mockRestore();
}
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@api/src/unraid-api/graph/resolvers/onboarding/onboarding-internal-boot.service.spec.ts`
around lines 227 - 251, The Logger.prototype.warn spy created as warnSpy in the
test leaks into other tests because it's not restored; update the test (the spec
containing the it('returns success when cache invalidation fails after setup and
logs a warning' ...) block) to restore the spy after assertions by calling
warnSpy.mockRestore() (or ensure restoration in a finally/afterEach cleanup) so
the Logger.prototype.warn implementation is returned to its original behavior.

@elibosley elibosley merged commit 8e4d44d into main Mar 16, 2026
13 checks passed
@elibosley elibosley deleted the codex/share-internal-boot-state branch March 16, 2026 22:31
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