Files
buzz/desktop/src/shared/api/relayReconnectReplay.test.mjs
Tyler 61ba9dfaa0 refactor(voice): extract reusable Pocket primitives + Pocket voice settings (relands #2467 + #3208) (#3910)
Relands **#2467** (extract `buzz-voice` crate) and **#3208** (Pocket
voice settings) onto main, after #3266 and #3180 merged.

## Why a fresh PR
The repo is squash-only with delete-branch-on-merge. Squashing #3266
deleted `jtennant/pocket-tts-2026-04`, which was #2467's base — GitHub
auto-closed #2467 and it cannot be reopened. Squash merges also sever
ancestry, so GitHub's natural merge-base reports phantom conflicts for
the whole remaining stack.

## Content provenance
- Byte-identical to the blessed `jt/buzz-voice-refactor` branch
(`93029c577`, tree `6729e0eff` — reviewed by Dawn (#2467) and Max
(#3208) at exact heads) **except** the three files where #3180 and #3208
genuinely interact.
- Three-file resolution (union of both sides):
- `huddle/mod.rs` — #3180's pipeline re-exports + #3208's
`agent_tts_routing` imports.
- `huddle/state.rs` — `reset_preserving_generation` preserves both
`huddle_generation` (#3180) and `tts_enabled` (#3208); test sets merged
into one `tests` module.
- `desktop/src/testing/e2eBridge.ts` — both switch arms kept; no
duplicate case labels.

## Verification at cf32dacd7
- `cargo test` (desktop/src-tauri, pinned 1.95.0): **2047 + 3 pass / 0
fail** (14 ignored: 8 keychain, 4 real_relay, 2 flag-gated)
- `cargo clippy --all-targets -- -D warnings`: clean; `cargo fmt
--check`: clean
- `cargo check --workspace` (root, includes new `buzz-voice` member):
clean; `cargo test -p buzz-voice`: 5/0
- `pnpm test`: **3885 / 0**; `tsc --noEmit`: clean; lint: clean

The 3180×3208 interaction resolution is getting an independent team
re-review before merge.

Buzz channel: buzz-desktop-voice `fd5fb402-b651-4238-89b1-bb3e2fa4dc96`,
thread `b4798ecc`.

Signed-off-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
2026-07-31 09:41:09 -04:00

609 lines
15 KiB
JavaScript

import assert from "node:assert/strict";
import test from "node:test";
import {
buildReconnectReplayFilter,
replayLiveSubscriptions,
REPLAY_BATCH_SIZE,
shouldPageReconnectReplay,
} from "./relayReconnectReplay.ts";
import { buildChannelFilter } from "./relayChannelFilters.ts";
// ── Fake-timer + Date.now setup for gate tests ────────────────────────────────
let fakeNow = 0;
const pendingTimers = new Map();
let nextTimerId = 1;
function fakeSetTimeout(fn, ms) {
const id = nextTimerId++;
pendingTimers.set(id, { fn, fireAt: fakeNow + ms });
return id;
}
function fakeClearTimeout(id) {
pendingTimers.delete(id);
}
function tickTo(ms) {
fakeNow = ms;
for (const [id, { fn, fireAt }] of Array.from(pendingTimers.entries())) {
if (fireAt <= fakeNow) {
pendingTimers.delete(id);
fn();
}
}
}
globalThis.window = {
setTimeout: fakeSetTimeout,
clearTimeout: fakeClearTimeout,
};
const origDateNow = Date.now;
function setFakeNow(ms) {
fakeNow = ms;
Date.now = () => fakeNow;
}
// Import gate module AFTER window shim so it picks up the fake timers.
const { activateRateLimit, resetRateLimitGate } = await import(
"./relayRateLimitGate.ts"
);
function resetGate(startMs = 0) {
pendingTimers.clear();
nextTimerId = 1;
setFakeNow(startMs);
resetRateLimitGate();
}
// ── Helpers ───────────────────────────────────────────────────────────────────
function event(id, createdAt) {
return {
id,
pubkey: "pubkey",
created_at: createdAt,
kind: 9,
tags: [],
content: "",
sig: "sig",
};
}
function eventRange(prefix, start, count) {
return Array.from({ length: count }, (_, index) =>
event(`${prefix}-${index}`, start + index),
);
}
function replayFilter(filter, since, until) {
return buildReconnectReplayFilter(filter, since, until);
}
// ── buildReconnectReplayFilter ────────────────────────────────────────────────
test("reconnect replay preserves small steady-state limits when adding since", () => {
const filter = {
kinds: [9, 40002],
"#h": ["channel-1"],
limit: 50,
};
assert.deepEqual(replayFilter(filter, 123), {
kinds: [9, 40002],
"#h": ["channel-1"],
limit: 50,
since: 123,
});
});
test("reconnect replay caps large steady-state limits", () => {
const filter = {
kinds: [9],
"#h": ["channel-1"],
limit: 1000,
};
assert.deepEqual(replayFilter(filter, 123), {
kinds: [9],
"#h": ["channel-1"],
limit: 500,
since: 123,
});
});
test("reconnect replay preserves the live-only zero-history contract", () => {
const filter = {
kinds: [9],
"#h": ["channel-1"],
limit: 0,
};
assert.deepEqual(replayFilter(filter, 123), {
kinds: [9],
"#h": ["channel-1"],
limit: 0,
since: 123,
});
});
test("live-only subscriptions do not page reconnect history", () => {
const filter = {
kinds: [9],
"#h": ["channel-1"],
limit: 0,
};
assert.equal(shouldPageReconnectReplay(filter), false);
});
test("reconnect replay keeps the stricter existing since window", () => {
const filter = {
kinds: [9],
"#h": ["channel-1"],
limit: 50,
since: 200,
};
assert.deepEqual(replayFilter(filter, 123), {
kinds: [9],
"#h": ["channel-1"],
limit: 50,
since: 200,
});
});
test("reconnect replay applies the stricter until window", () => {
const filter = {
kinds: [9],
"#h": ["channel-1"],
limit: 50,
until: 300,
};
assert.deepEqual(replayFilter(filter, 123, 400), {
kinds: [9],
"#h": ["channel-1"],
limit: 50,
since: 123,
until: 300,
});
});
test("initial subscription replay preserves the original filter", () => {
const filter = {
kinds: [9],
"#h": ["channel-1"],
limit: 50,
};
assert.equal(replayFilter(filter, undefined), filter);
});
// ── Batching: REPLAY_BATCH_SIZE cap ──────────────────────────────────────────
test("replay sends all subs in one batch when count equals REPLAY_BATCH_SIZE", async () => {
resetGate();
let delayCount = 0;
const sentIds = [];
const subscriptions = new Map(
Array.from({ length: REPLAY_BATCH_SIZE }, (_, i) => [
`sub-${i}`,
{
mode: "live",
filter: { kinds: [9], "#h": [`ch-${i}`], limit: 50 },
onEvent: () => {},
lastSeenCreatedAt: undefined,
},
]),
);
await replayLiveSubscriptions({
subscriptions,
sendRaw: async (payload) => {
sentIds.push(payload[1]);
},
requestHistory: async () => [],
setTimeoutFn: (fn, _ms) => {
delayCount++;
fn();
return 0;
},
});
assert.equal(sentIds.length, REPLAY_BATCH_SIZE);
assert.equal(delayCount, 0, "no inter-batch delay for exactly one batch");
});
test("replay splits subscriptions into batches of REPLAY_BATCH_SIZE", async () => {
resetGate();
let delayCount = 0;
const sentIds = [];
const batchBreakpoints = []; // indices where a delay fired
const subCount = REPLAY_BATCH_SIZE + 3;
const subscriptions = new Map(
Array.from({ length: subCount }, (_, i) => [
`sub-${i}`,
{
mode: "live",
filter: { kinds: [9], "#h": [`ch-${i}`], limit: 50 },
onEvent: () => {},
lastSeenCreatedAt: undefined,
},
]),
);
await replayLiveSubscriptions({
subscriptions,
sendRaw: async (payload) => {
sentIds.push(payload[1]);
},
requestHistory: async () => [],
setTimeoutFn: (fn, _ms) => {
delayCount++;
batchBreakpoints.push(sentIds.length);
fn();
return 0;
},
});
assert.equal(delayCount, 1, "one inter-batch delay between two batches");
assert.equal(sentIds.length, subCount, "all subs sent");
// The delay fired after the first batch (REPLAY_BATCH_SIZE subs sent).
assert.equal(batchBreakpoints[0], REPLAY_BATCH_SIZE);
});
// ── Visible-channel priority ──────────────────────────────────────────────────
test("visible channel subscription is sent first", async () => {
resetGate();
const sentOrder = [];
const subscriptions = new Map([
[
"other-1",
{
mode: "live",
filter: { kinds: [9], "#h": ["ch-other"], limit: 50 },
onEvent: () => {},
lastSeenCreatedAt: undefined,
},
],
[
"visible-sub",
{
mode: "live",
filter: { kinds: [9], "#h": ["ch-visible"], limit: 50 },
onEvent: () => {},
lastSeenCreatedAt: undefined,
},
],
[
"other-2",
{
mode: "live",
filter: { kinds: [9], "#h": ["ch-other2"], limit: 50 },
onEvent: () => {},
lastSeenCreatedAt: undefined,
},
],
]);
await replayLiveSubscriptions({
subscriptions,
sendRaw: async (payload) => {
sentOrder.push(payload[1]);
},
requestHistory: async () => [],
visibleChannelId: "ch-visible",
});
assert.equal(sentOrder[0], "visible-sub", "visible sub sent first");
assert.equal(sentOrder.length, 3);
});
// ── Rate-limit gate deferral ──────────────────────────────────────────────────
test("replay waits for rate-limit gate before sending REQs", async () => {
resetGate(0);
activateRateLimit(5); // gate active for 5 seconds
const sentIds = [];
const replayPromise = replayLiveSubscriptions({
subscriptions: new Map([
[
"sub-1",
{
mode: "live",
filter: { kinds: [9], "#h": ["ch-1"], limit: 50 },
onEvent: () => {},
lastSeenCreatedAt: undefined,
},
],
]),
sendRaw: async (payload) => {
sentIds.push(payload[1]);
},
requestHistory: async () => [],
setTimeoutFn: (fn, _ms) => {
fn();
return 0;
},
});
// Gate expires — replay should proceed now.
tickTo(5_001);
await replayPromise;
assert.equal(sentIds.length, 1, "REQ sent after gate expired");
});
// ── Connection-generation guard ───────────────────────────────────────────────
test("stale replay sends no REQs when generation advances while gate was active", async () => {
resetGate(0);
activateRateLimit(5); // gate active for 5 seconds
let generationActive = true; // true = current, false = stale
const sentIds = [];
const replayPromise = replayLiveSubscriptions({
subscriptions: new Map([
[
"sub-1",
{
mode: "live",
filter: { kinds: [9], "#h": ["ch-1"], limit: 50 },
onEvent: () => {},
lastSeenCreatedAt: undefined,
},
],
]),
sendRaw: async (payload) => {
sentIds.push(payload[1]);
},
requestHistory: async () => [],
isActive: () => generationActive,
});
// Advance the generation (simulate new connection) before the gate resolves.
generationActive = false;
// Fire the gate timer.
tickTo(5_001);
await replayPromise;
assert.equal(sentIds.length, 0, "no REQs sent for a stale replay");
});
// ── Paged replay (existing behaviour) ────────────────────────────────────────
test("channel reconnect replay pages the missed window until a short page", async () => {
resetGate();
const delivered = [];
const historyFilters = [];
const sentPayloads = [];
const pages = [
eventRange("newest", 1501, 500),
eventRange("middle", 1002, 500),
eventRange("oldest", 995, 8),
];
const filter = buildChannelFilter("channel-1", 50);
const subscriptions = new Map([
[
"live-1",
{
mode: "live",
filter,
onEvent: (event) => delivered.push(event),
lastSeenCreatedAt: 1000,
},
],
]);
await replayLiveSubscriptions({
subscriptions,
now: 2000,
sendRaw: async (payload) => {
sentPayloads.push(payload);
},
requestHistory: async (filter) => {
historyFilters.push(filter);
return pages.shift() ?? [];
},
});
assert.deepEqual(sentPayloads, [
[
"REQ",
"live-1",
{
kinds: filter.kinds,
"#h": ["channel-1"],
limit: 50,
},
],
]);
assert.deepEqual(historyFilters, [
{
kinds: filter.kinds,
"#h": ["channel-1"],
limit: 500,
since: 995,
until: 2000,
},
{
kinds: filter.kinds,
"#h": ["channel-1"],
limit: 500,
since: 995,
until: 1501,
},
{
kinds: filter.kinds,
"#h": ["channel-1"],
limit: 500,
since: 995,
until: 1002,
},
]);
assert.equal(delivered.length, 1008);
});
test("reconnect replay starts live REQs in parallel and preserves per-sub page order", async () => {
resetGate();
const sentPayloads = [];
const sendResolvers = [];
const historyFiltersByChannel = {
"channel-1": [],
"channel-2": [],
};
const pagesByChannel = {
"channel-1": [
eventRange("c1-full", 1501, 500),
eventRange("c1-short", 1490, 2),
],
"channel-2": [
eventRange("c2-full", 1701, 500),
eventRange("c2-short", 1690, 2),
],
};
const subscriptions = new Map([
[
"live-1",
{
mode: "live",
filter: buildChannelFilter("channel-1", 50),
onEvent: () => {},
lastSeenCreatedAt: 1000,
},
],
[
"live-2",
{
mode: "live",
filter: buildChannelFilter("channel-2", 50),
onEvent: () => {},
lastSeenCreatedAt: 1000,
},
],
]);
const replayPromise = replayLiveSubscriptions({
subscriptions,
now: 2000,
pageReplayConcurrency: 2,
sendRaw: (payload) => {
sentPayloads.push(payload);
return new Promise((resolve) => {
sendResolvers.push(resolve);
});
},
requestHistory: async (filter) => {
const channelId = filter["#h"]?.[0];
historyFiltersByChannel[channelId].push(filter.until);
return pagesByChannel[channelId].shift() ?? [];
},
});
await Promise.resolve();
assert.deepEqual(
sentPayloads.map((payload) => payload[1]),
["live-1", "live-2"],
);
assert.equal(sendResolvers.length, 2);
assert.deepEqual(historyFiltersByChannel, {
"channel-1": [],
"channel-2": [],
});
for (const resolve of sendResolvers) {
resolve();
}
await replayPromise;
assert.deepEqual(historyFiltersByChannel, {
"channel-1": [2000, 1501],
"channel-2": [2000, 1701],
});
});
// ── Per-batch gate re-check (F2 fix) ─────────────────────────────────────────
test("batch-1 arms gate mid-replay: batch-2 is withheld until gate expires", async () => {
// Gate is inactive at the start of replay. Batch 1 fires and (simulating the
// relay's admission control) activates the gate. Batch 2 must wait until the
// gate clears before its REQs are sent.
resetGate(0);
const BATCH = REPLAY_BATCH_SIZE;
const sentAtMs = []; // record the fakeNow when each REQ fires
// Build BATCH+1 subscriptions so there are exactly two batches.
const subscriptions = new Map(
Array.from({ length: BATCH + 1 }, (_, i) => [
`sub-${i}`,
{
mode: "live",
filter: { kinds: [9], "#h": [`ch-${i}`], limit: 50 },
onEvent: () => {},
lastSeenCreatedAt: undefined,
},
]),
);
let _batchCount = 0;
const replayPromise = replayLiveSubscriptions({
subscriptions,
sendRaw: async (payload) => {
sentAtMs.push({ id: payload[1], ms: fakeNow });
// After the first full batch is sent, arm the gate for 5 s.
// This simulates the relay responding to batch-1 traffic with back-pressure.
if (sentAtMs.length === BATCH) {
_batchCount += 1;
activateRateLimit(5);
}
},
requestHistory: async () => [],
setTimeoutFn: (fn, _ms) => {
fn();
return 0;
},
});
// Advance time to expire the gate while the replay is suspended in the
// per-batch gate await. This unblocks the second batch.
tickTo(5_001);
await replayPromise;
const batch1Ids = sentAtMs.filter((r) => r.ms < 5_001).map((r) => r.id);
const batch2Ids = sentAtMs.filter((r) => r.ms >= 5_001).map((r) => r.id);
assert.equal(
batch1Ids.length,
BATCH,
"batch 1 must send exactly REPLAY_BATCH_SIZE REQs",
);
assert.equal(
batch2Ids.length,
1,
"batch 2 must send the remaining sub after the gate expires",
);
});
// ── Teardown ──────────────────────────────────────────────────────────────────
test("teardown — restore Date.now", () => {
Date.now = origDateNow;
assert.ok(true);
});