Discord: main DRB bot optional end to end + owner-flow integration tests (#225 S8) #271

Merged
logan merged 1 commits from feat/225-s8-main-bot-optional into main 2026-10-05 03:29:56 -04:00
Owner

Slice S8 of #225 (plan: #225 comment 3108). Built on main f49dc86 (S1–S7 and #226/#227 merged). Do not merge until reviewed.

What changed

  • Copy: the DRB server bot is optional everywhere a customer reads it.
    • Node picker (NodeDiscordRadio.tsx): empty state points to Settings → Organization → Connect a Discord server; "The DRB bot has not reported this server's channels" now says a radio bot lists them (or the server bot, if there).
    • Admin Settings → Discord: How it works tab gets a "server bot is optional" note, and step 1/2 now name the node picker and the permission check. Servers tab copy fixed; Streams empty state names the node panel.
    • Org Linked Discord servers card footer: commands are the server bot's; a radio bot alone is enough to stream.
    • Bot /help: /drb link field names the no-bot path (Connect a Discord server); Radio bots field names the permissions and that radio bots work without this bot.
    • ERROR_TEXT["limit"] (reaches the web picker too) no longer says only "/leave".
  • Admin overview "main bot present" (plan S8): GET /discord/admin/overview guilds gain main_bot_present (true/false/null = never heard), radio_bots (names), linked_via, directory_source; Servers tab gets a Bots column.
  • discord_web.py docstring (plan finding 5): states the reversed "C2 never calls Discord on the browser's behalf" rule, its guards, and that the server bot is optional end to end.
  • Optional plan item done: the server bot's directory cog re-pushes (which drops C2's cached permission answers for that server) on permission-overwrite changes, role permission changes and role deletes. Docstring updated.
  • storage.download_audio() removed. No caller on main: routers/media.py moved to download_audio_range in #233. Only stale branches with no open PR still import it (there are no open PRs).
  • Health gate confirmed: CI's /health check hits c2-core only, and nothing in compose depends on discord-bot. A dead server bot can't fail a deploy.

Integration tests: tests/test_discord_owner_flows.py (4 tests)

Real route handlers and real internals (streams, leases, discord_rest, OAuth, guild link, perms). Firestore is fake_firestore, MQTT is recorded, and Discord is a modelled DiscordWorld behind httpx.MockTransport. The model covers servers, roles, channel overrides, bot apps, REST and the OAuth2 token/revoke endpoints. Authorize-in-Discord makes the bot a member.

  1. One-bot org, no server bot:

    1. The admin adds the BYO bot. inspect_token runs against the model.
    2. The admin sets its OAuth secret. The client-credentials check runs.
    3. The owner marks the bot preferred for their system (Q2).
    4. oauth/options and oauth/start from the node pick that bot (preferred).
    5. Code exchange: oauth/complete links via oauth and checks permissions in the same request (1 ready).
    6. join-options lists the server with main_bot_present: false, and channels come from the radio bot.
    7. voice-check reports ready.
    8. start_node_stream leases the preferred bot and publishes a token id to n1. The node fetches the string with its key.
    9. Every Discord call used only that bot's token, and no response carries a token or secret.

    Plus: the reconciler re-places the persistent stream on n2 when n1 dies, still with no server bot.

  2. Full setup: the server bot pushes its directory and links with a /drb link code. Four radio bots: BYO muted on #dispatch, two pool bots, and one pool bot not in the server.

    1. #fireground (nobody may Connect): a 409 missing_permissions names all 3 member bots and the @everyone override, and offers an invite for the 4th.
    2. /join County P25 on #dispatch: the BYO bot is denied Speak via @Muted, so DRB Radio 1 takes it.
    3. /join City Fire on the same channel: its preferred bot, DRB Radio 2, plays beside DRB Radio 1.
    4. EMS on #dispatch from the picker: the voice-check says needs_fix, with pool bots busy_here and BYO missing Speak. The 409 text names the bot, the channel, the permission and the fix.
    5. EMS on #lobby: the BYO bot plays it.
    6. Naming a busy bot gets a preferred_busy refusal that names its stream.
    7. Each node's desired list holds only its own token id.
    8. Admin overview: main_bot_present: true, radio_bots lists 3 bots.

    Plus: the server bot is kicked (present:false). The picker still lists channels and starts work through the radio bots.

I mutation-checked these: turning DISCORD_PERM_CHECK_ENABLED off fails both main flows.

Results

  • c2-core: 927 passed, 4 warnings in 22.61s
  • frontend: tsc --noEmit exit 0 (scratch copy, portable Node 20.20.2)
  • discord bot: py_compile only. There are no tests for that service, so the /help and cog changes are unverified at runtime.

What still depends on the main bot (by design or out of scope)

  • Slash commands /join, /leave, /status, /drb link, /help, and the trips commands are the server bot's. Without it, streams are started and stopped from the web: the node page picker, or the admin Settings → Discord page.
  • The /drb link code path needs the server bot. Connect a Discord server (per-bot OAuth) doesn't.
  • Fallback-org servers (DISCORD_UNLINKED_GUILD_ORG_ID) still appear only while the server bot is there. A radio bot's membership never discovers a server (plan S5 guard, kept on purpose).
  • Gateway-fresh channel list: without the server bot, the list is a REST read through a radio bot, refreshed when the picker opens if it's older than discord_directory_refresh_seconds (300 s). A channel created a moment ago is found on Start by one inline re-read.
  • Not the main bot, but still an owner blocker: adding a BYO radio bot (POST /tokens) and setting its OAuth secret (PUT /tokens/{id}/oauth) are platform-admin only. A one-bot org therefore needs a DRB admin once, to register the bot. I did not change that: it is out of S8's scope and touches token-secret handling. Filed as a follow-up (see the #225 comment).

Not done here

  • Closing #225: left to the main session after review and merge, per "nobody merges".

🤖 Generated with Claude Code

Slice S8 of #225 (plan: #225 comment 3108). Built on main f49dc86 (S1–S7 and #226/#227 merged). **Do not merge until reviewed.** ## What changed - **Copy: the DRB server bot is optional everywhere a customer reads it.** - Node picker (`NodeDiscordRadio.tsx`): empty state points to Settings → Organization → *Connect a Discord server*; "The DRB bot has not reported this server's channels" now says a radio bot lists them (or the server bot, if there). - Admin Settings → Discord: *How it works* tab gets a "server bot is optional" note, and step 1/2 now name the node picker and the permission check. Servers tab copy fixed; Streams empty state names the node panel. - Org *Linked Discord servers* card footer: commands are the server bot's; a radio bot alone is enough to stream. - Bot `/help`: `/drb link` field names the no-bot path (Connect a Discord server); Radio bots field names the permissions and that radio bots work without this bot. - `ERROR_TEXT["limit"]` (reaches the web picker too) no longer says only "/leave". - **Admin overview "main bot present"** (plan S8): `GET /discord/admin/overview` guilds gain `main_bot_present` (true/false/null = never heard), `radio_bots` (names), `linked_via`, `directory_source`; Servers tab gets a **Bots** column. - **`discord_web.py` docstring** (plan finding 5): states the reversed "C2 never calls Discord on the browser's behalf" rule, its guards, and that the server bot is optional end to end. - **Optional plan item done:** the server bot's directory cog re-pushes (which drops C2's cached permission answers for that server) on permission-overwrite changes, role permission changes and role deletes. Docstring updated. - **`storage.download_audio()` removed.** No caller on main: `routers/media.py` moved to `download_audio_range` in #233. Only stale branches with no open PR still import it (there are no open PRs). - **Health gate confirmed:** CI's `/health` check hits c2-core only, and nothing in compose depends on `discord-bot`. A dead server bot can't fail a deploy. ## Integration tests: `tests/test_discord_owner_flows.py` (4 tests) Real route handlers and real internals (streams, leases, discord_rest, OAuth, guild link, perms). Firestore is `fake_firestore`, MQTT is recorded, and Discord is a modelled `DiscordWorld` behind `httpx.MockTransport`. The model covers servers, roles, channel overrides, bot apps, REST and the OAuth2 token/revoke endpoints. Authorize-in-Discord makes the bot a member. 1. **One-bot org, no server bot:** 1. The admin adds the BYO bot. `inspect_token` runs against the model. 2. The admin sets its OAuth secret. The client-credentials check runs. 3. The **owner** marks the bot preferred for their system (Q2). 4. `oauth/options` and `oauth/start` from the node pick that bot (`preferred`). 5. Code exchange: `oauth/complete` links via `oauth` and checks permissions in the same request (1 ready). 6. `join-options` lists the server with `main_bot_present: false`, and channels come from the radio bot. 7. `voice-check` reports ready. 8. `start_node_stream` leases the preferred bot and publishes a token **id** to n1. The node fetches the string with its key. 9. Every Discord call used only that bot's token, and no response carries a token or secret. Plus: the reconciler re-places the persistent stream on n2 when n1 dies, still with no server bot. 2. **Full setup:** the server bot pushes its directory and links with a `/drb link` code. Four radio bots: BYO muted on #dispatch, two pool bots, and one pool bot not in the server. 1. #fireground (nobody may Connect): a 409 `missing_permissions` names all 3 member bots and the `@everyone` override, and offers an invite for the 4th. 2. `/join` County P25 on #dispatch: the BYO bot is denied Speak via `@Muted`, so DRB Radio 1 takes it. 3. `/join` City Fire on **the same channel**: its preferred bot, DRB Radio 2, plays beside DRB Radio 1. 4. EMS on #dispatch from the picker: the voice-check says `needs_fix`, with pool bots `busy_here` and BYO missing Speak. The 409 text names the bot, the channel, the permission and the fix. 5. EMS on #lobby: the BYO bot plays it. 6. Naming a busy bot gets a `preferred_busy` refusal that names its stream. 7. Each node's desired list holds only its own token id. 8. Admin overview: `main_bot_present: true`, `radio_bots` lists 3 bots. Plus: the server bot is kicked (`present:false`). The picker still lists channels and starts work through the radio bots. I mutation-checked these: turning `DISCORD_PERM_CHECK_ENABLED` off fails both main flows. ## Results - c2-core: `927 passed, 4 warnings in 22.61s` - frontend: `tsc --noEmit` exit 0 (scratch copy, portable Node 20.20.2) - discord bot: `py_compile` only. There are no tests for that service, so the `/help` and cog changes are unverified at runtime. ## What still depends on the main bot (by design or out of scope) - **Slash commands** `/join`, `/leave`, `/status`, `/drb link`, `/help`, and the trips commands are the server bot's. Without it, streams are started and stopped from the web: the node page picker, or the admin Settings → Discord page. - **The `/drb link` code path** needs the server bot. *Connect a Discord server* (per-bot OAuth) doesn't. - **Fallback-org servers** (`DISCORD_UNLINKED_GUILD_ORG_ID`) still appear only while the server bot is there. A radio bot's membership never discovers a server (plan S5 guard, kept on purpose). - **Gateway-fresh channel list:** without the server bot, the list is a REST read through a radio bot, refreshed when the picker opens if it's older than `discord_directory_refresh_seconds` (300 s). A channel created a moment ago is found on Start by one inline re-read. - **Not the main bot, but still an owner blocker:** adding a BYO radio bot (`POST /tokens`) and setting its OAuth secret (`PUT /tokens/{id}/oauth`) are **platform-admin only**. A one-bot org therefore needs a DRB admin once, to register the bot. I did not change that: it is out of S8's scope and touches token-secret handling. Filed as a follow-up (see the #225 comment). ## Not done here - Closing #225: left to the main session after review and merge, per "nobody merges". 🤖 Generated with [Claude Code](https://claude.com/claude-code)
logan added 1 commit 2026-10-05 03:25:37 -04:00
- Copy: picker, admin Servers/Streams/How-it-works, org Discord card, bot
  /help and the server-limit refusal no longer imply the DRB server bot is
  required; a radio bot alone is enough.
- Admin overview: per-server main_bot_present, radio_bots, linked_via,
  directory_source; Servers tab gets a Bots column.
- discord_web docstring: states the reversed "never calls Discord" rule
  (plan finding 5) and the optional server bot.
- Server bot directory cog: re-push (which drops C2's cached permission
  answers) on overwrite changes, role permission changes and role deletes.
- tests/test_discord_owner_flows.py: one-bot org (BYO bot, owner-set
  preferred, OAuth link, picker, start, reconciler) and full setup (server
  bot + 4 radio bots: placement, shared channel, every refusal explained),
  against a modelled Discord over httpx.MockTransport.
- storage.download_audio removed: no caller on main since #233.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Author
Owner

S8 (main bot optional + cleanup): PR #271 — #271. Adds integration tests for both owner flows (one-bot org via BYO + OAuth; full setup with server bot + 4 radio bots), admin Bots column, copy + docstring updates, removes dead storage.download_audio. Remaining main-bot dependencies are listed in the PR body. Follow-up filed: #273 (BYO bot registration + OAuth secret are platform-admin only). Leave #225 open until #271 is reviewed and merged.

S8 (main bot optional + cleanup): PR #271 — https://git.vpn.cusano.net/logan/server-26/pulls/271. Adds integration tests for both owner flows (one-bot org via BYO + OAuth; full setup with server bot + 4 radio bots), admin Bots column, copy + docstring updates, removes dead storage.download_audio. Remaining main-bot dependencies are listed in the PR body. Follow-up filed: #273 (BYO bot registration + OAuth secret are platform-admin only). Leave #225 open until #271 is reviewed and merged.
logan merged commit 3ef8d586ea into main 2026-10-05 03:29:56 -04:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: logan/server-26#271