sync-transport.test.ts

3.3 KB · raw

1// Subnet-probe rate-limit tests. The full Noise KK handshake side of the2// transport requires a live WebSocket peer, so we exercise the cooldown3// ledger directly — it is the only Phase 2 invariant we want to pin4// regardless of network reachability.56import { afterEach, beforeEach, describe, expect, it } from "vitest";7import {8  _resetSubnetProbeCooldown,9  probeSubnetForPeer,10  subnetProbeOnCooldown,11  SUBNET_PROBE_COOLDOWN_MS,12  type SyncPeerIdentity,13} from "./sync-transport";14import { generateKeyPair } from "@stablelib/x25519";1516const PEER_PUBKEY = "ab".repeat(32);17const RENDEZVOUS_IDS = ["A".repeat(43), "B".repeat(43), "C".repeat(43)];18const TEST_KEYS = generateKeyPair();19const TEST_IDENTITY: SyncPeerIdentity = {20  deviceId: "test-device",21  deviceName: "Test",22  deviceType: "extension",23  longTermPublicKeyHex: Array.from(24    TEST_KEYS.publicKey,25    (b) => b.toString(16).padStart(2, "0"),26  ).join(""),27  longTermPrivateKey: TEST_KEYS.secretKey,28};2930describe("subnetProbeOnCooldown", () => {31  beforeEach(() => _resetSubnetProbeCooldown());32  afterEach(() => _resetSubnetProbeCooldown());3334  it("returns false before any probe has happened", () => {35    expect(subnetProbeOnCooldown(PEER_PUBKEY)).toBe(false);36  });3738  it("returns true within the cooldown window after a probe", async () => {39    const now = 1_000_000;40    // Fire-and-forget; the probe itself fails fast because no peer exists.41    // We only care about the ledger side-effect of recording `now`.42    await probeSubnetForPeer(43      "10.0.0",44      49142,45      TEST_IDENTITY,46      PEER_PUBKEY,47      RENDEZVOUS_IDS,48      { batch: 254, timeoutMs: 1, now },49    );50    expect(subnetProbeOnCooldown(PEER_PUBKEY, now + 1)).toBe(true);51    expect(subnetProbeOnCooldown(PEER_PUBKEY, now + SUBNET_PROBE_COOLDOWN_MS - 1)).toBe(true);52  });5354  it("releases the cooldown after the window expires", async () => {55    const now = 1_000_000;56    await probeSubnetForPeer(57      "10.0.0",58      49142,59      TEST_IDENTITY,60      PEER_PUBKEY,61      RENDEZVOUS_IDS,62      { batch: 254, timeoutMs: 1, now },63    );64    expect(subnetProbeOnCooldown(PEER_PUBKEY, now + SUBNET_PROBE_COOLDOWN_MS)).toBe(false);65  });6667  it("returns null and skips the scan when on cooldown", async () => {68    const now = 1_000_000;69    await probeSubnetForPeer(70      "10.0.0",71      49142,72      TEST_IDENTITY,73      PEER_PUBKEY,74      RENDEZVOUS_IDS,75      { batch: 254, timeoutMs: 1, now },76    );77    // Second call within the window must short-circuit before any78    // WebSocket attempt is made. We can't easily observe "no WebSocket"79    // directly here, but null + cooldown=true is the contract.80    const result = await probeSubnetForPeer(81      "10.0.0",82      49142,83      TEST_IDENTITY,84      PEER_PUBKEY,85      RENDEZVOUS_IDS,86      { batch: 254, timeoutMs: 1, now: now + 1_000 },87    );88    expect(result).toBeNull();89  });9091  it("tracks cooldown independently per peer pubkey", async () => {92    const now = 1_000_000;93    const other = "cd".repeat(32);94    await probeSubnetForPeer(95      "10.0.0",96      49142,97      TEST_IDENTITY,98      PEER_PUBKEY,99      RENDEZVOUS_IDS,100      { batch: 254, timeoutMs: 1, now },101    );102    expect(subnetProbeOnCooldown(PEER_PUBKEY, now + 1)).toBe(true);103    expect(subnetProbeOnCooldown(other, now + 1)).toBe(false);104  });105});