import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { newFixture, type Fixture } from '../helpers/fixture.js'; import { cpuTemperature, historyTemperatures, validateTemperatures, type HostTemperatureSensor } from '../../src/lib/host-temperatures.js'; const sensor = (changes: Partial = {}): HostTemperatureSensor => ({ id: 'package', device: 'coretemp', label: 'Package id 0', kind: 'cpu-package', celsius: 65, ...changes }); describe('temperature readings', () => { it('keeps valid sensors while dropping malformed, duplicate and out-of-range entries', () => { const good = sensor(); const readings = [ good, good, null, 'bad', ...[NaN, Infinity, -Infinity, null, '65', true, -101, 201].map((celsius) => ({ ...good, id: 'invalid', celsius })), { ...good, id: 'x'.repeat(101) }, { ...good, label: '' }, { ...good, kind: 'cpu-control' }, sensor({ id: 'zero', celsius: 0 }) ]; expect(validateTemperatures(readings)).toEqual([good, sensor({ id: 'zero', celsius: 0 })]); expect(validateTemperatures({})).toEqual([]); expect(validateTemperatures(undefined)).toEqual([]); }); it('chooses the hottest package and uses die/core readings only as fallbacks', () => { const packages = [sensor(), sensor({ id: 'socket2', label: 'Package id 1', celsius: 72 })]; const core = sensor({ id: 'core', label: 'Core 0', kind: 'cpu-core', celsius: 80 }); const die = sensor({ id: 'die', label: 'Tdie', kind: 'cpu-die', celsius: 64 }); expect(cpuTemperature([...packages, core])?.celsius).toBe(72); expect(cpuTemperature([die, core])?.celsius).toBe(64); expect(cpuTemperature([core])?.celsius).toBe(80); expect(cpuTemperature()).toBeNull(); const drive = sensor({ id: 'drive', kind: 'nvme', device: 'nvme', label: 'Composite', celsius: 50.9 }); expect( historyTemperatures([ ...packages, core, drive, { ...drive, id: 'internal', label: 'Sensor 1' } ]) ).toEqual([{ ...packages[1], id: 'cpu', label: 'CPU temperature' }, drive]); }); }); describe('temperature persistence', () => { let fixture: Fixture; beforeEach(async () => { vi.resetModules(); fixture = await newFixture([]); }); afterEach(async () => { vi.restoreAllMocks(); await fixture.cleanup(); }); it('accepts older agents and faulty temperature payloads without losing host metrics', async () => { const { createHost, heartbeat, validateInventory } = await import('../../src/lib/server/hosts/index.js'); const host = createHost({ name: 'ec-2' }); expect(validateInventory({ cpuUsagePercent: 25 }).temperatures).toBeUndefined(); expect( heartbeat(host.id, { cpuUsagePercent: 25, temperatures: [sensor(), { celsius: 'bad' }] }).inventory ).toMatchObject({ cpuUsagePercent: 25, temperatures: [sensor()] }); expect( heartbeat(host.id, { cpuUsagePercent: 30, temperatures: [] }).inventory.temperatures ).toEqual([]); expect( heartbeat(host.id, { cpuUsagePercent: 35, temperatures: 'bad' }).inventory.temperatures ).toEqual([]); const { db } = await import('../../src/lib/server/db/index.js'); expect( db() .prepare('SELECT celsius FROM ec_host_temperature_samples WHERE host_id=?') .all(host.id) ).toEqual([{ celsius: 65 }]); }); it('retains one peak per minute and applies host history retention, including offline hosts', async () => { const { createHost } = await import('../../src/lib/server/hosts/index.js'); const { recordTemperatures } = await import('../../src/lib/server/hosts/temperatures.js'); const { db } = await import('../../src/lib/server/db/index.js'); const { fleetSettings, saveFleetSettings } = await import('../../src/lib/server/settings.js'); const { pruneSampleHistory } = await import('../../src/lib/server/retention.js'); const host = createHost({ name: 'ec-2' }); const minute = Math.floor((Date.now() - 3 * 86400_000) / 60_000) * 60_000; for (const [i, celsius] of [65, 78, 67].entries()) recordTemperatures( host.id, { temperatures: [sensor({ celsius })] }, new Date(minute + i * 15_000).toISOString() ); expect(db().prepare('SELECT celsius FROM ec_host_temperature_samples').all()).toEqual([ { celsius: 78 } ]); saveFleetSettings({ ...fleetSettings(), hostResourceKeepDays: 0 }); pruneSampleHistory(); expect(db().prepare('SELECT COUNT(*) AS n FROM ec_host_temperature_samples').get()).toEqual( { n: 1 } ); recordTemperatures(host.id, { temperatures: [sensor()] }, new Date().toISOString()); saveFleetSettings({ ...fleetSettings(), hostResourceKeepDays: 1 }); pruneSampleHistory(); expect(db().prepare('SELECT celsius FROM ec_host_temperature_samples').all()).toEqual([ { celsius: 65 } ]); }); it('upgrades an existing database without rewriting its resource samples', async () => { const { db, closeDb } = await import('../../src/lib/server/db/index.js'); const { createHost } = await import('../../src/lib/server/hosts/index.js'); const host = createHost({ name: 'old' }); db() .prepare( 'INSERT INTO ec_host_resource_samples(host_id,sampled_at,cpu_usage_percent) VALUES(?,?,?)' ) .run(host.id, '2026-10-13T12:00:00Z', 25); db().exec('DROP TABLE ec_host_temperature_samples; PRAGMA user_version=22;'); closeDb(); expect(db().prepare('SELECT * FROM ec_host_temperature_samples').all()).toEqual([]); expect( db().prepare('SELECT cpu_usage_percent FROM ec_host_resource_samples').all() ).toEqual([{ cpu_usage_percent: 25 }]); expect(db().pragma('foreign_key_check')).toEqual([]); }); });