Discord radio channels: wire leases + desired-state streams (server-26#203, #204)
c2-core
- discord_streams/discord_leases wired in: POST /discord/streams,
POST /discord/guilds/{id}/stop, GET /discord/guilds/{id}/overview,
GET /discord/streams/{id}, PUT /discord/guilds/{id}/link (admin),
GET /discord/node/{node}/tokens/{id} (node key, lease holder only).
- Retained nodes/{id}/discord/desired (token ids, never token strings),
nodes/+/discord/actual subscribed and handled; per-node publish lock keeps
the retained list the newest; failed publishes retried by the reconciler.
- Tokens freed only after the node acks logout (or offline past
discord_lease_offline_release_seconds) - sweeper and C2 startup no longer
release leases.
- /nodes/{id}/command discord_join/leave map onto streams; CommandPayload
keeps preferred_token_id. /tokens: admin-only list, validate on add against
Discord, refresh, flush via transactional force release.
- Guild -> org via discord_guilds, unlinked guilds fall back to
DISCORD_UNLINKED_GUILD_ORG_ID (default "founding").
- tests/fake_firestore.py: optimistic-transaction fake; 27 lease/race tests.
discord bot
- /join, /leave, /status, autocomplete scoped to the calling guild; /leave
never touches another server; Manage Channels default permission;
/join edits its reply with the node's real result; /help copy fixed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
0a724a064f
commit
a31a8df17c
@@ -35,16 +35,10 @@ def _node_naive(node_id, status, age_seconds):
|
||||
async def test_stale_online_node_marked_offline():
|
||||
nodes = [_node("node-01", "online", age_seconds=120)]
|
||||
|
||||
# A stale node also triggers app.routers.tokens.release_token(node_id) —
|
||||
# added by the PulseAudio/Discord-token work (commit 2a690ec). It's
|
||||
# imported inline inside _sweep, so it must be patched at its source
|
||||
# module rather than relying on the global asyncio.to_thread patch above,
|
||||
# which is scoped to the node-query call and would otherwise feed
|
||||
# release_token's own internal to_thread call the wrong shape of data
|
||||
# (raw node dicts instead of Firestore doc snapshots with .id).
|
||||
# The sweeper no longer frees Discord tokens (node-26#25) — the Discord
|
||||
# reconciler does, after the lease grace. See test_discord_leases.py.
|
||||
with patch("asyncio.to_thread", new=AsyncMock(return_value=nodes)), \
|
||||
patch("app.internal.node_sweeper.fstore") as mock_fstore, \
|
||||
patch("app.routers.tokens.release_token", new=AsyncMock()):
|
||||
patch("app.internal.node_sweeper.fstore") as mock_fstore:
|
||||
mock_fstore.doc_update = AsyncMock()
|
||||
await _sweep()
|
||||
|
||||
@@ -58,8 +52,7 @@ async def test_stale_recording_node_marked_offline():
|
||||
nodes = [_node("node-02", "recording", age_seconds=200)]
|
||||
|
||||
with patch("asyncio.to_thread", new=AsyncMock(return_value=nodes)), \
|
||||
patch("app.internal.node_sweeper.fstore") as mock_fstore, \
|
||||
patch("app.routers.tokens.release_token", new=AsyncMock()):
|
||||
patch("app.internal.node_sweeper.fstore") as mock_fstore:
|
||||
mock_fstore.doc_update = AsyncMock()
|
||||
await _sweep()
|
||||
|
||||
@@ -115,8 +108,7 @@ async def test_tz_naive_last_seen_is_handled():
|
||||
nodes = [_node_naive("node-06", "online", age_seconds=120)]
|
||||
|
||||
with patch("asyncio.to_thread", new=AsyncMock(return_value=nodes)), \
|
||||
patch("app.internal.node_sweeper.fstore") as mock_fstore, \
|
||||
patch("app.routers.tokens.release_token", new=AsyncMock()):
|
||||
patch("app.internal.node_sweeper.fstore") as mock_fstore:
|
||||
mock_fstore.doc_update = AsyncMock()
|
||||
await _sweep()
|
||||
|
||||
@@ -151,8 +143,7 @@ async def test_only_stale_nodes_updated_in_batch():
|
||||
]
|
||||
|
||||
with patch("asyncio.to_thread", new=AsyncMock(return_value=nodes)), \
|
||||
patch("app.internal.node_sweeper.fstore") as mock_fstore, \
|
||||
patch("app.routers.tokens.release_token", new=AsyncMock()) as mock_release:
|
||||
patch("app.internal.node_sweeper.fstore") as mock_fstore:
|
||||
mock_fstore.doc_update = AsyncMock()
|
||||
await _sweep()
|
||||
|
||||
@@ -160,7 +151,6 @@ async def test_only_stale_nodes_updated_in_batch():
|
||||
updated_ids = {call.args[1] for call in mock_fstore.doc_update.call_args_list}
|
||||
assert updated_ids == {"node-08", "node-11"}
|
||||
|
||||
# Both newly-offline nodes should have their Discord token freed.
|
||||
assert mock_release.call_count == 2
|
||||
released_ids = {call.args[0] for call in mock_release.call_args_list}
|
||||
assert released_ids == {"node-08", "node-11"}
|
||||
# Marking a node offline must NOT free its radio-bot tokens: the node may
|
||||
# still be logged in on them (node-26#25). Only status was written.
|
||||
assert all(call.args[2] == {"status": "offline"} for call in mock_fstore.doc_update.call_args_list)
|
||||
|
||||
Reference in New Issue
Block a user