fix(calibration.js): cap unit at 16 UTF-8 bytes, matching both hubs
normaliseCal was using String.length/.slice() (UTF-16 code units), so multi-byte characters like °, Ω, µ could slip through oversized. Now uses TextEncoder to slice at 16 bytes, then repairs any incomplete trailing UTF-8 sequence by walking back over continuation bytes to find the lead byte and dropping the incomplete rune — exactly mirroring Go's utf8.DecodeLastRuneInString loop and the C++ walk-back in StreamHub::SetCalibrationEntry. Adds 4 new test cases covering the non-ASCII/boundary scenarios, and corrects the canonical test command in the task-6 report to 'cd Client/udpstreamer && node --test'. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
21d084d2ea
commit
48d62c1f80
@@ -34,6 +34,52 @@ test('normaliseCal truncates an over-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', () => {
|
||||
// 7 ASCII chars + 'Ω' (2 bytes) + 6 ASCII chars + 'µ' (2 bytes) - 1 = let's
|
||||
// build exactly 16 bytes ending on a complete 2-byte rune.
|
||||
// 7 × 'a' (7 bytes) + 'Ω' (2 bytes) + 5 × 'b' (5 bytes) + '°' (2 bytes) =
|
||||
// 7 + 2 + 5 + 2 = 16 bytes.
|
||||
const exact = 'aaaaaaаbbbbb°'; // avoid confusion: use simple construction below
|
||||
// Simple: 'abcdefgΩhijklµ' → 7 + 2 + 5 + 2 = 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);
|
||||
|
||||
Reference in New Issue
Block a user