mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-06 02:41:48 +00:00
feat(kanban-dashboard): per-platform home-channel notification toggles (#19864)
* revert: auto-subscribe gateway chat on tool-driven kanban_create (#19718)
Reverts ff3d2773e2. Teknium reviewed the merged PR and decided this
behavior isn't wanted — tool-driven kanban_create should not mirror
the slash-command path's auto-subscribe. Orchestrators that want
their originating chat notified can call kanban_notify-subscribe
explicitly; we're not going to make it implicit.
* feat(kanban-dashboard): per-platform home-channel notification toggles
Adds a "Notify home channels" section to the task drawer in the kanban
dashboard plugin. Each platform where the user has set a home channel
(/sethome, TELEGRAM_HOME_CHANNEL env var, gateway.platforms.<p>.home_channel
in config.yaml) gets a toggle pill. Toggling on writes a kanban_notify_subs
row keyed to that platform's home (chat_id + thread_id); toggling off
removes it. The existing gateway notifier watcher delivers completed /
blocked / gave_up events without any new plumbing — this is purely a GUI
surface over existing machinery.
Replaces the reverted auto-subscribe behavior from #19718 with an explicit,
per-task, per-platform, user-controlled opt-in. No implicit subscription
on tool-driven kanban_create; no CLI commands; no slash commands. Just a
toggle in the drawer.
Backend (plugins/kanban/dashboard/plugin_api.py):
- GET /api/plugins/kanban/home-channels[?task_id=X]
Returns every platform with a configured home, plus a per-entry
subscribed: bool relative to task_id (false when task_id omitted).
Reads the live GatewayConfig via load_gateway_config() so env-var
overlays stay honored.
- POST /api/plugins/kanban/tasks/:id/home-subscribe/:platform
Idempotent add_notify_sub keyed to the platform's home.
- DELETE /api/plugins/kanban/tasks/:id/home-subscribe/:platform
remove_notify_sub for the same tuple.
- 404 when the platform has no home configured, or task_id doesn't
exist (POST only).
Frontend (plugins/kanban/dashboard/dist/index.js):
- TaskDrawer fetches /home-channels on open, keyed on task_id.
- HomeSubsSection renders nothing when zero platforms have a home (so
users who haven't set one up don't see an empty UI block).
- Optimistic toggle with busy flag + revert-on-failure. One pill per
platform; ✓ prefix and --on class indicate the subscribed state.
CSS (plugins/kanban/dashboard/dist/style.css):
- .hermes-kanban-home-subs flex row + .hermes-kanban-home-sub pill
style + --on subscribed variant (subtle ring-colored background).
Live-tested against a dashboard with TELEGRAM + DISCORD_BOT_TOKEN /
HOME_CHANNEL env vars set: drawer shows both pills, toggling each
flips its visual state AND writes/removes the correct kanban_notify_subs
row (verified via direct DB read).
Tests (tests/plugins/test_kanban_dashboard_plugin.py, 11 new, 53/53
pass total):
- home-channels lists only platforms with a home (slack with a
token but no home is excluded)
- no task_id -> all subscribed=false
- subscribe creates notify_sub row with correct chat/thread/platform
- subscribed=true reflected in subsequent GET
- idempotent re-subscribe
- unknown platform -> 404
- unknown task -> 404
- unsubscribe removes the row
- telegram + discord subscribe/unsubscribe independent
- zero homes -> empty list
This commit is contained in:
parent
3db6b9cc87
commit
1c7c7c3c5f
4 changed files with 423 additions and 0 deletions
|
|
@ -914,3 +914,161 @@ def test_create_task_probe_error_does_not_break_create(client, monkeypatch):
|
|||
)
|
||||
assert r.status_code == 200
|
||||
assert r.json()["task"]["title"] == "resilient"
|
||||
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Home-channel subscription endpoints (#19534 follow-up: GUI opt-in)
|
||||
# ---------------------------------------------------------------------------
|
||||
#
|
||||
# Dashboard surface for per-task, per-platform notification toggles. The
|
||||
# backend endpoints read the live GatewayConfig, so tests set env vars
|
||||
# (BOT_TOKEN + HOME_CHANNEL) to simulate a user who has run /sethome on
|
||||
# telegram and discord.
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def with_home_channels(monkeypatch):
|
||||
"""Simulate a user with home channels set on telegram and discord."""
|
||||
monkeypatch.setenv("TELEGRAM_BOT_TOKEN", "abc:fake")
|
||||
monkeypatch.setenv("TELEGRAM_HOME_CHANNEL", "1234567")
|
||||
monkeypatch.setenv("TELEGRAM_HOME_CHANNEL_THREAD_ID", "42")
|
||||
monkeypatch.setenv("TELEGRAM_HOME_CHANNEL_NAME", "Main TG")
|
||||
monkeypatch.setenv("DISCORD_BOT_TOKEN", "disc_fake")
|
||||
monkeypatch.setenv("DISCORD_HOME_CHANNEL", "9999999")
|
||||
monkeypatch.setenv("DISCORD_HOME_CHANNEL_NAME", "Main Discord")
|
||||
# Slack has a token but NO home — should be excluded from the list.
|
||||
monkeypatch.setenv("SLACK_BOT_TOKEN", "slack_fake")
|
||||
|
||||
|
||||
def test_home_channels_lists_only_platforms_with_home(client, with_home_channels):
|
||||
"""GET /home-channels returns entries only for platforms where the
|
||||
user has set a home; untoggled-subscribed bool is false by default."""
|
||||
r = client.get("/api/plugins/kanban/home-channels")
|
||||
assert r.status_code == 200
|
||||
platforms = {h["platform"] for h in r.json()["home_channels"]}
|
||||
assert platforms == {"telegram", "discord"}, (
|
||||
f"slack has a token but no home — must not appear. got {platforms}"
|
||||
)
|
||||
for h in r.json()["home_channels"]:
|
||||
assert h["subscribed"] is False
|
||||
|
||||
|
||||
def test_home_channels_no_task_id_all_unsubscribed(client, with_home_channels):
|
||||
"""Without task_id, every entry's subscribed=false (UI "no task" state)."""
|
||||
r = client.get("/api/plugins/kanban/home-channels")
|
||||
assert r.status_code == 200
|
||||
assert all(not h["subscribed"] for h in r.json()["home_channels"])
|
||||
|
||||
|
||||
def test_home_subscribe_creates_notify_sub_row(client, with_home_channels):
|
||||
"""POST .../home-subscribe/telegram writes a kanban_notify_subs row
|
||||
keyed to the telegram home's (chat_id, thread_id)."""
|
||||
from hermes_cli import kanban_db as kb
|
||||
t = client.post("/api/plugins/kanban/tasks", json={"title": "x"}).json()["task"]
|
||||
|
||||
r = client.post(f"/api/plugins/kanban/tasks/{t['id']}/home-subscribe/telegram")
|
||||
assert r.status_code == 200
|
||||
assert r.json()["ok"] is True
|
||||
|
||||
conn = kb.connect()
|
||||
try:
|
||||
subs = kb.list_notify_subs(conn, t["id"])
|
||||
finally:
|
||||
conn.close()
|
||||
assert len(subs) == 1
|
||||
assert subs[0]["platform"] == "telegram"
|
||||
assert subs[0]["chat_id"] == "1234567"
|
||||
assert subs[0]["thread_id"] == "42"
|
||||
|
||||
|
||||
def test_home_subscribe_flips_subscribed_flag_in_subsequent_get(client, with_home_channels):
|
||||
"""After subscribe, the GET endpoint reports subscribed=true for that
|
||||
platform and false for the others."""
|
||||
t = client.post("/api/plugins/kanban/tasks", json={"title": "x"}).json()["task"]
|
||||
client.post(f"/api/plugins/kanban/tasks/{t['id']}/home-subscribe/telegram")
|
||||
|
||||
r = client.get(f"/api/plugins/kanban/home-channels?task_id={t['id']}")
|
||||
flags = {h["platform"]: h["subscribed"] for h in r.json()["home_channels"]}
|
||||
assert flags == {"telegram": True, "discord": False}
|
||||
|
||||
|
||||
def test_home_subscribe_is_idempotent(client, with_home_channels):
|
||||
"""Re-subscribing keeps a single row at the DB layer."""
|
||||
from hermes_cli import kanban_db as kb
|
||||
t = client.post("/api/plugins/kanban/tasks", json={"title": "x"}).json()["task"]
|
||||
client.post(f"/api/plugins/kanban/tasks/{t['id']}/home-subscribe/telegram")
|
||||
client.post(f"/api/plugins/kanban/tasks/{t['id']}/home-subscribe/telegram")
|
||||
client.post(f"/api/plugins/kanban/tasks/{t['id']}/home-subscribe/telegram")
|
||||
conn = kb.connect()
|
||||
try:
|
||||
assert len(kb.list_notify_subs(conn, t["id"])) == 1
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def test_home_subscribe_unknown_platform_returns_404(client, with_home_channels):
|
||||
"""Platforms without a home configured (slack in the fixture) return 404."""
|
||||
t = client.post("/api/plugins/kanban/tasks", json={"title": "x"}).json()["task"]
|
||||
r = client.post(f"/api/plugins/kanban/tasks/{t['id']}/home-subscribe/slack")
|
||||
assert r.status_code == 404
|
||||
assert "slack" in r.json()["detail"]
|
||||
|
||||
|
||||
def test_home_subscribe_unknown_task_returns_404(client, with_home_channels):
|
||||
r = client.post("/api/plugins/kanban/tasks/t_nonexistent/home-subscribe/telegram")
|
||||
assert r.status_code == 404
|
||||
|
||||
|
||||
def test_home_unsubscribe_removes_notify_sub_row(client, with_home_channels):
|
||||
"""DELETE .../home-subscribe/telegram removes the matching row."""
|
||||
from hermes_cli import kanban_db as kb
|
||||
t = client.post("/api/plugins/kanban/tasks", json={"title": "x"}).json()["task"]
|
||||
client.post(f"/api/plugins/kanban/tasks/{t['id']}/home-subscribe/telegram")
|
||||
r = client.delete(f"/api/plugins/kanban/tasks/{t['id']}/home-subscribe/telegram")
|
||||
assert r.status_code == 200
|
||||
|
||||
conn = kb.connect()
|
||||
try:
|
||||
assert kb.list_notify_subs(conn, t["id"]) == []
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def test_home_subscribe_multiple_platforms_independent(client, with_home_channels):
|
||||
"""Subscribing on telegram does not affect discord and vice versa."""
|
||||
from hermes_cli import kanban_db as kb
|
||||
t = client.post("/api/plugins/kanban/tasks", json={"title": "x"}).json()["task"]
|
||||
|
||||
client.post(f"/api/plugins/kanban/tasks/{t['id']}/home-subscribe/telegram")
|
||||
client.post(f"/api/plugins/kanban/tasks/{t['id']}/home-subscribe/discord")
|
||||
|
||||
conn = kb.connect()
|
||||
try:
|
||||
subs = {s["platform"]: s for s in kb.list_notify_subs(conn, t["id"])}
|
||||
finally:
|
||||
conn.close()
|
||||
assert set(subs) == {"telegram", "discord"}
|
||||
|
||||
# Unsubscribe telegram only.
|
||||
client.delete(f"/api/plugins/kanban/tasks/{t['id']}/home-subscribe/telegram")
|
||||
conn = kb.connect()
|
||||
try:
|
||||
subs = {s["platform"]: s for s in kb.list_notify_subs(conn, t["id"])}
|
||||
finally:
|
||||
conn.close()
|
||||
assert set(subs) == {"discord"}
|
||||
|
||||
|
||||
def test_home_channels_empty_when_no_homes_configured(client, monkeypatch):
|
||||
"""Zero platforms with a home -> empty list (UI hides the section)."""
|
||||
# No BOT_TOKEN env vars set → load_gateway_config().platforms is empty.
|
||||
for var in [
|
||||
"TELEGRAM_BOT_TOKEN", "TELEGRAM_HOME_CHANNEL",
|
||||
"DISCORD_BOT_TOKEN", "DISCORD_HOME_CHANNEL",
|
||||
"SLACK_BOT_TOKEN",
|
||||
]:
|
||||
monkeypatch.delenv(var, raising=False)
|
||||
r = client.get("/api/plugins/kanban/home-channels")
|
||||
assert r.status_code == 200
|
||||
assert r.json()["home_channels"] == []
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue