Skip to content

Add RoomType field to Subscription model#137

Merged
mliu33 merged 14 commits into
mainfrom
claude/add-roomtype-subscription-Uqow3
Apr 29, 2026
Merged

Add RoomType field to Subscription model#137
mliu33 merged 14 commits into
mainfrom
claude/add-roomtype-subscription-Uqow3

Conversation

@saurav-err-137
Copy link
Copy Markdown
Collaborator

@saurav-err-137 saurav-err-137 commented Apr 28, 2026

Summary

This PR adds a RoomType field to model.Subscription and introduces two new room type constants (RoomTypeBotDM and RoomTypeDiscussion). The change denormalizes room type information onto subscription documents so downstream consumers can route on subscription type without requiring an additional room lookup.

Key Changes

  • Model updates (pkg/model/)

    • Added RoomTypeBotDM and RoomTypeDiscussion constants to the RoomType enum
    • Added RoomType field to Subscription struct (positioned between RoomID and SiteID)
    • Updated model tests to exercise the new field
  • room-service (room-service/handler.go)

    • Populate RoomType on the auto-created owner subscription during CreateRoom by reading from req.Type
  • room-worker (room-worker/handler.go)

    • processAddMembers: Hardcode RoomTypeChannel on created subscriptions (DM/botDM rooms reject member.add upstream)
    • processRemoveIndividual and processRemoveOrg: Fetch the room once and stamp RoomType onto the partial Subscription literal in SubscriptionUpdateEvent payloads (non-fatal; logs warning on fetch failure)
    • Added lookupRoomType helper method for consistent room-fetch error handling
  • inbox-worker (inbox-worker/handler.go)

    • Hardcode RoomTypeChannel on cross-site member_added subscriptions (only channel/discussion rooms produce cross-site events)
  • Tests updated across all services to assert RoomType is correctly populated at each creation site

Implementation Details

  • The new field defaults to empty string ("") for backward compatibility; existing call sites compile without modification
  • Room fetches in remove paths are best-effort: if GetRoom fails, the removal proceeds with an empty RoomType and a warning is logged
  • No store interface changes required; room-worker already has GetRoom available
  • All changes follow the existing test-driven development pattern with failing tests written first

https://claude.ai/code/session_01HQF5tspEtJBg69TT89oCdx

Summary by CodeRabbit

  • New Features

    • Subscriptions now carry explicit room type metadata (channel, DM, bot DM, discussion) and are populated consistently during create/add/remove/member flows.
  • Documentation

    • Added design and execution plan detailing the rollout for room type on subscriptions.
  • Tests

    • Expanded tests to assert room type values, JSON round-trip, and correct propagation across subscription and event flows.

claude added 12 commits April 27, 2026 01:53
Resets the spec onto current main: PR #118 already removed RoomTypeGroup
and renamed broadcast-worker's switch case, role-update guard, and
errRoomTypeGuard sentinel — those scope items are dropped. PR #118 also
deleted processInvite, so the invite-side population step is gone too.
Surviving work: the new constants, the Subscription.RoomType field, and
populating it on three sub-creation sites and two remove-event payloads.
Adds the new RoomTypeBotDM and RoomTypeDiscussion constants and a
RoomType field on Subscription so that downstream consumers can route on
room kind without an extra room lookup. The TestSubscriptionJSON
round-trip now exercises the new field; TestRoomTypeValues asserts each
of the four constants.
The owner subscription auto-created by handleCreateRoom now carries the
room type from the create request so downstream consumers can route on
the kind without re-fetching the room.
processAddMembers hardcodes RoomTypeChannel — DM and botDM rooms do not
support add-member. processRemoveIndividual and processRemoveOrg each
do one GetRoom per operation and stamp room.Type onto the partial
Subscription carried by SubscriptionUpdateEvent. The room fetch is
non-fatal: a lookup error logs at warn and the event still fires with
an empty RoomType, matching the existing best-effort treatment of
notification payloads.
Cross-site member_added events only originate from rooms that support
add-member (channels and discussions). Hardcoding RoomTypeChannel mirrors
room-worker's processAddMembers logic so every persisted subscription
carries a non-empty RoomType.
Removes stale claims that this PR removes RoomTypeGroup or renames its
call sites — PR #118 already did both. Updates the commit-strategy
section to reflect the five commits actually made instead of the
single-commit plan that predated the rebase.
Adds the plan header, file structure table, prerequisites, and the
four-task outline. Subsequent commits will fill in each task body
one at a time.
Adds the bite-sized TDD steps for introducing RoomTypeBotDM and
RoomTypeDiscussion constants and the RoomType field on Subscription.
Adds the TDD steps for stamping req.Type onto the auto-created owner
subscription returned by handleCreateRoom.
Adds the TDD steps for hardcoding RoomTypeChannel on processAddMembers
and for fetching the room (non-fatal) inside processRemoveIndividual
and processRemoveOrg so the partial Subscription on each removed event
carries RoomType. Covers all five existing tests that need GetRoom mocks
plus the two outbox-failure tests that hit the same publish path.
Adds the TDD steps for hardcoding RoomTypeChannel on the cross-site
member_added handler and a final verification + push checklist
covering full make test, make lint, integration tests, and the push
command (with --force-with-lease guidance for branches that already
have a prior tip on the remote). Also includes a Risks and Rollback
section.
Pulls the duplicated GetRoom-then-warn pattern out of
processRemoveIndividual and processRemoveOrg into a
Handler.lookupRoomType helper that returns "" on lookup failure. Adds
brief WHY notes at the two RoomTypeChannel hardcodes (room-worker
processAddMembers and inbox-worker handleMemberAdded) so future readers
see the upstream invariant that prevents non-channel rooms from
reaching those paths. Drops a narrative test comment that the
following assertion already conveys.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 28, 2026

📝 Walkthrough

Walkthrough

Adds a RoomType field to model.Subscription, new room-type constants, and populates RoomType across subscription creation/removal paths (room-service owner creation, room-worker add/remove flows, inbox-worker cross-site member_added), with corresponding tests and documentation updates.

Changes

Cohort / File(s) Summary
Documentation
docs/superpowers/specs/2026-04-23-roomtype-on-subscription-design.md, docs/superpowers/plans/2026-04-27-roomtype-on-subscription-plan.md
Design and implementation plan describing RoomType denormalization onto model.Subscription, population rules per creation path, test updates, and verification checklist.
Model Layer
pkg/model/room.go, pkg/model/subscription.go, pkg/model/model_test.go
Adds RoomTypeBotDM and RoomTypeDiscussion constants; adds RoomType field to Subscription with JSON/BSON tags; updates JSON round-trip and constants tests.
Room Service
room-service/handler.go, room-service/handler_test.go
Sets owner Subscription.RoomType from req.Type during room creation; test updated to assert RoomType on created subscription.
Room Worker
room-worker/handler.go, room-worker/handler_test.go
Populates Subscription.RoomType for bulk-created member subscriptions and sets RoomType on subscription-update/remove event payloads (hardcoded to RoomTypeChannel where applicable); tests validate RoomType presence in published/persisted payloads.
Inbox Worker
inbox-worker/handler.go, inbox-worker/handler_test.go
Hardcodes RoomTypeChannel for subscriptions created from cross-site member_added events; test asserts RoomType is set.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • mliu33
  • hmchangw
  • yenta

Poem

🐰 I hopped through code with eager paws,
Adding room-type labels, no more pause.
Owner, member, inbox too—now they know,
Channel, DM, or discussion where they go.
A tiny stamp, a tidy trail—hip hop, hooray! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% 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 title accurately reflects the main change: adding a RoomType field to the Subscription model, which is the central structural modification across all changes in the PR.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/add-roomtype-subscription-Uqow3

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
Review rate limit: 0/1 reviews remaining, refill in 60 minutes.

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: 2

🧹 Nitpick comments (4)
room-service/handler_test.go (1)

50-52: Strengthen this test to verify passthrough, not just value presence.

Because the request in this test uses RoomTypeChannel, a hardcoded implementation would still pass. Consider using a non-channel room type here to prove handleCreateRoom copies req.Type.

♻️ Suggested test tweak
-req := model.CreateRoomRequest{Name: "general", Type: model.RoomTypeChannel, CreatedBy: "u1", CreatedByAccount: "alice", SiteID: "site-a"}
+req := model.CreateRoomRequest{Name: "general", Type: model.RoomTypeDiscussion, CreatedBy: "u1", CreatedByAccount: "alice", SiteID: "site-a"}

-if capturedSub != nil && capturedSub.RoomType != model.RoomTypeChannel {
-	t.Errorf("expected owner subscription RoomType=%q, got %q", model.RoomTypeChannel, capturedSub.RoomType)
+if capturedSub != nil && capturedSub.RoomType != model.RoomTypeDiscussion {
+	t.Errorf("expected owner subscription RoomType=%q, got %q", model.RoomTypeDiscussion, capturedSub.RoomType)
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@room-service/handler_test.go` around lines 50 - 52, The test currently only
asserts capturedSub.RoomType is not Channel; instead set the request's type to a
non-channel value (e.g., model.RoomTypeDirect or another enum) and assert that
handleCreateRoom copies req.Type through to the created subscription by checking
capturedSub.RoomType == req.Type; update the test setup where the request is
constructed and the assertion that currently references model.RoomTypeChannel to
compare against the request's Type value (symbols: handleCreateRoom, req.Type,
capturedSub, model.RoomTypeChannel).
room-worker/handler_test.go (1)

313-316: Add one failure-path test for GetRoom best-effort lookup.

These paths now expect GetRoom success, but the implementation contract is “warn and continue with empty RoomType” when lookup fails. A dedicated test would lock that behavior and prevent regressions.

🧪 Suggested test to add
+func TestHandler_ProcessRemoveMember_GetRoomError_StillPublishesWithEmptyRoomType(t *testing.T) {
+	ctrl := gomock.NewController(t)
+	store := NewMockSubscriptionStore(ctrl)
+
+	const roomID, account, siteID = "room-1", "alice", "site-a"
+	store.EXPECT().GetUserWithMembership(gomock.Any(), roomID, account).Return(&UserWithMembership{
+		User: model.User{ID: "u1", Account: account, SiteID: siteID},
+	}, nil)
+	store.EXPECT().DeleteSubscription(gomock.Any(), roomID, account).Return(int64(1), nil)
+	store.EXPECT().DeleteRoomMember(gomock.Any(), roomID, model.RoomMemberIndividual, "u1").Return(nil)
+	store.EXPECT().ReconcileUserCount(gomock.Any(), roomID).Return(nil)
+	store.EXPECT().GetRoom(gomock.Any(), roomID).Return(nil, fmt.Errorf("db down"))
+
+	var published []publishedMsg
+	h := NewHandler(store, siteID, func(_ context.Context, subj string, data []byte, _ string) error {
+		published = append(published, publishedMsg{subj: subj, data: data})
+		return nil
+	})
+
+	req := model.RemoveMemberRequest{RoomID: roomID, Requester: account, Account: account}
+	data, _ := json.Marshal(req)
+	require.NoError(t, h.processRemoveMember(context.Background(), data))
+
+	for _, p := range published {
+		if p.subj != subject.SubscriptionUpdate(account) {
+			continue
+		}
+		var evt model.SubscriptionUpdateEvent
+		require.NoError(t, json.Unmarshal(p.data, &evt))
+		assert.Equal(t, model.RoomType(""), evt.Subscription.RoomType)
+	}
+}
As per coding guidelines: Every handler method must have tests for: valid input, invalid/malformed input, store errors, and edge cases.

Also applies to: 498-501, 880-883, 941-944, 1168-1171, 1209-1212

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@room-worker/handler_test.go` around lines 313 - 316, Tests currently assume
store.GetRoom always succeeds; add a failure-path test that verifies the handler
performs a best-effort lookup (logs/warns and proceeds with an empty RoomType)
when GetRoom returns an error. In handler_test.go add a mock expectation on
store.EXPECT().GetRoom(...).Return(nil, someError) and assert the handler
continues without failing and that the resulting model.Room.Type is empty (or
default), and that appropriate warning/log behavior occurs; apply the same
pattern for other handler tests referencing GetRoom (lines around the other
noted blocks) to lock this contract.
room-worker/handler.go (2)

432-439: Prefer room.Type over hardcoded RoomTypeChannel in add-members.

room is already loaded in this function, so persisting room.Type avoids silent drift if add-member eligibility expands beyond channel later.

♻️ Suggested refactor
 		sub := &model.Subscription{
 			ID:       idgen.GenerateID(),
 			User:     model.SubscriptionUser{ID: user.ID, Account: user.Account},
 			RoomID:   req.RoomID,
-			RoomType: model.RoomTypeChannel,
+			RoomType: room.Type,
 			SiteID:   room.SiteID,
 			Roles:    []model.Role{model.RoleMember},
 			JoinedAt: acceptedAt,
 		}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@room-worker/handler.go` around lines 432 - 439, The Subscription being
created in the add-members handler hardcodes RoomType to model.RoomTypeChannel;
change it to use the already-loaded room.Type so the persisted subscription
reflects the actual room kind. Locate the code that constructs sub :=
&model.Subscription{... RoomType: model.RoomTypeChannel, ...} and replace the
hardcoded value with RoomType: room.Type (ensuring room is in scope and of the
expected type) so future non-channel room types won’t drift silently.

681-683: Log the nil-room path in lookupRoomType for diagnostics.

Returning empty RoomType is fine, but a warning on room == nil would make this best-effort fallback observable in production.

📝 Optional observability tweak
 	if room == nil {
+		slog.Warn("room not found", "roomID", roomID, "op", op)
 		return ""
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@room-worker/handler.go` around lines 681 - 683, In lookupRoomType, when the
early-return branch sees room == nil, emit a warning-level log before returning
the empty RoomType so this fallback is observable; use the file's existing
logger (the same logger used elsewhere in handler.go) and include identifying
context such as the room ID or request/context info (e.g., roomID or id
variable) and a short message like "lookupRoomType: room is nil, returning empty
RoomType" to aid diagnostics, then return "" as before.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/superpowers/plans/2026-04-27-roomtype-on-subscription-plan.md`:
- Around line 45-46: The comment notes that inbox-worker's handleMemberAdded
currently always stamps RoomTypeChannel for cross-site member_added events;
update handleMemberAdded to determine the actual room type from the event
payload (or the room metadata) and only use RoomTypeChannel when the payload
indicates a channel, otherwise stamp the correct type (e.g., RoomTypeDiscussion
or RoomTypeDM) so discussion rooms are not misclassified; change any hardcoded
uses of RoomTypeChannel in handleMemberAdded to a conditional/mapper that
derives the room type from event.room_type or similar field.
- Around line 104-109: Several fenced code blocks in the markdown are missing
language labels (MD040); update each unlabeled triple-backtick block by adding
the appropriate language tag (e.g., bash for make commands like the block
containing "make test SERVICE=pkg/model" or "make test SERVICE=room-worker",
text for raw test output such as the block starting with "--- FAIL:
TestHandleEvent_MemberAdded", and go for Go snippets) so markdownlint passes;
apply the same change to the other occurrences called out in the review (the
blocks around the listed ranges) ensuring each fenced block has the correct
language label.

---

Nitpick comments:
In `@room-service/handler_test.go`:
- Around line 50-52: The test currently only asserts capturedSub.RoomType is not
Channel; instead set the request's type to a non-channel value (e.g.,
model.RoomTypeDirect or another enum) and assert that handleCreateRoom copies
req.Type through to the created subscription by checking capturedSub.RoomType ==
req.Type; update the test setup where the request is constructed and the
assertion that currently references model.RoomTypeChannel to compare against the
request's Type value (symbols: handleCreateRoom, req.Type, capturedSub,
model.RoomTypeChannel).

In `@room-worker/handler_test.go`:
- Around line 313-316: Tests currently assume store.GetRoom always succeeds; add
a failure-path test that verifies the handler performs a best-effort lookup
(logs/warns and proceeds with an empty RoomType) when GetRoom returns an error.
In handler_test.go add a mock expectation on
store.EXPECT().GetRoom(...).Return(nil, someError) and assert the handler
continues without failing and that the resulting model.Room.Type is empty (or
default), and that appropriate warning/log behavior occurs; apply the same
pattern for other handler tests referencing GetRoom (lines around the other
noted blocks) to lock this contract.

In `@room-worker/handler.go`:
- Around line 432-439: The Subscription being created in the add-members handler
hardcodes RoomType to model.RoomTypeChannel; change it to use the already-loaded
room.Type so the persisted subscription reflects the actual room kind. Locate
the code that constructs sub := &model.Subscription{... RoomType:
model.RoomTypeChannel, ...} and replace the hardcoded value with RoomType:
room.Type (ensuring room is in scope and of the expected type) so future
non-channel room types won’t drift silently.
- Around line 681-683: In lookupRoomType, when the early-return branch sees room
== nil, emit a warning-level log before returning the empty RoomType so this
fallback is observable; use the file's existing logger (the same logger used
elsewhere in handler.go) and include identifying context such as the room ID or
request/context info (e.g., roomID or id variable) and a short message like
"lookupRoomType: room is nil, returning empty RoomType" to aid diagnostics, then
return "" as before.
🪄 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: 66b129b3-64e4-434c-a6b4-ce89fc34bdcd

📥 Commits

Reviewing files that changed from the base of the PR and between 510ff2e and 45d2a0a.

📒 Files selected for processing (11)
  • docs/superpowers/plans/2026-04-27-roomtype-on-subscription-plan.md
  • docs/superpowers/specs/2026-04-23-roomtype-on-subscription-design.md
  • inbox-worker/handler.go
  • inbox-worker/handler_test.go
  • pkg/model/model_test.go
  • pkg/model/room.go
  • pkg/model/subscription.go
  • room-service/handler.go
  • room-service/handler_test.go
  • room-worker/handler.go
  • room-worker/handler_test.go

Comment on lines +45 to +46
4. **`inbox-worker`** — cross-site `handleMemberAdded` hardcodes `RoomTypeChannel` (only channel/discussion-style rooms ever produce cross-site `member_added` events, never DM/botDM).

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Clarify discussion-room behavior vs hardcoded channel stamping.

This plan says member_added can come from channels and discussions, but the implementation instruction still hardcodes RoomTypeChannel. Please tighten the rule so future changes don’t persist an incorrect room type for discussion rooms.

Also applies to: 484-485

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/superpowers/plans/2026-04-27-roomtype-on-subscription-plan.md` around
lines 45 - 46, The comment notes that inbox-worker's handleMemberAdded currently
always stamps RoomTypeChannel for cross-site member_added events; update
handleMemberAdded to determine the actual room type from the event payload (or
the room metadata) and only use RoomTypeChannel when the payload indicates a
channel, otherwise stamp the correct type (e.g., RoomTypeDiscussion or
RoomTypeDM) so discussion rooms are not misclassified; change any hardcoded uses
of RoomTypeChannel in handleMemberAdded to a conditional/mapper that derives the
room type from event.room_type or similar field.

Comment on lines +104 to +109
```
make test SERVICE=pkg/model
```

Expected output (verbatim):
```
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add language labels to fenced code blocks (MD040).

Several fenced blocks are unlabeled; add bash, go, or text to keep markdownlint clean.

🧩 Example fix pattern
-  ```
+  ```bash
   make test SERVICE=room-worker
  • --- FAIL: TestHandleEvent_MemberAdded (0.00s)
        handler_test.go:222: subscription RoomType = "", want "channel"
    
</details>



Also applies to: 150-155, 199-204, 229-237, 365-365, 453-461, 498-503, 527-535, 556-564, 572-572, 582-587

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 markdownlint-cli2 (0.22.1)</summary>

[warning] 104-104: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

---

[warning] 109-109: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

</details>

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against the current code and only fix it if needed.

In @docs/superpowers/plans/2026-04-27-roomtype-on-subscription-plan.md around
lines 104 - 109, Several fenced code blocks in the markdown are missing language
labels (MD040); update each unlabeled triple-backtick block by adding the
appropriate language tag (e.g., bash for make commands like the block containing
"make test SERVICE=pkg/model" or "make test SERVICE=room-worker", text for raw
test output such as the block starting with "--- FAIL:
TestHandleEvent_MemberAdded", and go for Go snippets) so markdownlint passes;
apply the same change to the other occurrences called out in the review (the
blocks around the listed ranges) ensuring each fenced block has the correct
language label.


</details>

<!-- fingerprinting:phantom:poseidon:hawk:5842c2f8-2fb3-4604-aff1-a717f91d0908 -->

<!-- d98c2f50 -->

<!-- This is an auto-generated comment by CodeRabbit -->

Comment thread room-worker/handler.go Outdated
RoomID: req.RoomID,
User: model.SubscriptionUser{ID: user.ID, Account: req.Account},
RoomID: req.RoomID,
RoomType: roomType,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

We can fix this to channel type too

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

hardcoded to roomTypeChannel.

Comment thread room-worker/handler.go Outdated
RoomID: req.RoomID,
User: model.SubscriptionUser{Account: m.Account},
RoomID: req.RoomID,
RoomType: roomType,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

We can fix this to channel too

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

hardcoded to RoomTypeChannel.

Comment thread room-worker/handler.go Outdated
// lookupRoomType fetches the room and returns its Type. On failure it logs at
// warn and returns "" — the field is best-effort metadata on notification
// payloads, never load-bearing.
func (h *Handler) lookupRoomType(ctx context.Context, roomID, op string) model.RoomType {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is not needed as we can only add/remove/assign role in a channel room

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

removed

claude added 2 commits April 29, 2026 03:23
Per mliu33's review on PR #137: member-management ops (member.add,
member.remove, member.role-update) only apply to channel rooms —
room-service rejects them for any other kind before they reach the
room-worker stream. So the runtime GetRoom round-trip in
processRemoveIndividual and processRemoveOrg is unnecessary; both sites
now hardcode RoomTypeChannel on the partial Subscription literal,
matching processAddMembers. The lookupRoomType helper is gone.

Tests drop the GetRoom mocks (six call sites) and keep the existing
RoomType assertion on the published SubscriptionUpdateEvent payload,
which now passes by virtue of the hardcode.

Resolves: #137 review threads on lines 183, 305, 675.
Spec §4 and plan Task 3 now describe hardcoding RoomTypeChannel at the
two remove sites instead of the previous fetch-room approach, matching
the room-worker code change in the prior commit.

Also addresses CodeRabbit's MD040 nit by adding language tags (bash for
shell commands, text for sample test output) to every previously
unlabeled fenced code block in the plan.
Copy link
Copy Markdown
Collaborator Author

Addressed the review feedback in c55c05f (code) + 0840ce6 (docs):

@mliu33 — three threads on room-worker/handler.go:

  • Lines 183 / 305: replaced the lookupRoomType call with a hardcoded model.RoomTypeChannel on both processRemoveIndividual and processRemoveOrg.
  • Line 675: deleted the lookupRoomType helper entirely.

Member-management ops (member.add / member.remove / member.role-update) are gated by room-service to channel rooms only, so the runtime room fetch was unnecessary. The hardcode now mirrors processAddMembers and the inbox-worker cross-site path. Tests dropped six GetRoom mocks; the existing RoomType assertion on the published SubscriptionUpdateEvent still passes.

@coderabbitai — two threads on the plan doc:

  • Discussion-room concern: now moot — every member-op site stamps RoomTypeChannel per the channel-only invariant. Spec §4 and plan Task 3 rewritten to match.
  • MD040: every previously unlabeled fenced block now has bash (shell commands) or text (sample test output).

The threads should appear outdated since the line anchors moved; please click "Resolve conversation" on each if it looks good.


Generated by Claude Code

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 29, 2026

Tip

For best results, initiate chat on the files or code changes.

@saurav-err-137 — thanks for the detailed summary!

Both my threads are addressed:

  • Discussion-room concern: Resolved. With every member-op site now hardcoding RoomTypeChannel (mirroring processAddMembers and the inbox-worker path), the speculative discussion-room scenario I raised is correctly moot given the room-service channel-only gate. The simplification is strictly better than the runtime fetch.
  • MD040: Resolved. Fenced blocks labeled bash/text satisfy the linter.

I've gone ahead and resolved my conversations.

[resolve]


✏️ Learnings added
Learnt from: saurav-err-137
URL: https://github.com/hmchangw/chat/pull/137

Timestamp: 2026-04-29T03:25:14.671Z
Learning: In `hmchangw/chat` (Go), member-management operations (`member.add`, `member.remove`, `member.role-update`) are gated by room-service to **channel rooms only**. As a consequence, all subscription creation/update sites that handle these operations — `processAddMembers`, `processRemoveIndividual`, `processRemoveOrg` in `room-worker/handler.go`, and `handleMemberAdded` in `inbox-worker/handler.go` — hardcode `RoomType: model.RoomTypeChannel` rather than doing a runtime room fetch. The `lookupRoomType` helper (introduced in PR `#137` and removed in the same PR after review) was unnecessary for this reason. Do not flag the absence of a dynamic room-type lookup in these paths.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: vjauhari-work
Repo: hmchangw/chat PR: 131
File: room-service/handler.go:120-134
Timestamp: 2026-04-28T08:01:13.105Z
Learning: In `room-service/handler.go` (Go), `handleCreateRoom` for `model.RoomTypeDM` intentionally creates only one subscription (for the creator) and sets `UserCount` to 1. The second participant (recipient) subscription is a known deferred defect — fixing it requires either a store lookup to resolve the recipient's `Account` or extending `CreateRoomRequest` with a `MembersAccount[]` companion field. A `TODO(idgen-rework follow-up)` marker was added (commit 5558bc1, PR `#131`). Do not flag the missing recipient subscription in `handleCreateRoom` as a new issue; it is tracked for a dedicated follow-up PR.

Learnt from: vjauhari-work
Repo: hmchangw/chat PR: 44
File: room-service/handler.go:392-400
Timestamp: 2026-04-01T10:43:13.466Z
Learning: In room-service/handler.go (Go), the handleAddMembers flow intentionally runs BulkCreateSubscriptions before writeRoomMembers without a transaction. The partial-commit inconsistency risk is mitigated by unique indexes on both `subscriptions` and `room_members` collections (added in PR `#44`), making retries idempotent (duplicate-key errors suppressed). A full MongoDB transactional approach is deferred to a follow-up PR; MongoDB transactions require a replica set.

Learnt from: vjauhari-work
Repo: hmchangw/chat PR: 44
File: room-service/handler.go:345-360
Timestamp: 2026-04-01T10:43:06.314Z
Learning: In `room-service/handler.go` (Go), the `handleAddMembers` function intentionally has no owner authorization gate — any authenticated user is allowed to add members to a room. This is a deliberate product/design decision confirmed by the PR author (PR `#44`). Do not flag the absence of an owner check in this handler as a security issue.

Learnt from: vjauhari-work
Repo: hmchangw/chat PR: 72
File: room-service/handler.go:0-0
Timestamp: 2026-04-14T01:20:03.653Z
Learning: In `room-service/handler.go` (Go), the `CountOwners` pre-publish check in `handleUpdateRole` is intentionally the authoritative last-owner guard. The concurrent-demotion race (two simultaneous demotions both observing count > 1 before the worker applies them) is a known edge case explicitly accepted by the PR author (PR `#72`). Do not flag this as a race condition issue.

Learnt from: Joey0538
Repo: hmchangw/chat PR: 64
File: docs/superpowers/specs/2026-04-09-room-spotlight-user-room-design.md:312-313
Timestamp: 2026-04-09T09:24:33.406Z
Learning: In hmchangw/chat, the design doc `docs/superpowers/specs/2026-04-09-room-spotlight-user-room-design.md` defers `userRoomCollection` (scripted ES updates for per-user rooms array) and `spotlightCollection` to a follow-up PR. When implementing `ActionUpdate` with `version_type: external` in the bulk API, note: (1) for ES 8.x and OpenSearch 2.x, bulk `update` supports `version` + `version_type: external`, requiring the incoming version to be strictly greater than the stored version (HTTP 409 on conflict); (2) for scripted upserts that are already idempotent via Painless contains/indexOf checks, skipping external versioning may be simpler and safer; (3) do not mix `version`/`version_type` with seq-no concurrency (`if_seq_no`/`if_primary_term`) in the same bulk request.

Learnt from: Joey0538
Repo: hmchangw/chat PR: 78
File: search-sync-worker/user_room.go:0-0
Timestamp: 2026-04-15T02:05:59.183Z
Learning: In `search-sync-worker/handler.go` (Go, hmchangw/chat), `isBulkItemSuccess` treats HTTP 404 with `document_missing_exception` on `ActionUpdate` and `ActionDelete` as success (the user doc simply doesn't exist yet). `index_not_found_exception` on 404 is treated as failure (fatal misconfiguration). This is the intentional design: `buildRemoveRoomUpdateBody` emits a bare scripted update with no upsert, and the handler absorbs the benign missing-doc 404 so each collection does not need its own defensive upsert. Adding an upsert to the remove path would create a ghost user-room doc on a remove-before-add race. Do not flag the absence of `scripted_upsert` on the member-removed path in `user_room.go` as a bug.

Learnt from: Joey0538
Repo: hmchangw/chat PR: 59
File: pkg/roomkeysender/roomkeysender.go:28-30
Timestamp: 2026-04-08T08:25:34.310Z
Learning: In the `hmchangw/chat` repository, the CLAUDE.md guideline explicitly states: "Don't add error handling, fallbacks, or validation for scenarios that can't happen. Trust internal code and framework guarantees. Only validate at system boundaries." Internal functions and methods (e.g., `pkg/roomkeysender/roomkeysender.go`'s `Send`) should NOT have nil-pointer guards or similar defensive checks added for pointer parameters when all callers are known to pass non-nil values. Do not flag the absence of such nil checks as issues in this codebase.

Learnt from: yenta
Repo: hmchangw/chat PR: 80
File: message-worker/handler.go:192-199
Timestamp: 2026-04-14T16:58:26.689Z
Learning: In `message-worker/handler.go` (Go), `handleFirstThreadReply` and `handleSubsequentThreadReply` intentionally return `nil` (after a `slog.Warn`) when `GetMessageSender` returns `errMessageNotFound` for the parent message. This skips all remaining steps — including replier subscription creation and `UpdateThreadRoomLastMessage` — by design: a missing parent is treated as an unresolvable thread context, and no partial Mongo state should be written. Do not flag this early-return pattern as a bug.

Learnt from: GITMateuszCharczuk
Repo: hmchangw/chat PR: 127
File: history-service/internal/cassrepo/thread_messages.go:13-31
Timestamp: 2026-04-28T05:45:22.843Z
Learning: In `hmchangw/chat` (Go, Cassandra), both `messages_by_room` and `thread_messages_by_room` follow a sparse-write / lazy-populate contract: `SaveMessage` / `SaveThreadMessage` in `message-worker/store_cassandra.go` INSERT only the core columns (room_id, message_id, sender, msg, site_id, mentions, type, sys_msg_data, etc.). Columns such as `attachments`, `file`, `card`, `card_action`, `quoted_parent_message`, `visible_to`, `unread`, `reactions`, `deleted`, `edited_at` are schema-declared but written as NULL on first insert and populated later by separate update flows (edit, reaction, delete paths). The reader projections in `baseColumns` / `threadMessageColumns` correctly include all these columns; NULLs on first read are expected and not a bug. Do not flag the mismatch between INSERT column list and SELECT column list as a missing-field bug.

Learnt from: Joey0538
Repo: hmchangw/chat PR: 115
File: search-sync-worker/user_room.go:116-123
Timestamp: 2026-04-22T05:06:20.669Z
Learning: In `search-sync-worker/user_room.go` (Go, hmchangw/chat), the `hss <= 0 → unrestricted` value is the intentional Go↔painless sentinel contract for `HistorySharedSince`. A `nil` pointer on the wire becomes `hss = 0` on the consumer side; the consumer does NOT validate non-nil non-positive values because: (1) the spec (`2026-04-21-search-service-sync-worker-extension-design.md`) designates the publisher as the source of truth; (2) `room-worker/handler.go` only constructs the pointer when `req.Timestamp > 0`; (3) publisher tests (`TestHandler_ProcessAddMembers_RestrictedPropagatesPointer` / `TestHandler_ProcessAddMembers_UnrestrictedOmitsFieldFromWire`) assert pointer propagation and wire omission; (4) consumer-side rejection would introduce a JetStream redelivery-loop failure mode. Do not flag the absence of `<= 0` guard at the consumer as a bug.

Learnt from: Joey0538
Repo: hmchangw/chat PR: 64
File: docs/superpowers/specs/2026-04-09-room-spotlight-user-room-design.md:62-67
Timestamp: 2026-04-09T09:24:14.331Z
Learning: In `docs/superpowers/specs/2026-04-09-room-spotlight-user-room-design.md` (hmchangw/chat), `SubscriptionChangeEvent.Timestamp` (int64, UnixMilli) is intentionally used as the external versioning field for Elasticsearch `version_type: external` indexing. The rationale: a single gatekeeper pod is the only publisher per siteID, making wall-clock milliseconds monotonically increasing in practice; for multi-pod scenarios, later events naturally carry higher timestamps. The design explicitly rejected a distributed counter as unnecessary coordination complexity. Do not flag this pattern as a versioning collision risk in this repo.

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

♻️ Duplicate comments (1)
docs/superpowers/plans/2026-04-27-roomtype-on-subscription-plan.md (1)

46-47: ⚠️ Potential issue | 🟡 Minor

Tighten the member-added rule to channel-only.

This still mentions channels and discussions, but the invariant in this repo is channel-only. Update the wording so future follow-ups don’t preserve an incorrect RoomType for discussion rooms.

Suggested tweak
- Cross-site `member_added` events only fire for rooms that support add-member (channels and discussions). Hardcoding RoomTypeChannel mirrors room-worker.processAddMembers so every persisted subscription carries a non-empty RoomType.
+ Cross-site `member_added` events only fire for channel rooms. Hardcoding RoomTypeChannel mirrors room-worker.processAddMembers so every persisted subscription carries a non-empty RoomType.

Based on learnings: member-management operations (member.add, member.remove, member.role-update) are gated by room-service to channel rooms only.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/superpowers/plans/2026-04-27-roomtype-on-subscription-plan.md` around
lines 46 - 47, Update the documentation text in the "inbox-worker" bullet to
state the channel-only invariant explicitly: replace the current wording that
mentions channels and discussions with a definitive statement that cross-site
handleMemberAdded (and the originating member.add gateway) applies only to
RoomTypeChannel; reference the function/event names "inbox-worker",
"handleMemberAdded", "member.add"/"member_added" and the enum/value
"RoomTypeChannel" so readers know the exact symbols enforcing this rule and
remove any phrasing that suggests discussion rooms are applicable.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/superpowers/specs/2026-04-23-roomtype-on-subscription-design.md`:
- Around line 157-163: Update the spec text to reflect the current
implementation: state that removed-event payloads embed the subscription with
RoomType set to the hardcoded RoomTypeChannel value (not a runtime lookup with
an empty fallback). Replace the sentence describing a runtime room
lookup/fallback for SubscriptionUpdateEvent.Subscription.RoomType with an
explicit note that removed events carry RoomTypeChannel, and keep the existing
note about old subscriptions without roomType being returned as RoomType: "" for
backwards compatibility.

In `@room-worker/handler_test.go`:
- Around line 338-345: The test only asserts evt.Subscription.RoomType for the
individual-removal publish path; add the same check for the org-removal path by
locating where processRemoveOrg is exercised and extending the published-events
loop (or a new loop) to match subject.SubscriptionUpdate for the org case and
unmarshal into model.SubscriptionUpdateEvent, then assert assert.Equal(t,
model.RoomTypeChannel, evt.Subscription.RoomType) for that event as well so both
processRemoveIndividual and processRemoveOrg are covered.

---

Duplicate comments:
In `@docs/superpowers/plans/2026-04-27-roomtype-on-subscription-plan.md`:
- Around line 46-47: Update the documentation text in the "inbox-worker" bullet
to state the channel-only invariant explicitly: replace the current wording that
mentions channels and discussions with a definitive statement that cross-site
handleMemberAdded (and the originating member.add gateway) applies only to
RoomTypeChannel; reference the function/event names "inbox-worker",
"handleMemberAdded", "member.add"/"member_added" and the enum/value
"RoomTypeChannel" so readers know the exact symbols enforcing this rule and
remove any phrasing that suggests discussion rooms are applicable.
🪄 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: 1aca51e5-b292-41d3-b834-9d7e2e9c9e8d

📥 Commits

Reviewing files that changed from the base of the PR and between 45d2a0a and 0840ce6.

📒 Files selected for processing (4)
  • docs/superpowers/plans/2026-04-27-roomtype-on-subscription-plan.md
  • docs/superpowers/specs/2026-04-23-roomtype-on-subscription-design.md
  • room-worker/handler.go
  • room-worker/handler_test.go

Comment on lines +157 to +163
- **Old subscriptions without `roomType`.** Returned as `RoomType: ""`. No
code currently reads the field, so this is harmless until a consumer starts
relying on it. A future backfill job (out of scope) can populate old
records.
- **Client code consuming `SubscriptionUpdateEvent.Subscription.RoomType` on
removed events.** Covered by populating the field from the fetched room; if
the fetch fails the field is empty, same as before the change.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Update the remove-path contract to match the implementation.

The spec still describes a runtime room lookup / empty fallback for removed-event payloads. The code now hardcodes RoomTypeChannel on the embedded subscription, so the design text should say that explicitly.

Suggested tweak
- Client code consuming SubscriptionUpdateEvent.Subscription.RoomType on removed events. Covered by populating the field from the fetched room; if the fetch fails the field is empty, same as before the change.
+ Client code consuming SubscriptionUpdateEvent.Subscription.RoomType on removed events. Covered by hardcoding `RoomTypeChannel` on the partial Subscription payloads in room-worker, matching the channel-only member-management invariant.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/superpowers/specs/2026-04-23-roomtype-on-subscription-design.md` around
lines 157 - 163, Update the spec text to reflect the current implementation:
state that removed-event payloads embed the subscription with RoomType set to
the hardcoded RoomTypeChannel value (not a runtime lookup with an empty
fallback). Replace the sentence describing a runtime room lookup/fallback for
SubscriptionUpdateEvent.Subscription.RoomType with an explicit note that removed
events carry RoomTypeChannel, and keep the existing note about old subscriptions
without roomType being returned as RoomType: "" for backwards compatibility.

Comment on lines +338 to +345
for _, p := range published {
if p.subj != subject.SubscriptionUpdate(account) {
continue
}
var evt model.SubscriptionUpdateEvent
require.NoError(t, json.Unmarshal(p.data, &evt))
assert.Equal(t, model.RoomTypeChannel, evt.Subscription.RoomType, "subscription update should carry RoomType")
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Extend the RoomType assertion to the org-removal path too.

This only exercises processRemoveIndividual; processRemoveOrg still has no assertion for the embedded Subscription.RoomType, so a regression there would slip through.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@room-worker/handler_test.go` around lines 338 - 345, The test only asserts
evt.Subscription.RoomType for the individual-removal publish path; add the same
check for the org-removal path by locating where processRemoveOrg is exercised
and extending the published-events loop (or a new loop) to match
subject.SubscriptionUpdate for the org case and unmarshal into
model.SubscriptionUpdateEvent, then assert assert.Equal(t,
model.RoomTypeChannel, evt.Subscription.RoomType) for that event as well so both
processRemoveIndividual and processRemoveOrg are covered.

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.

Great work, thanks!

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