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} 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") @app.post("/api/internal/setup-hooks")
async def setup_hooks() -> dict: async def setup_hooks() -> dict:
"""Re-register tmux hooks. Call after tmux server restarts.""" """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 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 # 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 assert local["remoteId"] is None
def test_federation_sessions_remote_id_is_integer_index( def test_federation_sessions_remote_id_is_integer_index(client, monkeypatch, tmp_path):
client, monkeypatch, tmp_path
):
"""GET /api/federation/sessions returns integer remoteId (index) for remote sessions. """GET /api/federation/sessions returns integer remoteId (index) for remote sessions.
remoteId must be the enumerate index (0, 1, 2...) of the remote in 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 # Mock local session data
monkeypatch.setattr("muxplex.main.get_session_list", lambda: ["alpha", "beta"]) monkeypatch.setattr("muxplex.main.get_session_list", lambda: ["alpha", "beta"])
monkeypatch.setattr( monkeypatch.setattr("muxplex.main.get_snapshots", lambda: {"alpha": "", "beta": ""})
"muxplex.main.get_snapshots", lambda: {"alpha": "", "beta": ""}
)
response = client.get("/api/federation/sessions") response = client.get("/api/federation/sessions")
assert response.status_code == 200 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 contents must match returned key. "
f"File: {file_contents!r}, key: {returned_key!r}" f"File: {file_contents!r}, key: {returned_key!r}"
) )
def test_get_auth_token_returns_401_when_not_authenticated(monkeypatch): def test_get_auth_token_returns_401_when_not_authenticated(monkeypatch):
"""GET /api/auth/token returns 401 when request has no valid session cookie.""" """GET /api/auth/token returns 401 when request has no valid session cookie."""
monkeypatch.setenv("MUXPLEX_PASSWORD", "test-password") monkeypatch.setenv("MUXPLEX_PASSWORD", "test-password")