feat(cli,snmp): fix APC runtime unit defaults and add interactive action editing

This commit is contained in:
2026-04-16 09:44:30 +00:00
parent c7b52c48d5
commit c42ebb56d3
22 changed files with 2001 additions and 863 deletions
+140 -1
View File
@@ -1,6 +1,10 @@
import { assert, assertEquals, assertExists } from 'jsr:@std/assert@^1.0.0';
import { NupstSnmp } from '../ts/snmp/manager.ts';
import { UpsOidSets } from '../ts/snmp/oid-sets.ts';
import {
convertRuntimeValueToMinutes,
getDefaultRuntimeUnitForUpsModel,
} from '../ts/snmp/runtime-units.ts';
import type { IOidSet, ISnmpConfig, TUpsModel } from '../ts/snmp/types.ts';
import {
analyzeConfigReload,
@@ -10,7 +14,7 @@ import {
import { type IPauseState, loadPauseSnapshot } from '../ts/pause-state.ts';
import { shortId } from '../ts/helpers/shortid.ts';
import { HTTP_SERVER, SNMP, THRESHOLDS, TIMING, UI } from '../ts/constants.ts';
import { Action, type IActionContext } from '../ts/actions/base-action.ts';
import { Action, type IActionConfig, type IActionContext } from '../ts/actions/base-action.ts';
import {
applyDefaultShutdownDelay,
buildUpsActionContext,
@@ -35,6 +39,9 @@ import {
evaluateGroupActionThreshold,
} from '../ts/group-monitoring.ts';
import { createInitialUpsStatus } from '../ts/ups-status.ts';
import { MigrationV4_2ToV4_3 } from '../ts/migrations/migration-v4.2-to-v4.3.ts';
import { MigrationV4_3ToV4_4 } from '../ts/migrations/migration-v4.3-to-v4.4.ts';
import { ActionHandler } from '../ts/cli/action-handler.ts';
import * as qenv from 'npm:@push.rocks/qenv@^6.0.0';
const testQenv = new qenv.Qenv('./', '.nogit/');
@@ -794,6 +801,74 @@ Deno.test('UpsOidSets: getStandardOids returns RFC 1628 OIDs', () => {
}
});
// -----------------------------------------------------------------------------
// Runtime Unit Tests
// -----------------------------------------------------------------------------
Deno.test('getDefaultRuntimeUnitForUpsModel: APC defaults to ticks', () => {
assertEquals(getDefaultRuntimeUnitForUpsModel('apc'), 'ticks');
assertEquals(getDefaultRuntimeUnitForUpsModel('cyberpower'), 'ticks');
assertEquals(getDefaultRuntimeUnitForUpsModel('eaton'), 'seconds');
});
Deno.test('convertRuntimeValueToMinutes: APC and explicit overrides convert correctly', () => {
assertEquals(convertRuntimeValueToMinutes({ upsModel: 'apc' }, 12000), 2);
assertEquals(convertRuntimeValueToMinutes({ upsModel: 'eaton' }, 600), 10);
assertEquals(convertRuntimeValueToMinutes({ upsModel: 'apc', runtimeUnit: 'minutes' }, 12), 12);
});
// -----------------------------------------------------------------------------
// Migration Tests
// -----------------------------------------------------------------------------
Deno.test('MigrationV4_2ToV4_3: assigns ticks to APC runtimeUnit', () => {
const migration = new MigrationV4_2ToV4_3();
const migrated = migration.migrate({
version: '4.2',
upsDevices: [
{
name: 'APC Rack UPS',
snmp: {
upsModel: 'apc',
},
},
],
});
const migratedDevice = (migrated.upsDevices as Array<Record<string, unknown>>)[0];
const snmp = migratedDevice.snmp as Record<string, unknown>;
assertEquals(migrated.version, '4.3');
assertEquals(snmp.runtimeUnit, 'ticks');
});
Deno.test('MigrationV4_3ToV4_4: corrects APC minutes runtimeUnit to ticks', () => {
const migration = new MigrationV4_3ToV4_4();
const migrated = migration.migrate({
version: '4.3',
upsDevices: [
{
name: 'APC Rack UPS',
snmp: {
upsModel: 'apc',
runtimeUnit: 'minutes',
},
},
{
name: 'Eaton UPS',
snmp: {
upsModel: 'eaton',
runtimeUnit: 'seconds',
},
},
],
});
const migratedDevices = migrated.upsDevices as Array<Record<string, unknown>>;
assertEquals(migrated.version, '4.4');
assertEquals((migratedDevices[0].snmp as Record<string, unknown>).runtimeUnit, 'ticks');
assertEquals((migratedDevices[1].snmp as Record<string, unknown>).runtimeUnit, 'seconds');
});
// -----------------------------------------------------------------------------
// Action Base Class Tests
// -----------------------------------------------------------------------------
@@ -954,6 +1029,70 @@ Deno.test('Action.shouldExecute: anyChange mode always returns true', () => {
);
});
// -----------------------------------------------------------------------------
// Action Handler Tests
// -----------------------------------------------------------------------------
Deno.test('ActionHandler.runEditProcess: updates an existing shutdown action', async () => {
const config: {
version: string;
defaultShutdownDelay: number;
checkInterval: number;
upsDevices: Array<{ id: string; name: string; groups: string[]; actions: IActionConfig[] }>;
groups: [];
} = {
version: '4.4',
defaultShutdownDelay: 5,
checkInterval: 30000,
upsDevices: [{
id: 'ups-1',
name: 'UPS 1',
groups: [],
actions: [{
type: 'shutdown',
triggerMode: 'onlyThresholds',
thresholds: {
battery: 40,
runtime: 12,
},
}],
}],
groups: [],
};
let savedConfig: typeof config | undefined;
const daemonMock = {
loadConfig: async () => config,
saveConfig: (nextConfig: typeof config) => {
savedConfig = JSON.parse(JSON.stringify(nextConfig));
},
getConfig: () => config,
};
const nupstMock = {
getDaemon: () => daemonMock,
} as unknown as ConstructorParameters<typeof ActionHandler>[0];
const handler = new ActionHandler(nupstMock);
const answers = ['', '12', '25', '8', '3'];
let answerIndex = 0;
const prompt = async (_question: string): Promise<string> => answers[answerIndex++] ?? '';
await handler.runEditProcess('ups-1', '0', prompt);
assertExists(savedConfig);
assertEquals(answerIndex, answers.length);
assertEquals(savedConfig.upsDevices[0].actions[0], {
type: 'shutdown',
shutdownDelay: 12,
thresholds: {
battery: 25,
runtime: 8,
},
triggerMode: 'powerChangesAndThresholds',
});
});
// -----------------------------------------------------------------------------
// NupstSnmp Class Tests (Unit tests - no real UPS needed)
// -----------------------------------------------------------------------------