B2.3 + P11.1 + P11.2 + monster schema fix

B2.3 LatestWinsGate fireAndWait:
  Added fireAndWait(value, ctx?) returning per-fire settlement promise.
  Supersede resolves with frozen sentinel {superseded: true} (no
  rejection — callers branch on value without try/catch). Dispatch
  errors also resolve (with undefined); error surfaces via gate.lastError.
  LatestWinsGate.js 75 → 116 lines. 12/12 tests pass.

P11.1 convert.possibilities(measure):
  New helper returning sorted+deduped unit names for a measure.
  Cached per measure. Reuses existing convert measures map. Also
  exposed convert.measures() listing all known measures.
  convert/index.js +21 lines. New test file: 90 lines, 12/12 tests.

P11.2 commandRegistry.units field:
  Pre-dispatch normalisation pipeline. descriptor.units = {measure,
  default}; commandRegistry extracts msg.payload + msg.unit (3 shapes),
  validates against measure, converts to default, falls back + warns
  with accepted-list on unknown/wrong-measure. Falls back gracefully
  if convert.possibilities is missing. commandRegistry.js 164 → 237.
  +7 new tests covering all 4 paths.

monster schema fix (P11.2 sibling):
  generalFunctions/src/configs/monster.json was stripping four
  legitimate constraint keys (nominalFlowMin, flowMax, maxRainRef,
  minSampleIntervalSec). Added them with defaults matching the
  legacy nodeClass coercion. Side effect: this also UNBLOCKED the
  monster cooldown-guard test (separate ROOT-CAUSE entry below).

CONTRACTS.md §4 + §8 updated. 144/144 basic tests + 206/206 full
generalFunctions tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
znetsixe
2026-05-11 17:29:14 +02:00
parent f11754635b
commit 5ea968eabc
8 changed files with 522 additions and 15 deletions

View File

@@ -0,0 +1,90 @@
'use strict';
const test = require('node:test');
const assert = require('node:assert/strict');
const convert = require('../../src/convert/index.js');
test('convert.possibilities — exported as a top-level function', () => {
assert.equal(typeof convert.possibilities, 'function');
});
test('convert.possibilities(volumeFlowRate) returns common flow units', () => {
const units = convert.possibilities('volumeFlowRate');
assert.ok(Array.isArray(units));
assert.ok(units.length > 0);
for (const u of ['m3/s', 'm3/h', 'l/s', 'l/min', 'l/h']) {
assert.ok(units.includes(u), `expected '${u}' in volumeFlowRate possibilities`);
}
});
test('convert.possibilities(pressure) returns common pressure units', () => {
const units = convert.possibilities('pressure');
for (const u of ['Pa', 'kPa', 'bar', 'mbar', 'psi']) {
assert.ok(units.includes(u), `expected '${u}' in pressure possibilities`);
}
});
test('convert.possibilities(power) returns common power units', () => {
const units = convert.possibilities('power');
for (const u of ['W', 'kW', 'MW']) {
assert.ok(units.includes(u), `expected '${u}' in power possibilities`);
}
});
test('convert.possibilities(temperature) returns K, C, F', () => {
const units = convert.possibilities('temperature');
for (const u of ['K', 'C', 'F']) {
assert.ok(units.includes(u), `expected '${u}' in temperature possibilities`);
}
});
test('convert.possibilities for length / mass / volume return non-empty', () => {
assert.ok(convert.possibilities('length').includes('m'));
assert.ok(convert.possibilities('mass').includes('kg'));
assert.ok(convert.possibilities('volume').includes('l'));
});
test('convert.possibilities(unknown) returns []', () => {
assert.deepEqual(convert.possibilities('foo'), []);
assert.deepEqual(convert.possibilities('bogus-measure'), []);
});
test('convert.possibilities handles invalid input safely', () => {
assert.deepEqual(convert.possibilities(), []);
assert.deepEqual(convert.possibilities(null), []);
assert.deepEqual(convert.possibilities(''), []);
assert.deepEqual(convert.possibilities(42), []);
});
test('convert.possibilities is sorted and deduplicated', () => {
const units = convert.possibilities('pressure');
const sorted = [...units].sort();
assert.deepEqual(units, sorted, 'result should be alphabetically sorted');
const set = new Set(units);
assert.equal(set.size, units.length, 'result should have no duplicates');
});
test('convert.possibilities returns stable / cached results across calls', () => {
const a = convert.possibilities('volumeFlowRate');
const b = convert.possibilities('volumeFlowRate');
assert.deepEqual(a, b, 'two calls must return equal arrays');
// Mutating the returned array must not poison the cache.
a.push('SHOULD_NOT_PERSIST');
const c = convert.possibilities('volumeFlowRate');
assert.ok(!c.includes('SHOULD_NOT_PERSIST'), 'cached array must be defensively copied');
assert.deepEqual(c, b);
});
test('convert.measures lists known measure names', () => {
const m = convert.measures();
assert.ok(Array.isArray(m));
for (const name of ['length', 'mass', 'volume', 'pressure', 'power', 'temperature', 'volumeFlowRate']) {
assert.ok(m.includes(name), `expected measure '${name}'`);
}
});
test('convert factory still works (regression — no breakage of existing API)', () => {
const result = convert(1).from('m').to('cm');
assert.equal(result, 100);
});