From dea4b89fa5746f82181f85af8c4a599842345108 Mon Sep 17 00:00:00 2001 From: Eli Bosley Date: Mon, 13 Oct 2025 11:55:07 -0400 Subject: [PATCH] feat(onboarding): update OnboardingTracker to support dynamic OS version path - Refactored `OnboardingTracker` to utilize a configurable OS version file path based on the data directory. - Enhanced tests to verify behavior when the data directory is unavailable, ensuring fallback to the default version path. - Updated related logic to improve version tracking and persistence during application bootstrap. This update improves flexibility in OS version management and enhances the reliability of onboarding processes. --- api/src/unraid-api/config/api-config.test.ts | 27 ++++++++++++++++--- .../config/onboarding-tracker.module.ts | 14 +++++++--- 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/api/src/unraid-api/config/api-config.test.ts b/api/src/unraid-api/config/api-config.test.ts index fe60fb262..f4000e7ee 100644 --- a/api/src/unraid-api/config/api-config.test.ts +++ b/api/src/unraid-api/config/api-config.test.ts @@ -106,6 +106,8 @@ describe('ApiConfigPersistence', () => { describe('OnboardingTracker', () => { const trackerPath = path.join(PATHS_CONFIG_MODULES, 'onboarding-tracker.json'); + const dataDir = '/tmp/unraid-data'; + const versionFilePath = path.join(dataDir, 'unraid-version'); let configService: ConfigService; let setMock: ReturnType; let configStore: Record; @@ -115,6 +117,7 @@ describe('OnboardingTracker', () => { setMock = vi.fn((key: string, value: unknown) => { configStore[key] = value; }); + configStore['PATHS_UNRAID_DATA'] = dataDir; configService = { set: setMock, get: vi.fn((key: string) => configStore[key]), @@ -127,7 +130,7 @@ describe('OnboardingTracker', () => { it('defers persisting last seen version until shutdown', async () => { mockReadFile.mockImplementation(async (filePath) => { - if (filePath === '/etc/unraid-version') { + if (filePath === versionFilePath) { return 'version="7.2.0-beta.3.4"\n'; } throw Object.assign(new Error('Not found'), { code: 'ENOENT' }); @@ -154,7 +157,7 @@ describe('OnboardingTracker', () => { it('does not rewrite when version has not changed', async () => { mockReadFile.mockImplementation(async (filePath) => { - if (filePath === '/etc/unraid-version') { + if (filePath === versionFilePath) { return 'version="6.12.0"\n'; } if (filePath === trackerPath) { @@ -192,9 +195,25 @@ describe('OnboardingTracker', () => { expect(mockAtomicWriteFile).not.toHaveBeenCalled(); }); - it('keeps previous version available to signal upgrade until shutdown', async () => { + it('falls back to default version path when data directory is unavailable', async () => { + delete configStore['PATHS_UNRAID_DATA']; + mockReadFile.mockImplementation(async (filePath) => { if (filePath === '/etc/unraid-version') { + return 'version="7.3.0"\n'; + } + throw Object.assign(new Error('Not found'), { code: 'ENOENT' }); + }); + + const tracker = new OnboardingTracker(configService); + await tracker.onApplicationBootstrap(); + + expect(setMock).toHaveBeenCalledWith('onboardingTracker.currentVersion', '7.3.0'); + }); + + it('keeps previous version available to signal upgrade until shutdown', async () => { + mockReadFile.mockImplementation(async (filePath) => { + if (filePath === versionFilePath) { return 'version="7.1.0"\n'; } if (filePath === trackerPath) { @@ -239,7 +258,7 @@ describe('OnboardingTracker', () => { it('marks onboarding steps complete for the current version without clearing upgrade flag', async () => { mockReadFile.mockImplementation(async (filePath) => { - if (filePath === '/etc/unraid-version') { + if (filePath === versionFilePath) { return 'version="7.2.0"\n'; } if (filePath === trackerPath) { diff --git a/api/src/unraid-api/config/onboarding-tracker.module.ts b/api/src/unraid-api/config/onboarding-tracker.module.ts index f257e7f04..d1ad7f14f 100644 --- a/api/src/unraid-api/config/onboarding-tracker.module.ts +++ b/api/src/unraid-api/config/onboarding-tracker.module.ts @@ -15,7 +15,7 @@ import { PATHS_CONFIG_MODULES } from '@app/environment.js'; const TRACKER_FILE_NAME = 'onboarding-tracker.json'; const CONFIG_PREFIX = 'onboardingTracker'; -const OS_VERSION_FILE_PATH = '/etc/unraid-version'; +const DEFAULT_OS_VERSION_FILE_PATH = '/etc/unraid-version'; type CompletedStepState = { version: string; @@ -41,8 +41,14 @@ export class OnboardingTracker implements OnApplicationBootstrap, OnApplicationS private state: TrackerState = {}; private sessionLastTrackedVersion?: string; private currentVersion?: string; + private readonly versionFilePath: string; - constructor(private readonly configService: ConfigService) {} + constructor(private readonly configService: ConfigService) { + const unraidDataDir = this.configService.get('PATHS_UNRAID_DATA'); + this.versionFilePath = unraidDataDir + ? path.join(unraidDataDir, 'unraid-version') + : DEFAULT_OS_VERSION_FILE_PATH; + } async onApplicationBootstrap() { this.currentVersion = await this.readCurrentVersion(); @@ -164,11 +170,11 @@ export class OnboardingTracker implements OnApplicationBootstrap, OnApplicationS private async readCurrentVersion(): Promise { try { - const contents = await readFile(OS_VERSION_FILE_PATH, 'utf8'); + const contents = await readFile(this.versionFilePath, 'utf8'); const match = contents.match(/^\s*version\s*=\s*"([^"]+)"\s*$/m); return match?.[1]?.trim() || undefined; } catch (error) { - this.logger.error(error, `Failed to read current OS version from ${OS_VERSION_FILE_PATH}`); + this.logger.error(error, `Failed to read current OS version from ${this.versionFilePath}`); return undefined; } }