ποΈ PUT-1761: tell the members when something is shared with the workspace - #3735
Conversation
Coverage Report
File Coverage
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
9c5dd19 to
6129272
Compare
6129272 to
b45f3e3
Compare
Review fixesA code review across the three phase 4 PRs raised 13 issues. All 13 are fixed; three land here. A blocked member was still told
The check belongs per-recipient, not per-share: the grant is one row against the group and cannot exclude a member, but the contact is exactly what a block refuses. Left deliberately: the member still holds the grant and can still reach the file. Excluding one member from a group grant is not representable without per-member exceptions, which this design does not have. Worth a reviewer's opinion on whether that is the right line. The fan-out cap was off by one, and re-read per item
Tests β all falsifiedβ The block test was vacuous twice before it worked, and both reasons are traps for anyone testing notifications here:
Full suite 7732 passed | 26 skipped, typecheck clean. Rebased onto current |
b45f3e3 to
874293c
Compare
Local validation (post-review)Engine: mysql 8 Β· The unit tests use a Both channels reach every member, and nobody elseOne share with the workspace, then the notification rows: And the mail actually delivered: The wording is "with you", as decided{"type": "share.received",
"title": "p4own80144 shared 3 items with you",
"fields": {"count": 3, "senders": [{"count": 3, "username": "p4own80144"}], ...}}The workspace is deliberately not named β that requirement was dropped, and The anti-spam property, which is why option 1 was acceptableLook at Per-seat email came out at one message per interruption window rather than one per share, for the same reason. β Not exercised in this run: Also not re-exercised here: the per-recipient hourly budget (10/hour). Three seats over a few shares stays well inside it. Full suite 7733 passed | 26 skipped, typecheck clean. |
874293c to
f25aee8
Compare
f25aee8 to
95e6d15
Compare
95e6d15 to
59d03c5
Compare
59d03c5 to
a98d8f5
Compare
|
Rebased onto One collision worth naming: those commits took migration Verified per commit rather than at the tip β each of the 9 typechecks on its own, so no intermediate commit is broken. Full suite 7883 passed | 26 skipped. |
a98d8f5 to
45e6494
Compare
| } | ||
|
|
||
| /** Members to announce a workspace share to; bounded, or the send is too. */ | ||
| async listMemberIdsByGroupId( |
Salazareo
left a comment
There was a problem hiding this comment.
feel free to merge after caching fixes
45e6494 to
332376b
Compare
Terminology: "workspace" is now "team", everywhereThe feature said Applied to every commit rather than added on top, so each PR shows its own code already named correctly and no intermediate commit references a name a later one changes. All 13 commits still typecheck individually. The contract-bearing renamesThese were free to change now and expensive after release, which is why they went in this pass:
Plus Scoped, and checked"workspace" means other things in this repo. These were deliberately left alone, and verified untouched:
Audited three ways: every changed line mentions team or workspace (no collateral edits), no stutters or mangled words ( One thing not renamedThe git branch names still say |
332376b to
e73fda1
Compare
Done. What is deliberately not cached: The part that took the thought
An existing test caught it immediately β this.services.team.emitSeatDeleted(seat);
if (seat) await this.services.team.forgetSeat(seat);Explicit busts also on VerificationNew test walks a roster through add β cascade-delete and asserts the list tracks it. Falsified both ways:
Backend 7893 passed | 26 skipped, typecheck clean, all 13 commits typecheck individually. |
e73fda1 to
d20b80c
Compare
Follow-up: the two authorization reads are cached now tooMy earlier reply said They are the hottest reads in the feature: every team route resolves the team through Every path that can change either value busts it:
The soft-delete case is the one that needed thinking about. Busting "every membership of this team" cannot be done by key pattern without a SCAN, so the member ids are read before the I also checked the weaker argument I could have leaned on: both Falsified individually β each bust removed on its own, only its own case fails: Four new tests, one per path, plus the roster test from before. Backend 7897 passed | 26 skipped, typecheck clean, all 13 commits typecheck individually and every test file parses at every commit. β Placement worth knowing: these three methods ( |
d20b80c to
3c0e1f8
Compare
`notifyShared` keyed every map off `ResolvedShare.holderId`. A team share has no individual holder -- the grant is one row against the group and `holder_user_id` is NULL -- so it was skipped everywhere and the call returned having told nobody. Nothing errored: the share was created, resolved and listed correctly. The failure mode was silence. `#recipientsOf` now expands a team share to its live members at announcement time, so the grant stays one row and only the telling fans out. `#announce` and `#emailHolder` are both inside the per-recipient loop, so members get the in-app notification and the email digest. Bounded by `NOTIFY_FANOUT_CAP`, which logs when it bites rather than truncating silently -- a shortened fan-out otherwise reads as "everyone knows". The existing per-recipient budgets absorb the volume from there. Wording is unchanged: "shared N items with you", not the team name. A member who joins later resolves the grant through the scan but is not retroactively told; announcements describe a moment, not a state.
3c0e1f8 to
212f7a5
Compare
Closes PUT-1761. Stacked on #3729.
The bug
Sharing with a workspace notified nobody, and said it had succeeded.
ShareNotificationService.notifySharedkeys every map offResolvedShare.holderIdβ one user id, documented as "who to notify". A workspace share has no individual holder: the grant is a single row against the group, andholder_user_idon the index row is NULL. Socounts,namedandtargetsall skipped it and the call returned normally.Nothing errored. The share was created correctly, resolved correctly for every member, and appeared in their listings. The failure mode was silence, which is why it needs a test that counts notifications β nothing else catches it.
The fix
#recipientsOf()expands a workspace share to its live members at announcement time. The grant stays one row; only the telling fans out.Because
#announceand#emailHolderwere already inside the same per-recipient loop, expanding the recipients fans out both channels β in-app notification and email digest β with no separate email path.TeamStore.listMemberIdsByGroupId()is the lookup, scoped to live workspaces.The anti-spam decision
PUT-1761 listed three options and asked for a deliberate choice. Option 1: expand per member, and let the existing per-recipient budgets (10/hour, 50/day) plus the digest absorb it. Those budgets were designed for exactly this shape.
The fan-out is bounded at
NOTIFY_FANOUT_CAP = 500β far above the default 50-seat cap, so it bounds a pathological workspace rather than a real one β and it logs when it bites instead of truncating silently. A shortened fan-out otherwise reads as "everyone knows".Wording deliberately unchanged
The ticket originally asked for the notification to name the workspace ("alice shared 3 items with Acme Design"). That was built and then reverted on review: it needed a
viafield threaded throughShareSender,mergeShareSender,DigestEntryanddigestSubject, plus a rule for what to say when one person receives a direct share and a workspace share inside the same grouping window.shareNotifyTitle.tsis therefore unchanged in this PR β the wording stays "shared N items with you" in both channels. The ticket has been updated to record that.Tests
TeamShareNotify.test.ts, five tests on the two-workspace fixture:The email leg uses a
sendRawspy, so the digest renders for real and nothing leaves the process.Falsified, not just run. Disabling the expansion reproduces the original silent skip:
Worth noting the three negative tests pass either way β they are trivially true when nobody is told. That is the trap this ticket warns about, and it is why the two positive tests are the ones carrying the coverage.
Full suite: 7530 passed | 26 skipped. Typecheck clean.
Note for review
listMemberIdsByGroupIdis a new unbounded-by-default read on a notification path. It takes an explicit limit with a documented cap rather than relying on the seat cap, becausemax_seats_per_workspaceis configurable and a deployment that raises it should not silently turn one share into thousands of sends.