import { describe, expect, it, beforeEach, afterEach } from "vitest"; import { CHIRP_PAYLOAD_LENGTH } from "./sync-types"; import { __setGgwaveModuleForTest, createChirpCodec, GgwaveProtocol, loadGgwave, } from "./ggwave"; function fakeGgwaveModule() { const initParams: unknown[] = []; const encodeCalls: Array<{ payload: Uint8Array; protocol: number; volume: number }> = []; const decodeCalls: Float32Array[] = []; let nextInstance = 1; const freed: number[] = []; return { initParams, encodeCalls, decodeCalls, freed, mod: { ProtocolId: { GGWAVE_PROTOCOL_ULTRASOUND_FASTEST: 5 }, GGWAVE_OPERATING_MODE_RX: 1, GGWAVE_OPERATING_MODE_TX: 2, GGWAVE_OPERATING_MODE_RX_AND_TX: 3, getDefaultParameters() { return { sampleRateInp: 48000, sampleRateOut: 48000, samplesPerFrame: 1024, soundMarkerThreshold: 3, payloadLength: -1, operatingMode: 0, }; }, init(p: unknown) { initParams.push(p); return nextInstance++; }, free(instance: number) { freed.push(instance); }, encode(_inst: number, data: Uint8Array | string, protocol: number, volume: number) { const payload = typeof data === "string" ? new TextEncoder().encode(data) : data; encodeCalls.push({ payload, protocol, volume }); return new Float32Array(payload.length * 100); // synthetic waveform }, decode(_inst: number, samples: Float32Array | Int16Array) { decodeCalls.push(samples as Float32Array); // half the time, "succeed"; tests inject deterministic samples if (samples.length === 999) return null; return new Uint8Array(CHIRP_PAYLOAD_LENGTH); // empty-but-correct-length }, }, }; } describe("ggwave loader", () => { beforeEach(() => __setGgwaveModuleForTest(null)); afterEach(() => __setGgwaveModuleForTest(null)); it("loadGgwave returns the test-injected module", async () => { const fake = fakeGgwaveModule(); __setGgwaveModuleForTest(fake.mod); const mod = await loadGgwave(); expect(mod).toBe(fake.mod); }); it("createChirpCodec configures sample rate, payload length, RX+TX mode", async () => { const fake = fakeGgwaveModule(); __setGgwaveModuleForTest(fake.mod); await createChirpCodec({ sampleRate: 44100 }); expect(fake.initParams).toHaveLength(1); const p = fake.initParams[0] as Record; expect(p.sampleRateInp).toBe(44100); expect(p.sampleRateOut).toBe(44100); expect(p.payloadLength).toBe(CHIRP_PAYLOAD_LENGTH); expect(p.operatingMode).toBe(3); // RX_AND_TX }); it("encode forwards the payload to ggwave with ultrasonic-fastest protocol and 10% volume by default", async () => { const fake = fakeGgwaveModule(); __setGgwaveModuleForTest(fake.mod); const codec = await createChirpCodec({ sampleRate: 48000 }); const payload = new Uint8Array(CHIRP_PAYLOAD_LENGTH); payload[0] = 0x50; const wave = codec.encode(payload); expect(wave.length).toBeGreaterThan(0); expect(fake.encodeCalls).toHaveLength(1); expect(fake.encodeCalls[0].payload).toEqual(payload); expect(fake.encodeCalls[0].protocol).toBe(GgwaveProtocol.ULTRASOUND_FASTEST); expect(fake.encodeCalls[0].volume).toBe(10); }); it("encode rejects wrong-length payload", async () => { const fake = fakeGgwaveModule(); __setGgwaveModuleForTest(fake.mod); const codec = await createChirpCodec({ sampleRate: 48000 }); expect(() => codec.encode(new Uint8Array(63))).toThrow(); }); it("decode forwards samples and returns the payload bytes", async () => { const fake = fakeGgwaveModule(); __setGgwaveModuleForTest(fake.mod); const codec = await createChirpCodec({ sampleRate: 48000 }); const samples = new Float32Array(2048); const decoded = codec.decode(samples); expect(decoded).toBeInstanceOf(Uint8Array); expect(decoded!.length).toBe(CHIRP_PAYLOAD_LENGTH); }); it("decode returns null when ggwave returns null (no chirp detected)", async () => { const fake = fakeGgwaveModule(); __setGgwaveModuleForTest(fake.mod); const codec = await createChirpCodec({ sampleRate: 48000 }); const samples = new Float32Array(999); expect(codec.decode(samples)).toBeNull(); }); it("free is idempotent and prevents further encode/decode", async () => { const fake = fakeGgwaveModule(); __setGgwaveModuleForTest(fake.mod); const codec = await createChirpCodec({ sampleRate: 48000 }); codec.free(); codec.free(); expect(fake.freed).toEqual([1]); expect(() => codec.encode(new Uint8Array(CHIRP_PAYLOAD_LENGTH))).toThrow(); expect(() => codec.decode(new Float32Array(0))).toThrow(); }); });