Files
machineGroupControl/test/basic/demand-telemetry.basic.test.js
znetsixe b59d8e60f7 feat(mgc): demand telemetry + movement gate (demand debounce)
- Movement gate: hold non-urgent demand while the group is 'working'
  (mid-ramp/sequencing) and flush it once 'ready', instead of aborting
  in-flight ramps on every incoming demand — which could freeze pumps at 0.
  Urgent demand (stop, mode/priority change, large step) still pre-empts.
- getMovementState()/_isUrgentDemand()/_maybeFlushPendingDemand() helpers.
- Demand telemetry: emit demandFlow (m³/h) and demandPct (0..100 of envelope)
  resolved by the last dispatch; omitted before the first demand (degraded).
- Capacity envelope now emitted in output flow unit (m³/h) not raw m³/s.
- Manifest + populated/degraded tests for the new outputs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 16:09:18 +02:00

84 lines
3.3 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use strict';
const test = require('node:test');
const assert = require('node:assert/strict');
const { getOutput } = require('../../src/io/output.js');
const MachineGroup = require('../../src/specificClass.js');
// Real declared unit policy so the m³/s → m³/h conversion is the production one.
const unitPolicy = MachineGroup.unitPolicy;
// Minimal MGC stand-in exposing exactly the surface getOutput reads. The
// measurement loop is short-circuited with an empty type list so the test
// isolates the demand telemetry without needing curves / CoolProp.
function mockMgc(overrides = {}) {
return {
measurements: { getTypes: () => [] },
unitPolicy,
mode: 'optimalControl',
scaling: 'absolute',
absDistFromPeak: 0,
relDistFromPeak: 0,
dynamicTotals: { flow: { min: 0.05, max: 0.25 } }, // m³/s
machines: {},
operatingPoint: {},
_lastDemand: null,
...overrides,
};
}
test('demandFlow + demandPct emitted once a demand is resolved', () => {
// Demand resolved to 0.15 m³/s inside a 0.05..0.25 envelope → midpoint = 50%.
const out = getOutput(mockMgc({ _lastDemand: { canonical: 0.15, clamped: 0.15 } }));
// m³/s → m³/h is ×3600. 0.15 m³/s = 540 m³/h.
assert.equal(out.demandFlow, 540);
assert.ok(Math.abs(out.demandPct - 50) < 1e-9, `expected ~50%, got ${out.demandPct}`);
});
test('demandPct reflects the clamped setpoint, not the raw request', () => {
// Operator asked for 0.40 m³/s but the envelope caps at 0.25 → 100%.
const out = getOutput(mockMgc({ _lastDemand: { canonical: 0.40, clamped: 0.25 } }));
assert.equal(out.demandFlow, 900); // 0.25 m³/s = 900 m³/h
assert.equal(out.demandPct, 100);
});
test('demandPct is 0 (never NaN) when the capacity span is zero', () => {
const out = getOutput(mockMgc({
dynamicTotals: { flow: { min: 0.1, max: 0.1 } },
_lastDemand: { canonical: 0.1, clamped: 0.1 },
}));
assert.equal(out.demandPct, 0);
assert.ok(Number.isFinite(out.demandFlow));
});
test('turnOff demand (0) emits a zero setpoint, not absent', () => {
const out = getOutput(mockMgc({ _lastDemand: { canonical: 0, clamped: 0 } }));
assert.equal(out.demandFlow, 0);
assert.equal(out.demandPct, 0);
});
test('demand telemetry is absent before the first demand (degraded state)', () => {
const out = getOutput(mockMgc({ _lastDemand: null }));
assert.ok(!('demandFlow' in out), 'demandFlow must be absent pre-first-demand');
assert.ok(!('demandPct' in out), 'demandPct must be absent pre-first-demand');
// The always-on capacity fields are still present, converted to the output
// flow unit (m³/h): 0.05 m³/s → 180, 0.25 m³/s → 900.
assert.equal(out.flowCapacityMin, 180);
assert.equal(out.flowCapacityMax, 900);
});
test('flow capacity is emitted in the output unit (m³/h), matching the flow series', () => {
const out = getOutput(mockMgc({ dynamicTotals: { flow: { min: 0.1, max: 0.3 } } }));
assert.equal(out.flowCapacityMin, 360); // 0.1 m³/s × 3600
assert.equal(out.flowCapacityMax, 1080); // 0.3 m³/s × 3600
});
test('flow capacity falls back to 0 when the envelope is unresolved (Infinity)', () => {
// Pre-first-equalize: dynamicTotals seeds min=Infinity, max=0.
const out = getOutput(mockMgc({ dynamicTotals: { flow: { min: Infinity, max: 0 } } }));
assert.equal(out.flowCapacityMin, 0);
assert.equal(out.flowCapacityMax, 0);
});