test/README.md
Rama dev · réplica de lectura del repositorio original.
# Tests
## Unit tests
Run with `pnpm test` (or `pnpm test:watch`). These run in CI.
## End-to-end tests
The e2e tests in this directory exercise real Discord connections:
- **Discord connection** – the bot under test logs into Discord with a real bot
token and joins a real voice channel. A second bot (the listener) joins the
same channel and confirms it can see the bot.
- **Audio playback** – the bot plays a generated reference signal over the voice
channel. The listener records the Opus packets that Discord relays, decodes
them to PCM and compares them against the reference. The mock Jellyfin server
serves the signal as Ogg/Opus, and `@discordjs/voice` transcodes it via ffmpeg
into the Opus stream played over Discord.
The bot under test is driven through the app's real `DiscordVoiceService`
(`test/helpers/e2e/bot-app.ts`). The full `AppModule` is intentionally **not**
booted: `@discord-nestjs/core` deadlocks during `app.init()` when run inside a
vitest harness (its `initSubject` never emits, so the boot stalls *before* the
Discord login — reproducing locally and in CI regardless of credentials). The
live voice pipeline (`joinVoiceChannel` → `AudioPlayer` → Opus over Discord) is
therefore exercised directly, which is exactly what these tests are for.
The listener runs in its **own child process** (`listener-worker.cjs`).
`@discordjs/voice` keeps voice connections in a process-global store keyed by
`guildId`, so a second client in the same process would silently reuse the
first bot's connection (never truly joining or receiving audio). A separate
process gives the listener its own independent voice stack, exactly like two
real Discord clients.
Because audio is never lossless (Opus at a reduced bitrate over a network
relay), the audio comparison is deliberately **lenient**. It does **not** do an
exact byte-for-byte check. Instead it verifies, within configurable tolerances,
that:
- the received audio is the *same content* (normalised cross-correlation),
- there is no significant residual error / distortion,
- there are no "clicks" (sparse distortion spikes),
- sections that should be audible were not dropped to silence.
See `test/helpers/e2e/audio-analysis.ts` for the exact metrics and their default
thresholds.
### Requirements
Running the e2e suite requires a real Discord guild, a voice channel and **two
bot accounts**:
| Variable | Purpose |
| ---------------------------- | ---------------------------------------------- |
| `DISCORD_CLIENT_TOKEN` | The bot under test (already used by the app). |
| `E2E_LISTENER_TOKEN` | A second bot that "hears" the playback. |
| `E2E_DISCORD_GUILD_ID` | A guild both bots can join. |
| `E2E_DISCORD_VOICE_CHANNEL_ID` | A voice channel in that guild. |
Both bots must be invited to the guild and have permission to connect + speak in
the voice channel. The bot under test also needs `ffmpeg` available on `PATH`
(it is used by `@discordjs/voice` to decode the audio stream), which the
Docker image already provides. The `ubuntu-latest` GitHub runners ship `ffmpeg`
preinstalled.
### Troubleshooting
The live tests should fail fast (< ~50s) with an actionable message. If the bots
do not come online or the suite hangs, check in this order:
1. **Tokens** – both must be real bot tokens (from the Discord Developer Portal
→ Application → Bot → Token). `DISCORD_CLIENT_TOKEN` and `E2E_LISTENER_TOKEN`
must be **two different bots** (one bot cannot observe its own voice output).
Do not reuse one token for both.
2. **Invite** – both bots must be invited to the guild (`E2E_DISCORD_GUILD_ID`)
with the `View Channels`, `Connect`, `Speak` and `Send/Messages` permissions.
3. **Intents** – no privileged intents are required for the tests. The test
clients only use the non-privileged `Guilds` and `GuildVoiceStates` intents,
so you do **not** need to enable `MessageContent`/`GuildMembers` in the
Developer Portal for either bot. (If you also run the real application with
the same bot, the app itself requests `MessageContent`, which *is*
privileged and must be enabled for that bot; it is unrelated to the tests.)
4. **Already connected** – if the same bot token is already online elsewhere
(e.g. a running dev instance), Discord can block or delay the new session.
5. **Environment** – confirm the secrets are set in the repository's
*Settings → Secrets and variables → Actions* page (Repository secrets) and
that the workflow passes them (see `.github/workflows/e2e-tests.yml`).
### Running
```bash
E2E_LISTENER_TOKEN="..." \
E2E_DISCORD_GUILD_ID="..." \
E2E_DISCORD_VOICE_CHANNEL_ID="..." \
pnpm test:e2e
```
`DISCORD_CLIENT_TOKEN`, `JELLYFIN_SERVER_ADDRESS`, `JELLYFIN_AUTHENTICATION_USERNAME`
and `JELLYFIN_AUTHENTICATION_PASSWORD` are also read from `.env` / the
environment. The e2e tests point the bot at an in-memory mock Jellyfin server,
so no real Jellyfin instance is required.
If any of the `E2E_*` variables (or `DISCORD_CLIENT_TOKEN`) are missing the
suite is skipped with a warning, so normal CI runs are unaffected.
> **Note:** the audio test takes a few seconds (it plays ~6s of audio over a
> real network). It is intentionally not run in CI unless credentials are
> supplied. The `e2e-tests` GitHub Actions workflow does run `pnpm test:e2e` on
> every push/PR – the web API e2e tests (which need no credentials) execute
> there, while the live Discord checks skip. To enable the live Discord checks
> on a self-hosted/scheduled run, set `E2E_LISTENER_TOKEN`,
> `E2E_DISCORD_GUILD_ID` and `E2E_DISCORD_VOICE_CHANNEL_ID` as repository
> secrets (see `.github/workflows/e2e-tests.yml`).