1import { describe, expect, it, vi } from "vitest";2import { computeVaultId } from "../shared/sync-identity";3import { routeSourcePostSASFrame } from "./chirp-pair";4import type { LocalIdentity, PairingTransport } from "../shared/chirp-handshake";56const localIdentity: LocalIdentity = {7 deviceId: "source-device",8 deviceName: "Source",9 deviceType: "extension",10 longTermPublicKeyHex: "ab".repeat(32),11 signingPublicKeyHex: "cd".repeat(32),12};1314function channel(): PairingTransport & { sent: Uint8Array[]; close: ReturnType<typeof vi.fn> } {15 const sent: Uint8Array[] = [];16 return {17 sent,18 close: vi.fn(async () => {}),19 async send(msg) { sent.push(msg); },20 async receive() {21 return new TextEncoder().encode(JSON.stringify({22 type: "ADOPT_IDENTITY",23 device_id: "receiver-device",24 device_name: "Receiver",25 device_type: "extension",26 long_term_public_key_hex: "ef".repeat(32),27 }));28 },29 };30}3132describe("source-side ADOPT_REQUEST routing", () => {33 it("T-A26: routes ADOPT_REQUEST to grantAdoption after SAS confirmation and consent", async () => {34 const rootKeyHex = "77".repeat(32);35 const ch = channel();36 const completePairing = vi.fn();37 const result = await routeSourcePostSASFrame(38 {39 sas: "895245",40 frame: {41 type: "ADOPT_REQUEST",42 device_id: "receiver-ephemeral",43 device_name: "Receiver",44 device_type: "extension",45 },46 channel: ch,47 },48 localIdentity,49 {50 confirmSAS: vi.fn(async () => true),51 completePairing,52 grantAdoptionConsent: vi.fn(async () => true),53 fetchAdoptionMaterial: vi.fn(async () => ({54 rootKeyHex,55 vaultId: await computeVaultId(Uint8Array.from(Buffer.from(rootKeyHex, "hex"))),56 license: "PPK-test",57 })),58 onProgress: vi.fn(),59 },60 );6162 const grant = JSON.parse(new TextDecoder().decode(ch.sent[0]));63 expect(result).toEqual(expect.objectContaining({ deviceId: "receiver-device" }));64 expect(grant).toMatchObject({65 type: "ADOPT_GRANT",66 root_key_hex: rootKeyHex,67 vault_id: await computeVaultId(Uint8Array.from(Buffer.from(rootKeyHex, "hex"))),68 license: "PPK-test",69 });70 expect(completePairing).toHaveBeenCalledWith(expect.objectContaining({ deviceId: "receiver-device" }));71 });7273 it("T-A27: routes PAIR_IDENTITY to the existing pairing path", async () => {74 const ch = channel();75 const completePairing = vi.fn();76 const result = await routeSourcePostSASFrame(77 {78 sas: "895245",79 frame: {80 type: "PAIR_IDENTITY",81 device_id: "phone",82 device_name: "Phone",83 device_type: "android",84 long_term_public_key_hex: "aa".repeat(32),85 },86 channel: ch,87 },88 localIdentity,89 {90 confirmSAS: vi.fn(async () => true),91 completePairing,92 onProgress: vi.fn(),93 },94 );9596 const response = JSON.parse(new TextDecoder().decode(ch.sent[0]));97 expect(response.type).toBe("PAIR_IDENTITY");98 expect(result).toEqual(expect.objectContaining({ deviceId: "phone" }));99 expect(completePairing).toHaveBeenCalledWith(expect.objectContaining({ deviceId: "phone" }));100 });101102 it("T-A28: unknown frame closes the channel gracefully", async () => {103 const ch = channel();104 await expect(routeSourcePostSASFrame(105 { sas: "895245", frame: { type: "WHAT_IS_THIS" }, channel: ch },106 localIdentity,107 {108 confirmSAS: vi.fn(async () => true),109 completePairing: vi.fn(),110 onProgress: vi.fn(),111 },112 )).rejects.toThrow("unexpected_pairing_frame");113114 expect(ch.close).toHaveBeenCalledTimes(1);115 });116117 it("T-A29: source root key bytes are zeroed after grantAdoption regardless of outcome", async () => {118 const fillSpy = vi.spyOn(Uint8Array.prototype, "fill");119 await routeSourcePostSASFrame(120 {121 sas: "895245",122 frame: {123 type: "ADOPT_REQUEST",124 device_id: "receiver-ephemeral",125 device_name: "Receiver",126 device_type: "extension",127 },128 channel: channel(),129 },130 localIdentity,131 {132 confirmSAS: vi.fn(async () => true),133 completePairing: vi.fn(),134 grantAdoptionConsent: vi.fn(async () => true),135 fetchAdoptionMaterial: vi.fn(async () => ({136 rootKeyHex: "88".repeat(32),137 vaultId: await computeVaultId(Uint8Array.from(Buffer.from("88".repeat(32), "hex"))),138 })),139 onProgress: vi.fn(),140 },141 );142143 expect(fillSpy).toHaveBeenCalledWith(0);144 });145});