Skip to content

Phase E: deletion succession — ownership transfer + sole-member org dissolution - #53

Merged
Bonobo791 merged 7 commits into
mainfrom
mt-e-deletion
Aug 3, 2026
Merged

Phase E: deletion succession — ownership transfer + sole-member org dissolution#53
Bonobo791 merged 7 commits into
mainfrom
mt-e-deletion

Conversation

@Bonobo791

@Bonobo791 Bonobo791 commented Aug 3, 2026

Copy link
Copy Markdown
Owner

User description

Behavior

Phase E of the multi-tenancy plan: makes account deletion (deleteUserRecords) tenancy-complete for shared organizations.

  • Ownership succession: when the deleting user is the last owner of a shared org with surviving members, ownership passes to the oldest admin, else the oldest member (seniority = membership createdAt, userId breaks ties). The promotion is logged loudly (console.info). A surviving shared org is never left ownerless.
  • Sole-member shared org dissolution: a shared org whose only member is the deleting user previously lingered (org row + detached channels with dead tokens). It now dissolves exactly like a personal org — channels, comments, moderation actions, audit rows, rules, invites, memberships, and the org row.
  • Personal-org handling is subsumed into a general per-membership fate loop (dissolve vs. survive); the personal-org data-bug guard still runs first and aborts loudly on inconsistency.

Reconciliation with main

The plan's E1 "find this anchor" is stale: main's deleteUserRecords is already the Phase-C tenancy-aware version, so this is an extension, not a replacement. Plan E2 test 5's "connected channel userId untouched" loses to main's detach semantics (reconcile-with-main rule): connector channels in surviving orgs keep userId = NULL + WIPED_REFRESH_TOKEN sentinel.

Tests (red-first, then green)

  • Plain member leaves a shared org → org/channels/roles untouched (regression guard)
  • Last owner deletes → oldest admin promoted (mutation-checked: neutering the UPDATE fails this test)
  • Last owner deletes, no admin → oldest member promoted (mutation-checked)
  • Another owner survives → no promotion, no succession log, roles byte-identical
  • Sole-member shared org dissolves fully (channels + moderation data + invites + org row)
  • All 12 pre-existing deletion/consent tests unchanged and green

Verification

  • npm run check — 0 errors, 0 warnings
  • npm run test — 45 files, 368 tests passed
  • npm run build — clean
  • codacy-analysis on both changed files — 0 issues

CodeAnt-AI Description

Preserve shared organizations during account deletion and transfer ownership when needed

What Changed

  • Shared organizations with surviving members remain intact when someone leaves; only the departing membership is removed.
  • If the departing user was the last owner, the oldest admin becomes owner, or the oldest member when no admin remains.
  • Shared organizations with no remaining members are fully dissolved, including channels, moderation data, invites, memberships, and organization records.
  • Connected channels in surviving organizations remain available to the team but are detached from the deleted account and have their credentials wiped.
  • Ownership transfers are reported only after account deletion succeeds, so failed deletions produce no misleading promotion notice.
  • Added coverage for member departures, ownership succession, sole-member dissolution, unchanged roles, channel handling, and rollback behavior.

Impact

✅ Shared organizations survive member deletion
✅ No ownerless organizations after account deletion
✅ Fewer abandoned organizations and channels

💡 Usage Guide

Checking Your Pull Request

Every time you make a pull request, our system automatically looks through it. We check for security issues, mistakes in how you're setting up your infrastructure, and common code problems. We do this to make sure your changes are solid and won't cause any trouble later.

Talking to CodeAnt AI

Got a question or need a hand with something in your pull request? You can easily get in touch with CodeAnt AI right here. Just type the following in a comment on your pull request, and replace "Your question here" with whatever you want to ask:

@codeant-ai ask: Your question here

This lets you have a chat with CodeAnt AI about your pull request, making it easier to understand and improve your code.

Example

@codeant-ai ask: Can you suggest a safer alternative to storing this secret?

Preserve Org Learnings with CodeAnt

You can record team preferences so CodeAnt AI applies them in future reviews. Reply directly to the specific CodeAnt AI suggestion (in the same thread) and replace "Your feedback here" with your input:

@codeant-ai: Your feedback here

This helps CodeAnt AI learn and adapt to your team's coding style and standards.

Example

@codeant-ai: Do not flag unused imports.

Retrigger review

Ask CodeAnt AI to review the PR again, by typing:

@codeant-ai: review

Check Your Repository Health

To analyze the health of your code repository, visit our dashboard at https://app.codeant.ai. This tool helps you identify potential issues and areas for improvement in your codebase, ensuring your repository maintains high standards of code health.

@cla-bot cla-bot Bot added the cla-signed label Aug 3, 2026
@codeant-ai

codeant-ai Bot commented Aug 3, 2026

Copy link
Copy Markdown

🤖 CodeAnt AI — Review Status

Status Commit Started (UTC) Finished (UTC)
✅ Incremental review completed 658d06e Aug 03, 2026 · 13:48 13:51
✅ Incremental review completed bde6738 Aug 03, 2026 · 13:35 13:38
✅ Reviewed your PR c343cc9 Aug 03, 2026 · 13:23 13:26

@netlify

netlify Bot commented Aug 3, 2026

Copy link
Copy Markdown

Deploy Preview for moderaty ready!

Name Link
🔨 Latest commit 6eacb8f
🔍 Latest deploy log https://app.netlify.com/projects/moderaty/deploys/6a709e58ab482d0008837584
😎 Deploy Preview https://deploy-preview-53--moderaty.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
Lighthouse
Lighthouse
1 paths audited
Performance: 91
Accessibility: 97
Best Practices: 100
SEO: 100
PWA: -
View the detailed breakdown and full score reports

To edit notification comments on pull requests, go to your Netlify project configuration.

@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@Bonobo791, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 19 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: a027a7a2-1569-42ff-9a66-008ed72ac24e

📥 Commits

Reviewing files that changed from the base of the PR and between bde6738 and 6eacb8f.

📒 Files selected for processing (1)
  • src/lib/server/deletion.test.ts
📝 Walkthrough

Summary by CodeRabbit

  • Bug Fixes
    • Improved account deletion handling across shared organizations.
    • Preserved shared organizations when members remain.
    • Automatically promotes the longest-serving admin or member when the last owner leaves.
    • Dissolves sole-member organizations and cleans up associated records.
    • Maintains validation for personal organization membership.

Walkthrough

Account deletion now evaluates every organization membership. Sole-member organizations are dissolved. Shared organizations remain, the deleting user is removed, and ownership transfers to the oldest eligible member when required. Tests cover cleanup, succession, preservation, and tombstoning.

Changes

Organization deletion lifecycle

Layer / File(s) Summary
Per-organization deletion and succession
src/lib/server/deletion.ts
Deletion validates personal organizations, evaluates each membership, dissolves sole-member organizations, preserves shared organizations, promotes the oldest eligible member, scopes cleanup to dissolved organizations, and logs promotions after commit.
Deletion behavior validation
src/lib/server/deletion.test.ts
Tests add shared-organization fixtures and cover membership removal, admin and member promotion, existing-owner preservation, dependent-record cleanup, succession logging, and user tombstoning.

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

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant Deletion as src/lib/server/deletion.ts
  participant Database as Organization database
  participant Logger as Succession logger
  User->>Deletion: Request account deletion
  Deletion->>Database: Inspect organization memberships
  Database-->>Deletion: Return surviving members and roles
  Deletion->>Database: Remove memberships or dissolve selected organizations
  Deletion->>Logger: Log ownership succession after commit
  Deletion-->>User: Tombstone account
Loading

Possibly related PRs

  • Bonobo791/Moderaty#42: Both PRs modify account-deletion logic and its tests.
  • Bonobo791/Moderaty#49: Both PRs modify organization membership and ownership lifecycle behavior.
  • Bonobo791/Moderaty#50: This PR extends the account-deletion logic introduced by that PR with shared-organization handling and owner succession.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
Title check ✅ Passed The title clearly summarizes ownership succession and sole-member organization dissolution, which are the main changes in the pull request.
Description check ✅ Passed The description directly explains shared-organization deletion behavior, ownership succession, dissolution, tests, and verification results.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch mt-e-deletion

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

@codeant-ai codeant-ai Bot added the size:L This PR changes 100-499 lines, ignoring generated files label Aug 3, 2026
@codeant-ai

codeant-ai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Sequence Diagram

This PR extends account deletion to evaluate every organization membership. Sole-member organizations are fully dissolved, while surviving shared organizations retain their data and promote a successor when the deleting user was the last owner.

sequenceDiagram
    participant User
    participant DeletionService
    participant Database
    participant SharedOrg

    User->>DeletionService: Request account deletion
    DeletionService->>Database: Find memberships and organization members

    alt Sole-member organization
        DeletionService->>Database: Delete organization data and organization
    else Shared organization survives
        alt Deleting user was the last owner
            DeletionService->>Database: Promote oldest admin or member
            Database-->>SharedOrg: Organization remains owned
        else Another owner survives
            Database-->>SharedOrg: Keep roles unchanged
        end
        DeletionService->>Database: Remove membership and detach connected channels
    end

    DeletionService->>Database: Delete sessions and tombstone user
    Database-->>User: Account deletion complete
Loading

Generated by CodeAnt AI

@codeant-ai

codeant-ai Bot commented Aug 3, 2026

Copy link
Copy Markdown

🏁 CodeAnt Quality Gate Results

Commit: 6eacb8ff
Scan Time: 2026-08-03 15:14:16 UTC

✅ Overall Status: PASSED

Quality Gate Details

Quality Gate Status Details
Secrets ✅ PASSED 0 secrets found
Duplicate Code ✅ PASSED 0.0% duplicated
SAST ✅ PASSED No security issues
Bugs ✅ PASSED Rating S: No bugs
IAC ✅ PASSED No IAC issues

View Full Results

@amazon-q-developer amazon-q-developer Bot 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.

The implementation successfully extends account deletion to handle shared organization ownership succession and sole-member organization dissolution. The logic correctly promotes the oldest admin (or oldest member as fallback) when the last owner leaves a shared organization, and properly dissolves sole-member shared organizations. The comprehensive test suite validates all scenarios including edge cases. All checks pass (0 errors, 368 tests passed).


You can now have the agent implement changes and create commits directly on your pull request's source branch. Simply comment with /q followed by your request in natural language to ask the agent to make changes.

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Account deletion: shared-org ownership succession + dissolve sole-member shared orgs

✨ Enhancement 🐞 Bug fix 🧪 Tests 🕐 40+ Minutes

Grey Divider

AI Description

• Make deleteUserRecords tenancy-complete for shared organizations during account deletion.
• Promote a successor owner when deleting the last shared-org owner to avoid ownerless orgs.
• Dissolve sole-member shared orgs fully (like personal orgs) and add regression-focused tests.
Diagram

graph TD
A["deleteUserRecords(userId)"] --> B[("DB: memberships")]
B --> C{"Other members exist?"}
C -->|"No"| D["Collect orgId to dissolve"] --> H["Delete org data + org row"]
C -->|"Yes"| E{"Was last owner?"}
E -->|"Yes"| F["Promote successor owner"] --> I["Delete user's membership"]
E -->|"No"| G["No role changes"] --> I
I --> J["Detach connected channels + delete sessions + tombstone user"]
subgraph Legend
  direction LR
  _db[("Database")] ~~~ _dec{"Decision"} ~~~ _proc["Process"]
end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Single SQL successor selection (window/order-by in DB)
  • ➕ Avoids per-org per-membership queries (reduces potential N+1 behavior)
  • ➕ Keeps selection logic close to data, potentially more consistent under concurrency
  • ➖ More complex SQL and less readable than the current TypeScript loop
  • ➖ Harder to unit test at the same granularity without DB-specific features
2. Persist succession events to audit table instead of console.info
  • ➕ Durable, queryable record of ownership succession actions
  • ➕ Avoids relying on runtime logging configuration
  • ➖ Requires schema/work to define event shape and retention policy
  • ➖ More moving parts for a deletion-path hot spot

Recommendation: The PR’s approach (transactional per-membership fate evaluation + explicit successor promotion) is a good fit given likely small org counts per user and the need for clear, testable business rules. If performance becomes a concern, consider collapsing successor ranking into a single SQL query per org (or a set-based query across orgs), but keep the current behavior-focused tests as the contract.

Files changed (2) +236 / -18

Enhancement (1) +56 / -17
deletion.tsMake deleteUserRecords handle shared-org succession and sole-member shared org teardown +56/-17

Make deleteUserRecords handle shared-org succession and sole-member shared org teardown

• Refactors account deletion tenancy handling from personal-org-only to a per-membership fate loop: dissolve orgs with no surviving members (personal orgs and now sole-member shared orgs), while preserving shared orgs with survivors. Adds last-owner succession to promote the oldest admin (else oldest member) to owner, and logs the promotion via 'console.info'. Updates deletion to only cascade org-level deletes for orgs marked for dissolution, while still detaching connected channels and removing remaining user memberships/invites.

src/lib/server/deletion.ts

Tests (1) +180 / -1
deletion.test.tsAdd shared-org deletion tests for membership-only, succession, and dissolution +180/-1

Add shared-org deletion tests for membership-only, succession, and dissolution

• Introduces helpers to seed bare users and shared org memberships with explicit join order. Adds new tests covering: member-only removal from shared orgs, last-owner succession (admin-first then member fallback), no-op when another owner exists, and full dissolution of sole-member shared orgs including channels and moderation/audit data. Uses 'vi' to assert succession logging behavior via 'console.info' spying.

src/lib/server/deletion.test.ts

@codacy-production

codacy-production Bot commented Aug 3, 2026

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 45 complexity · 0 duplication

Metric Results
Complexity 45 (≤ 100 complexity)
Duplication 0 (≤ 1 duplication)

View in Codacy

AI Reviewer: first review requested successfully. AI can make mistakes. Always validate suggestions.

Run reviewer

TIP This summary will be updated as you push new changes.

@codacy-production codacy-production 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.

Pull Request Overview

The PR successfully implements organization ownership succession and dissolution logic, addressing several critical account deletion workflows. However, the current implementation is not up to standards according to Codacy, primarily due to code duplication in the test suite and an N+1 query pattern in the succession logic.

A significant gap was identified in the test coverage: there is no test scenario verifying the requirement that userId must be used as a tie-breaker for seniority when createdAt timestamps are identical. Additionally, the use of localeCompare on a field that might be returned as a Date object by the database driver poses a runtime stability risk.

About this PR

  • The test suite for deletion succession contains several patterns of duplicated logic, specifically regarding console mock capturing and organization dissolution assertions. Centralizing these into helpers would improve the resilience of the test suite as the deletion logic evolves.

Test suggestions

  • Plain member leaves a shared organization: verify only the user's membership is removed while the org and team data remain.
  • Last owner of a shared organization leaves with admins surviving: verify the oldest admin is promoted to owner and the event is logged.
  • Last owner of a shared organization leaves with no admins surviving: verify the oldest member is promoted to owner.
  • Multiple owners exist: verify no succession or promotion occurs when one owner deletes their account.
  • Sole member of a shared organization deletes: verify the organization and all associated data (channels, comments, moderation history) are dissolved.
  • Succession tie-breaking: verify 'userId' is used to determine seniority when 'createdAt' timestamps are identical.
Prompt proposal for missing tests
Consider implementing these tests if applicable:
1. Succession tie-breaking: verify 'userId' is used to determine seniority when 'createdAt' timestamps are identical.

TIP Improve review quality by adding custom instructions
TIP How was this review? Give us feedback

Comment thread src/lib/server/deletion.test.ts Outdated
Comment on lines +365 to +371
]);
const info = vi.spyOn(console, 'info').mockImplementation(() => {});
await deleteUserRecords(userId);
const successionLogged = info.mock.calls.length > 0;
info.mockRestore();

const remaining = await teamMemberships('org-team');

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 MEDIUM RISK

Suggestion: This test logic for capturing succession logs is duplicated. Consider extracting it into a helper to keep the test suite DRY.

Try running the following prompt in your IDE agent:

In src/lib/server/deletion.test.ts, create a helper function called runDeletionWithLogCapture(userId: string) that mocks console.info, executes deleteUserRecords, and returns the mock's call state. Use this helper in the tests for admin promotion and survivor role checks.

Comment thread src/lib/server/deletion.ts Outdated
Comment on lines +131 to +135
const others = await tx
.select({ userId: memberships.userId, role: memberships.role, createdAt: memberships.createdAt })
.from(memberships)
.where(and(eq(memberships.orgId, membership.orgId), ne(memberships.userId, userId)))
.all();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 MEDIUM RISK

Suggestion: The query for 'others' inside the loop over userMemberships creates an N+1 query pattern, which increases database roundtrips and holds transaction locks longer than necessary.

Refactor the succession logic to fetch all membership rows for all organizations in userMemberships using a single inArray query before the loop, then process the ranking and succession logic in-memory.

@coderabbitai coderabbitai Bot 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.

Warning

CodeRabbit couldn't request changes on this pull request because it doesn't have sufficient GitHub permissions.

Please grant CodeRabbit Pull requests: Read and write permission and re-run the review.

👉 Steps to fix this

Actionable comments posted: 2

🤖 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 `@src/lib/server/deletion.test.ts`:
- Around line 317-322: Replace the shared info.mock.calls.length checks in
src/lib/server/deletion.test.ts at lines 317-322 and 366-381 with one reusable
helper that extracts logged succession messages from the console.info spy. In
the positive test, assert exactly one message reports promotion of admin-old in
org-team; in the negative test, assert no message reports a promotion in
org-team.
- Around line 281-297: Extend the surviving shared-org test around
deleteUserRecords by seeding a second org-team channel owned by the deleting
user, and import the exported WIPED_REFRESH_TOKEN from deletion.ts. After
deletion, assert this connector channel remains with userId null and
refreshTokenEnc set to WIPED_REFRESH_TOKEN, while preserving the existing
assertions for UC-team and team memberships.
🪄 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: ASSERTIVE

Plan: Pro Plus

Run ID: 82edc68a-d47e-42c5-a173-ae357b4c5b61

📥 Commits

Reviewing files that changed from the base of the PR and between 8f59d6a and c343cc9.

📒 Files selected for processing (2)
  • src/lib/server/deletion.test.ts
  • src/lib/server/deletion.ts

Comment thread src/lib/server/deletion.test.ts Outdated
Comment thread src/lib/server/deletion.test.ts Outdated
@qodo-code-review

qodo-code-review Bot commented Aug 3, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📜 Skill insights (0)

Context used
✅ Compliance rules (platform): 74 rules

Grey Divider


Remediation recommended

1. N+1 membership queries ✓ Resolved 🐞 Bug ➹ Performance
Description
deleteUserRecords now issues one SELECT per org membership to load “other members”, which increases
transaction time and can make deletions slow for users in many orgs. This extra work is done inside
a single transaction, increasing contention risk and cost per deletion.
Code

src/lib/server/deletion.ts[R130-135]

+		for (const membership of userMemberships) {
+			const others = await tx
+				.select({ userId: memberships.userId, role: memberships.role, createdAt: memberships.createdAt })
+				.from(memberships)
+				.where(and(eq(memberships.orgId, membership.orgId), ne(memberships.userId, userId)))
+				.all();
Relevance

●●● Strong

Team previously accepted reducing DB round-trips/hot spots (merge updates, push ordering into SQL)
in PR #49.

PR-#49

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The new for-loop performs a query each iteration to load other members for that org. The DB is
created via @libsql/client, so each additional query is an extra database operation, and prior
work in this codebase moved ordering into SQL to avoid JS sorting/instability concerns.

src/lib/server/deletion.ts[124-151]
src/lib/server/db/index.ts[19-48]
PR-#49

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`deleteUserRecords` loops over `userMemberships` and runs a separate query per org to fetch `others`. This is an N+1 query pattern inside a transaction.

### Issue Context
The DB layer uses `@libsql/client`, and the repo already has examples of pushing ordering/selection into SQL to reduce app-side work.

### Fix Focus Areas
- src/lib/server/deletion.ts[124-155]

### Implementation notes
- Fetch all relevant memberships for all orgs in one query (e.g., `where inArray(memberships.orgId, orgIds) and ne(userId, deletingUser)`), then group by `orgId` in memory.
- Alternatively, compute successors with a single SQL query per org using `orderBy(...)` and `limit 1`, but prefer a fully batched approach when possible.
- If keeping per-org selection, at least add `orderBy(memberships.createdAt, memberships.userId)` in SQL and avoid JS `.sort(localeCompare)` for consistency/perf.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Pre-commit succession logging ✓ Resolved 🐞 Bug ◔ Observability
Description
The succession console.info is emitted inside the transaction before it commits, so if later
statements fail and the transaction rolls back, logs will claim a promotion that never persisted.
This can mislead operational debugging/audit trails around account deletion events.
Code

src/lib/server/deletion.ts[R147-149]

+				console.info(
+					`account deletion: promoted user ${successor.userId} to owner of org ${membership.orgId} (last owner ${userId} was deleted)`
+				);
Relevance

●● Moderate

No clear prior guidance on deferring logs until after commit; repo already logs inside DB flows
(e.g., PR #49).

PR-#49

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The log is printed immediately after the UPDATE while still inside db.transaction(...), and more
transactional work follows; any later error causes rollback but cannot retract the already-emitted
log line.

src/lib/server/deletion.ts[79-181]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`console.info` for ownership promotion runs before the surrounding transaction commits. On rollback, the log remains even though the promotion did not.

### Issue Context
The promotion happens mid-transaction, followed by multiple deletes/updates that could still throw.

### Fix Focus Areas
- src/lib/server/deletion.ts[140-180]

### Implementation notes
- Collect promotion events in an array within the transaction callback.
- After `await db.transaction(...)` resolves successfully, emit the logs outside the transaction.
- Combine with a `.returning(...)` check so you only log promotions that actually updated a row.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

3. Non-atomic ownership succession ✓ Resolved 🐞 Bug ≡ Correctness
Description
deleteUserRecords promotes a successor owner with an unconditional UPDATE and does not verify any
row was actually updated, so concurrent membership changes can strand a surviving shared org without
an owner. This also breaks the PR’s stated invariant (“never left ownerless”) because the
transaction continues and deletes the departing owner’s membership regardless.
Code

src/lib/server/deletion.ts[R140-146]

+			if (membership.role === 'owner' && !others.some((m) => m.role === 'owner')) {
+				const ranked = [...others].sort((a, b) => a.createdAt.localeCompare(b.createdAt) || a.userId.localeCompare(b.userId));
+				const successor = ranked.find((m) => m.role === 'admin') ?? ranked[0];
+				await tx
+					.update(memberships)
+					.set({ role: 'owner' })
+					.where(and(eq(memberships.orgId, membership.orgId), eq(memberships.userId, successor.userId)));
Relevance

● Weak

Similar “verify transactional operations succeeded / check affected rows” suggestion was rejected in
account deletion flow (PR #37).

PR-#37

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The new succession logic updates the successor role without checking whether the UPDATE actually
affected a row, so it can silently fail while the deletion proceeds. The org-management codebase
shows an established pattern of using conditional updates + returning checks to prevent “ownerless
org” races, which the new deletion logic does not follow.

src/lib/server/deletion.ts[124-151]
src/lib/server/org.ts[345-363]
src/lib/server/org.ts[390-413]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
Ownership succession in `deleteUserRecords` is decided via a read-then-write sequence, but the promotion UPDATE is unconditional and its affected-row count is not checked. Under concurrent membership changes, this can result in the chosen successor no longer being a member at UPDATE time, leaving the org ownerless after the deleting owner’s membership is removed.

### Issue Context
Other org membership mutations in the codebase explicitly use conditional updates/returning checks to preserve the “never ownerless” invariant under concurrency.

### Fix Focus Areas
- src/lib/server/deletion.ts[124-151]

### Implementation notes
- Perform the promotion as a conditional UPDATE with `.returning(...)` and throw/abort the deletion if `updated.length !== 1`.
- Encode the invariant in SQL similarly to org.ts patterns (e.g., ensure the deleting user is currently the last owner at write time, and the successor membership still exists).
- Prefer selecting the successor in SQL (`orderBy` with role priority + createdAt + userId) to reduce the window between selection and update.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

To customize comments, go to the Qodo configuration screen, or learn more in the docs.

Qodo Logo

Comment thread src/lib/server/deletion.ts Outdated
Comment thread src/lib/server/deletion.ts Outdated
@Bonobo791

Copy link
Copy Markdown
Owner Author

Review triage (commit bde6738)

All inline findings were valid; all fixed:

  • Codacy duplication gate (4 clones)fix 2ee0869: extracted seedBareUser / seedModerationData fixtures; all seed call sites deduplicated.
  • Codacy + Qodo: N+1 membership queries — co-members across all of the user's orgs are now fetched in ONE batched inArray query and grouped in memory; no per-org roundtrips in the transaction.
  • Qodo: pre-commit succession logging — promotions are collected inside the transaction and logged only after it commits, and only when the UPDATE ... RETURNING confirms a row changed (empty RETURNING now fails loudly and aborts).
  • CodeRabbit: detach path unexercised for surviving orgs — the plain-member test now seeds a connector channel owned by the departing user and asserts userId: null + WIPED_REFRESH_TOKEN. Mutation-checked: skipping the detach update fails this test.
  • CodeRabbit + Codacy: log call-count assertions — replaced with a shared deleteWithSuccessionLogs helper asserting message CONTENT (names the promoted user and org; empty when no succession). Mutation-checked: a generic log message fails the promotion test.

Non-findings: Amazon Q (positive, no issues), SonarQube (gate passed, 0 issues), CodeAnt (gate passed), Qodo high-level assessment (endorses the current approach over its alternatives).

Gates after fixes: npm run check 0/0, npm run test 368/368, npm run build clean, codacy-analysis 0 issues.

@codeant-ai codeant-ai Bot added size:L This PR changes 100-499 lines, ignoring generated files and removed size:L This PR changes 100-499 lines, ignoring generated files labels Aug 3, 2026

@coderabbitai coderabbitai Bot 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.

Warning

CodeRabbit couldn't request changes on this pull request because it doesn't have sufficient GitHub permissions.

Please grant CodeRabbit Pull requests: Read and write permission and re-run the review.

👉 Steps to fix this

Actionable comments posted: 1

🤖 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 `@src/lib/server/deletion.test.ts`:
- Around line 258-267: Extend the deletion tests around deleteWithSuccessionLogs
and the related assertions to cover a failed final tombstone update: force that
update to reject, assert deleteUserRecords rejects, and verify console.info
receives no calls. Ensure the setup reaches the rollback boundary and restores
mocks so the test specifically fails if succession logs are emitted before the
transaction commits.
🪄 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: ASSERTIVE

Plan: Pro Plus

Run ID: 673d45f2-7016-4728-896f-39bfb1d2aab3

📥 Commits

Reviewing files that changed from the base of the PR and between c343cc9 and bde6738.

📒 Files selected for processing (2)
  • src/lib/server/deletion.test.ts
  • src/lib/server/deletion.ts

Comment thread src/lib/server/deletion.test.ts
@codeant-ai

codeant-ai Bot commented Aug 3, 2026

Copy link
Copy Markdown

PR Code Suggestions ✨

Previous suggestions up to commit bde6738
CategorySuggestion                                                                                                                                    SeverityGenerated at (UTC)
Incomplete implementation
Channels owned by the deleting user without an organization are detached instead of erased

This cleanup query finds channels to delete only through channels.orgId. During the
documented expand window, a channel can have userId = userId while orgId is still
NULL; such a channel is excluded from the personal-org deletion, then the later
broad update detaches it and wipes its token instead of deleting the channel and its
moderation history. Include the user's owned channels with a null organization in
the dissolution set, or otherwise fail loudly on this inconsistent ownership state.

src/lib/server/deletion.ts [178-180]

Why it matters? 🤔
  • ❌ Account deletion leaves owned channels orphaned.
  • ❌ Personal moderation data remains after deletion.
  • ⚠️ Detached channels retain rows and wiped-token state.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** src/lib/server/deletion.ts
**Line:** 178:180
**Comment:**
	*Incomplete Implementation: This cleanup query finds channels to delete only through `channels.orgId`. During the documented expand window, a channel can have `userId = userId` while `orgId` is still `NULL`; such a channel is excluded from the personal-org deletion, then the later broad update detaches it and wipes its token instead of deleting the channel and its moderation history. Include the user's owned channels with a null organization in the dissolution set, or otherwise fail loudly on this inconsistent ownership state.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
Major2026-08-03 13:38

Latest suggestions up to commit 658d06e
CategorySuggestion                                                                                                                                    SeverityGenerated at (UTC)
Race condition
Non-conditional succession updates can overwrite concurrent membership changes

The promotion decision is based on an earlier snapshot, but the UPDATE only matches
orgId and successor.userId. A concurrent role change or membership change can
therefore overwrite a newer role or promote a member whose selected role is no
longer valid. Include the last-owner and successor-role predicates in the write-time
condition, and abort if the conditional update affects no rows.

src/lib/server/deletion.ts [163-167]

Why it matters? 🤔
  • ⚠️ Concurrent organization changes can select stale succession data.
  • ❌ A changed member may be promoted unexpectedly.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** src/lib/server/deletion.ts
**Line:** 163:167
**Comment:**
	*Race Condition: The promotion decision is based on an earlier snapshot, but the UPDATE only matches `orgId` and `successor.userId`. A concurrent role change or membership change can therefore overwrite a newer role or promote a member whose selected role is no longer valid. Include the last-owner and successor-role predicates in the write-time condition, and abort if the conditional update affects no rows.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
Major2026-08-03 13:51
Stale reference
Detached channels remain active, allowing in-flight processing to act after account deletion

Detaching a surviving channel only wipes its token and clears userId; it leaves
active unchanged. runChannel rechecks only active before enforcement, so an
in-flight run that loaded the old access token before deletion can pass the
post-deletion check and still execute YouTube moderation actions after the connector
has been deleted. Mark the channel inactive as part of deletion, and ensure the
processing checks observe that state before enforcement.

src/lib/server/deletion.ts [191-194]

Why it matters? 🤔
  • ❌ Deleted accounts can trigger in-flight YouTube actions.
  • ⚠️ Post-deletion moderation writes may be staged.

Fix in Cursor Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** src/lib/server/deletion.ts
**Line:** 191:194
**Comment:**
	*Stale Reference: Detaching a surviving channel only wipes its token and clears `userId`; it leaves `active` unchanged. `runChannel` rechecks only `active` before enforcement, so an in-flight run that loaded the old access token before deletion can pass the post-deletion check and still execute YouTube moderation actions after the connector has been deleted. Mark the channel inactive as part of deletion, and ensure the processing checks observe that state before enforcement.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
Major2026-08-03 13:51

@Bonobo791

Bonobo791 commented Aug 3, 2026

Copy link
Copy Markdown
Owner Author

Review triage round 2 (commit 3e25f11)

  • CodeRabbit: rollback boundary for succession logs — valid; added deleteUserRecords logs no succession when the transaction rolls back: a fail_tombstone trigger aborts the final users UPDATE, forcing a rollback after the promotion. Asserts zero console.info calls, the promoted role restored to admin, and no tombstone. Mutation-checked: re-introducing a pre-commit log fails this test (and the no-succession test).

Gates: check 0/0, test 369/369, build clean, codacy-analysis 0 issues.

@codeant-ai codeant-ai Bot added size:L This PR changes 100-499 lines, ignoring generated files and removed size:L This PR changes 100-499 lines, ignoring generated files labels Aug 3, 2026
@sonarqubecloud

sonarqubecloud Bot commented Aug 3, 2026

Copy link
Copy Markdown

@Bonobo791

Copy link
Copy Markdown
Owner Author

Review triage round 3 (commit 6eacb8f) — Codacy gate green

  • Codacy duplication metric (2 remaining clones) — fixed across 658d06e/af594b4/6eacb8f: extracted seedChannel + expectOrgRoles + expectAllTablesEmpty helpers and replaced the local bare-user fixture with testdb's shared seedUser. Codacy PR gate now reports Up to standards ✓ (0 issues, 0 duplication).
  • Checks: CodeQL ✓, Codacy Static Analysis ✓, CodeAnt quality gates ✓ (0.0% duplication, Rating S), SonarQube ✓. The Snyk security check failure is the pre-existing low-severity Dependabot alert on main (this PR changes no dependencies); code/snyk reports "test limit reached" (quota, not a finding).
  • No new inline findings from the round-2/3 bot passes on the fix commits.

Final state: npm run check 0/0 · npm run test 369/369 (18 deletion tests, all mutation-checked) · npm run build clean · codacy-analysis 0 issues.

Ready for human review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cla-signed size:L This PR changes 100-499 lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant