Skip to content

refactor(fga): make committee member FGA publishing asynchronous-only - #164

Merged
prabodhcs merged 6 commits into
mainfrom
LFXV2-2856
Aug 7, 2026
Merged

refactor(fga): make committee member FGA publishing asynchronous-only#164
prabodhcs merged 6 commits into
mainfrom
LFXV2-2856

Conversation

@prabodhcs

Copy link
Copy Markdown
Contributor

Summary

  • Removes the generic, sync-capable CommitteePublisher.Access(subject, msg, sync) method and replaces it with dedicated MemberPut/MemberRemove methods that always core-publish (fire-and-forget), mirroring the existing UpdateAccess/DeleteAccess methods.
  • Reverts AcceptInvite's CreateMember call to sync=false. JetStream (LFXV2-2831) cannot preserve request/reply semantics for FGA subjects, so keeping a synchronous FGA path would either time out or silently degrade to a storage ack instead of an fga-sync completion ack. Removing it now, before the JetStream migration, avoids that failure mode entirely.
  • The read-your-writes window this reopens (a page load immediately after accepting an invite can still see a stale FGA tuple) is intended to be covered by client-side polling in Self Serve before this ships — see the companion frontend ticket linked below.

Why

PR #163 made AcceptInvite's member_put synchronous (request/reply) to close a post-accept 403 window. That directly conflicts with this ticket's requirement that FGA membership publishing be asynchronous-only, and it would break outright once NATS core publishes migrate to JetStream, since JetStream stream-backed subjects ack on durable storage rather than waiting for the fga-sync consumer to actually process the message — so the synchronous wait would either time out (if NoAck is required) or silently stop guaranteeing what it appears to guarantee.

Breaking changes

  • AcceptInvite no longer waits for FGA processing before returning. This reverts PR [LFXV2-2645] fix(invites): sync FGA member_put on AcceptInvite to prevent post-accept 403 #163's sync=true workaround. The eventual-consistency window it reopens is mitigated by client-side polling in Self Serve, which should ship before/alongside this change.
  • X-Sync: true no longer has any effect on FGA membership publishing for any caller. It continues to gate only the indexer publish (unchanged). No current caller sends X-Sync: true on a member endpoint.

Changes

  • internal/domain/port/committee_publisher.go: interface now exposes MemberPut/MemberRemove instead of the generic Access method.
  • internal/infrastructure/nats/messaging_publish.go: implements MemberPut/MemberRemove; extracts a shared publishAccessAsync helper used by UpdateAccess/DeleteAccess/MemberPut/MemberRemove.
  • internal/service/committee_member_writer.go: member write orchestration calls the new dedicated methods instead of Access(subject, msg, sync).
  • cmd/committee-api/service/committee_service.go: AcceptInvite reverted to CreateMember(ctx, member, false, false), with a comment explaining the deliberate revert and its dependency on the Self Serve polling fix.
  • cmd/committee-api/design/type.go, docs/fga-contract.md: documentation updated to state member_put/member_remove (along with update_access/delete_access) are always asynchronous, regardless of X-Sync.
  • Mocks, stubs, and tests updated across internal/infrastructure/mock, internal/infrastructure/nats, internal/service, and cmd/committee-api/service/cmd/committee-cli to reflect the new interface shape.
  • Generated Goa output (gen/...) regenerated for the design doc change.

Live evidence (real NATS traffic, IDs/usernames redacted below)

Verified against a real local nats-server (JetStream enabled), using the production publisher and orchestrator with only the repository layer mocked. Full write-up lives in the change's evidence doc (not part of this diff; local-only OpenSpec artifact).

Scenario 1 — CreateMember(sync=false), i.e. AcceptInvite's reverted path:

CreateMember returned in 483.417µs with member UID=<member-uid-1>

Returns in microseconds — no blocking on FGA at all.

Scenario 2 — DeleteMember, happy path:

DeleteMember returned in 205.792µs, error=<nil>

Scenario 3 — CreateMember(sync=true) — proves sync reaches only the indexer, never FGA:

ERROR failed to send synchronous request to NATS error="context deadline exceeded" subject=lfx.index.committee_member message_type=indexer timeout=10s
CreateMember(sync=true) returned in 10.001728167s with member UID=<member-uid-2>

The call blocked for the full 10s NATS request timeout only on the indexer subject (no subscriber replied). The member_put for this same call still went out as a core publish with no reply subject.

Captured wire evidence across all three scenarios:

subject Reply empty (core publish)? notes
lfx.fga-sync.member_put true Scenario 1 grant
lfx.index.committee_member true Scenario 1 indexer (sync=false)
lfx.index.committee_member true Scenario 2 indexer delete (sync=false)
lfx.fga-sync.member_remove true Scenario 2 revoke
lfx.fga-sync.member_put true Scenario 3 grant, even though sync=true
lfx.index.committee_member false Scenario 3 indexer create (sync=true ⇒ request/reply)

Every lfx.fga-sync.member_put/member_remove message across all three scenarios had an empty Reply, including Scenario 3 where the indexer message for the same CreateMember call had a non-empty Reply and blocked for 10s. This is the change's core guarantee, observed on real NATS wire traffic: FGA membership publication is unconditionally asynchronous, and sync affects only the indexer.

Raw captured payloads (IDs/usernames redacted)
subject=lfx.fga-sync.member_put          no_reply=true payload={"object_type":"committee","operation":"member_put","data":{"uid":"<committee-uid>","username":"<username-1>","relations":["member"],"mutually_exclusive_with":null}}
subject=lfx.index.committee_member       no_reply=true payload={"action":"created", ... "uid":"<member-uid-1>", ...}
subject=lfx.index.committee_member       no_reply=true payload={"action":"deleted","data":"<member-uid-1>"}
subject=lfx.fga-sync.member_remove       no_reply=true payload={"object_type":"committee","operation":"member_remove","data":{"uid":"<committee-uid>","username":"<username-1>","relations":[],"mutually_exclusive_with":null}}
subject=lfx.fga-sync.member_put          no_reply=true payload={"object_type":"committee","operation":"member_put","data":{"uid":"<committee-uid>","username":"<username-2>","relations":["member"],"mutually_exclusive_with":null}}
subject=lfx.index.committee_member       no_reply=false payload={"action":"created", ... "uid":"<member-uid-2>", ...}

Not covered by this harness (already covered by go test -race ./...):

  • HTTP-layer behavior (AcceptInvite endpoint, Heimdall/JWT, X-Sync header parsing) — requires infra not stood up here.
  • DeleteMember immediate-publish-failure-is-returned, and changed-username revoke-then-put ordering — covered by TestCommitteeWriterOrchestrator_DeleteMember_MessagePublishingFailure and TestPublishMemberMessages_ChangedUsername_RevokesOldBeforeGrantingNew.

Test plan

  • go build ./...
  • go test -race ./...
  • Live evidence run against a real local nats-server (JetStream enabled) — see above
  • Self Serve polling fix (LFXV2-2890) deployed before/alongside this change, to bound the AcceptInvite eventual-consistency window

Jira: LFXV2-2856

Made with Cursor

Copilot AI balanced review requested due to automatic review settings July 29, 2026 16:04
@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

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

The change replaces generic FGA access publishing with dedicated asynchronous MemberPut and MemberRemove methods. AcceptInvite now creates members asynchronously. Member writer behavior, contracts, mocks, documentation, and tests reflect the new publication model.

Changes

Asynchronous FGA membership

Layer / File(s) Summary
Publisher contract and asynchronous transport
internal/domain/port/committee_publisher.go, internal/infrastructure/nats/messaging_publish.go, internal/infrastructure/mock/committee.go, internal/infrastructure/nats/messaging_publish_test.go, docs/fga-contract.md, cmd/committee-api/design/type.go
The publisher removes generic Access routing and adds asynchronous MemberPut and MemberRemove methods. Shared publication logic and error tests cover all four access operations.
Member writer publication behavior
internal/service/committee_member_writer.go, internal/service/committee_member_writer_test.go
Member grants, deletions, and stale-username cleanup use dedicated member methods. Tests cover publication failures, cleanup ordering, and independent indexer synchronization.
Invite flow and call-site validation
cmd/committee-api/service/committee_service.go, cmd/committee-api/service/committee_service_test.go, cmd/committee-api/service/group_weekly_brief_test.go, cmd/committee-cli/commands/sync/reindex_invites_test.go, internal/service/committee_writer_test.go, internal/service/document_writer_test.go, internal/service/message_handler_test.go
AcceptInvite passes sync=false for member creation. Test publishers implement the new methods and assert that unrelated invite and routing flows do not publish membership messages.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant AcceptInvite
  participant committee_member_writer
  participant messagePublisher
  participant CoreNATS
  AcceptInvite->>committee_member_writer: CreateMember with sync=false
  committee_member_writer->>messagePublisher: MemberPut
  messagePublisher->>CoreNATS: Publish member_put asynchronously
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 45.45% 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
Title check ✅ Passed The title clearly and concisely summarizes the main change: committee member FGA publishing is now asynchronous-only.
Description check ✅ Passed The description directly explains the asynchronous FGA publishing refactor, affected APIs, behavior changes, testing, and deployment dependency.
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.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch LFXV2-2856

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

@prabodhcs prabodhcs added the do-not-merge Indicates that the pull request should NOT be merged. label Jul 29, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Refactors committee-member FGA publishing to use asynchronous core NATS exclusively.

Changes:

  • Replaces generic Access publishing with dedicated MemberPut and MemberRemove methods.
  • Reverts AcceptInvite to asynchronous member creation publishing.
  • Updates tests, FGA documentation, Goa design, and generated API artifacts.

Reviewed changes

Copilot reviewed 17 out of 20 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
internal/service/message_handler_test.go Updates publisher spy methods.
internal/service/document_writer_test.go Updates error publisher stub.
internal/service/committee_writer_test.go Updates mocks and routing assertions.
internal/service/committee_member_writer.go Routes membership FGA messages through dedicated methods.
internal/service/committee_member_writer_test.go Tests publishing failures, ordering, and sync behavior.
internal/infrastructure/nats/messaging_publish.go Adds shared asynchronous FGA publisher.
internal/infrastructure/nats/messaging_publish_test.go Verifies core publishing and error paths.
internal/infrastructure/mock/committee.go Adds membership publisher captures.
internal/domain/port/committee_publisher.go Replaces the generic access interface.
gen/http/openapi3.yaml Regenerates OpenAPI 3 documentation.
gen/http/openapi.yaml Regenerates OpenAPI documentation.
gen/committee_service/service.go Regenerates Goa payload documentation.
docs/fga-contract.md Documents asynchronous membership publishing.
cmd/committee-cli/commands/sync/reindex_invites_test.go Updates CLI publisher mock.
cmd/committee-api/service/group_weekly_brief_test.go Updates publisher stub.
cmd/committee-api/service/committee_service.go Makes AcceptInvite use the asynchronous path.
cmd/committee-api/service/committee_service_test.go Updates the service mock and acceptance tests.
cmd/committee-api/design/type.go Updates the X-Sync API description.
Files not reviewed (1)
  • gen/committee_service/service.go: Generated file

Comment thread cmd/committee-api/service/committee_service_test.go Outdated
Remove the generic, sync-capable Access(subject, msg, sync) publisher method
and replace it with dedicated MemberPut/MemberRemove methods that always
core-publish, mirroring UpdateAccess/DeleteAccess. JetStream (LFXV2-2831)
cannot preserve request/reply semantics for these subjects, so removing them
now avoids a silent degradation to storage-ack instead of fga-sync
completion-ack later.

BREAKING: AcceptInvite no longer waits for FGA processing before returning,
reverting PR #163's sync=true workaround (which itself addressed LFXV2-2645).
The eventual-consistency window this reopens is mitigated by client-side
polling in Self Serve (LFXV2-2890), which must deploy before this change.

BREAKING: X-Sync no longer has any effect on FGA membership publishing for
any caller (none currently send X-Sync: true on a member endpoint).

Jira: LFXV2-2856
Link: https://linuxfoundation.atlassian.net/browse/LFXV2-2856
Signed-off-by: Prabodh Chaudhari <pchaudhari@linuxfoundation.org>
Co-authored-by: Cursor <cursoragent@cursor.com>
Copilot AI review requested due to automatic review settings July 31, 2026 11:57
@prabodhcs
prabodhcs marked this pull request as ready for review July 31, 2026 11:57
@prabodhcs
prabodhcs requested a review from a team as a code owner July 31, 2026 11:57

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 17 out of 20 changed files in this pull request and generated no new comments.

Files not reviewed (1)
  • gen/committee_service/service.go: Generated file
Suppressed comments (1)

cmd/committee-api/service/committee_service_test.go:120

  • Keep recording the sync argument and assert that successful AcceptInvite calls pass false. If the call regresses to true, membership FGA remains asynchronous, but CreateMember can still block on its synchronous indexer request for up to 10 seconds. Removing the previous argument assertion leaves this endpoint-level behavior untested; the new orchestrator test only verifies how a supplied flag is routed.
	createMemberCalls []*model.CommitteeMember

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
cmd/committee-api/service/committee_service_test.go (1)

112-121: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Restore test coverage for the sync argument passed to CreateMember in AcceptInvite.

createMemberSyncArgs tracking was removed instead of updated. TestAcceptInvite no longer verifies that CreateMember receives sync=false. This PR's stated purpose is to guarantee AcceptInvite always passes sync=false to CreateMember. Without this assertion, a future regression that reintroduces sync=true in AcceptInvite would not be caught by this test.

Track the sync argument again and assert it in TestAcceptInvite.

♻️ Proposed fix to restore sync-argument coverage
 type mockCommitteeWriterOrchestrator struct {
 	deleteError       error
 	deleteCalls       []deleteCall
 	updateMember      *model.CommitteeMember
 	updateMemberErr   error
 	updateMemberCalls []updateMemberCall
 	createMember      *model.CommitteeMember
 	createMemberErr   error
 	createMemberCalls []*model.CommitteeMember
+	createMemberSyncArgs []bool
 }

 func (m *mockCommitteeWriterOrchestrator) CreateMember(ctx context.Context, member *model.CommitteeMember, sync bool, skipEnrichment bool) (*model.CommitteeMember, error) {
 	m.createMemberCalls = append(m.createMemberCalls, member)
+	m.createMemberSyncArgs = append(m.createMemberSyncArgs, sync)
 	if m.createMemberErr != nil {
 		return nil, m.createMemberErr
 	}
 	if m.createMember != nil {
 		return m.createMember, nil
 	}
 	return nil, errs.NewUnexpected("not implemented for test")
 }

Then in TestAcceptInvite, after a successful accept:

 				require.NotNil(t, result)
 				assert.Equal(t, "Active", result.Status)
+				require.NotEmpty(t, mockOrch.createMemberSyncArgs)
+				assert.False(t, mockOrch.createMemberSyncArgs[len(mockOrch.createMemberSyncArgs)-1], "AcceptInvite must call CreateMember with sync=false")

Also applies to: 149-158, 1416-1430

🤖 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 `@cmd/committee-api/service/committee_service_test.go` around lines 112 - 121,
The mockCommitteeWriterOrchestrator no longer records the sync argument passed
to CreateMember. Restore sync tracking in the mock’s CreateMember call data,
then update TestAcceptInvite’s successful-accept case to assert that
CreateMember received sync=false, preserving coverage against regressions.
🤖 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 `@internal/infrastructure/mock/committee.go`:
- Around line 1312-1325: Replace the new slog.InfoContext calls in
MockCommitteePublisher.MemberPut and MemberRemove with the committee service
pkg/log helper, preserving the existing context and message fields. Ensure these
internal mock logging calls follow the pkg/log convention.

---

Nitpick comments:
In `@cmd/committee-api/service/committee_service_test.go`:
- Around line 112-121: The mockCommitteeWriterOrchestrator no longer records the
sync argument passed to CreateMember. Restore sync tracking in the mock’s
CreateMember call data, then update TestAcceptInvite’s successful-accept case to
assert that CreateMember received sync=false, preserving coverage against
regressions.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 43187683-0b0a-4126-967d-099a6c522a37

📥 Commits

Reviewing files that changed from the base of the PR and between bd39fe9 and a2b355e.

⛔ Files ignored due to path filters (5)
  • gen/committee_service/service.go is excluded by !**/gen/**
  • gen/http/openapi.json is excluded by !**/gen/**
  • gen/http/openapi.yaml is excluded by !**/gen/**
  • gen/http/openapi3.json is excluded by !**/gen/**
  • gen/http/openapi3.yaml is excluded by !**/gen/**
📒 Files selected for processing (15)
  • cmd/committee-api/design/type.go
  • cmd/committee-api/service/committee_service.go
  • cmd/committee-api/service/committee_service_test.go
  • cmd/committee-api/service/group_weekly_brief_test.go
  • cmd/committee-cli/commands/sync/reindex_invites_test.go
  • docs/fga-contract.md
  • internal/domain/port/committee_publisher.go
  • internal/infrastructure/mock/committee.go
  • internal/infrastructure/nats/messaging_publish.go
  • internal/infrastructure/nats/messaging_publish_test.go
  • internal/service/committee_member_writer.go
  • internal/service/committee_member_writer_test.go
  • internal/service/committee_writer_test.go
  • internal/service/document_writer_test.go
  • internal/service/message_handler_test.go

Comment thread internal/infrastructure/mock/committee.go
TestAcceptInvite no longer verified that CreateMember always receives
sync=false, since createMemberSyncArgs tracking was dropped when the
generic sync-argument assertion was removed. Restore it: a regression to
sync=true would reintroduce the request/reply indexer timeout this change
removed, and nothing else in the suite catches that at the endpoint level.

Addresses PR #164 review feedback (Copilot, CodeRabbit).

Jira: LFXV2-2856
Link: https://linuxfoundation.atlassian.net/browse/LFXV2-2856
Signed-off-by: Prabodh Chaudhari <pchaudhari@linuxfoundation.org>
Co-authored-by: Cursor <cursoragent@cursor.com>
Copilot AI review requested due to automatic review settings July 31, 2026 12:25

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 17 out of 20 changed files in this pull request and generated no new comments.

Files not reviewed (1)
  • gen/committee_service/service.go: Generated file

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
cmd/committee-api/service/committee_service_test.go (1)

118-121: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Use the shared infrastructure fake for this test state.

createMemberSyncArgs extends mockCommitteeWriterOrchestrator, a fake declared in this test file. Move this behavior to a compatible fake in internal/infrastructure/mock, or add the orchestrator fake there before using it here.

As per coding guidelines, *_test.go files must use table-driven tests with internal/infrastructure/mock fakes.

🤖 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 `@cmd/committee-api/service/committee_service_test.go` around lines 118 - 121,
Move the mockCommitteeWriterOrchestrator fake from the committee_service_test.go
file to internal/infrastructure/mock so it can be reused as a shared test
infrastructure. Add the createMemberSyncArgs field to the moved orchestrator
fake definition. Update the test to import and use the orchestrator fake from
the shared mock package instead of declaring it locally, ensuring the test
follows the table-driven test pattern with infrastructure fakes as required by
the coding guidelines.

Source: Coding guidelines

🤖 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 `@cmd/committee-api/service/committee_service_test.go`:
- Around line 1434-1442: Update the AcceptInvite test table and assertions
around mockOrch.createMemberSyncArgs to include the expected CreateMember call
count per case. Assert exactly one call with sync=false for pending and declined
invites, and zero calls for accepted and revoked invites; remove the conditional
last-value-only check.

---

Nitpick comments:
In `@cmd/committee-api/service/committee_service_test.go`:
- Around line 118-121: Move the mockCommitteeWriterOrchestrator fake from the
committee_service_test.go file to internal/infrastructure/mock so it can be
reused as a shared test infrastructure. Add the createMemberSyncArgs field to
the moved orchestrator fake definition. Update the test to import and use the
orchestrator fake from the shared mock package instead of declaring it locally,
ensuring the test follows the table-driven test pattern with infrastructure
fakes as required by the coding guidelines.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b7e48c0f-c6c7-4e96-b548-18330cc0be5b

📥 Commits

Reviewing files that changed from the base of the PR and between a2b355e and 28e4cfc.

📒 Files selected for processing (1)
  • cmd/committee-api/service/committee_service_test.go

Comment thread cmd/committee-api/service/committee_service_test.go
@prabodhcs

Copy link
Copy Markdown
Contributor Author

Postflight summary

Thread Action Commit
Copilot @ committee_service_test.go:120 — restore sync=false regression assertion Fixed 28e4cfc
CodeRabbit @ committee_service_test.go:112-121 — same finding Fixed 28e4cfc
CodeRabbit @ mock/committee.go:1312-1325 — use pkg/log instead of slog.InfoContext Declined (see reply)

All threads resolved. CI green (Build and Test, Analyze, CodeQL, DCO, License checks, MegaLinter). No stale human approvals to dismiss.

The prior sync=false assertion only inspected the last recorded arg, so it
would silently pass even if CreateMember were called an unexpected number
of times (or not at all) for a given case. Add an explicit
expectCreateMemberCalled flag per test case and assert exactly one
sync=false call for pending/declined accepts, and zero calls for the
idempotent accepted/revoked paths.

Addresses PR #164 review feedback (CodeRabbit).

Jira: LFXV2-2856
Link: https://linuxfoundation.atlassian.net/browse/LFXV2-2856
Signed-off-by: Prabodh Chaudhari <pchaudhari@linuxfoundation.org>
Co-authored-by: Cursor <cursoragent@cursor.com>
Copilot AI review requested due to automatic review settings July 31, 2026 12:50

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 17 out of 20 changed files in this pull request and generated no new comments.

Files not reviewed (1)
  • gen/committee_service/service.go: Generated file

@prabodhcs

Copy link
Copy Markdown
Contributor Author

Postflight summary (round 2)

Thread Action Commit
CodeRabbit @ committee_service_test.go:1442 — assertion only checked last recorded sync value, allowed unexpected CreateMember calls Fixed f0e1447
CodeRabbit (review body only, no thread) — move local mockCommitteeWriterOrchestrator fake to internal/infrastructure/mock Declined — pre-existing pattern predating this PR, out of scope for a 1-line test-coverage fix (CodeRabbit itself labeled it "Heavy lift")

All 3 threads on this PR are now resolved. CI green (Build and Test, Analyze, CodeQL, DCO, License checks, MegaLinter, CodeRabbit re-review).

Copilot AI review requested due to automatic review settings August 5, 2026 10:01

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 17 out of 20 changed files in this pull request and generated no new comments.

Files not reviewed (1)
  • gen/committee_service/service.go: Generated file

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

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

⚠️ Outside diff range comments (3)
internal/service/committee_member_writer_test.go (3)

2285-2351: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Assert the publication order.

The test checks MemberRemove and MemberPut in separate slices. It does not prove that member_remove occurs before member_put. A regression that grants new_username first would still pass. Record one ordered publication sequence and assert the required order. (raw.githubusercontent.com)

🤖 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 `@internal/service/committee_member_writer_test.go` around lines 2285 - 2351,
The test TestPublishMemberMessages_ChangedUsername_RevokesOldBeforeGrantingNew
currently validates message contents but not publication order. Add an ordered
publication sequence to spyCommitteePublisher, record both member_remove and
member_put events as they are published, and assert in the successful revoke
subtest that the remove event precedes the put event while preserving the
existing identity assertions.

Source: MCP tools


2365-2393: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Cover member_remove in the sync-independence test.

The test invokes only ActionCreated and asserts MemberPutCallCount. It never exercises ActionDeleted or MemberRemove, although the test claims both membership publications ignore sync. Add a table-driven created/deleted matrix and assert the corresponding FGA call for each action. As per coding guidelines, files matching **/*_test.go must use table-driven tests with internal/infrastructure/mock fakes. (raw.githubusercontent.com)

🤖 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 `@internal/service/committee_member_writer_test.go` around lines 2365 - 2393,
The TestPublishMemberMessages_SyncControlsOnlyIndexer test currently covers only
ActionCreated and MemberPutCallCount. Convert it to a table-driven
created/deleted matrix using the existing internal/infrastructure/mock fakes,
invoke publishMemberMessages for each action with sync=true, and assert indexer
synchronization plus the corresponding MemberPutCallCount for ActionCreated and
MemberRemoveCallCount for ActionDeleted.

Sources: Coding guidelines, MCP tools


1011-1030: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Verify the uniqueness reservation is removed.

UniqueMember stores uniqueKey in memberWriter.keys, but the fake DeleteMember only deletes memberWriter.members[uid]. wasDeleted(uniqueKey) can pass while the uniqueness reservation remains. Add key-map cleanup to the fake or call UniqueMember again after rollback and require no conflict. (raw.githubusercontent.com)

🤖 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 `@internal/service/committee_member_writer_test.go` around lines 1011 - 1030,
Update the fake writer cleanup used by the rollback test around
memberWriter.wasDeleted so DeleteMember also removes the corresponding entry
from memberWriter.keys, or verify after rollback by calling UniqueMember again
and requiring no conflict. Ensure the assertion confirms the uniqueness
reservation itself was removed, not only memberWriter.members[uniqueKey].

Source: MCP tools

🤖 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.

Outside diff comments:
In `@internal/service/committee_member_writer_test.go`:
- Around line 2285-2351: The test
TestPublishMemberMessages_ChangedUsername_RevokesOldBeforeGrantingNew currently
validates message contents but not publication order. Add an ordered publication
sequence to spyCommitteePublisher, record both member_remove and member_put
events as they are published, and assert in the successful revoke subtest that
the remove event precedes the put event while preserving the existing identity
assertions.
- Around line 2365-2393: The TestPublishMemberMessages_SyncControlsOnlyIndexer
test currently covers only ActionCreated and MemberPutCallCount. Convert it to a
table-driven created/deleted matrix using the existing
internal/infrastructure/mock fakes, invoke publishMemberMessages for each action
with sync=true, and assert indexer synchronization plus the corresponding
MemberPutCallCount for ActionCreated and MemberRemoveCallCount for
ActionDeleted.
- Around line 1011-1030: Update the fake writer cleanup used by the rollback
test around memberWriter.wasDeleted so DeleteMember also removes the
corresponding entry from memberWriter.keys, or verify after rollback by calling
UniqueMember again and requiring no conflict. Ensure the assertion confirms the
uniqueness reservation itself was removed, not only
memberWriter.members[uniqueKey].

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 921d4a04-da2c-44e8-b841-37be513bbbe7

📥 Commits

Reviewing files that changed from the base of the PR and between f0e1447 and 4de84ef.

📒 Files selected for processing (2)
  • internal/service/committee_member_writer.go
  • internal/service/committee_member_writer_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/service/committee_member_writer.go

@prabodhcs prabodhcs removed the do-not-merge Indicates that the pull request should NOT be merged. label Aug 5, 2026
andrest50
andrest50 previously approved these changes Aug 5, 2026

@andrest50 andrest50 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review: refactor(fga): make committee member FGA publishing asynchronous-only

Approving. The refactor is clean and the breaking change is correctly characterized. Some minor test-coverage gaps worth noting.


What this does and why it's correct

The Access(subject, msg, sync bool) method was a footgun: a caller could accidentally pass sync=true on member_put/member_remove subjects even after they're captured by a JetStream stream, at which point the reply comes from the broker acknowledging storage, not from fga-sync completing the operation — a silent false-positive that looks like convergence. Replacing it with dedicated MemberPut/MemberRemove methods that are unconditionally async removes the option entirely. No current caller sends X-Sync: true on a member endpoint so this is a safe removal.

The AcceptInvite revert (sync=truesync=false) is correctly characterized as a deliberate reversal of PR #163's workaround. The live evidence in the PR description confirms the guarantee — member_put goes out with an empty Reply header even when sync=true is passed to CreateMember (which now affects only the indexer).


Acceptance criteria

Requirement Status
member_put / member_remove always use core-NATS publish, never request/reply publishAccessAsync is called unconditionally from MemberPut/MemberRemove; the old Access dispatch is gone
No callsite can select request/reply for FGA membership MemberPut/MemberRemove take no sync parameter; the interface enforces it
AcceptInvite does not block on FGA processing CreateMember(ctx, member, false, false) — confirmed by TestAcceptInvite asserting exactly one sync=false call
sync continues to gate only the indexer TestPublishMemberMessages_SyncControlsOnlyIndexer verifies IndexerSyncValues[0] is true while MemberPutCallCount is 1 regardless
Changed-username revoke fires before the new grant ✅ Structurally enforced — MemberRemove error returns before MemberPut is reached; "revoke failure prevents the put" subtest verifies this
Existing message subjects and payloads unchanged ✅ Subjects come from fgaconstants.GenericMemberPutSubject/GenericMemberRemoveSubject (unchanged); only the dispatch path changed
Tests cover membership add, removal, and publish failures TestCommitteeWriterOrchestrator_DeleteMember_MessagePublishingFailure, TestPublishMemberMessages_ChangedUsername_RevokesOldBeforeGrantingNew, TestPublishMemberMessages_SyncControlsOnlyIndexer

Minor test gaps (CodeRabbit findings, out-of-diff)

1. Changed-username ordering not asserted in the success path

TestPublishMemberMessages_ChangedUsername_RevokesOldBeforeGrantingNew stores capturedMemberRemoveMsgs and capturedMemberPutMsgs in separate slices and checks their contents, but not their order. If someone swapped the MemberRemove/MemberPut call order in the production code, both slices would still contain one message each with the right content. The "revoke failure prevents the put" subtest protects the error path but not the success ordering. Adding a combined ordered call log to spyCommitteePublisher (e.g. callOrder []string) and asserting ["member_remove", "member_put"] would close this.

In practice the structural guarantee is there (the production code gates MemberPut on MemberRemove returning nil), so this is a test-quality gap, not a correctness gap.

2. Sync-independence test covers only ActionCreated

TestPublishMemberMessages_SyncControlsOnlyIndexer tests ActionCreated and asserts MemberPutCallCount. It never exercises ActionDeleted or asserts MemberRemoveCallCount. The test's own name says "sync controls ONLY the indexer" — a table-driven created/deleted matrix would verify both membership paths ignore sync.


Deployment gate

This PR's commit message and PR description both state: "[PR #1323's self-serve polling fix] must ship before this change." That ordering isn't CI-enforced — it's a rollout-process constraint. Since PR #1323 (LFXV2-2890) is open and has had its own rounds of review, please ensure it is deployed first. Landing this without the frontend mitigation reopens the post-accept 403 window with no user-visible fallback.

Copilot AI review requested due to automatic review settings August 6, 2026 09:54

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 17 out of 20 changed files in this pull request and generated no new comments.

Files not reviewed (1)
  • gen/committee_service/service.go: Generated file
Suppressed comments (1)

docs/fga-contract.md:41

  • This update leaves the contract internally inconsistent. The changed member path calls MemberRemove on ActionUpdated when a username is cleared or changed (committee_member_writer.go:1251-1269), but the member_remove section at lines 111-113 and the trigger table at line 189 still describe it as delete-only. Update those sections to document both update-time revocation cases so consumers have the complete emission contract.
`member_put` and `member_remove` publication follows the same asymmetry as committee writes: create and update publish errors are logged and best-effort, while `DeleteMember` still returns an immediate publish error after the member record is deleted. `AcceptInvite` no longer uses request/reply for `member_put`; access checks issued immediately after acceptance may not yet see the membership.

Co-authored-by: Cursor <cursoragent@cursor.com>

# Conflicts:
#	gen/http/openapi.json
#	gen/http/openapi3.json
#	gen/http/openapi3.yaml
Copilot AI review requested due to automatic review settings August 7, 2026 07:00

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 17 out of 20 changed files in this pull request and generated no new comments.

Files not reviewed (1)
  • gen/committee_service/service.go: Generated file
Suppressed comments (1)

docs/fga-contract.md:41

  • The contract still describes member_remove as delete-only in the dedicated section and trigger table (docs/fga-contract.md:111-113,189), but the changed path publishes it when an update clears or changes a username (committee_member_writer.go:1253-1269). Since this PR is updating the FGA delivery contract, please also document those update triggers so downstream consumers have an internally consistent contract.
`member_put` and `member_remove` publication follows the same asymmetry as committee writes: create and update publish errors are logged and best-effort, while `DeleteMember` still returns an immediate publish error after the member record is deleted. `AcceptInvite` no longer uses request/reply for `member_put`; access checks issued immediately after acceptance may not yet see the membership.

@prabodhcs
prabodhcs merged commit 988d9ca into main Aug 7, 2026
11 checks passed
@prabodhcs
prabodhcs deleted the LFXV2-2856 branch August 7, 2026 16:13
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