diff options
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 |