feat: add POST /api/sessions/{name}/bell/clear endpoint

This commit is contained in:
Brian Krabach
2026-04-04 06:46:31 -07:00
parent 42142c0954
commit 5f9d6a593e
2 changed files with 82 additions and 6 deletions
+18
View File
@@ -516,6 +516,24 @@ async def receive_bell(name: str) -> dict:
return {"ok": True, "session": name}
@app.post("/api/sessions/{name}/bell/clear")
async def clear_bell(name: str) -> dict:
"""Clear unseen bell count for session *name*.
Resets unseen_count to 0 and sets seen_at to now.
Called by the frontend when a user opens a session to acknowledge bells.
No-op if the session or bell sub-dict does not exist.
"""
async with state_lock:
state = load_state()
session = state.get("sessions", {}).get(name)
if session and "bell" in session:
session["bell"]["unseen_count"] = 0
session["bell"]["seen_at"] = time.time()
save_state(state)
return {"ok": True, "session": name}
@app.post("/api/internal/setup-hooks")
async def setup_hooks() -> dict:
"""Re-register tmux hooks. Call after tmux server restarts."""
+64 -6
View File
@@ -543,6 +543,66 @@ def test_receive_bell_sets_last_fired_at(client):
assert before <= bell["last_fired_at"] <= after
# ---------------------------------------------------------------------------
# POST /api/sessions/{name}/bell/clear
# ---------------------------------------------------------------------------
def test_bell_clear_returns_ok(client):
"""POST /api/sessions/{name}/bell/clear returns {\"ok\": True, \"session\": name}."""
response = client.post("/api/sessions/web-tmux/bell/clear")
assert response.status_code == 200
data = response.json()
assert data["ok"] is True
assert data["session"] == "web-tmux"
def test_bell_clear_resets_unseen_count(client):
"""After clearing, unseen_count is 0."""
from muxplex.state import load_state
# First fire some bells to set unseen_count > 0
for _ in range(3):
client.post("/api/sessions/clear-test/bell")
state = load_state()
assert state["sessions"]["clear-test"]["bell"]["unseen_count"] == 3
# Now clear
client.post("/api/sessions/clear-test/bell/clear")
state = load_state()
assert state["sessions"]["clear-test"]["bell"]["unseen_count"] == 0
def test_bell_clear_sets_seen_at(client):
"""After clearing, seen_at is set to a recent timestamp."""
import time
from muxplex.state import load_state
# Fire a bell first to create the bell state
client.post("/api/sessions/seen-test/bell")
before = time.time()
client.post("/api/sessions/seen-test/bell/clear")
after = time.time()
state = load_state()
bell = state["sessions"]["seen-test"]["bell"]
assert bell["seen_at"] is not None
assert before <= bell["seen_at"] <= after
def test_bell_clear_noop_when_no_session(client):
"""No-op when session has no bell state (still returns 200 + ok)."""
response = client.post("/api/sessions/nonexistent-session/bell/clear")
assert response.status_code == 200
data = response.json()
assert data["ok"] is True
assert data["session"] == "nonexistent-session"
# ---------------------------------------------------------------------------
# POST /api/internal/setup-hooks
# ---------------------------------------------------------------------------
@@ -1571,9 +1631,7 @@ def test_federation_sessions_returns_local_sessions(client, monkeypatch, tmp_pat
assert local["remoteId"] is None
def test_federation_sessions_remote_id_is_integer_index(
client, monkeypatch, tmp_path
):
def test_federation_sessions_remote_id_is_integer_index(client, monkeypatch, tmp_path):
"""GET /api/federation/sessions returns integer remoteId (index) for remote sessions.
remoteId must be the enumerate index (0, 1, 2...) of the remote in
@@ -1684,9 +1742,7 @@ def test_federation_sessions_local_sessions_have_no_session_key(
# Mock local session data
monkeypatch.setattr("muxplex.main.get_session_list", lambda: ["alpha", "beta"])
monkeypatch.setattr(
"muxplex.main.get_snapshots", lambda: {"alpha": "", "beta": ""}
)
monkeypatch.setattr("muxplex.main.get_snapshots", lambda: {"alpha": "", "beta": ""})
response = client.get("/api/federation/sessions")
assert response.status_code == 200
@@ -2135,6 +2191,8 @@ def test_federation_generate_key_creates_file(client, tmp_path, monkeypatch):
f"File contents must match returned key. "
f"File: {file_contents!r}, key: {returned_key!r}"
)
def test_get_auth_token_returns_401_when_not_authenticated(monkeypatch):
"""GET /api/auth/token returns 401 when request has no valid session cookie."""
monkeypatch.setenv("MUXPLEX_PASSWORD", "test-password")