Viewing is open to everyone. Downloading and starring need a GitHub sign-in.
Sign in with GitHub
api-fake.test.mjs
v0.1.0-build.85
Latest
Commit ede38f5
Sign in with GitHub to download. Each download is recorded with your GitHub username.
| 1 | // Pure-JS tests of the wrapper's marshalling (js/api.mjs) against a fake |
| 2 | // Emscripten module that mimics the bridge's C contract: malloc'd results + |
| 3 | // ckw_last_len, NUL-terminated strings, u32/f64 out-parameters, and a heap |
| 4 | // that GROWS (replacing HEAPU8) so stale-view bugs show up. It also checks |
| 5 | // that every temporary allocation is freed. The real core is exercised by |
| 6 | // wasm.test.mjs (CI) and test/native/bridge_test.cpp. |
| 7 | import test from 'node:test'; |
| 8 | import assert from 'node:assert/strict'; |
| 9 | import { createCKey } from '../js/api.mjs'; |
| 10 | import { loadCKey } from '../js/ckey.mjs'; |
| 11 | |
| 12 | function fakeModule() { |
| 13 | const enc = new TextEncoder(); |
| 14 | const M = { HEAPU8: new Uint8Array(1024) }; |
| 15 | let top = 8; |
| 16 | const live = new Map(); // ptr -> size |
| 17 | const symbols = new Map(); // ptr -> { payload, version, level, layout } |
| 18 | const styles = new Map(); |
| 19 | let lastLen = 0; |
| 20 | let errPtr = 0; |
| 21 | const tel = { mode: 1, client: null, events: [], batchId: 0, inflight: 0 }; |
| 22 | |
| 23 | M._ckw_malloc = (n) => { |
| 24 | n = Math.max(1, n); |
| 25 | if (top + n > M.HEAPU8.length) { |
| 26 | const grown = new Uint8Array(Math.max(M.HEAPU8.length * 2, top + n + 1024)); |
| 27 | grown.set(M.HEAPU8); |
| 28 | M.HEAPU8 = grown; // views on the old buffer are now stale |
| 29 | } |
| 30 | const p = top; |
| 31 | top += (n + 7) & ~7; |
| 32 | live.set(p, n); |
| 33 | return p; |
| 34 | }; |
| 35 | M._ckw_free = (p) => { |
| 36 | if (p) { |
| 37 | assert.ok(live.has(p), `free of unknown pointer ${p}`); |
| 38 | live.delete(p); |
| 39 | } |
| 40 | }; |
| 41 | const putBytes = (bytes) => { |
| 42 | const p = M._ckw_malloc(bytes.length); |
| 43 | M.HEAPU8.set(bytes, p); |
| 44 | lastLen = bytes.length; |
| 45 | return p; |
| 46 | }; |
| 47 | const putCString = (s, track = true) => { |
| 48 | const b = enc.encode(s); |
| 49 | const p = M._ckw_malloc(b.length + 1); |
| 50 | M.HEAPU8.set(b, p); |
| 51 | M.HEAPU8[p + b.length] = 0; |
| 52 | if (track) lastLen = b.length; |
| 53 | return p; |
| 54 | }; |
| 55 | const readCString = (p) => { |
| 56 | let e = p; |
| 57 | while (M.HEAPU8[e]) e++; |
| 58 | return new TextDecoder().decode(M.HEAPU8.subarray(p, e)); |
| 59 | }; |
| 60 | const setError = (s) => { |
| 61 | if (errPtr) M._ckw_free(errPtr); |
| 62 | errPtr = putCString(s, false); |
| 63 | }; |
| 64 | setError(''); |
| 65 | const u32 = (p, i, v) => new DataView(M.HEAPU8.buffer).setUint32(p + 4 * i, v, true); |
| 66 | const f64 = (p, i, v) => new DataView(M.HEAPU8.buffer).setFloat64(p + 8 * i, v, true); |
| 67 | |
| 68 | M._ckw_last_error = () => errPtr; |
| 69 | M._ckw_last_len = () => lastLen; |
| 70 | const versionPtr = putCString('0.1.0-build.63', false); |
| 71 | M._ckw_software_version = () => versionPtr; |
| 72 | M._ckw_abi_version = () => 4; |
| 73 | M._ckw_scalable_capacity = () => 100; |
| 74 | M._ckw_scalable_modules_across = (v) => 28 + v; |
| 75 | M._ckw_encode = (p, len, version, level, layout) => { |
| 76 | if (len > 100) return setError('payload exceeds the selected Profile 0 version and level'), 0; |
| 77 | const s = M._ckw_malloc(8192); |
| 78 | symbols.set(s, { payload: M.HEAPU8.slice(p, p + len), version: version || 3, level, layout }); |
| 79 | return s; |
| 80 | }; |
| 81 | M._ckw_symbol_free = (s) => { |
| 82 | assert.ok(symbols.delete(s), 'double symbol free'); |
| 83 | M._ckw_free(s); |
| 84 | }; |
| 85 | M._ckw_symbol_info = (s, out) => { |
| 86 | const x = symbols.get(s); |
| 87 | [x.version, x.level, x.layout, x.payload.length, 1].forEach((v, i) => u32(out, i, v)); |
| 88 | }; |
| 89 | M._ckw_style_new = (preset) => { |
| 90 | if (preset > 1) return setError('unknown style preset'), 0; |
| 91 | const p = M._ckw_malloc(64); |
| 92 | styles.set(p, { preset, rgb: {}, num: {}, logo: null }); |
| 93 | return p; |
| 94 | }; |
| 95 | M._ckw_style_free = (p) => (styles.delete(p), M._ckw_free(p)); |
| 96 | M._ckw_style_set_rgb = (p, f, r, g, b) => ((styles.get(p).rgb[f] = [r, g, b]), 1); |
| 97 | M._ckw_style_set_num = (p, f, v) => ((styles.get(p).num[f] = v), 1); |
| 98 | M._ckw_style_set_logo = (p, s) => ((styles.get(p).logo = readCString(s)), 1); |
| 99 | const refused = (ws) => ws && styles.get(ws).rgb[2]?.[0] > 200 && styles.get(ws).num[10] !== 2; |
| 100 | M._ckw_style_check = (s, ws) => (refused(ws) ? (setError('ink is 1.0:1; need 3:1'), 1) : (setError('style OK'), 0)); |
| 101 | M._ckw_render_raster = (s, ws, size, policy, transparent) => { |
| 102 | if (refused(ws)) return setError('ink is 1.0:1; need 3:1'), 0; |
| 103 | const px = new Uint8Array(size * size * 4).fill(255); |
| 104 | if (transparent) for (let i = 3; i < px.length; i += 4) px[i] = 0; |
| 105 | px[1] = [...symbols.keys()].indexOf(s); // the fake "symbol" the decoder recognises |
| 106 | px[2] = 0xc4; |
| 107 | return putBytes(px); |
| 108 | }; |
| 109 | M._ckw_encode_png_rgba = (p, w, h) => putBytes(Uint8Array.of(0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, w, h)); |
| 110 | M._ckw_render_svg = (s, ws, scale) => (refused(ws) ? (setError('ink is 1.0:1; need 3:1'), 0) : putCString(`<svg width="${31 * scale}"></svg>`)); |
| 111 | M._ckw_render_pdf = () => putBytes(enc.encode('%PDF-1.4\n')); |
| 112 | M._ckw_has_certify = () => 1; |
| 113 | M._ckw_certify = (s, ws, transparent, colours, n, px, w, h, ppm, blur, noise, glare, trials, out) => { |
| 114 | [transparent ? 0 : 1, 0, 1 | 32, 3, transparent ? 0 : 3, 21, 0.9, 0].forEach((v, i) => f64(out, i, v)); |
| 115 | setError(transparent && !n ? 'background unknown' : 'certified'); |
| 116 | return transparent && !n ? 1 : 0; |
| 117 | }; |
| 118 | const decode = (p, w, h, stride, info, channels) => { |
| 119 | if (channels === 4 && M.HEAPU8[p + 2] === 0xc4) { |
| 120 | const s = [...symbols.keys()][M.HEAPU8[p + 1]]; |
| 121 | const x = symbols.get(s); |
| 122 | [x.layout, x.version, x.layout, 1].forEach((v, i) => u32(info, i, v)); |
| 123 | return putBytes(x.payload); |
| 124 | } |
| 125 | setError('no CKey border found'); |
| 126 | return 0; |
| 127 | }; |
| 128 | M._ckw_decode_rgba = (p, w, h, s, info) => decode(p, w, h, s, info, 4); |
| 129 | M._ckw_decode_gray = (p, w, h, s, info) => decode(p, w, h, s, info, 1); |
| 130 | M._ckw_tel_set_mode = (m) => (m >= 0 && m <= 2 ? ((tel.mode = m), 0) : 1); |
| 131 | M._ckw_tel_get_mode = () => tel.mode; |
| 132 | M._ckw_tel_set_client = (a, b, c) => ((tel.client = [readCString(a), readCString(b), readCString(c)]), 0); |
| 133 | M._ckw_tel_session_begin = (t) => tel.events.push(['begin', t]); |
| 134 | M._ckw_tel_frame = (t, ms) => tel.events.push(['frame', t, ms]); |
| 135 | M._ckw_tel_session_success = (t, code) => (tel.events.push(['success', t, code]), 0); |
| 136 | M._ckw_tel_session_end = (t) => tel.events.push(['end', t]); |
| 137 | M._ckw_tel_should_send = () => (tel.mode === 1 && !tel.inflight ? 1 : 0); |
| 138 | M._ckw_tel_take_batch = (day) => { |
| 139 | tel.inflight = ++tel.batchId; |
| 140 | return putCString(`{"schema":1,"sdk":"web","day":"${readCString(day)}"}`); |
| 141 | }; |
| 142 | M._ckw_tel_last_batch_id = () => tel.batchId; |
| 143 | M._ckw_tel_batch_result = (id, status) => (id === tel.inflight ? ((tel.inflight = 0), (tel.status = status), 0) : 1); |
| 144 | M._ckw_tel_snapshot = (p) => { |
| 145 | for (let i = 0; i < 33; i++) f64(p, i, i); |
| 146 | return 0; |
| 147 | }; |
| 148 | M._ckw_tel_reset_for_testing = () => {}; |
| 149 | return { M, live, symbols, tel, base: () => new Set([errPtr, versionPtr]) }; |
| 150 | } |
| 151 | |
| 152 | const leaks = (f) => [...f.live.keys()].filter((p) => !f.base().has(p) && !f.symbols.has(p)); |
| 153 | |
| 154 | test('encode, render, decode marshalling with a growing heap and no leaks', () => { |
| 155 | const f = fakeModule(); |
| 156 | const ckey = createCKey(f.M, { fetch: null }); |
| 157 | assert.equal(ckey.version, '0.1.0-build.63'); |
| 158 | assert.deepEqual(f.tel.client, ['web', '0.1.0-build.63', 'web']); |
| 159 | const sym = ckey.encode('héllo', { level: 'Q', layout: 'print' }); |
| 160 | assert.deepEqual( |
| 161 | { v: sym.version, l: sym.level, y: sym.layout, n: sym.payloadLength, c: sym.capacity, m: sym.modules }, |
| 162 | { v: 3, l: 'Q', y: 'print', n: 6, c: 100, m: 31 }, |
| 163 | ); |
| 164 | const img = ckey.renderPixels(sym, { size: 64, transparent: true }); // forces heap growth |
| 165 | assert.equal(img.data.length, 64 * 64 * 4); |
| 166 | assert.equal(img.data[7], 0); |
| 167 | const r = ckey.decodeImageData(img); |
| 168 | assert.equal(r.text, 'héllo'); |
| 169 | assert.equal(r.profile, '0.1'); |
| 170 | assert.equal(r.layout, 'print'); |
| 171 | assert.equal(r.version, 3); |
| 172 | assert.equal('profileCode' in r, false); |
| 173 | assert.equal(ckey.decodeImageData({ width: 2, height: 2, data: new Uint8ClampedArray(16) }), null); |
| 174 | assert.equal(ckey.decodeGray({ width: 2, height: 2, data: new Uint8Array(4) }), null); |
| 175 | assert.deepEqual([...ckey.renderPNG(sym, { size: 32 }).subarray(0, 4)], [0x89, 0x50, 0x4e, 0x47]); |
| 176 | assert.equal(ckey.renderSVG(sym, { scale: 2 }), '<svg width="62"></svg>'); |
| 177 | assert.equal(new TextDecoder().decode(ckey.renderPDF(sym)).slice(0, 5), '%PDF-'); |
| 178 | assert.throws(() => ckey.encode('x'.repeat(101)), /payload exceeds/); |
| 179 | assert.throws(() => ckey.renderSVG(sym, { style: { ink: '#fafafa' } }), /need 3:1/); |
| 180 | assert.ok(ckey.renderSVG(sym, { style: { ink: '#fafafa', policy: 'override' } })); |
| 181 | assert.deepEqual(ckey.checkStyle(sym, { preset: 'concept' }), { ok: true, message: 'style OK' }); |
| 182 | const cert = ckey.certify(sym, { backdrop: { colours: ['#000'] } }); |
| 183 | assert.equal(cert.certified, true); |
| 184 | assert.deepEqual(cert.adjustments, ['ink', 'materials']); |
| 185 | assert.equal(cert.policy, 'strict'); |
| 186 | assert.equal(ckey.certify(sym, { transparent: true }).ok, false); |
| 187 | assert.deepEqual(leaks(f).filter((p) => f.live.get(p) !== 64 * 64 * 4), [], 'only the frame scratch buffer stays'); |
| 188 | sym.free(); |
| 189 | sym.free(); |
| 190 | assert.equal(sym.freed, true); |
| 191 | assert.throws(() => ckey.renderSVG(sym), /freed/); |
| 192 | assert.throws(() => ckey.renderSVG({}), TypeError); |
| 193 | }); |
| 194 | |
| 195 | test('telemetry property, counters and scanner wiring', async () => { |
| 196 | const f = fakeModule(); |
| 197 | let clock = 0; |
| 198 | const sent = []; |
| 199 | const ckey = createCKey(f.M, { fetch: async (u, init) => (sent.push(init.body), { status: 204 }), now: () => (clock += 10), day: () => '2026-01-02' }); |
| 200 | assert.equal(ckey.telemetry, 1); |
| 201 | assert.throws(() => (ckey.telemetry = 5), RangeError); |
| 202 | assert.equal(ckey.telemetry, 1); |
| 203 | ckey.telemetry = 2; |
| 204 | assert.equal(f.tel.mode, 2); |
| 205 | ckey.telemetry = 1; |
| 206 | const sym = ckey.encode('scan me'); |
| 207 | const frame = ckey.renderPixels(sym, { size: 16 }); |
| 208 | const got = []; |
| 209 | const sc = ckey.createScanner({ onResult: (r) => got.push(r.text) }); |
| 210 | sc.processFrame(frame); |
| 211 | sc.stop(); |
| 212 | assert.deepEqual(f.tel.events.map((e) => e[0]), ['begin', 'frame', 'success', 'end']); |
| 213 | assert.deepEqual(got, ['scan me']); |
| 214 | assert.equal(await ckey.flushTelemetry(), 204); |
| 215 | assert.deepEqual(sent, ['{"schema":1,"sdk":"web","day":"2026-01-02"}']); |
| 216 | assert.equal(f.tel.status, 204); |
| 217 | const c = ckey.telemetryCounters(); |
| 218 | assert.deepEqual(c.sessions, { started: 0, succeeded: 1, failed: 2 }); |
| 219 | assert.deepEqual(c.time_to_decode_ms.buckets, [11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); |
| 220 | assert.equal(c.frame_ms.count, 21); |
| 221 | ckey._testing.sender.disarm(); |
| 222 | assert.throws(() => createCKey(fakeModule().M, { telemetry: '0' }), RangeError); |
| 223 | assert.equal(createCKey(fakeModule().M, { telemetry: 0 }).telemetry, 0); |
| 224 | }); |
| 225 | |
| 226 | test('loadCKey with an injected factory and wasm binary', async () => { |
| 227 | const f = fakeModule(); |
| 228 | let args; |
| 229 | const ckey = await loadCKey({ |
| 230 | moduleFactory: async (a) => ((args = a), f.M), |
| 231 | wasmBinary: new Uint8Array([0, 97, 115, 109]), |
| 232 | wasmUrl: 'https://cdn.example/ckey/ckey.wasm', |
| 233 | fetch: null, |
| 234 | }); |
| 235 | assert.equal(args.locateFile('ckey-core.wasm'), 'https://cdn.example/ckey/ckey.wasm'); |
| 236 | assert.equal(args.locateFile('other.js'), 'other.js'); |
| 237 | assert.ok(args.wasmBinary instanceof Uint8Array); |
| 238 | assert.equal(ckey.classifyPayload('https://openckey.org').label, 'Open openckey.org'); |
| 239 | assert.throws(() => loadCKey(5), TypeError); |
| 240 | }); |