diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-09-02 12:55:05 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-09-02 12:55:05 +0200 |
| commit | 16b5e5c463ec998c6434bbf2ef39113ed01da801 (patch) | |
| tree | e8c7f65517af3235013a660641079d02251e679f /packages/meshbay-hub/tests | |
| parent | a194b333169efb5e25bc05c94567600eec8bb823 (diff) | |
| download | meshbay-16b5e5c463ec998c6434bbf2ef39113ed01da801.tar.gz | |
fix(hub): dismissing a notification deletes it
The previous commit filtered the list to unread, which corrected what the
reader saw and left every dismissed row in the table, invisible for ever. That
is a place to hide the disagreement rather than a resolution, and the operator
said so: "elles s'accumulent pour rien."
So dismissing drops the row. It is the reasoning `purge_notifications` has
carried all along — "these are signals, not a record: the group is still
there, the message is still in the chat, the invitation is still an
invitation" — applied one at a time instead of only in bulk.
- `DELETE /v1/notifications/{id}` is the honest name and what the SPA calls.
- `POST /{id}/read` reaches the same handler and now deletes too. It has to
keep working: the interface ships inside the desktop package, so a hub is
always answering some client older than itself, and giving the old path the
new behaviour means those clients stop hoarding as well rather than only the
updated ones.
- `read-all` deletes rather than marking, which makes it `DELETE ""` under an
older name. Marking would have made it the one route still filling the
table. Nothing in this repo calls it, but a reachable endpoint is one that
can be called.
`Notification.read` is now vestigial — nothing stored can be read, because
reading it deletes it. It stays because dropping a column is a migration for
no gain, and `unread_only` stays because a SPA newer than its hub still needs
it to be right. Both are said in the module docstring rather than left to be
worked out.
Two existing tests encoded the old semantics and now assert the opposite;
test_notification_dismissal.py gains one for the old `/read` path, because
version skew is the normal case here and not the exception. 617 hub tests
pass. docs/USERGUIDE.md's endpoint table updated in both places it lists them.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014UtzVrzM7e2tG9fSpkR9ML
Diffstat (limited to 'packages/meshbay-hub/tests')
| -rw-r--r-- | packages/meshbay-hub/tests/test_notification_dismissal.py | 48 | ||||
| -rw-r--r-- | packages/meshbay-hub/tests/test_notifications.py | 13 |
2 files changed, 49 insertions, 12 deletions
diff --git a/packages/meshbay-hub/tests/test_notification_dismissal.py b/packages/meshbay-hub/tests/test_notification_dismissal.py index 7324656..07cf715 100644 --- a/packages/meshbay-hub/tests/test_notification_dismissal.py +++ b/packages/meshbay-hub/tests/test_notification_dismissal.py @@ -15,8 +15,15 @@ notification came straight back. The feed still carried the fossil of the older intent — `class="notif-item ${n.read ? '' : 'notif-unread'}"`, styling for a read entry rendered greyed out, from before clicking meant dismissing. -So the contract is between the two halves, and that is what this pins: the -hub's filter behaves, and the SPA asks for it. +Filtering the list to unread was the first fix. It corrected what the user saw +and left the rows accumulating for nothing, invisible for ever, which is not a +resolution so much as a place to hide the disagreement. **Dismissing one now +deletes it** — the reasoning `purge_notifications` always carried, applied one +at a time: these are signals, not a record. + +So what this pins is the contract between the two halves: the hub drops the +row by either path, old client or new, and the SPA still sends the filter for +the case where it is newer than the hub it is talking to. """ import re @@ -91,19 +98,42 @@ async def test_a_read_notification_is_gone_from_what_the_spa_asks_for(client): @pytest.mark.asyncio -async def test_without_the_filter_it_does_come_back(client): - """The other half of the same fact, so the fix cannot be read as a - coincidence: unfiltered, the endpoint still returns it, and that is the - request the SPA used to make.""" +async def test_it_is_gone_from_the_unfiltered_list_too(client): + """The filter is no longer what makes it disappear — the row is. + + This assertion is the inverse of what it was. Filtering to unread fixed + what the user saw and left the rows behind, invisible for ever; dismissing + now deletes, so even a client that asks for everything sees nothing. The + filter stays because an interface older than the hub still sends it. + """ token = await _login_and_notify(client) auth = {"Authorization": f"Bearer {token}"} nid = (await client.get("/v1/notifications", headers=auth)).json()["notifications"][0]["id"] - await client.post(f"/v1/notifications/{nid}/read", headers=auth) + await client.delete(f"/v1/notifications/{nid}", headers=auth) unfiltered = (await client.get("/v1/notifications?limit=20", headers=auth)).json() - assert len(unfiltered["notifications"]) == 1 - assert unfiltered["notifications"][0]["read"] is True + assert unfiltered["notifications"] == [] + + +@pytest.mark.asyncio +async def test_the_old_read_path_dismisses_too(client): + """Version skew is the normal case here, not the exception. + + The SPA ships inside the desktop package, so a hub is always talking to + some interface older than itself — CLAUDE.md records that as a standing + consequence of shipping the UI in a package. An old client calling + `/read` must stop accumulating rows as well, or the fix only reaches + whoever updated. + """ + token = await _login_and_notify(client) + auth = {"Authorization": f"Bearer {token}"} + + nid = (await client.get("/v1/notifications", headers=auth)).json()["notifications"][0]["id"] + r = await client.post(f"/v1/notifications/{nid}/read", headers=auth) + assert r.status_code == 200 + + assert (await client.get("/v1/notifications", headers=auth)).json()["notifications"] == [] # `_one_notification_for_alice` is the setup; this alias keeps the tests reading diff --git a/packages/meshbay-hub/tests/test_notifications.py b/packages/meshbay-hub/tests/test_notifications.py index 8da589a..35ed288 100644 --- a/packages/meshbay-hub/tests/test_notifications.py +++ b/packages/meshbay-hub/tests/test_notifications.py @@ -92,7 +92,7 @@ async def test_notification_on_suspend(client): @pytest.mark.asyncio -async def test_mark_notification_read(client): +async def test_dismissing_one_deletes_it(client): _, admin_token = await _setup_admin(client) uid = await _register(client, "alice", email="a@x.com") alice_token = await _login(client, "alice") @@ -105,6 +105,8 @@ async def test_mark_notification_read(client): headers={"Authorization": f"Bearer {alice_token}"}) nid = r.json()["notifications"][0]["id"] + # `/read` is the old path and still the one older clients call. It + # dismisses, like DELETE — see api/notifications.py. r = await client.post(f"/v1/notifications/{nid}/read", headers={"Authorization": f"Bearer {alice_token}"}) assert r.status_code == 200 @@ -112,11 +114,13 @@ async def test_mark_notification_read(client): r = await client.get("/v1/notifications", headers={"Authorization": f"Bearer {alice_token}"}) assert r.json()["unread_count"] == 0 - assert r.json()["notifications"][0]["read"] is True + assert r.json()["notifications"] == [], ( + "a dismissed notification is deleted, not kept as a row nothing can " + "ever show again") @pytest.mark.asyncio -async def test_mark_all_read(client): +async def test_dismissing_all_deletes_them(client): _, admin_token = await _setup_admin(client) uid = await _register(client, "alice", email="a@x.com") alice_token = await _login(client, "alice") @@ -135,10 +139,13 @@ async def test_mark_all_read(client): r = await client.post("/v1/notifications/read-all", headers={"Authorization": f"Bearer {alice_token}"}) assert r.status_code == 200 + assert r.json()["removed"] == 2 r = await client.get("/v1/notifications", headers={"Authorization": f"Bearer {alice_token}"}) assert r.json()["unread_count"] == 0 + assert r.json()["notifications"] == [], ( + "read-all must not be the one route that still hoards rows") @pytest.mark.asyncio |