#!/usr/bin/env node /* zebra-report web protocol tests — run on the real code in web/chat.html, * no browser or second device needed: node test/web-protocol.test.js * * Pulls the pure protocol functions straight out of chat.html (so the tests * track the shipped code) and exercises them across three tiers: * unit crc32, frame build/parse (DATA/ACK/HELLO), Hamming, Gray * integration multi-level modem TX -> channel(+noise) -> RX roundtrip, * and FEC recovery of off-by-one symbol errors * functional full frame -> modem -> FEC -> demod -> FrameAssembler -> parse, * plus the delivery-ACK roundtrip * * The modem layer is configured for N=4 levels, FEC on (the shipped defaults). */ const fs = require('fs'); const path = require('path'); const src = fs.readFileSync(path.join(__dirname, '..', 'web', 'chat.html'), 'utf8'); function extract(re) { const m = src.match(re); if (!m) throw new Error('could not find ' + re + ' in chat.html'); let i = src.indexOf('{', m.index + m[0].length), depth = 0, j = i; for (; j < src.length; j++) { if (src[j] === '{') depth++; else if (src[j] === '}') { if (--depth === 0) { j++; break; } } } return src.slice(m.index, j); } const harness = ` /* ---- constants + helpers lifted from chat.html (N=4, FEC on) ---- */ const HS_MAGIC=[0x5A,0x42], T_OFFER=1, T_READY=2, T_DATA=3, T_HELLO=4, T_ACK=5, HS_FRAME_LEN=6; const ZEBRA_LEVELS=4, ZEBRA_BITS_PER_SYM=2, ZEBRA_CW_BITS=12, ZEBRA_SYMS_PER_CW=6, ZEBRA_FEC=true; const ZEBRA_VOL_MARK=0.95, ZEBRA_VOL_SPACE=0.10; const _HAM_DATA_POS=[3,5,6,7,9,10,11,12]; function ampForLevel(L){ return ZEBRA_VOL_SPACE + (L/(ZEBRA_LEVELS-1))*(ZEBRA_VOL_MARK-ZEBRA_VOL_SPACE); } async function senderIdBytes(){ return new Uint8Array([0xAA,0xBB,0xCC,0xDD]); } ${src.match(/const CRC32_TABLE = \(\(\) => \{[\s\S]*?return t;\s*\}\)\(\);/)[0]} ${extract(/function crc32\(/)} ${extract(/async function buildDataFrame\(/)} ${extract(/async function buildAckFrame\(/)} ${extract(/async function buildHelloFrame\(/)} ${extract(/function buildHandshakeFrame\(/)} ${extract(/function xorChecksum\(/)} ${extract(/function parseFrame\(/)} ${extract(/function frameCrcOf\(/)} ${extract(/function hammingEncode\(/)} ${extract(/function hammingDecode\(/)} const grayVal2Phys=new Array(ZEBRA_LEVELS), grayPhys2Val=new Array(ZEBRA_LEVELS); for(let L=0;L>1); grayPhys2Val[L]=v; grayVal2Phys[v]=L;} ${extract(/class MultiLevelDecoder\b/)} ${extract(/class FrameAssembler\b/)} /* ---- modem TX mirror of _txOne (FEC + Gray), and a sample-hold channel ---- */ const N1=ZEBRA_LEVELS-1, effRate=48000/128; function txSyms(bytes, perturb){ const s=[]; for(let i=0;i<10;i++) s.push(ZEBRA_VOL_MARK); for(const byte of bytes){ const cw=ZEBRA_FEC?hammingEncode(byte):byte; const lv=[0]; for(let j=0;j>(j*ZEBRA_BITS_PER_SYM))&N1]); lv.push(N1); if(perturb){ const k=1+(Math.random()*ZEBRA_SYMS_PER_CW|0); lv[k]=Math.max(0,Math.min(N1,lv[k]+(Math.random()<.5?1:-1))); } for(const L of lv) s.push(ampForLevel(L)); } for(let i=0;i<5;i++) s.push(ZEBRA_VOL_MARK); return s; } function demod(bytes, baud, perturb, noise){ const syms=txSyms(bytes,perturb), period=1/baud, out=[]; const dec=new MultiLevelDecoder(baud, effRate, ZEBRA_LEVELS); for(let t=0;t{ if(c) pass++; else { fail++; fails.push(m); } }; (async () => { /* ===== UNIT ===== */ ok(crc32(new Uint8Array([1,2,3]))===crc32(new Uint8Array([1,2,3])), 'crc32 deterministic'); ok(crc32(new Uint8Array([1,2,3]))!==crc32(new Uint8Array([1,2,4])), 'crc32 distinguishes'); const df=await buildDataFrame(new Uint8Array([9,8,7,6,5])); const pf=parseFrame(df); ok(pf.type===T_DATA && pf.payload.length===5 && pf.payload[0]===9, 'DATA frame build/parse'); ok(frameCrcOf(df)===pf.crc, 'frameCrcOf == parsed crc (message id)'); const ack=await buildAckFrame(pf.crc); const pa=parseFrame(ack); ok(pa.type===T_ACK && (pa.ackCrc>>>0)===(pf.crc>>>0), 'ACK echoes DATA crc'); const hf=await buildHelloFrame('fxhp', 20); const ph=parseFrame(hf); ok(ph.type===T_HELLO && ph.handle==='fxhp' && ph.maxBaud===20, 'HELLO frame build/parse'); const corrupt=Uint8Array.from(df); corrupt[5]^=0xFF; ok((parseFrame(corrupt)||{}).error==='crc', 'corrupt frame flagged by crc'); for(let b=0;b<256;b++){ ok(hammingDecode(hammingEncode(b))===b, 'hamming clean '+b); for(let p=0;p<12;p++) ok(hammingDecode(hammingEncode(b)^(1<=37, 'FEC recovers >=37/40 with one +/-1 symbol error per byte (got '+recv+')'); /* ===== FUNCTIONAL ===== */ // full frame -> modem -> FEC -> demod -> FrameAssembler -> parse const payload=new Uint8Array([1,2,3,4,5,6,7,8]); const frame=await buildDataFrame(payload); const decoded=demod(Array.from(frame),50,false,0); const asm=new FrameAssembler(); let got=null; for(const b of decoded){ const f=asm.push(b); if(f) got=f; } const gp=got?parseFrame(got):null; ok(gp && gp.type===T_DATA && Buffer.from(gp.payload).equals(Buffer.from(payload)), 'functional: frame survives modem+FEC+assembler'); // ACK roundtrip through the assembler const ackFrame=await buildAckFrame(gp.crc); const ad=demod(Array.from(ackFrame),50,false,0); const asm2=new FrameAssembler(); let agot=null; for(const b of ad){ const f=asm2.push(b); if(f) agot=f; } const ap=agot?parseFrame(agot):null; ok(ap && ap.type===T_ACK && (ap.ackCrc>>>0)===(gp.crc>>>0), 'functional: ACK matches sent frame crc'); console.log((fail?'':'\\u2713 ')+'zebra-report web protocol: '+pass+' passed, '+fail+' failed'); if(fail){ console.log(' failures:'); fails.slice(0,20).forEach(f=>console.log(' - '+f)); } globalThis.__fail=fail; })(); `; eval(harness); setTimeout(() => process.exit(globalThis.__fail ? 1 : 0), 200);