Skip to content

fix(room-worker): fix org-member duplication and empty sys-message content#185

Merged
mliu33 merged 15 commits into
mainfrom
claude/fix-org-membership-duplication-l5LfI
May 18, 2026
Merged

fix(room-worker): fix org-member duplication and empty sys-message content#185
mliu33 merged 15 commits into
mainfrom
claude/fix-org-membership-duplication-l5LfI

Conversation

@vjauhari-work
Copy link
Copy Markdown
Collaborator

@vjauhari-work vjauhari-work commented May 15, 2026

Summary

Fixes three room-worker bugs affecting membership management and system messages:

  1. Org-member duplication: Users added via org expansion were getting both individual and org room_members docs, appearing twice in listings
  2. Empty members_added content: System messages lacked Content field, rendering blank in UI
  3. Missing sender and empty content in member_removed/member_left: System messages had no UserAccount or Content, showing "Unknown sent the message" with blank body

All fixes are internal to room-worker with no wire-schema or data-migration changes.

Key Changes

New Files

  • room-worker/sysmsg.go: Five formatter functions (formatAddedSingle, formatAddedMulti, formatRemovedUser, formatRemovedOrg, formatLeft) and a displayName helper that renders user names from EngName and ChineseName fields. Includes validateUserNames to enforce non-empty name fields required for system-message rendering.
  • room-worker/sysmsg_test.go: Unit tests for all formatter functions, including edge cases for empty name fields.
  • pkg/model/room_test.go: Tests for BuildDMParticipants helper (sorting and account permutation).

Modified Files

room-worker/handler.go

  • Backfill gate tightening (§2.2): Restructured HasOrgRoomMembers call to always execute first, then tighten backfill condition to fire only on first-org transition (len(req.Orgs) > 0 && !hadOrgsBefore), preventing re-introduction of duplication on subsequent org adds.
  • Individual room_members filter in processAddMembers (§2.1): Added whitelist check so individual docs are written only for accounts in req.Users, not for all org-expanded members.
  • Individual room_members filter in processCreateRoomChannel (§2.1): Added whitelist check so individual docs are written only for accounts in req.ResolvedUsers ∪ {requester.Account}.
  • Requester fetch and validation in processAddMembers (§2.3): Added store.GetUser call to fetch requester and validate both requester and added users have non-empty EngName and ChineseName fields; permanent error on miss or empty names.
  • System-message content in processRemoveIndividual (§2.4):
    • Set UserAccount to requester (sender envelope)
    • Populate Content using formatLeft or formatRemovedUser based on whether removal is self-leave or forced
    • Use model constants for message types (MessageTypeMemberLeft, MessageTypeMemberRemoved, OutboxMemberRemoved)
  • System-message content in processRemoveOrg (§2.4):
    • Set UserAccount to requester
    • Populate Content using formatRemovedOrg with org's SectName
  • System-message content in publishChannelSysMessages (§2.5):
    • For members_added: set UserAccount to requester and Content using formatAddedSingle (single user) or formatAddedMulti (multiple users)
    • For members_added with single user: use formatAddedSingle(requester, addedUser)
    • For members_added with multiple users: use formatAddedMulti(requester)

room-worker/handler_test.go

  • Updated all existing processAddMembers tests to include RequesterAccount in request and mock GetUser expectation with valid name fields
  • Updated all processRemoveIndividual and processRemoveOrg tests to include EngName and ChineseName in user fixtures
  • Added new test TestHandler_ProcessRemoveMember_OwnerRemovesOrg

https://claude.ai/code/session_01WDo6nRMn4Fm2jwMR7bV6Pe

Summary by CodeRabbit

  • New Features

    • DM/botDM rooms now persist ordered participant identifiers (uids/accounts) for deterministic pairing.
    • System messages include explicit server-rendered human-readable content and identify the responsible actor.
  • Bug Fixes

    • Improved member add/remove/leave handling with clearer self-vs-other semantics and org-removal fixes.
    • Stronger validation for required user name fields and stricter request-ID checks; backfill/backfill-trigger tightened.
  • Documentation

    • API docs updated to document DM participant fields and known system-message types.
  • Tests

    • Expanded tests for sys-message formatting, membership/backfill semantics, DM persistence, and sync/async DM flows.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 15, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds sys-message formatting and name-validation helpers; introduces typed member-left/removed message constants; persists DM participant uids/accounts with deterministic pairing; extends SubscriptionStore and Mongo impl for UpdateDMParticipants; wires DM participant update into sync/async create flows; updates handler logic for validated adds/removes, backfill gating, and formatted system messages; and adds comprehensive tests and docs.

Changes

Room-worker membership fixes & DM participant persistence

Layer / File(s) Summary
Model: message types & DM participant shape and helpers
pkg/model/event.go, pkg/model/room.go, pkg/model/model_test.go, pkg/model/room_test.go
Adds MessageTypeMemberRemoved/MessageTypeMemberLeft constants, Room.UIDs/Room.Accounts fields with omitempty, BuildDMParticipants helper (deterministic pairing), and JSON/tests to validate DM participant encoding/omission.
System-message formatters & validation helpers
room-worker/sysmsg.go, room-worker/sysmsg_test.go
Adds displayName/validateUserNames helpers and formatter functions for members-added/removed/left/org-removed sys-messages, with unit tests asserting exact rendered strings and permanent-error validation semantics.
Subscription store + Mongo + mock
room-worker/store.go, room-worker/store_mongo.go, room-worker/mock_store_test.go
Adds SubscriptionStore.UpdateDMParticipants to interface, implements MongoStore.UpdateDMParticipants to $set uids/accounts, and updates GoMock with UpdateDMParticipants mock.
Handler: add/remove/create flows, typing, formatting, DM participant wiring
room-worker/handler.go
Validates requester/added/removed user names, switches member event types and outbox subjects to typed constants, computes formatted Content and UserAccount for member sys-messages, tightens org-backfill to first-org transition with allowedIndividual filtering, computes DM participant slices via BuildDMParticipants, calls UpdateDMParticipants for async creates, and populates room.UIDs/room.Accounts on sync create (duplicate-key forward-only).
Handler test updates & new test suite
room-worker/handler_test.go
Expands fixtures with EngName/ChineseName and testRequestID, and adds many tests covering backfill gating, individual/org filtering, permanent-error paths, sys-message content and sender assertions, DM participant persistence, sync DM validation/idempotency/outbox behavior, and async-job error sanitization.
Docs, plan and spec updates
docs/client-api.md, docs/superpowers/plans/2026-05-14-room-worker-membership-fixes.md, docs/superpowers/specs/2026-05-13-room-worker-membership-fixes-design.md
Documents Client API Room/Message schema changes (DM uids/accounts and enumerated system-message types), provides an implementation plan and design spec outlining the membership/sys-message fixes, DM participant persistence, and acceptance criteria.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • hmchangw/chat#79: Prior PR touching member removal typing/content and related handler surfaces.
  • hmchangw/chat#118: Overlapping work on room-worker membership handling and handler logic.
  • hmchangw/chat#145: Related changes around member add/remove publishing and outbox behavior.

Suggested reviewers

  • mliu33

"I'm a rabbit who trims the names just right,
I sort DM pairs in morning light,
I stamp messages with typed delight,
I wire the store and tests take flight,
and handlers hop clean through the night." 🐇✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 46.32% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'fix(room-worker): fix org-member duplication and empty sys-message content' accurately summarizes the main changes: it fixes org-member duplication and empty system message content issues in room-worker, matching the primary objectives.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/fix-org-membership-duplication-l5LfI

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (1)
room-worker/handler.go (1)

479-481: ⚡ Quick win

Use message-type constants for MemberRemoveEvent.Type.

On Line 480 and Line 542, MemberRemoveEvent.Type is set from model.OutboxMemberRemoved. Even if literals currently match, this couples member-event payload semantics to outbox constants. Prefer model.MessageTypeMemberRemoved for payload contract clarity.

💡 Suggested fix
-			Type:      model.OutboxMemberRemoved,
+			Type:      model.MessageTypeMemberRemoved,
...
-			Type:      model.OutboxMemberRemoved,
+			Type:      model.MessageTypeMemberRemoved,

Also applies to: 541-543

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@room-worker/handler.go` around lines 479 - 481, The MemberRemoveEvent is
using the outbox constant model.OutboxMemberRemoved for its Type, which couples
payload semantics to outbox constants; change the assignment in the memberEvt
creation (and the other occurrence where MemberRemoveEvent.Type is set) to use
the payload-specific constant model.MessageTypeMemberRemoved instead (update the
Type field in the MemberRemoveEvent initialization and any subsequent
assignments that set Type from model.OutboxMemberRemoved).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/client-api.md`:
- Line 950: The system-message doc row uses non-schema names `content` and
`userAccount`; update that text to use the schema-consistent field names `msg`
and `sender` (e.g., "For all four, `msg` is populated with a server-rendered
human-readable body and `sender` is the responsible actor..."), keeping the
`type` values and examples intact so clients can map directly to the Message
fields `type`, `msg`, and `sender`.

In `@docs/superpowers/plans/2026-05-14-room-worker-membership-fixes.md`:
- Around line 1207-1211: The verification step wrongly expects no diffs after
running `make generate SERVICE=room-worker` because later changes expand the
`SubscriptionStore` interface and legitimately regenerate `mock_store_test.go`;
update the Step 3 checkpoint (and the duplicate at 1383-1387) to instead assert
that generated changes are intentional or to run `git diff --stat` and compare
against an approved golden (or skip this check when `SubscriptionStore` was
modified), and mention `SubscriptionStore` and `mock_store_test.go` by name so
reviewers know to accept the regenerated mock output rather than treating it as
a failure.
- Around line 7-8: The plan header incorrectly states "no store/model changes"
while Tasks 11–12 introduce model fields and a store API; update the plan text
and affected task summaries to reflect the true scope by noting the
Room.UIDs/Accounts model additions, the new BuildDMParticipants helper, and the
store interface change UpdateDMParticipants, and adjust execution/review
checkpoints accordingly (also ensure internal files referenced like
room-worker/sysmsg.go and handlers processAddMembers, processCreateRoomChannel,
processRemoveIndividual, processRemoveOrg, publishChannelSysMessages mention the
dependency on these model/store changes where relevant).

In `@docs/superpowers/specs/2026-05-13-room-worker-membership-fixes-design.md`:
- Around line 94-95: The documentation contradicts itself by stating "No store
interface changes" while the diff adds UpdateDMParticipants to the
SubscriptionStore; update the spec so the summary and any top-level bullets
reflect that SubscriptionStore has been extended, or remove the added method
from the diff. Specifically, ensure the document text around the header that
currently claims no store changes is edited to mention the new SubscriptionStore
method UpdateDMParticipants (or revert the addition of UpdateDMParticipants) so
implementers see a consistent contract referencing the SubscriptionStore
interface and the new UpdateDMParticipants operation.

In `@room-worker/handler.go`:
- Around line 683-688: The code currently treats an error from
h.store.HasOrgRoomMembers(ctx, req.RoomID) as hadOrgsBefore=false and continues;
instead, fail closed by returning the error (or wrapping it) so the handler
retries rather than mutating state. Update the block around HasOrgRoomMembers in
handler.go to check err and return it (or a wrapped context-aware error)
immediately instead of logging and continuing; ensure downstream logic that sets
writeIndividuals (which depends on hadOrgsBefore and req.Orgs) only runs when
HasOrgRoomMembers successfully returned.

In `@room-worker/store_mongo.go`:
- Around line 138-147: UpdateDMParticipants currently treats UpdateOne miss as
success; after calling s.rooms.UpdateOne(ctx, ...), inspect the returned
UpdateResult (the first return value) and if result.MatchedCount == 0 return an
error indicating no room matched for the given roomID (e.g., "no room found" or
similar). Keep the existing error wrapping for actual UpdateOne errors from
s.rooms.UpdateOne and only return the new "no match" error when there is no
match; reference the function UpdateDMParticipants and the s.rooms.UpdateOne
call to locate where to add the check.

---

Nitpick comments:
In `@room-worker/handler.go`:
- Around line 479-481: The MemberRemoveEvent is using the outbox constant
model.OutboxMemberRemoved for its Type, which couples payload semantics to
outbox constants; change the assignment in the memberEvt creation (and the other
occurrence where MemberRemoveEvent.Type is set) to use the payload-specific
constant model.MessageTypeMemberRemoved instead (update the Type field in the
MemberRemoveEvent initialization and any subsequent assignments that set Type
from model.OutboxMemberRemoved).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 10590e1e-fee2-461f-8cc4-ba88a645ad2f

📥 Commits

Reviewing files that changed from the base of the PR and between 80beb86 and 11c3f44.

📒 Files selected for processing (14)
  • docs/client-api.md
  • docs/superpowers/plans/2026-05-14-room-worker-membership-fixes.md
  • docs/superpowers/specs/2026-05-13-room-worker-membership-fixes-design.md
  • pkg/model/event.go
  • pkg/model/model_test.go
  • pkg/model/room.go
  • pkg/model/room_test.go
  • room-worker/handler.go
  • room-worker/handler_test.go
  • room-worker/mock_store_test.go
  • room-worker/store.go
  • room-worker/store_mongo.go
  • room-worker/sysmsg.go
  • room-worker/sysmsg_test.go

Comment thread docs/client-api.md Outdated
Comment thread docs/superpowers/plans/2026-05-14-room-worker-membership-fixes.md Outdated
Comment thread docs/superpowers/specs/2026-05-13-room-worker-membership-fixes-design.md Outdated
Comment thread room-worker/handler.go
Comment thread room-worker/store_mongo.go Outdated
@vjauhari-work vjauhari-work force-pushed the claude/fix-org-membership-duplication-l5LfI branch from 11c3f44 to 0bd38b0 Compare May 15, 2026 05:57
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

♻️ Duplicate comments (2)
docs/client-api.md (1)

948-948: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use the history-service field names in this schema note.

This table is documenting the projected Message payload, so the note should reference msg and sender, not content and userAccount. As written, clients are sent looking for fields that are absent from this response shape.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/client-api.md` at line 948, Update the schema note for the Message
payload to use the history-service field names: refer to `msg` instead of
`content` and `sender` instead of `userAccount`; keep the `type` values
(`"room_created"`, `"members_added"`, `"member_removed"`, `"member_left"`) and
clarify that for those system types `msg` contains the server-rendered
human-readable body and `sender` is the responsible actor (e.g., the requester
for adds/removes-by-other and room-creates, and the leaving user for
self-leave).
room-worker/handler.go (1)

683-686: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fail closed when HasOrgRoomMembers cannot be read.

If this lookup fails and hadOrgsBefore silently falls back to false, the handler can either re-run first-org backfill or skip required individual room_members writes. That reopens the duplication/missing-doc cases this patch is trying to eliminate.

Suggested fix
 hadOrgsBefore, err := h.store.HasOrgRoomMembers(ctx, req.RoomID)
 if err != nil {
-	slog.Warn("check existing org room members failed", "error", err, "roomID", req.RoomID)
+	return fmt.Errorf("check existing org room members: %w", err)
 }
 writeIndividuals := len(req.Orgs) > 0 || hadOrgsBefore

As per coding guidelines "Always wrap errors with context using fmt.Errorf("short description: %w", err)".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@room-worker/handler.go` around lines 683 - 686, The call to
h.store.HasOrgRoomMembers currently swallows errors and leaves hadOrgsBefore
false; change this to "fail closed" by returning the error instead of just
logging: if h.store.HasOrgRoomMembers(ctx, req.RoomID) returns an error, return
fmt.Errorf("check existing org room members: %w", err) (or otherwise propagate
the wrapped error) so the handler aborts rather than proceeding with a
potentially incorrect hadOrgsBefore; update the code around the hadOrgsBefore
assignment in handler.go to replace the slog.Warn branch with error propagation
using fmt.Errorf.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/superpowers/plans/2026-05-14-room-worker-membership-fixes.md`:
- Around line 21-22: The markdown links to code files in this plan use
repository-relative paths like room-worker/handler.go which won't resolve from
docs/superpowers/plans; update those links to proper repo-root-relative paths by
prefixing with the correct number of ../ (e.g., ../../.. or ../../ depending on
directory depth) so they point to the actual files; specifically fix links that
reference room-worker/handler.go and room-worker/handler_test.go (and their
anchors for functions like processAddMembers, processCreateRoomChannel,
processRemoveIndividual, processRemoveOrg, publishChannelSysMessages) and apply
the same normalization to the other occurrences noted around lines 26 and 30–33.

In `@docs/superpowers/specs/2026-05-13-room-worker-membership-fixes-design.md`:
- Around line 21-23: The doc uses relative source links like
room-worker/handler.go#L649-L661 which are not resolvable from
docs/superpowers/specs/..., so update all broken links (e.g., the references for
processAddMembers and processCreateRoomChannel) to correct relative paths or
absolute repository paths that point to the real file and line anchors; search
for occurrences of room-worker/handler.go#... (also at the other noted lines
26,30,48,51-52,54,62,72,74) and replace them with the proper path from the docs
directory (or a full GitHub/Repo URL with `#L`... anchors) so reviewers can
navigate directly to the handler.go locations for functions like
processAddMembers and processCreateRoomChannel.

In `@room-worker/handler_test.go`:
- Around line 3412-3433: TestHandler_ProcessAddMembers_AddedUserEmptyName
currently asserts processAddMembers returns errPermanent but does not verify the
async-job error was published; update the test to capture/inspect the
Handler.publish call (or replace publish with a test stub) and assert that an
async-job error event was published for RequestID "test-req-task5-d3" with
status indicating failure and a sanitized error message when processAddMembers
returns errPermanent; place this assertion after the require.Error and
assert.ErrorIs checks and reference the Handler.publish stub and
processAddMembers function when implementing the verification.
- Around line 3362-3384: The test
TestHandler_ProcessAddMembers_RequesterNotFound fails to assert that
publishAsyncJobResult was called for permanent errors; update the test to verify
the async-job error publication by instrumenting the Handler.publish callback
(or mocking publishAsyncJobResult) and asserting that when processAddMembers
returns errPermanent it also published an async-job result to the requester's
response subject with status "error" and a sanitized message that contains
"missing-requester" — use the same pattern as the other permanent-error tests
(they assert publish was invoked with the request ID from
natsutil.WithRequestID, status "error", and the sanitized error string) and
refer to Handler.processAddMembers, publish (or publishAsyncJobResult) and
errPermanent to locate the relevant test and assertions.
- Around line 3387-3409: The test
TestHandler_ProcessAddMembers_RequesterEmptyName fails to assert that the
handler publishes an async-job error for permanent errors; update the test to
wrap the Handler.publish with a mock/assertion that captures the published
subject/payload when processAddMembers returns errPermanent and verify the
published event contains the request ID from natsutil.WithRequestID(ctx), a
status indicating a permanent error, and a sanitized error message; reference
the Handler.publish function and the processAddMembers call and assert the
published payload fields (status and message) match the permanent-error contract
for async-job events.

In `@room-worker/handler.go`:
- Around line 572-575: The code currently accepts any non-empty request ID from
RequestIDFromContext and returns newPermanent("missing X-Request-ID") only when
empty; update these handlers to validate the format using idgen.IsValidUUID and
reject non-UUIDs the same way handleSyncCreateDM does: after obtaining requestID
:= natsutil.RequestIDFromContext(ctx), call idgen.IsValidUUID(requestID)
(case-insensitive, hyphenated v4/v7) and return newPermanent("invalid
X-Request-ID") when it returns false; apply this change to the occurrences
around RequestIDFromContext in the current block and the other location
referenced (lines ~945-948) so reply subjects and deterministic IDs only receive
validated UUIDs.

---

Duplicate comments:
In `@docs/client-api.md`:
- Line 948: Update the schema note for the Message payload to use the
history-service field names: refer to `msg` instead of `content` and `sender`
instead of `userAccount`; keep the `type` values (`"room_created"`,
`"members_added"`, `"member_removed"`, `"member_left"`) and clarify that for
those system types `msg` contains the server-rendered human-readable body and
`sender` is the responsible actor (e.g., the requester for adds/removes-by-other
and room-creates, and the leaving user for self-leave).

In `@room-worker/handler.go`:
- Around line 683-686: The call to h.store.HasOrgRoomMembers currently swallows
errors and leaves hadOrgsBefore false; change this to "fail closed" by returning
the error instead of just logging: if h.store.HasOrgRoomMembers(ctx, req.RoomID)
returns an error, return fmt.Errorf("check existing org room members: %w", err)
(or otherwise propagate the wrapped error) so the handler aborts rather than
proceeding with a potentially incorrect hadOrgsBefore; update the code around
the hadOrgsBefore assignment in handler.go to replace the slog.Warn branch with
error propagation using fmt.Errorf.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 528d2659-3257-477a-8229-d75fefb242e8

📥 Commits

Reviewing files that changed from the base of the PR and between 11c3f44 and 0bd38b0.

📒 Files selected for processing (11)
  • docs/client-api.md
  • docs/superpowers/plans/2026-05-14-room-worker-membership-fixes.md
  • docs/superpowers/specs/2026-05-13-room-worker-membership-fixes-design.md
  • pkg/model/event.go
  • pkg/model/model_test.go
  • pkg/model/room.go
  • pkg/model/room_test.go
  • room-worker/handler.go
  • room-worker/handler_test.go
  • room-worker/sysmsg.go
  • room-worker/sysmsg_test.go
✅ Files skipped from review due to trivial changes (1)
  • pkg/model/room.go
🚧 Files skipped from review as they are similar to previous changes (5)
  • room-worker/sysmsg.go
  • pkg/model/event.go
  • pkg/model/model_test.go
  • room-worker/sysmsg_test.go
  • pkg/model/room_test.go

Comment thread docs/superpowers/plans/2026-05-14-room-worker-membership-fixes.md
Comment thread docs/superpowers/specs/2026-05-13-room-worker-membership-fixes-design.md Outdated
Comment thread room-worker/handler_test.go
Comment thread room-worker/handler_test.go Outdated
Comment thread room-worker/handler_test.go Outdated
Comment thread room-worker/handler.go
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
room-worker/handler.go (1)

731-760: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fail closed on first-org backfill read failures.

This path is one-shot. If GetSubscriptionAccounts or FindUsersByAccounts fails and you continue, backfill is skipped permanently after org membership is written.

💡 Suggested fix
 	if len(req.Orgs) > 0 && !hadOrgsBefore {
 		existingAccounts, err := h.store.GetSubscriptionAccounts(ctx, req.RoomID)
 		if err != nil {
-			slog.Warn("get subscription accounts for backfill failed", "error", err)
-		} else {
-			var backfillAccounts []string
-			for _, account := range existingAccounts {
-				if _, isNew := resolvedAccountSet[account]; !isNew {
-					backfillAccounts = append(backfillAccounts, account)
-				}
-			}
-			if len(backfillAccounts) > 0 {
-				backfillUsers, err := h.store.FindUsersByAccounts(ctx, backfillAccounts)
-				if err != nil {
-					slog.Warn("find users for backfill failed", "error", err)
-				} else {
-					for i := range backfillUsers {
-						roomMembers = append(roomMembers, &model.RoomMember{
-							ID:     idgen.GenerateUUIDv7(),
-							RoomID: req.RoomID,
-							Ts:     acceptedAt,
-							Member: model.RoomMemberEntry{
-								ID:      backfillUsers[i].ID,
-								Type:    model.RoomMemberIndividual,
-								Account: backfillUsers[i].Account,
-							},
-						})
-					}
-				}
-			}
+			return fmt.Errorf("get subscription accounts for backfill: %w", err)
+		}
+		var backfillAccounts []string
+		for _, account := range existingAccounts {
+			if _, isNew := resolvedAccountSet[account]; !isNew {
+				backfillAccounts = append(backfillAccounts, account)
+			}
+		}
+		if len(backfillAccounts) > 0 {
+			backfillUsers, err := h.store.FindUsersByAccounts(ctx, backfillAccounts)
+			if err != nil {
+				return fmt.Errorf("find users for backfill: %w", err)
+			}
+			for i := range backfillUsers {
+				roomMembers = append(roomMembers, &model.RoomMember{
+					ID:     idgen.GenerateUUIDv7(),
+					RoomID: req.RoomID,
+					Ts:     acceptedAt,
+					Member: model.RoomMemberEntry{
+						ID:      backfillUsers[i].ID,
+						Type:    model.RoomMemberIndividual,
+						Account: backfillUsers[i].Account,
+					},
+				})
+			}
 		}
 	}
As per coding guidelines "Never ignore errors silently — comment if intentionally discarded (blank import or explicit comment required)".
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@room-worker/handler.go` around lines 731 - 760, The code currently logs and
continues when h.store.GetSubscriptionAccounts or h.store.FindUsersByAccounts
fails during the org backfill, which causes the one-shot backfill to be skipped
permanently; change these warning-only branches to return the error (or wrap it
with context) so the handler fails fast instead of proceeding and silently
dropping backfill work. Locate the GetSubscriptionAccounts call and its
associated slog.Warn and replace the warn-path to return an error (e.g., wrap
with fmt.Errorf or errors.Join) and do the same for the FindUsersByAccounts
error branch; this ensures the handler for req.RoomID, hadOrgsBefore,
resolvedAccountSet and roomMembers does not continue when those store reads
fail.
♻️ Duplicate comments (1)
room-worker/store_mongo.go (1)

138-147: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Handle UpdateOne no-match as an error.

UpdateDMParticipants currently returns success even when roomID matches no room. That silently drops DM participant persistence.

💡 Suggested fix
 func (s *MongoStore) UpdateDMParticipants(ctx context.Context, roomID string, uids, accounts []string) error {
-	_, err := s.rooms.UpdateOne(ctx,
+	res, err := s.rooms.UpdateOne(ctx,
 		bson.M{"_id": roomID},
 		bson.M{"$set": bson.M{"uids": uids, "accounts": accounts}},
 	)
 	if err != nil {
 		return fmt.Errorf("update dm participants (room %s): %w", roomID, err)
 	}
+	if res.MatchedCount == 0 {
+		return fmt.Errorf("update dm participants (room %s): room not found", roomID)
+	}
 	return nil
 }
As per coding guidelines "Check `mongo.ErrNoDocuments` explicitly when a missing record is expected in MongoDB queries".
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@room-worker/store_mongo.go` around lines 138 - 147, UpdateDMParticipants
currently treats a zero-match UpdateOne as success; change it to treat no-match
as an error by capturing the UpdateResult (res) from s.rooms.UpdateOne and
checking res.MatchedCount (or res.ModifiedCount) after the call, and if it is 0
return an explicit error (wrap or return mongo.ErrNoDocuments or a descriptive
fmt.Errorf like "room not found: %s"). Ensure this logic is added inside
UpdateDMParticipants immediately after s.rooms.UpdateOne and before returning
nil so missing roomIDs are surfaced instead of silently succeeding.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/superpowers/plans/2026-05-14-room-worker-membership-fixes.md`:
- Around line 957-958: The test snippets marshal req into a local variable named
data that is never used (data, _ := json.Marshal(req)), causing an unused
variable compile error; remove the unused json.Marshal call or change it to
discard the result (e.g., use only json.Marshal(req) with no assignment or
assign to _), and keep the call to require.NoError(t,
h.processRemoveIndividual(context.Background(), &req)); update both occurrences
(the one referencing data and the second instance noted) so no unused variable
remains.
- Around line 239-243: The current code calls h.store.HasOrgRoomMembers(ctx,
req.RoomID) and only logs the error, then proceeds to set writeIndividuals based
on hadOrgsBefore; change this to fail-closed: if HasOrgRoomMembers returns an
error, return that error (or wrap and return) instead of continuing, so the
handler (the function containing this call) does not proceed with
writeIndividuals computation or further processing when hadOrgsBefore is
unknown; adjust the error handling around HasOrgRoomMembers and remove the
permissive fall-through that sets writeIndividuals using an uninitialized
hadOrgsBefore.

---

Outside diff comments:
In `@room-worker/handler.go`:
- Around line 731-760: The code currently logs and continues when
h.store.GetSubscriptionAccounts or h.store.FindUsersByAccounts fails during the
org backfill, which causes the one-shot backfill to be skipped permanently;
change these warning-only branches to return the error (or wrap it with context)
so the handler fails fast instead of proceeding and silently dropping backfill
work. Locate the GetSubscriptionAccounts call and its associated slog.Warn and
replace the warn-path to return an error (e.g., wrap with fmt.Errorf or
errors.Join) and do the same for the FindUsersByAccounts error branch; this
ensures the handler for req.RoomID, hadOrgsBefore, resolvedAccountSet and
roomMembers does not continue when those store reads fail.

---

Duplicate comments:
In `@room-worker/store_mongo.go`:
- Around line 138-147: UpdateDMParticipants currently treats a zero-match
UpdateOne as success; change it to treat no-match as an error by capturing the
UpdateResult (res) from s.rooms.UpdateOne and checking res.MatchedCount (or
res.ModifiedCount) after the call, and if it is 0 return an explicit error (wrap
or return mongo.ErrNoDocuments or a descriptive fmt.Errorf like "room not found:
%s"). Ensure this logic is added inside UpdateDMParticipants immediately after
s.rooms.UpdateOne and before returning nil so missing roomIDs are surfaced
instead of silently succeeding.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9c510884-ebfd-4447-98e3-b82a49c422b0

📥 Commits

Reviewing files that changed from the base of the PR and between 0bd38b0 and 44eab49.

📒 Files selected for processing (8)
  • docs/client-api.md
  • docs/superpowers/plans/2026-05-14-room-worker-membership-fixes.md
  • docs/superpowers/specs/2026-05-13-room-worker-membership-fixes-design.md
  • room-worker/handler.go
  • room-worker/handler_test.go
  • room-worker/mock_store_test.go
  • room-worker/store.go
  • room-worker/store_mongo.go
✅ Files skipped from review due to trivial changes (2)
  • room-worker/mock_store_test.go
  • docs/client-api.md

Comment thread docs/superpowers/plans/2026-05-14-room-worker-membership-fixes.md
Comment thread docs/superpowers/plans/2026-05-14-room-worker-membership-fixes.md Outdated
@vjauhari-work vjauhari-work force-pushed the claude/fix-org-membership-duplication-l5LfI branch from b7467d7 to ca4f612 Compare May 15, 2026 11:04
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
docs/superpowers/specs/2026-05-13-room-worker-membership-fixes-design.md (1)

96-96: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Misleading "No store interface changes" statement.

Line 96 states "No store interface changes" at the conclusion of §2.6 (Helpers), but §3.4 (lines 142-155) introduces UpdateDMParticipants to the SubscriptionStore interface for DM participant persistence. While the statement is technically accurate for the formatters section in isolation, it misleads readers about the overall scope of the specification.

This issue was flagged in a previous review and marked as addressed, but the text remains unchanged. Either remove the sentence or qualify it to clarify that it applies only to §2 (membership fixes), not §3 (DM fields).

Suggested fix
-Unit tests in `room-worker/sysmsg_test.go`. No store interface changes.
+Unit tests in `room-worker/sysmsg_test.go`. §2 (membership fixes) requires no store interface changes; §3 (DM participant fields) adds `UpdateDMParticipants`.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/superpowers/specs/2026-05-13-room-worker-membership-fixes-design.md` at
line 96, The concluding sentence "No store interface changes" in §2.6 is
misleading because §3.4 adds UpdateDMParticipants to the SubscriptionStore
interface; update the text to either remove that sentence or reword it to
clarify scope (e.g., state that no store interface changes apply to the
formatters/membership fixes in §2, while §3.4 introduces UpdateDMParticipants on
SubscriptionStore for DM participant persistence), and ensure you reference the
SubscriptionStore and UpdateDMParticipants identifiers so readers can find the
related change.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/superpowers/plans/2026-05-14-room-worker-membership-fixes.md`:
- Around line 7-8: Update the scope summary and related statements in the
document to acknowledge the model and store interface changes introduced by the
DM participant persistence work: explicitly mention that Task 11 adds Room.UIDs
and Room.Accounts to pkg/model/room.go and the BuildDMParticipants helper, and
that Task 12 adds UpdateDMParticipants(...) to the SubscriptionStore interface
(room-worker/store.go); replace any claims like "no store interface, model, or
wire-protocol changes" and "No `store.go` changes" with text that reflects these
specific additions so the scope is accurate and no longer contradicts the tasks.

---

Duplicate comments:
In `@docs/superpowers/specs/2026-05-13-room-worker-membership-fixes-design.md`:
- Line 96: The concluding sentence "No store interface changes" in §2.6 is
misleading because §3.4 adds UpdateDMParticipants to the SubscriptionStore
interface; update the text to either remove that sentence or reword it to
clarify scope (e.g., state that no store interface changes apply to the
formatters/membership fixes in §2, while §3.4 introduces UpdateDMParticipants on
SubscriptionStore for DM participant persistence), and ensure you reference the
SubscriptionStore and UpdateDMParticipants identifiers so readers can find the
related change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0c2a55cd-9251-4865-8112-0de940a43676

📥 Commits

Reviewing files that changed from the base of the PR and between 44eab49 and ca4f612.

📒 Files selected for processing (14)
  • docs/client-api.md
  • docs/superpowers/plans/2026-05-14-room-worker-membership-fixes.md
  • docs/superpowers/specs/2026-05-13-room-worker-membership-fixes-design.md
  • pkg/model/event.go
  • pkg/model/model_test.go
  • pkg/model/room.go
  • pkg/model/room_test.go
  • room-worker/handler.go
  • room-worker/handler_test.go
  • room-worker/mock_store_test.go
  • room-worker/store.go
  • room-worker/store_mongo.go
  • room-worker/sysmsg.go
  • room-worker/sysmsg_test.go
✅ Files skipped from review due to trivial changes (2)
  • room-worker/mock_store_test.go
  • docs/client-api.md
🚧 Files skipped from review as they are similar to previous changes (10)
  • pkg/model/model_test.go
  • pkg/model/event.go
  • room-worker/store_mongo.go
  • room-worker/sysmsg.go
  • room-worker/store.go
  • pkg/model/room_test.go
  • pkg/model/room.go
  • room-worker/sysmsg_test.go
  • room-worker/handler.go
  • room-worker/handler_test.go

Comment thread docs/superpowers/plans/2026-05-14-room-worker-membership-fixes.md Outdated
@vjauhari-work vjauhari-work force-pushed the claude/fix-org-membership-duplication-l5LfI branch from ca4f612 to 2c0e500 Compare May 17, 2026 06:32
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (2)
docs/superpowers/specs/2026-05-13-room-worker-membership-fixes-design.md (1)

96-96: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

"No store interface changes" still contradicts section 3.4.

Line 96 states "No store interface changes," but section 3.4 (lines 142-151) explicitly adds UpdateDMParticipants to the SubscriptionStore interface in room-worker/store.go. This contradiction was flagged in a previous review and marked as addressed in commits 9ccf2c1–0bd38b0, but the text has not been corrected.

Please update line 96 to reflect the store interface extension.

Suggested fix
-Unit tests in `room-worker/sysmsg_test.go`. No store interface changes.
+Unit tests in `room-worker/sysmsg_test.go`. Store interface extended in §3.4 to add `UpdateDMParticipants`.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/superpowers/specs/2026-05-13-room-worker-membership-fixes-design.md` at
line 96, Update the sentence at line 96 to remove the assertion that there are
"No store interface changes" and instead state that the SubscriptionStore
interface was extended; specifically mention that section 3.4 adds the
UpdateDMParticipants method to the SubscriptionStore (referencing
SubscriptionStore and UpdateDMParticipants) so the doc no longer contradicts the
interface change described later.
docs/superpowers/plans/2026-05-14-room-worker-membership-fixes.md (1)

7-8: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Scope claims still contradict Tasks 11-12 despite being marked as addressed.

Lines 7-8 state "no store interface, model, or wire-protocol changes" and lines 24-25 claim "No store.go changes", but:

  • Task 11 (lines 1231-1338) adds Room.UIDs/Room.Accounts fields to pkg/model/room.go plus the BuildDMParticipants helper — these are model additions.
  • Task 12 (lines 1341-1401) adds UpdateDMParticipants(...) to the SubscriptionStore interface in room-worker/store.go — this is a store interface extension.

This contradiction was flagged in a previous review and marked as addressed in commits a741f34–44eab49, but the summary text has not been updated. Please revise the scope statements to acknowledge the model and store interface changes introduced by the DM participant persistence work.

Suggested fix
-**Architecture:** Introduce a `room-worker/sysmsg.go` helper file with five formatter functions. Apply localized changes inside `processAddMembers`, `processCreateRoomChannel`, `processRemoveIndividual`, `processRemoveOrg`, and `publishChannelSysMessages` in `room-worker/handler.go`. All changes are internal to `room-worker`; no store interface, model, or wire-protocol changes.
+**Architecture:** Introduce `room-worker/sysmsg.go` formatter helpers and update `room-worker/handler.go` membership flows. Also add DM participant persistence spanning `pkg/model` (`Room.UIDs/Accounts`, `BuildDMParticipants`) and `room-worker` store contract (`UpdateDMParticipants`). No wire-protocol changes.
-No `store.go` changes — every method the plan needs is already on the `SubscriptionStore` interface.
+`store.go` is extended in Task 12 to add `UpdateDMParticipants` to the `SubscriptionStore` interface for DM participant persistence.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/superpowers/plans/2026-05-14-room-worker-membership-fixes.md` around
lines 7 - 8, Update the document's scope/summary statements to acknowledge that
DM participant persistence introduced model and store-interface changes:
explicitly note the addition of Room.UIDs and Room.Accounts fields and the
BuildDMParticipants helper to the Room model, and the addition of
UpdateDMParticipants(...) to the SubscriptionStore interface; replace the lines
claiming "no store interface, model, or wire-protocol changes" and "No
`store.go` changes" with text that accurately reflects these model and store
interface changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@docs/superpowers/plans/2026-05-14-room-worker-membership-fixes.md`:
- Around line 7-8: Update the document's scope/summary statements to acknowledge
that DM participant persistence introduced model and store-interface changes:
explicitly note the addition of Room.UIDs and Room.Accounts fields and the
BuildDMParticipants helper to the Room model, and the addition of
UpdateDMParticipants(...) to the SubscriptionStore interface; replace the lines
claiming "no store interface, model, or wire-protocol changes" and "No
`store.go` changes" with text that accurately reflects these model and store
interface changes.

In `@docs/superpowers/specs/2026-05-13-room-worker-membership-fixes-design.md`:
- Line 96: Update the sentence at line 96 to remove the assertion that there are
"No store interface changes" and instead state that the SubscriptionStore
interface was extended; specifically mention that section 3.4 adds the
UpdateDMParticipants method to the SubscriptionStore (referencing
SubscriptionStore and UpdateDMParticipants) so the doc no longer contradicts the
interface change described later.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6b8760d2-1d7c-40b1-8df5-72f57b8cc408

📥 Commits

Reviewing files that changed from the base of the PR and between ca4f612 and 2c0e500.

📒 Files selected for processing (14)
  • docs/client-api.md
  • docs/superpowers/plans/2026-05-14-room-worker-membership-fixes.md
  • docs/superpowers/specs/2026-05-13-room-worker-membership-fixes-design.md
  • pkg/model/event.go
  • pkg/model/model_test.go
  • pkg/model/room.go
  • pkg/model/room_test.go
  • room-worker/handler.go
  • room-worker/handler_test.go
  • room-worker/mock_store_test.go
  • room-worker/store.go
  • room-worker/store_mongo.go
  • room-worker/sysmsg.go
  • room-worker/sysmsg_test.go
✅ Files skipped from review due to trivial changes (2)
  • room-worker/mock_store_test.go
  • docs/client-api.md
🚧 Files skipped from review as they are similar to previous changes (10)
  • room-worker/store_mongo.go
  • pkg/model/room_test.go
  • room-worker/store.go
  • pkg/model/model_test.go
  • pkg/model/event.go
  • pkg/model/room.go
  • room-worker/sysmsg.go
  • room-worker/sysmsg_test.go
  • room-worker/handler.go
  • room-worker/handler_test.go

@vjauhari-work vjauhari-work force-pushed the claude/fix-org-membership-duplication-l5LfI branch 2 times, most recently from f986c3d to f813cf2 Compare May 18, 2026 02:58
vjauhari-work and others added 11 commits May 18, 2026 03:40
Spec for the room-worker membership filtering, system-message
sender/content population, request-ID validation, fail-closed
HasOrgRoomMembers handling, and DM participant fields work.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Task-by-task plan covering the membership-fix work: filter individual
room docs, populate sender/content on system messages, fail-closed
HasOrgRoomMembers, X-Request-ID UUID validation, and DM participant
fields (Tasks 11-17).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Align the system-message type row with the new msg/sender population
and document the new Room.uids and Room.accounts DM-participant fields.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…lper

DM rooms now carry both the deterministic UID list and the matching
account list as siblings, so clients can render DM members without an
extra round-trip. BuildDMParticipants returns the canonically sorted
pair for any two users.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
\$set's uids/accounts on a dm/botDM room after the counterpart account
has been resolved. Idempotent under JetStream redelivery; returns an
error when the target room is missing (the caller treats this as a
permanent failure rather than silently skipping).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…eate/add

The MongoDB org-membership backfill was over-broad: it created individual
sub-room docs for every member of the source org rooms, not just the users
being added. processAddMembers now filters indiv docs to req.Users only,
createRoom filters to ResolvedUsers ∪ {requester}, and the backfill gate
fires only on the first-org transition so subsequent orgs don't replay it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously member_added / member_removed / member_left system messages
went out with bare member lists and no sender, leaving clients nothing
to render the actor or the affected users with. The room-worker now
fetches the requester (validating EngName / SectName / SectID along
the way), harvests SectName from the unfiltered org members so sect
details survive the indiv-doc filter, and renders Content via new
sysmsg formatters. Single- and multi-form Content variants cover the
individual path and the org-bearing path; a 1-member org expansion
correctly stays on multi form so future org members are accounted for.

Adds pkg/model.MessageTypeMemberRemoved and MessageTypeMemberLeft to
name the two new sys-msg variants the room-worker now emits.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…osed org backfill

Two defensive fixes in the same path:

- processAddMembers and processCreateRoom now validate that the
  X-Request-ID header carries a well-formed UUID (v4 or v7) via
  idgen.IsValidUUID; non-UUIDs return a permanent error so log
  correlation stays usable.
- HasOrgRoomMembers failures no longer fall through silently with the
  zero-value bool — defaulting hadOrgsBefore=false on error would
  spuriously trigger first-org backfill on a room that already has
  org docs, and the duplicate inserts would only be masked by the
  Mongo unique index. Surface the error so JetStream redelivery
  retries with fresh state.

Comments on these branches trimmed to single-line.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
DM and botDM rooms now persist matching uids/accounts pairs at room
creation across all three create paths:

- Async DM create (processCreateRoom, dm type) — calls
  UpdateDMParticipants after counterpart resolution.
- Async botDM create (processCreateRoom, botDM type) — same path,
  bot account resolved like a regular user.
- Sync DM create (handleSyncCreateDM) — sets UIDs/Accounts on the
  CreateRoom literal so the participants land in the same write as
  the room itself; on a dup-key (the DM existed already), the path
  is forward-only and does not backfill — the participants are
  immutable for a DM room, so no recovery work is needed.

UpdateDMParticipants returns an error when no room matches the ID
(room-not-found is permanent, not silently skipped). The channel
create path is left untouched; a guard test pins that channels
must omit UIDs/Accounts so omitempty drops them from BSON.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After rebasing onto main (which brought in #171 room encryption keys),
two classes of test adaptations were needed:

1. #171 tests of processAddMembers / processRemoveIndividual now hit the
   PR's stricter name-field validation. Added EngName/ChineseName to user
   fixtures and added the GetUser expectation for the requester account.

2. PR tests of processAddMembers that build the Handler via struct
   literal now reach the unconditional buildAndFanOutRoomKey call added
   in #171. Added keyStore: testKeyStore, keySender: testKeySender to all
   such Handler inits so the no-op stubs satisfy the fan-out call.

Also retired one non-UUID request ID literal ("req-add-members-ordering")
that the PR's UUID-format validation now rejects.

No production-code changes — purely test-side reconciliation between the
two feature branches.
…d integration tests

Documentation: the plan's architecture summary and file-map preamble,
and the spec's §2.6 closing line, all claimed there were no model or
store interface changes — contradicting Tasks 11-12 (Room.UIDs/
Accounts + BuildDMParticipants, UpdateDMParticipants on
SubscriptionStore). Updated the three lines so the scope statements
match what the plan and spec actually deliver.

Tests: the existing room-worker feature integration tests exercised
the code paths for the four bug fixes but did not assert the
user-facing artifacts those fixes produce. Extended:

- TestSyncCreateDM_DM_PersistsRoomAndSubs + TestProcessCreateRoomDM-
  PersistsTwoSubsAndZeroMembers: assert room.UIDs and room.Accounts
  are persisted (sorted by uid, paired by index) — covers DM
  participant fields persistence.

- TestProcessAddMembers_PublishesLocalInbox_Integration: assert the
  canonical members_added sys-message carries UserAccount=requester
  and formatter-rendered Content — covers empty-Content + missing-
  sender bugs on add.

- TestProcessRemoveIndividual_PublishesLocalInbox_Integration: same
  assertion for the member_removed sys-message — covers empty-Content
  + missing-sender bugs on forced removal.

Org-member duplication remains covered by handler_test.go unit tests;
the existing AddMembers integration test uses only direct users so it
doesn't exercise the duplication scenario.
Comment thread room-worker/handler.go
Comment thread room-worker/handler.go Outdated
Comment thread room-worker/sysmsg.go Outdated
vjauhari-work and others added 2 commits May 18, 2026 04:15
…ounts; skip bots

fanOutMutationEvent previously called ListSubscriptions on every DM edit/delete
to recover recipient accounts — but Room.Accounts (denormalized onto the room
doc by room-worker for DM/botDM rooms) already carries the same data. Drop the
Mongo round-trip and iterate room.Accounts directly. Merge the DM and BotDM
cases (BotDM previously fell into the unknown-room-type default branch and
logged a misleading warning) and skip bot accounts so the bot in a botDM does
not receive its own edit/delete echo on a subject nothing listens on.

Add a local isBot helper mirroring the predicate already present in
message-gatekeeper and room-service; promotion to a shared pkg/botid is the
right cleanup once a fourth duplicate is no longer acceptable.

Tests:
- existing DM edit/delete fan-out tests switched fixtures from a
  ListSubscriptions mock to Accounts on the Room struct (same coverage)
- new TestHandleUpdated_BotDMRoom_SkipsBotAccount asserts the bot is filtered
  in a botDM room

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Addresses two PR #185 review comments from mliu:

1. processCreateRoom previously inserted the room and then ran a follow-up
   UpdateDMParticipants UpdateOne to backfill UIDs/Accounts after the
   counterpart user was resolved. That second Mongo round-trip is avoidable
   for new rooms: fetch the counterpart upfront, populate Room.UIDs/Accounts
   on the literal, and let CreateRoom persist the complete document in one
   write — matching the sync DM path's pattern. UpdateDMParticipants is
   removed from SubscriptionStore (interface + Mongo impl + mock); no
   callers remain. The duplicate-key replay branch keeps the existing
   identity-equality semantics (Type/SiteID/Name/CreatedBy must match);
   it no longer rewrites UIDs/Accounts onto the existing doc, since rooms
   created by this code path already have those fields and legacy rooms
   are out of scope for the hot path. The newly-similar
   processCreateRoomDM/BotDM helpers collapsed into the switch, mirroring
   the inline if-RoomTypeBotDM pattern handleSyncCreateDM already uses.

2. displayName(u) used to return TrimSpace(EngName + " " + ChineseName).
   For account-only users where EngName == ChineseName, that rendered as
   "Bob Bob". Short-circuit to a single rendering when the two fields are
   equal. Channels remain the only consumer of system-message content;
   DM/botDM paths don't render via this helper.

Tests:
- F1/F2 (DM/BotDM SetsParticipantFields) now capture the Room passed to
  CreateRoom and assert UIDs/Accounts on it, replacing the previous
  UpdateDMParticipants arg-pin
- CollisionMismatchType test threads the upfront counterpart GetUser
- TestFormatLeft_EngEqualsChineseRendersOnce pins the new dedupe contract
- mocks regenerated; integration test compile-clean

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@vjauhari-work vjauhari-work force-pushed the claude/fix-org-membership-duplication-l5LfI branch from f813cf2 to 3c6f817 Compare May 18, 2026 04:46
Comment thread room-worker/sysmsg.go Outdated
Three follow-ups from PR #185 review:

1. Drop validateUserNames + its five call sites (mliu PR comment). EngName
   and ChineseName are now treated as optional. displayName falls through
   to a single non-empty side, and finally to the Account string when both
   are blank — the rendered body never collapses to a bare quote pair.

2. Quote display names in every channel sys-message:
   `"Alice Anderson 王愛麗" added "Frank Fischer 費法蘭" to the channel` (and
   analogous shapes for added-multi / removed-user / removed-org / left).
   formatRemovedOrg quotes the SectName too.

3. Populate the sender envelope (Message.UserID + UserAccount) on the
   member_left / member_removed sys-messages emitted by
   processRemoveIndividual and processRemoveOrg. message-worker looks up
   the user by UserID to fill the Cassandra `sender` Participant column;
   previously these rows landed with sender = null. Self-leave reuses the
   already-fetched leaving user; forced-removal and org-removal fetch the
   requester via the existing store.GetUser (no new store method needed).
   Older Cassandra rows are intentionally not backfilled — only newly
   emitted messages get the populated sender.

Also capitalise "A new room has been created" on the room_created
sys-message body.

Tests:
- sysmsg_test: drop TestValidateUserNames; add TestDisplayName covering
  every fallback branch and TestFormatLeft_FallsBackToAccount /
  TestFormatAddedSingle_SingleNameSide; update existing formatter
  assertions for the new quoted shape.
- handler_test: drop the four empty-name permanent-error tests
  (TestHandler_ProcessAddMembers_RequesterEmptyName, _AddedUserEmptyName,
  TestHandler_ProcessRemoveIndividual_EmptyName,
  TestProcessCreateRoom_RequesterEmptyName_ReturnsPermanent); update
  existing content assertions to the quoted format; mock GetUser on the
  requester for forced-remove / org-remove paths and assert
  sysMsg.UserID is set to the requester ID.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…msg shape

Two integration assertions still expected the unquoted sys-message bodies;
mirror the change from the prior commit so the live MongoDB+Cassandra
container test agrees with the in-memory handler tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Collaborator

@mliu33 mliu33 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excellent, thanks!

@mliu33 mliu33 merged commit 74c915c into main May 18, 2026
6 checks passed
@vjauhari-work vjauhari-work deleted the claude/fix-org-membership-duplication-l5LfI branch May 18, 2026 08:19
vjauhari-work added a commit that referenced this pull request May 18, 2026
…ectly (#193)

SystemMessage previously decoded `sysMsgData` and synthesised the
visible text client-side (`Alice added bob, dave`, etc). The
room-worker now publishes the rendered string in Message.Content
(PR #185 for members_added; room_created still ships a placeholder),
so the frontend can just render `message.content` verbatim and the
formatting lives in one place — the server.

Drop decodeSysData, senderLabel, describeMembersAdded,
describeRoomCreated, and the type switch. Drop the unused
data-system-type attribute (no CSS or JS consumer). Test suite
collapses to a parametrised "renders Content verbatim" case plus
a timestamp assertion.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants