fix(teams): make "replace the whole set" actually replace it
All checks were successful
PR Checks / bot-install (pull_request) Successful in 17s
PR Checks / client-build (pull_request) Successful in 30s
PR Checks / server-tests (pull_request) Successful in 8m53s

Found walking the live rig, which is the only place it could be found: every unit
test and the settings screen itself send every row, so the bug was invisible to
both.

`PUT /auth/me/notifications/teams` documents itself as replacing the whole set. It
did not — it wrote the entries it was given and left every other preference
standing. So `{"teams": []}` cleared nothing, which is precisely the body the route
requires the array for: the field is mandatory even when empty so that clearing
everything is expressible, and it was the one thing that did not work.

A Team the caller could have named and did not now returns to its defaults. RESET
rather than deleted, and the difference is `last_digest_at`: that column is the
digest worker's state and not a preference, so dropping the row with it would make
every visit to the settings screen re-open a day-wide digest window and mail
somebody a summary they had already read.

Walked again after the fix on the real database: the empty set clears, an entry
naming a Team the caller is not in is still dropped, and the digest stamp survives.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-08-18 18:01:59 -05:00
parent 5fa88baa0a
commit 13312d7fc3
2 changed files with 48 additions and 0 deletions

View File

@@ -82,6 +82,24 @@ async function replacePrefs(userId, entries) {
await db.setPref(userId, teamId, { muted: Boolean(entry.muted), emailMode })
written.push(teamId)
}
// A Team the caller COULD have named and did not is returned to its defaults.
//
// Without this, "replace the whole set" was a lie the endpoint told: omitting an
// entry left the old preference standing, which made `teams: []` — the body the
// route requires precisely so that clearing everything is expressible — clear
// nothing at all.
//
// Reset rather than deleted, and the difference is `last_digest_at`. That column
// is the digest worker's state, not a preference; dropping the row with it would
// make every visit to the settings screen re-open a day-wide digest window and
// mail somebody a summary they already read.
for (const teamId of allowed.keys()) {
if (written.includes(teamId)) continue
// eslint-disable-next-line no-await-in-loop
await db.setPref(userId, teamId, { muted: false, emailMode: 'off' })
}
return { written, prefs: await listPrefs(userId) }
}