Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,7 @@ export class DiskSensorsService implements TemperatureSensorProvider {
constructor(private readonly disksService: DisksService) {}

async isAvailable(): Promise<boolean> {
// Disks are always "available" since DisksService exists
try {
const disks = await this.disksService.getDisks();
return disks.length > 0;
} catch {
return false;
}
return true;
}

async read(): Promise<RawTemperatureSensor[]> {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { describe, expect, it, vi } from 'vitest';

import type { AppReadyEvent } from '@app/unraid-api/app/app-lifecycle.events.js';
import { TemperatureStartupTasksListener, scheduleTemperatureStartupTasks } from '@app/unraid-api/graph/resolvers/metrics/temperature/temperature-startup-tasks.js';

Check failure on line 4 in api/src/unraid-api/graph/resolvers/metrics/temperature/temperature-startup-tasks.spec.ts

View workflow job for this annotation

GitHub Actions / Test API

Replace `·TemperatureStartupTasksListener,·scheduleTemperatureStartupTasks·` with `⏎····scheduleTemperatureStartupTasks,⏎····TemperatureStartupTasksListener,⏎`

describe('scheduleTemperatureStartupTasks', () => {
it('schedules temperature startup work after the provided delay', async () => {
vi.useFakeTimers();

const initializeProviders = vi.fn().mockResolvedValue(undefined);
const logger = {
info: vi.fn(),
warn: vi.fn(),
};

scheduleTemperatureStartupTasks({ initializeProviders }, logger, 250);

expect(initializeProviders).not.toHaveBeenCalled();

await vi.advanceTimersByTimeAsync(250);

expect(initializeProviders).toHaveBeenCalledTimes(1);

vi.useRealTimers();
});

it('warns when background temperature startup rejects', async () => {
vi.useFakeTimers();

const backgroundError = new Error('disk scan failed');
const logger = {
info: vi.fn(),
warn: vi.fn(),
};

scheduleTemperatureStartupTasks(
{
initializeProviders: vi.fn().mockRejectedValue(backgroundError),
},
logger,
250
);

await vi.advanceTimersByTimeAsync(250);
await vi.runAllTicks();

expect(logger.warn).toHaveBeenCalledWith(
backgroundError,
'Temperature provider initialization after startup failed'
);

vi.useRealTimers();
});

it('does nothing when the temperature service is unavailable', () => {
const logger = {
info: vi.fn(),
warn: vi.fn(),
};

expect(() => scheduleTemperatureStartupTasks(undefined, logger)).not.toThrow();
expect(logger.info).not.toHaveBeenCalled();
});
});

describe('TemperatureStartupTasksListener', () => {
it('schedules temperature startup work when the app ready event is emitted', async () => {
vi.useFakeTimers();

const initializeProviders = vi.fn().mockResolvedValue(undefined);
const listener = new TemperatureStartupTasksListener({ initializeProviders });
const event: AppReadyEvent = {
reason: 'nestjs-server-listening',
};

listener.handleAppReady(event);

await vi.advanceTimersByTimeAsync(0);

expect(initializeProviders).toHaveBeenCalledTimes(1);

vi.useRealTimers();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Inject, Injectable } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';

import type { AppReadyEvent } from '@app/unraid-api/app/app-lifecycle.events.js';
import { APP_READY_EVENT } from '@app/unraid-api/app/app-lifecycle.events.js';

Check failure on line 5 in api/src/unraid-api/graph/resolvers/metrics/temperature/temperature-startup-tasks.ts

View workflow job for this annotation

GitHub Actions / Test API

Replace `APP_READY_EVENT·}·from·'@app/unraid-api/app/app-lifecycle.events.js';⏎import·{·apiLogger·}·from·'@app/core/log` with `apiLogger·}·from·'@app/core/log.js';⏎import·{·APP_READY_EVENT·}·from·'@app/unraid-api/app/app-lifecycle.events`
import { apiLogger } from '@app/core/log.js';
import { TemperatureService } from '@app/unraid-api/graph/resolvers/metrics/temperature/temperature.service.js';

const DEFAULT_TEMPERATURE_STARTUP_DELAY_MS = 0;

interface TemperatureStartupLogger {
info: (message: string, ...args: unknown[]) => void;
warn: (error: unknown, message: string, ...args: unknown[]) => void;
}

interface TemperatureStartupService {
initializeProviders: () => Promise<void>;
}

export const scheduleTemperatureStartupTasks = (
temperatureService: TemperatureStartupService | null | undefined,
logger: TemperatureStartupLogger,
delayMs = DEFAULT_TEMPERATURE_STARTUP_DELAY_MS
): void => {
if (!temperatureService) {
return;
}

logger.info('Scheduling temperature startup tasks to run in %dms', delayMs);

setTimeout(() => {
void temperatureService.initializeProviders().catch((error: unknown) => {
logger.warn(error, 'Temperature provider initialization after startup failed');
});
}, delayMs);
};

@Injectable()
export class TemperatureStartupTasksListener {
constructor(
@Inject(TemperatureService)
private readonly temperatureService: TemperatureStartupService
) {}

@OnEvent(APP_READY_EVENT)
handleAppReady(_event: AppReadyEvent): void {
scheduleTemperatureStartupTasks(this.temperatureService, apiLogger);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { IpmiSensorsService } from '@app/unraid-api/graph/resolvers/metrics/temp
import { LmSensorsService } from '@app/unraid-api/graph/resolvers/metrics/temperature/sensors/lm_sensors.service.js';
import { TemperatureHistoryService } from '@app/unraid-api/graph/resolvers/metrics/temperature/temperature_history.service.js';
import { TemperatureConfigService } from '@app/unraid-api/graph/resolvers/metrics/temperature/temperature-config.service.js';
import { TemperatureStartupTasksListener } from '@app/unraid-api/graph/resolvers/metrics/temperature/temperature-startup-tasks.js';
import { TemperatureService } from '@app/unraid-api/graph/resolvers/metrics/temperature/temperature.service.js';

@Module({
Expand All @@ -23,6 +24,7 @@ import { TemperatureService } from '@app/unraid-api/graph/resolvers/metrics/temp
inject: [ConfigService],
},
TemperatureService,
TemperatureStartupTasksListener,
LmSensorsService,
DiskSensorsService,
IpmiSensorsService,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ describe('TemperatureService', () => {

describe('initialization', () => {
it('should initialize available providers', async () => {
await service.onModuleInit();
await service.initializeProviders();

expect(lmSensors.isAvailable).toHaveBeenCalled();
expect(diskSensors.isAvailable).toHaveBeenCalled();
Expand All @@ -89,7 +89,7 @@ describe('TemperatureService', () => {
it('should handle provider initialization errors gracefully', async () => {
vi.mocked(lmSensors.isAvailable!).mockRejectedValue(new Error('Failed'));

await service.onModuleInit();
await service.initializeProviders();

// Should not throw
const metrics = await service.getMetrics();
Expand All @@ -99,7 +99,7 @@ describe('TemperatureService', () => {

describe('getMetrics', () => {
beforeEach(async () => {
await service.onModuleInit();
await service.initializeProviders();
});

it('should return temperature metrics', async () => {
Expand All @@ -123,7 +123,7 @@ describe('TemperatureService', () => {
history,
temperatureConfigService
);
await emptyService.onModuleInit();
await emptyService.initializeProviders();

const metrics = await emptyService.getMetrics();
expect(metrics).toBeNull();
Expand Down Expand Up @@ -155,7 +155,7 @@ describe('TemperatureService', () => {
thresholds: { cpu_warning: 60, cpu_critical: 80 },
});

await service.onModuleInit();
await service.initializeProviders();

vi.mocked(lmSensors.read!).mockResolvedValue([
{
Expand All @@ -182,7 +182,7 @@ describe('TemperatureService', () => {
thresholds: {},
});

await service.onModuleInit();
await service.initializeProviders();

vi.mocked(lmSensors.read!).mockResolvedValue([
{
Expand Down Expand Up @@ -210,7 +210,7 @@ describe('TemperatureService', () => {
thresholds: {},
});

await service.onModuleInit();
await service.initializeProviders();

vi.mocked(lmSensors.read!).mockResolvedValue([
{
Expand Down Expand Up @@ -239,7 +239,7 @@ describe('TemperatureService', () => {
thresholds: {},
});

await service.onModuleInit();
await service.initializeProviders();

vi.mocked(lmSensors.read!).mockResolvedValue([
{
Expand Down Expand Up @@ -269,7 +269,7 @@ describe('TemperatureService', () => {
thresholds: { cpu_warning: 160 },
});

await service.onModuleInit();
await service.initializeProviders();

vi.mocked(lmSensors.read!).mockResolvedValue([
{
Expand All @@ -294,7 +294,7 @@ describe('TemperatureService', () => {

describe('buildSummary', () => {
it('should calculate correct average', async () => {
await service.onModuleInit();
await service.initializeProviders();
vi.mocked(lmSensors.read!).mockResolvedValue([
{
id: 'sensor1',
Expand All @@ -317,7 +317,7 @@ describe('TemperatureService', () => {
});

it('should identify hottest and coolest sensors', async () => {
await service.onModuleInit();
await service.initializeProviders();
vi.mocked(lmSensors.read!).mockResolvedValue([
{
id: 's1',
Expand Down Expand Up @@ -349,7 +349,7 @@ describe('TemperatureService', () => {
});
describe('edge cases', () => {
it('should handle provider read timeout gracefully', async () => {
await service.onModuleInit();
await service.initializeProviders();

// Simulate a slow/hanging provider
vi.mocked(lmSensors.read!).mockImplementation(
Expand All @@ -362,7 +362,7 @@ describe('TemperatureService', () => {
}, 10000);

it('should deduplicate sensors with same ID from different providers', async () => {
await service.onModuleInit();
await service.initializeProviders();

// Both providers return a sensor with the same ID
vi.mocked(lmSensors.read!).mockResolvedValue([
Expand Down Expand Up @@ -391,7 +391,7 @@ describe('TemperatureService', () => {
});

it('should handle empty sensor name', async () => {
await service.onModuleInit();
await service.initializeProviders();

vi.mocked(lmSensors.read!).mockResolvedValue([
{
Expand All @@ -409,7 +409,7 @@ describe('TemperatureService', () => {
});

it('should handle negative temperature values', async () => {
await service.onModuleInit();
await service.initializeProviders();

vi.mocked(lmSensors.read!).mockResolvedValue([
{
Expand All @@ -428,7 +428,7 @@ describe('TemperatureService', () => {
});

it('should handle extremely high temperature values', async () => {
await service.onModuleInit();
await service.initializeProviders();

vi.mocked(lmSensors.read!).mockResolvedValue([
{
Expand All @@ -447,7 +447,7 @@ describe('TemperatureService', () => {
});

it('should handle NaN temperature values', async () => {
await service.onModuleInit();
await service.initializeProviders();

vi.mocked(lmSensors.read!).mockResolvedValue([
{
Expand All @@ -465,7 +465,7 @@ describe('TemperatureService', () => {
});

it('should handle mix of valid and NaN temperature values', async () => {
await service.onModuleInit();
await service.initializeProviders();

vi.mocked(lmSensors.read!).mockResolvedValue([
{
Expand Down Expand Up @@ -493,7 +493,7 @@ describe('TemperatureService', () => {
});

it('should handle all providers failing', async () => {
await service.onModuleInit();
await service.initializeProviders();

vi.mocked(lmSensors.read!).mockRejectedValue(new Error('lm-sensors failed'));
vi.mocked(diskSensors.read!).mockRejectedValue(new Error('disk sensors failed'));
Expand All @@ -504,7 +504,7 @@ describe('TemperatureService', () => {
});

it('should handle partial provider failures', async () => {
await service.onModuleInit();
await service.initializeProviders();

vi.mocked(lmSensors.read!).mockRejectedValue(new Error('lm-sensors failed'));
vi.mocked(diskSensors.read!).mockResolvedValue([
Expand Down
Loading
Loading