import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { runChirpPairing, ChirpPairingFailure } from "./chirp-pair"; import { __setGgwaveModuleForTest } from "../shared/ggwave"; import { encodeChirpPayload } from "../shared/chirp-payload"; import { generateKeyPair } from "@stablelib/x25519"; import { CHIRP_PAYLOAD_LENGTH, DEVICE_TYPE_ANDROID, type ChirpPayload, type PairingProgress, } from "../shared/sync-types"; function fakeGgwave() { return { 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() { return 1; }, free() {}, encode(_inst: number, data: Uint8Array | string) { const bytes = typeof data === "string" ? new TextEncoder().encode(data) : data; const out = new Float32Array(Math.max(bytes.length, 128)); for (let i = 0; i < bytes.length; i++) out[i] = bytes[i] / 255; for (let i = bytes.length; i < out.length; i++) out[i] = 99; return out; }, decode(_inst: number, samples: Float32Array | Int16Array) { if (samples.length < CHIRP_PAYLOAD_LENGTH) return null; const buf = new Uint8Array(CHIRP_PAYLOAD_LENGTH); for (let i = 0; i < CHIRP_PAYLOAD_LENGTH; i++) { const v = (samples as Float32Array)[i]; if (v === 99) return null; buf[i] = Math.round(v * 255); } return buf; }, }; } function fakeAudio(injectedChirp: ChirpPayload | null) { let pending: Float32Array; if (injectedChirp) { const bytes = encodeChirpPayload(injectedChirp); pending = new Float32Array(128); for (let i = 0; i < bytes.length; i++) pending[i] = bytes[i] / 255; for (let i = bytes.length; i < pending.length; i++) pending[i] = 99; } else { pending = new Float32Array(0); } return async () => ({ sampleRate: 48000, async play() { /* no-op */ }, async pullSamples() { const out = pending; pending = new Float32Array(0); return out; }, async close() { /* no-op */ }, }); } describe("runChirpPairing (popup orchestrator)", () => { beforeEach(() => __setGgwaveModuleForTest(fakeGgwave())); afterEach(() => __setGgwaveModuleForTest(null)); it("emits listening progress before audio I/O starts", async () => { const trail: PairingProgress[] = []; // Use a short-timeout chirp exchange by injecting no-chirp audio. // The orchestrator will time out, but we only assert on early progress. try { await runChirpPairing({ async fetchLocalIdentity() { return { deviceId: "ext", deviceName: "Firefox", deviceType: "extension", longTermPublicKeyHex: "0".repeat(64) }; }, async completePairing() { /* unreached */ }, async getLocalSubnet() { return "192.168.1"; }, chirpTimeoutMs: 200, openAudio: fakeAudio(null), async openPairingTransport() { throw new Error("unreached"); }, onProgress(p) { trail.push(p); }, }); } catch { /* expected timeout */ } expect(trail[0].phase).toBe("idle"); expect(trail[1].phase).toBe("listening"); }); it("emits handshake then handshake_failed when no peer responds on the subnet", async () => { const peerEphemeral = generateKeyPair(); const peerChirp: ChirpPayload = { version: 0x01, deviceType: DEVICE_TYPE_ANDROID, ephemeralPublicKey: peerEphemeral.publicKey, sessionNonce: new Uint8Array(16).fill(3), }; const trail: PairingProgress[] = []; await expect( runChirpPairing({ async fetchLocalIdentity() { return { deviceId: "ext", deviceName: "Firefox", deviceType: "extension", longTermPublicKeyHex: "0".repeat(64) }; }, async completePairing() { /* unreached */ }, async getLocalSubnet() { return "192.168.1"; }, openAudio: fakeAudio(peerChirp), async openPairingTransport() { throw new Error("connection refused"); }, onProgress(p) { trail.push(p); }, }), ).rejects.toBeInstanceOf(ChirpPairingFailure); expect(trail.some((p) => p.phase === "handshake")).toBe(true); expect(trail.at(-1)?.phase).toBe("handshake_failed"); }, 30_000); it("returns handshake_failed when getLocalSubnet returns null", async () => { const peerEphemeral = generateKeyPair(); const peerChirp: ChirpPayload = { version: 0x01, deviceType: DEVICE_TYPE_ANDROID, ephemeralPublicKey: peerEphemeral.publicKey, sessionNonce: new Uint8Array(16).fill(2), }; const trail: PairingProgress[] = []; await expect( runChirpPairing({ async fetchLocalIdentity() { return { deviceId: "ext", deviceName: "Firefox", deviceType: "extension", longTermPublicKeyHex: "0".repeat(64) }; }, async completePairing() { /* unreached */ }, async getLocalSubnet() { return null; }, openAudio: fakeAudio(peerChirp), async openPairingTransport() { throw new Error("unreached"); }, onProgress(p) { trail.push(p); }, }), ).rejects.toMatchObject({ phase: "handshake_failed" }); expect(trail.at(-1)?.errorMessage).toBe("no_subnet"); }); it("rejects responder role with non-extension local identity", async () => { await expect( runChirpPairing({ async fetchLocalIdentity() { return { deviceId: "android", deviceName: "Pixel", deviceType: "android", longTermPublicKeyHex: "0".repeat(64) }; }, async completePairing() { /* unreached */ }, async getLocalSubnet() { return "192.168.1"; }, chirpTimeoutMs: 200, openAudio: fakeAudio(null), async openPairingTransport() { throw new Error("unreached"); }, onProgress() { /* noop */ }, }, "responder"), ).rejects.toThrow(/responder requires extension/); }); it("rejects initiator role with extension local identity", async () => { await expect( runChirpPairing({ async fetchLocalIdentity() { return { deviceId: "ext", deviceName: "Firefox", deviceType: "extension", longTermPublicKeyHex: "0".repeat(64) }; }, async completePairing() { /* unreached */ }, async getLocalSubnet() { return null; }, chirpTimeoutMs: 200, openAudio: fakeAudio(null), async openPairingTransport() { throw new Error("unreached"); }, async openPairingServer() { throw new Error("unreached"); }, onProgress() { /* noop */ }, }, "initiator"), ).rejects.toThrow(/initiator requires non-extension/); }); it("rejects initiator role when openPairingServer dep is missing", async () => { await expect( runChirpPairing({ async fetchLocalIdentity() { return { deviceId: "android", deviceName: "Pixel", deviceType: "android", longTermPublicKeyHex: "0".repeat(64) }; }, async completePairing() { /* unreached */ }, async getLocalSubnet() { return null; }, chirpTimeoutMs: 200, openAudio: fakeAudio(null), async openPairingTransport() { throw new Error("unreached"); }, onProgress() { /* noop */ }, }, "initiator"), ).rejects.toThrow(/openPairingServer/); }); });