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>
This commit is contained in:
Tyler
2026-07-31 09:41:09 -04:00
committed by GitHub
parent 689617af7a
commit 61ba9dfaa0
63 changed files with 5019 additions and 698 deletions

View File

@@ -0,0 +1,119 @@
import { expect, test } from "@playwright/test";
import { waitForAnimations } from "../helpers/animations";
import { installMockBridge } from "../helpers/bridge";
import { openSettings } from "../helpers/settings";
const SCREENSHOT_PATH = "test-results/voice-settings/pocket-voices.png";
test.describe("Pocket voice settings", () => {
test.use({ viewport: { width: 1100, height: 760 } });
test("selects and retains a bundled voice while text to speech is off", async ({
page,
}) => {
await installMockBridge(page);
await page.goto("/", { waitUntil: "domcontentloaded" });
await openSettings(page, "voice");
const card = page.getByTestId("settings-voice");
await expect(card).toBeVisible();
await expect(
page.getByText("Agent text to speech", { exact: true }),
).toBeVisible();
await expect(
page.getByText("Pocket TTS voice", { exact: true }),
).toBeVisible();
await expect(card).not.toContainText("April INT8");
await page.getByTestId("pocket-voice-selector").click();
await expect(page.getByRole("menuitemradio")).toHaveCount(12);
await page.getByRole("menuitemradio", { name: "Eve" }).click();
await expect(page.getByTestId("pocket-voice-selector")).toContainText(
"Eve",
);
await expect(
page.getByRole("button", { name: "Pocket TTS voice: Eve" }),
).toBeVisible();
await page.getByTestId("agent-text-to-speech-toggle").click();
await expect(
page.getByTestId("agent-text-to-speech-toggle"),
).toHaveAttribute("aria-checked", "false");
await expect(page.getByTestId("pocket-voice-controls")).toHaveAttribute(
"aria-disabled",
"true",
);
await expect(page.getByTestId("pocket-voice-selector")).toContainText(
"Eve",
);
const savedCommands = await page.evaluate(() =>
(window.__BUZZ_E2E_COMMAND_LOG__ ?? [])
.filter((entry) =>
["set_pocket_voice", "set_tts_enabled"].includes(entry.command),
)
.map((entry) => ({ command: entry.command, payload: entry.payload })),
);
expect(savedCommands).toEqual([
{
command: "set_pocket_voice",
payload: { voiceKey: "pocket:eve" },
},
{
command: "set_tts_enabled",
payload: { enabled: false },
},
]);
});
test("captures the complete VCTK preset settings surface", async ({
page,
}) => {
await installMockBridge(page, {
ttsSettings: {
version: 1,
agentTextToSpeech: true,
voicePreferences: ["pocket:eve"],
},
});
await page.goto("/", { waitUntil: "domcontentloaded" });
await openSettings(page, "voice");
const card = page.getByTestId("settings-voice");
await expect(card).toBeVisible();
await expect(page.getByTestId("pocket-voice-selector")).toContainText(
"Eve",
);
await page.getByTestId("pocket-voice-selector").click();
await expect(page.getByRole("menuitemradio")).toHaveCount(12);
const menu = page.getByRole("menu");
await expect(menu).toBeVisible();
await waitForAnimations(page);
const cardBox = await card.boundingBox();
const menuBox = await menu.boundingBox();
const viewport = page.viewportSize();
if (!cardBox || !menuBox || !viewport) {
throw new Error("Voice settings screenshot bounds are unavailable");
}
const x = Math.max(0, Math.min(cardBox.x, menuBox.x) - 16);
const y = Math.max(0, Math.min(cardBox.y, menuBox.y) - 16);
const right = Math.min(
viewport.width,
Math.max(cardBox.x + cardBox.width, menuBox.x + menuBox.width) + 16,
);
const bottom = Math.min(
viewport.height,
Math.max(cardBox.y + cardBox.height, menuBox.y + menuBox.height) + 16,
);
await page.screenshot({
path: SCREENSHOT_PATH,
clip: {
x: Math.floor(x),
y: Math.floor(y),
width: Math.ceil(right - x),
height: Math.ceil(bottom - y),
},
});
});
});

View File

@@ -159,6 +159,11 @@ type MockInstallRuntimeResult = {
};
type MockBridgeOptions = {
ttsSettings?: {
version: number;
agentTextToSpeech: boolean;
voicePreferences: string[];
};
/** Advertised HEAD for the first mock project without adding that branch. */
projectHeadBranch?: string;
/** Relay NIP-11 identity used to sign authoritative repository state. */

View File

@@ -3,6 +3,7 @@ import { expect, type Page } from "@playwright/test";
type SettingsSection =
| "profile"
| "notifications"
| "voice"
| "agents"
| "channel-templates"
| "compute"