Finding 1: revert the comma/quote strip added in 3cb998c from
normaliseCal() in calibration.js. The strip broke byte-identical parity
with Go CalConfig.Normalise and C++ StreamHub::SetCalibrationEntry, both
of which only trim whitespace and cap at 16 UTF-8 bytes. Delete the
companion test that asserted the now-removed behaviour (suite returns to
18 tests).
Finding 2: fix the actual CSV-safety problem at the point of use in
exportAllCSV() in app.js. Header cells (time column and signal columns)
are now RFC 4180-quoted: wrapped in double quotes with any embedded
double quote doubled. This safely handles units or signal names that
contain commas or quotes without touching normaliseCal.
Finding 3: update two stale comments in app.js that called the trigger
threshold or rawFromNorm result 'raw' — Task 9 moved trig.threshold into
calibrated units throughout, so the comments now say 'calibrated'.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
153 lines
6.9 KiB
JavaScript
153 lines
6.9 KiB
JavaScript
const test = require('node:test');
|
||
const assert = require('node:assert');
|
||
const C = require('../static/calibration.js');
|
||
|
||
test('baseSignalName strips an element suffix', () => {
|
||
assert.strictEqual(C.baseSignalName('Adc'), 'Adc');
|
||
assert.strictEqual(C.baseSignalName('Adc[3]'), 'Adc');
|
||
assert.strictEqual(C.baseSignalName('Adc[12]'), 'Adc');
|
||
assert.strictEqual(C.baseSignalName('A[1]B'), 'A[1]B');
|
||
assert.strictEqual(C.baseSignalName(''), '');
|
||
});
|
||
|
||
test('calKey is stable and separates the two fields', () => {
|
||
assert.strictEqual(C.calKey('a', 'b'), C.calKey('a', 'b'));
|
||
assert.notStrictEqual(C.calKey('ab', 'c'), C.calKey('a', 'bc'));
|
||
});
|
||
|
||
test('normaliseCal accepts a valid entry and fills defaults', () => {
|
||
assert.deepStrictEqual(
|
||
C.normaliseCal({source: ' wave ', signal: ' Adc ', scale: 2, offset: -1, unit: ' V '}),
|
||
{source: 'wave', signal: 'Adc', scale: 2, offset: -1, unit: 'V'});
|
||
assert.deepStrictEqual(
|
||
C.normaliseCal({source: 'wave', signal: 'Adc'}),
|
||
{source: 'wave', signal: 'Adc', scale: 1, offset: 0, unit: ''});
|
||
});
|
||
|
||
test('normaliseCal strips an element suffix from the signal name', () => {
|
||
assert.strictEqual(C.normaliseCal({source: 'w', signal: 'Adc[3]'}).signal, 'Adc');
|
||
});
|
||
|
||
test('normaliseCal truncates an over-long unit', () => {
|
||
const long = 'abcdefghijklmnopqrstuvwxyz';
|
||
assert.strictEqual(C.normaliseCal({source: 'w', signal: 's', unit: long}).unit,
|
||
long.slice(0, C.MAX_UNIT_LEN));
|
||
});
|
||
|
||
test('normaliseCal leaves short non-ASCII units untouched', () => {
|
||
// 'Ω' is U+03A9, 2 UTF-8 bytes — well within 16 bytes.
|
||
assert.strictEqual(C.normaliseCal({source: 'w', signal: 's', unit: 'Ω'}).unit, 'Ω');
|
||
// 'µs' is U+00B5 + U+0073, 3 UTF-8 bytes.
|
||
assert.strictEqual(C.normaliseCal({source: 'w', signal: 's', unit: 'µs'}).unit, 'µs');
|
||
// '°C' is U+00B0 + U+0043, 3 UTF-8 bytes.
|
||
assert.strictEqual(C.normaliseCal({source: 'w', signal: 's', unit: '°C'}).unit, '°C');
|
||
});
|
||
|
||
test('normaliseCal truncates an over-long ASCII unit to exactly 16 bytes', () => {
|
||
// 20 ASCII characters — each 1 byte, so cut at character 16.
|
||
const long = 'abcdefghijklmnopqrst'; // 20 chars
|
||
const result = C.normaliseCal({source: 'w', signal: 's', unit: long}).unit;
|
||
assert.strictEqual(result, 'abcdefghijklmnop'); // first 16 bytes/chars
|
||
assert.strictEqual(new TextEncoder().encode(result).length, 16);
|
||
});
|
||
|
||
test('normaliseCal cuts a mid-rune byte boundary back to the last complete rune', () => {
|
||
// Each 'Ω' (U+03A9) is 2 UTF-8 bytes (CE A9).
|
||
// 8 × 'Ω' = 16 bytes exactly — fits without truncation.
|
||
const fits = 'ΩΩΩΩΩΩΩΩ';
|
||
assert.strictEqual(new TextEncoder().encode(fits).length, 16);
|
||
assert.strictEqual(C.normaliseCal({source: 'w', signal: 's', unit: fits}).unit, fits);
|
||
|
||
// 9 × 'Ω' = 18 bytes. Slicing at 16 bytes lands in the middle of the 9th
|
||
// 'Ω' (only 1 of its 2 bytes is in the window) so only 8 'Ω' should survive.
|
||
// No U+FFFD replacement character must appear.
|
||
const toolong = 'ΩΩΩΩΩΩΩΩΩ';
|
||
const result = C.normaliseCal({source: 'w', signal: 's', unit: toolong}).unit;
|
||
assert.strictEqual(result, 'ΩΩΩΩΩΩΩΩ');
|
||
assert.ok(!result.includes('\uFFFD'), 'must not contain U+FFFD replacement character');
|
||
assert.strictEqual(new TextEncoder().encode(result).length, 16);
|
||
});
|
||
|
||
test('normaliseCal leaves a unit that is exactly 16 bytes ending on a complete multi-byte rune untouched', () => {
|
||
// 'abcdefgΩhijklµ' → 7 ASCII + 'Ω' (2 bytes) + 5 ASCII + 'µ' (2 bytes) = 16 bytes
|
||
const u = 'abcdefgΩhijklµ';
|
||
assert.strictEqual(new TextEncoder().encode(u).length, 16);
|
||
assert.strictEqual(C.normaliseCal({source: 'w', signal: 's', unit: u}).unit, u);
|
||
});
|
||
|
||
test('normaliseCal rejects invalid entries', () => {
|
||
assert.strictEqual(C.normaliseCal(null), null);
|
||
assert.strictEqual(C.normaliseCal({signal: 's'}), null);
|
||
assert.strictEqual(C.normaliseCal({source: 'w'}), null);
|
||
assert.strictEqual(C.normaliseCal({source: ' ', signal: 's'}), null);
|
||
assert.strictEqual(C.normaliseCal({source: 'w', signal: 's', scale: 0}), null);
|
||
assert.strictEqual(C.normaliseCal({source: 'w', signal: 's', scale: NaN}), null);
|
||
assert.strictEqual(C.normaliseCal({source: 'w', signal: 's', scale: Infinity}), null);
|
||
assert.strictEqual(C.normaliseCal({source: 'w', signal: 's', offset: NaN}), null);
|
||
assert.strictEqual(C.normaliseCal({source: 'w', signal: 's', scale: '2'}), null);
|
||
});
|
||
|
||
test('applyCal and invertCal round-trip', () => {
|
||
const cal = {scale: 0.5, offset: -1.25, unit: 'V'};
|
||
assert.strictEqual(C.applyCal(10, cal), 3.75);
|
||
assert.strictEqual(C.invertCal(3.75, cal), 10);
|
||
assert.strictEqual(C.applyCal(7, C.IDENTITY), 7);
|
||
assert.strictEqual(C.invertCal(7, C.IDENTITY), 7);
|
||
});
|
||
|
||
test('applyCal passes non-finite samples through untouched', () => {
|
||
assert.ok(Number.isNaN(C.applyCal(NaN, {scale: 2, offset: 1, unit: ''})));
|
||
});
|
||
|
||
test('calRange re-orders when the scale is negative', () => {
|
||
assert.deepStrictEqual(C.calRange(0, 10, {scale: 2, offset: 1, unit: ''}), [1, 21]);
|
||
assert.deepStrictEqual(C.calRange(0, 10, {scale: -2, offset: 1, unit: ''}), [-19, 1]);
|
||
});
|
||
|
||
test('CalTable.get returns IDENTITY for an unknown signal', () => {
|
||
const t = new C.CalTable();
|
||
assert.deepStrictEqual(t.get('w', 'Adc'), C.IDENTITY);
|
||
});
|
||
|
||
test('CalTable.get resolves an element name to its base signal', () => {
|
||
const t = new C.CalTable();
|
||
t.set({source: 'w', signal: 'Adc', scale: 3, offset: 0, unit: ''});
|
||
assert.strictEqual(t.get('w', 'Adc[7]').scale, 3);
|
||
});
|
||
|
||
test('CalTable.set stores, overwrites, and deletes identity entries', () => {
|
||
const t = new C.CalTable();
|
||
assert.strictEqual(t.set({source: 'w', signal: 'Adc', scale: 2}), true);
|
||
assert.strictEqual(t.get('w', 'Adc').scale, 2);
|
||
t.set({source: 'w', signal: 'Adc', scale: 5});
|
||
assert.strictEqual(t.get('w', 'Adc').scale, 5);
|
||
assert.strictEqual(t.list().length, 1);
|
||
// Resetting to identity removes the entry entirely.
|
||
assert.strictEqual(t.set({source: 'w', signal: 'Adc', scale: 1, offset: 0, unit: ''}), true);
|
||
assert.strictEqual(t.list().length, 0);
|
||
// An invalid entry is refused and changes nothing.
|
||
assert.strictEqual(t.set({source: 'w', signal: 'Adc', scale: 0}), false);
|
||
assert.strictEqual(t.list().length, 0);
|
||
});
|
||
|
||
test('CalTable.replaceAll drops the previous contents', () => {
|
||
const t = new C.CalTable();
|
||
t.set({source: 'w', signal: 'Old', scale: 2});
|
||
t.replaceAll([
|
||
{source: 'w', signal: 'B', scale: 2},
|
||
{source: 'w', signal: 'A', scale: 3},
|
||
{source: 'w', signal: 'Bad', scale: 0},
|
||
{source: 'w', signal: 'Ident', scale: 1, offset: 0, unit: ''},
|
||
]);
|
||
assert.deepStrictEqual(t.list().map(e => e.signal), ['A', 'B']);
|
||
});
|
||
|
||
test('CalTable.list is sorted by source then signal', () => {
|
||
const t = new C.CalTable();
|
||
t.set({source: 'z', signal: 'a', scale: 2});
|
||
t.set({source: 'a', signal: 'z', scale: 2});
|
||
t.set({source: 'a', signal: 'b', scale: 2});
|
||
assert.deepStrictEqual(t.list().map(e => e.source + '/' + e.signal),
|
||
['a/b', 'a/z', 'z/a']);
|
||
});
|