import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { computeVaultId } from "../shared/sync-identity"; import { routeSourcePostSASFrame, runChirpAdoptionReceiver } from "./chirp-pair"; import { memoryTransportPair, runPairingResponderOpenFrame, type LocalIdentity, type PairingTransport, } from "../shared/chirp-handshake"; import { createPairingAttempt } from "../shared/chirp-pairing"; import { __setGgwaveModuleForTest } from "../shared/ggwave"; import { encodeChirpPayload } from "../shared/chirp-payload"; import { CHIRP_PAYLOAD_LENGTH, DEVICE_TYPE_EXTENSION, type ChirpPayload } from "../shared/sync-types"; const localIdentity: LocalIdentity = { deviceId: "source-device", deviceName: "Source", deviceType: "extension", longTermPublicKeyHex: "ab".repeat(32), signingPublicKeyHex: "cd".repeat(32), }; function channel(): PairingTransport & { sent: Uint8Array[]; close: ReturnType } { const sent: Uint8Array[] = []; return { sent, close: vi.fn(async () => {}), async send(msg) { sent.push(msg); }, async receive() { return new TextEncoder().encode(JSON.stringify({ type: "ADOPT_IDENTITY", device_id: "receiver-device", device_name: "Receiver", device_type: "extension", long_term_public_key_hex: "ef".repeat(32), })); }, }; } 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) { const bytes = encodeChirpPayload(injectedChirp); let 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; return async () => ({ sampleRate: 48000, async play() { /* no-op */ }, async pullSamples() { const out = pending; pending = new Float32Array(0); return out; }, async close() { /* no-op */ }, }); } describe("source-side ADOPT_REQUEST routing", () => { beforeEach(() => __setGgwaveModuleForTest(fakeGgwave())); afterEach(() => __setGgwaveModuleForTest(null)); it("T-A26: routes ADOPT_REQUEST to grantAdoption after SAS confirmation and consent", async () => { const rootKeyHex = "77".repeat(32); const ch = channel(); const completePairing = vi.fn(); const result = await routeSourcePostSASFrame( { sas: "895245", frame: { type: "ADOPT_REQUEST", device_id: "receiver-ephemeral", device_name: "Receiver", device_type: "extension", }, channel: ch, }, localIdentity, { confirmSAS: vi.fn(async () => true), completePairing, grantAdoptionConsent: vi.fn(async () => true), fetchAdoptionMaterial: vi.fn(async () => ({ rootKeyHex, vaultId: await computeVaultId(Uint8Array.from(Buffer.from(rootKeyHex, "hex"))), license: "PPK-test", })), onProgress: vi.fn(), }, ); const grant = JSON.parse(new TextDecoder().decode(ch.sent[0])); expect(result).toEqual(expect.objectContaining({ deviceId: "receiver-device" })); expect(grant).toMatchObject({ type: "ADOPT_GRANT", root_key_hex: rootKeyHex, vault_id: await computeVaultId(Uint8Array.from(Buffer.from(rootKeyHex, "hex"))), license: "PPK-test", }); expect(completePairing).toHaveBeenCalledWith(expect.objectContaining({ deviceId: "receiver-device" })); }); it("T-A27: routes PAIR_IDENTITY to the existing pairing path", async () => { const ch = channel(); const completePairing = vi.fn(); const result = await routeSourcePostSASFrame( { sas: "895245", frame: { type: "PAIR_IDENTITY", device_id: "phone", device_name: "Phone", device_type: "android", long_term_public_key_hex: "aa".repeat(32), }, channel: ch, }, localIdentity, { confirmSAS: vi.fn(async () => true), completePairing, onProgress: vi.fn(), }, ); const response = JSON.parse(new TextDecoder().decode(ch.sent[0])); expect(response.type).toBe("PAIR_IDENTITY"); expect(result).toEqual(expect.objectContaining({ deviceId: "phone" })); expect(completePairing).toHaveBeenCalledWith(expect.objectContaining({ deviceId: "phone" })); }); it("T-A28: unknown frame closes the channel gracefully", async () => { const ch = channel(); await expect(routeSourcePostSASFrame( { sas: "895245", frame: { type: "WHAT_IS_THIS" }, channel: ch }, localIdentity, { confirmSAS: vi.fn(async () => true), completePairing: vi.fn(), onProgress: vi.fn(), }, )).rejects.toThrow("unexpected_pairing_frame"); expect(ch.close).toHaveBeenCalledTimes(1); }); it("T-A29: source root key bytes are zeroed after grantAdoption regardless of outcome", async () => { const fillSpy = vi.spyOn(Uint8Array.prototype, "fill"); await routeSourcePostSASFrame( { sas: "895245", frame: { type: "ADOPT_REQUEST", device_id: "receiver-ephemeral", device_name: "Receiver", device_type: "extension", }, channel: channel(), }, localIdentity, { confirmSAS: vi.fn(async () => true), completePairing: vi.fn(), grantAdoptionConsent: vi.fn(async () => true), fetchAdoptionMaterial: vi.fn(async () => ({ rootKeyHex: "88".repeat(32), vaultId: await computeVaultId(Uint8Array.from(Buffer.from("88".repeat(32), "hex"))), })), onProgress: vi.fn(), }, ); expect(fillSpy).toHaveBeenCalledWith(0); }); it("T-A33: receiver onboarding orchestrator sends ADOPT_REQUEST and persists the granted source", async () => { const sourceAttempt = createPairingAttempt(DEVICE_TYPE_EXTENSION); const [receiverTransport, sourceTransport] = memoryTransportPair(); const rootKeyHex = "99".repeat(32); const vaultId = await computeVaultId(Uint8Array.from(Buffer.from(rootKeyHex, "hex"))); const receiverUpsert = vi.fn(); const sourceUpsert = vi.fn(); const source = (async () => { const postNoise = await runPairingResponderOpenFrame( sourceAttempt.ephemeralPrivateKey, sourceAttempt.ephemeralPublicKey, sourceTransport, ); return routeSourcePostSASFrame(postNoise, localIdentity, { confirmSAS: vi.fn(async () => true), completePairing: sourceUpsert, grantAdoptionConsent: vi.fn(async () => true), fetchAdoptionMaterial: vi.fn(async () => ({ rootKeyHex, vaultId })), onProgress: vi.fn(), }); })(); const receiver = await runChirpAdoptionReceiver({ deviceName: "Receiver", deviceType: "extension", openAudio: fakeAudio(sourceAttempt.outgoingPayload), openPairingServer: vi.fn(async () => ({ transport: { ...receiverTransport, close: vi.fn(async () => {}) }, peerIp: "127.0.0.1", })), confirmSAS: vi.fn(async () => true), createVaultFromRootKey: vi.fn(async (adoptedRootKeyHex) => { expect(adoptedRootKeyHex).toBe(rootKeyHex); return { vaultId }; }), deriveLocalIdentity: vi.fn(async () => ({ deviceId: "receiver-device", deviceName: "Receiver", deviceType: "extension" as const, longTermPublicKeyHex: "ef".repeat(32), signingPublicKeyHex: "12".repeat(32), })), completePairing: receiverUpsert, onProgress: vi.fn(), }); await source; expect(receiver).toEqual(expect.objectContaining({ deviceId: "source-device" })); expect(receiverUpsert).toHaveBeenCalledWith(expect.objectContaining({ deviceId: "source-device" })); expect(sourceUpsert).toHaveBeenCalledWith(expect.objectContaining({ deviceId: "receiver-device" })); }); });