Skip to content

feat(xp): level-up action pipeline engine (Phase 1)#373

Merged
BillChirico merged 36 commits intomainfrom
worktree-feat+xp-level-up-actions
Mar 26, 2026
Merged

feat(xp): level-up action pipeline engine (Phase 1)#373
BillChirico merged 36 commits intomainfrom
worktree-feat+xp-level-up-actions

Conversation

@BillChirico
Copy link
Copy Markdown
Collaborator

Summary

  • Replace inline level-up side-effects in reputation.js with a configurable action pipeline under config.xp
  • Add pipeline executor with action registry, level-skip handling, and per-action error isolation
  • Implement grantRole and removeRole action handlers with permission checks, rate limiting, and stack/replace mode
  • Add template interpolation engine with 20 variables for message rendering
  • Wire pipeline into reputation.js, rank.js, community.js, and members.js

Closes #365, closes #366, closes #367

What Changed

New files:

File Purpose
src/modules/xpDefaults.js Default config.xp values
src/utils/templateEngine.js renderTemplate(), buildTemplateContext(), validateLength()
src/modules/levelUpActions.js Pipeline executor + action registry
src/modules/actions/roleUtils.js Permission checks, rate limiter, stack/replace, enforceRoleLevelDown
src/modules/actions/grantRole.js Grant role action handler
src/modules/actions/removeRole.js Remove role action handler

Modified files:

  • config.json — new xp section; trimmed reputation section
  • src/modules/reputation.js — removed ~90 lines of inline role/announcement logic, replaced with executeLevelUpPipeline() call
  • src/modules/reputationDefaults.js — removed roleRewards, announceChannelId, levelThresholds
  • src/api/utils/configAllowlist.js — added 'xp' to SAFE_CONFIG_KEYS
  • src/commands/rank.js — reads levelThresholds from config.xp
  • src/api/routes/community.js — reads levelThresholds from config.xp
  • src/api/routes/members.js — reads levelThresholds from config.xp; calls enforceRoleLevelDown on XP reduction

Test coverage: 69 new tests across 5 test files. All 4123 bot tests passing.

Design Decisions

  • Action registry patternMap<string, handler> makes Phase 2 action types (sendDm, announce, xpBonus, etc.) a one-line registration each
  • Level skip handling — if a user jumps from level 4→12, the pipeline fires actions for every crossed level (5, 6, 7, 8, 9, 10, 11, 12)
  • defaultActions vs levelActions — per-level entries fully override defaults (not additive)
  • Rate limiting — in-memory sliding window: max 2 role changes per user per 60s
  • Template engine is stateless/pure — DB queries happen in buildTemplateContext(), rendering is sync

Test plan

  • Pipeline resolves correct actions for exact level match, defaults, and level skips
  • Actions execute sequentially with per-action error isolation
  • Unknown action types warn and skip
  • Template variables render correctly (all 20 variables, nulls, unknowns)
  • Role permission checks (MANAGE_ROLES, hierarchy, missing role)
  • Rate limiter blocks 3rd change in 60s window, allows after expiry
  • Stack mode keeps all roles; replace mode removes other XP-managed roles
  • enforceRoleLevelDown removes roles above new level
  • reputation.js calls pipeline on level-up, skips when disabled
  • Full bot test suite passes (4123 tests)

Copilot AI review requested due to automatic review settings March 25, 2026 23:39
@github-project-automation github-project-automation Bot moved this to Backlog in Volvox.Bot Mar 25, 2026
@chatgpt-codex-connector
Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@railway-app
Copy link
Copy Markdown

railway-app Bot commented Mar 25, 2026

🚅 Deployed to the volvox-bot-pr-373 environment in volvox-bot

Service Status Web Updated (UTC)
bot ❌ Build Failed (View Logs) Web Mar 26, 2026 at 5:07 am
web ✅ Success (View Logs) Mar 26, 2026 at 5:06 am

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 25, 2026

📝 Walkthrough

Summary by CodeRabbit

Release Notes

  • New Features

    • Configurable level-up actions system allowing customization of what happens when members advance levels
    • Role grant/removal on level progression with stacking and level-down removal options
    • Customizable level-up notifications using template variables (member name, level, XP, rank, and more)
  • Refactor

    • Reorganized leveling configuration into new dedicated XP settings section for improved structure
    • Transitioned from direct announcements to flexible action-based system for level events

Walkthrough

Moves XP/leveling config from reputation into a top-level xp (with XP_DEFAULTS), implements a level-up action pipeline, adds grant/remove role handlers and role utilities (permissions, rate limiting, level-down enforcement), a template engine, and updates API, commands, web types, and tests to use the new XP model.

Changes

Cohort / File(s) Summary
Config & defaults
config.json, src/modules/xpDefaults.js, src/modules/reputationDefaults.js, src/api/utils/configAllowlist.js, web/src/types/config.ts
Introduce top-level xp config and XP_DEFAULTS; remove XP/announce fields from REPUTATION_DEFAULTS; add 'xp' to safe allowlist; add TS types for XpConfig.
Core integration
src/modules/reputation.js, src/api/routes/community.js, src/api/routes/members.js, src/commands/rank.js
Switch runtime to getXpConfig/XP_DEFAULTS; compute levels from xp.levelThresholds; delegate level-up side effects to pipeline; recompute levels for admin XP adjustments and adjust next-level XP semantics.
Action pipeline & handlers
src/modules/levelUpActions.js, src/modules/actions/grantRole.js, src/modules/actions/removeRole.js, src/modules/actions/roleUtils.js
Add pipeline registry/executor, action resolution across skipped levels, grantRole/removeRole handlers, role utilities (permission checks, per-user 2-per-60s limiter, batch removals), level-down enforcement, and managed-role collection.
Template engine & context
src/utils/templateEngine.js
Add renderTemplate, validateLength, and buildTemplateContext to render templates and build DB-enhanced context with fallbacks and formatting.
API & members tooling
src/api/routes/members.js, src/api/routes/community.js
Update leaderboard/profile/member exports to compute level from XP thresholds; admin XP endpoint recalculates old/new levels and conditionally enforces role removals on level-down.
Commands & CLI
src/commands/rank.js, tests/commands/rank.test.js
Switch /rank command to use xp config and thresholds; update/add tests covering xp.enabled variations.
Tests — actions & pipeline
tests/modules/actions/*, tests/modules/levelUpActions.test.js, tests/utils/templateEngine.test.js, tests/modules/reputation.test.js, tests/modules/reputationDefaults.test.js
Add unit tests for grant/remove handlers, role-utils (permissions, rate-limits, sweep), pipeline resolution/execution/error isolation, and template engine; update reputation-related tests to assert pipeline invocation and new defaults shape.
Web dashboard & types
web/src/components/dashboard/config-sections/CommunitySettingsSection.tsx, web/src/components/dashboard/config-categories/onboarding-growth.tsx, web/src/components/dashboard/config-workspace/config-categories.ts, web/src/components/dashboard/config-workspace/types.ts
Expose xp section and new xp-level-actions feature id; move threshold and role controls into xp UI; add search items and update feature labels/types.
UI components & landing
web/src/components/ui/neo-minimal-footer.tsx, web/src/components/ui/simple-icon.tsx, web/src/components/landing/Footer.tsx, web/src/components/landing/DashboardShowcase.tsx, related tests
Add NeoMinimalFooter and SimpleIcon, landing/footer markup updates, minor dashboard UI/key fixes, and matching tests.

Possibly related issues

  • #365 — feat(xp): implement level-up action pipeline engine — PR implements the pipeline executor, level-skip handling, per-level/default actions, error isolation, and gating under xp.enabled.
  • #366 — feat(xp): role grant/remove actions with stack & level-down support — PR implements grantRole/removeRole, stack vs replace, remove-on-level-down, permission checks, 2/60s rate limiting, batch removals, and tests.
  • #367 — feat(xp): template interpolation engine with 20 variables — PR adds renderTemplate and buildTemplateContext covering the requested variables, fallbacks, and length validation.

Possibly related PRs

  • #119 — Members XP adjustment endpoint changes: touches the same members API and admin XP adjustment flow (level recomputation and conditional role revocation).
  • #109 — Reputation → XP refactor: overlaps the migration of XP fields out of reputation and centralizing XP defaults/behavior.
  • #253 — Dashboard config workspace changes: both modify dashboard config metadata (feature ids/section keys) and onboarding-growth configuration surface.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The PR title 'feat(xp): level-up action pipeline engine (Phase 1)' directly and clearly summarizes the main change: a new level-up action pipeline engine for the XP system.
Description check ✅ Passed The PR description comprehensively details the changes, including new files, modified files, design decisions, and test plan, all directly related to the changeset's implementation of the level-up action pipeline.
Linked Issues check ✅ Passed All coding requirements from linked issues #365, #366, #367 are met: pipeline executor with level-skip handling [#365], grantRole/removeRole with stacking and level-down [#366], and template engine with 20 variables [#367].
Out of Scope Changes check ✅ Passed Minor web UI changes (footer refactoring, bento chart keys, neo-minimal-footer component) appear tangential but are consistent with the overall feature scope and do not represent material scope creep.
Docstring Coverage ✅ Passed Docstring coverage is 85.29% which is sufficient. The required threshold is 80.00%.

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

📋 Issue Planner

Built with CodeRabbit's Coding Plans for faster development and fewer bugs.

View plans used: #365, #366, #367

✨ 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 worktree-feat+xp-level-up-actions
  • 🛠️ Publish Changes: Commit on current branch
  • 🛠️ Publish Changes: Create PR

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

❤️ Share

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

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Mar 25, 2026

Greptile Summary

This PR replaces the hardcoded inline level-up side-effects in reputation.js with a configurable, extensible action pipeline under config.xp. It introduces an action registry pattern, per-level and default action resolution (including level-skip handling), role permission checks, an in-memory rate limiter, a template interpolation engine, and wires everything into reputation.js, rank.js, community.js, and members.js. The 33-file change is well-structured and ships 69 new tests covering the happy path, edge cases, and error isolation.\n\nNotable items:\n\n- oldXp = newXp - amount is incorrect under XP clamping (members.js:977) — when a large negative adjustment triggers GREATEST(0, ...), newXp - amount yields the unclamped value rather than the real pre-update XP. This causes enforceRoleLevelDown to fire and strip roles from users who never earned them. rows[0].level (pre-update, since only xp is in the SET clause) is the correct source.\n- Two hardcoded 'en-US' locale strings in templateEngine.js (formatNumber and joinDate) violate the project's no-hardcoded-locale rule.\n- Unnecessary dynamic import of enforceRoleLevelDown in members.js — no circular dependency exists; a static import is cleaner.\n- Prior open threadsenforceRoleLevelDown/collectRolesToRemove still only walks levelActions (not defaultActions), and handleGrantRole's final roles.add still runs without a rate-limit check.\n- Positive changes from prior review: collectXpManagedRoles now correctly covers defaultActions; /rank no longer gates on xp.enabled (backward compat preserved); XP threshold input sorts before saving; max-level nextLevelXp returns 0 instead of repeating the last threshold.

Confidence Score: 3/5

Not safe to merge yet — the oldXp = newXp - amount clamping bug can silently strip roles from users who never earned them, and two prior-review issues remain open.

The pipeline architecture is solid and test coverage is strong. Several prior concerns were addressed. However, a new P1 logic bug was introduced in the XP-reduction level-down path, and two P1 issues from the prior review round remain unresolved.

src/api/routes/members.js (oldXp clamping bug), src/modules/actions/roleUtils.js (enforceRoleLevelDown defaultActions gap), src/modules/actions/grantRole.js (unconditional roles.add)

Important Files Changed

Filename Overview
src/modules/levelUpActions.js New pipeline executor: resolves actions per level (including skips), manages rate-limit quota centrally, and dispatches to action registry handlers with per-action error isolation. Design is solid.
src/modules/actions/roleUtils.js Role permission helpers, sliding-window rate limiter, and enforceRoleLevelDown. collectXpManagedRoles now correctly covers both levelActions and defaultActions; collectRolesToRemove still only walks levelActions, leaving defaultActions-granted roles unrevoked on level-down.
src/modules/actions/grantRole.js Grant role handler with replace-mode loop and per-removal canManageRole guard. The final member.roles.add still executes without a rate-limit check (flagged in a prior review thread).
src/api/routes/members.js XP adjustment endpoint now attempts to compute oldLevel via newXp - amount, but this gives a wrong result when GREATEST(0,...) clamps XP — causing enforceRoleLevelDown to fire spuriously. rows[0].level is the correct pre-update source. Also uses an unnecessary dynamic import.
src/utils/templateEngine.js Stateless template renderer and context builder. Two hardcoded 'en-US' locale strings in formatNumber and joinDate violate the project's no-hardcoded-locale rule.
src/modules/reputation.js Cleaned up nicely — ~90 lines of inline side-effect logic removed, replaced with an executeLevelUpPipeline call gated on xpCfg.enabled. XP/level thresholds now read from config.xp.
src/modules/xpDefaults.js Single source of truth for XP config defaults; clean and straightforward.
src/commands/rank.js Switched to XP_DEFAULTS/config.xp for thresholds. Does NOT gate on xp.enabled, preserving backward compatibility for existing guilds.
src/api/routes/community.js Migrated from getRepConfig/REPUTATION_DEFAULTS to getXpConfig; max-level nextLevelXp now correctly returns 0 instead of repeating the last threshold.
web/src/components/dashboard/config-sections/CommunitySettingsSection.tsx New Level-Up Actions settings card added. XP threshold input now sorts values before saving. Badge list key updated to use stable composite key.
web/src/types/config.ts New XpConfig, XpLevelAction, and XpLevelActionEntry types added cleanly; BotConfig and ConfigSection union updated accordingly.

Sequence Diagram

sequenceDiagram
    participant M as Message Event
    participant R as reputation.js
    participant P as levelUpActions.js
    participant RL as roleUtils.js
    participant GR as grantRole.js
    participant D as Discord API

    M->>R: handleXpGain(message)
    R->>R: computeLevel(newXp, thresholds)
    R->>R: UPDATE reputation SET level
    R->>P: executeLevelUpPipeline({member, guild, previousLevel, newLevel, xp, config})
    P->>P: resolveActions(previousLevel, newLevel, config)
    P->>RL: checkRoleRateLimit(guildId, userId)
    RL-->>P: rateLimitOk (quota=2)
    loop For each level action
        P->>P: buildTemplateContext(level)
        P->>P: roleChangesRemaining--
        P->>GR: handleGrantRole(action, context)
        GR->>RL: canManageRole(guild, roleId)
        alt stackRoles == false
            GR->>RL: checkRoleRateLimit / recordRoleChange
            GR->>D: member.roles.remove(oldRoleId)
        end
        GR->>D: member.roles.add(roleId)
        GR->>RL: recordRoleChange
    end

    note over M,D: XP API Reduction path
    participant API as POST /xp
    API->>API: UPDATE xp = GREATEST(0, xp+amount) RETURNING xp level
    API->>API: oldLevel = rows[0].level (pre-update stored level)
    API->>API: newLevel = computeLevel(newXp, thresholds)
    alt newLevel < oldLevel and removeOnLevelDown
        API->>RL: enforceRoleLevelDown(member, newLevel, xpConfig)
        RL->>RL: collectRolesToRemove (levelActions only)
        RL->>D: member.roles.remove(roleIds[])
    end
Loading

Comments Outside Diff (1)

  1. src/api/routes/members.js, line 977-978 (link)

    oldXp = newXp - amount is wrong when XP is clamped by GREATEST(0, ...)

    The SQL clause SET xp = GREATEST(0, reputation.xp + $3) clamps the result to zero. When a large negative amount would push XP below zero, newXp is 0 — but newXp - amount gives back the unclamped value, not the real pre-update XP.

    Concrete example: user has 50 XP (level 0), admin removes 200 (amount = -200):

    • newXp = GREATEST(0, 50 − 200) = 0
    • oldXp = 0 − (−200) = 200 ← wrong; real old XP was 50
    • oldLevel = computeLevel(200, [100, 300, ...]) = 2 ← wrong; user was level 0

    newLevel (0) < oldLevel (2) evaluates to true, so enforceRoleLevelDown fires and strips roles the user never earned.

    rows[0].level from the RETURNING clause is already the pre-update stored level (only xp is in the SET clause). Use it directly:

Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/api/routes/members.js
Line: 977-978

Comment:
**`oldXp = newXp - amount` is wrong when XP is clamped by `GREATEST(0, ...)`**

The SQL clause `SET xp = GREATEST(0, reputation.xp + $3)` clamps the result to zero. When a large negative `amount` would push XP below zero, `newXp` is 0 — but `newXp - amount` gives back the unclamped value, not the real pre-update XP.

Concrete example: user has 50 XP (level 0), admin removes 200 (`amount = -200`):
- `newXp = GREATEST(0, 50 − 200) = 0`
- `oldXp = 0 − (−200) = 200` ← wrong; real old XP was 50
- `oldLevel = computeLevel(200, [100, 300, ...]) = 2` ← wrong; user was level 0

`newLevel (0) < oldLevel (2)` evaluates to `true`, so `enforceRoleLevelDown` fires and strips roles the user never earned.

`rows[0].level` from the `RETURNING` clause is already the pre-update stored level (only `xp` is in the `SET` clause). Use it directly:

```suggestion
        oldLevel = rows[0].level;
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: src/utils/templateEngine.js
Line: 53

Comment:
**Hardcoded `'en-US'` locale in `formatNumber` and `joinDate`**

Two places in this file hardcode `'en-US'`, violating the project's no-hardcoded-locale rule — use `undefined` to inherit the runtime locale:

- Line 53 (`formatNumber`):
```suggestion
  return Number(n).toLocaleString(undefined);
```

- Line 154–158 (`joinDate`):
```
    joinDate: member.joinedAt
      ? member.joinedAt.toLocaleDateString(undefined, {
          month: 'short',
          day: 'numeric',
          year: 'numeric',
        })
      : '',
```

**Rule Used:** Do not hardcode locale strings like 'en-US' in Int... ([source](https://app.greptile.com/review/custom-context?memory=no-hardcoded-locale))

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: src/api/routes/members.js
Line: 1004

Comment:
**Unnecessary dynamic import — prefer static import**

`enforceRoleLevelDown` is imported dynamically here, but there is no circular dependency that would require it. Dynamic `import()` defers module loading to runtime and adds async overhead on every XP reduction that triggers level-down enforcement. A static import at the top of the file would be cleaner and consistent with how the rest of this file's dependencies are handled.

```
// At the top of members.js with the other imports:
import { enforceRoleLevelDown } from '../../modules/actions/roleUtils.js';
```

Then the call site becomes:
```suggestion
            await enforceRoleLevelDown(member, newLevel, xpConfig);
```

How can I resolve this? If you propose a fix, please make it concise.

Reviews (13): Last reviewed commit: "test: add enforceRoleLevelDown coverage ..." | Re-trigger Greptile

Comment thread src/modules/actions/roleUtils.js
Comment thread src/modules/actions/grantRole.js
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Introduces a configurable XP level-up “action pipeline” under config.xp, replacing the prior inline role/announcement side effects and wiring the new XP config into bot commands and API routes.

Changes:

  • Adds XP defaults + a level-up pipeline executor with an action registry and level-skip handling.
  • Implements Phase 1 role actions (grantRole, removeRole) plus shared permission/rate-limit utilities and level-down enforcement.
  • Adds a template interpolation/context engine and updates config + call sites to read levelThresholds from config.xp (with accompanying tests).

Reviewed changes

Copilot reviewed 20 out of 20 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
config.json Moves XP thresholds/settings into new xp section.
src/modules/xpDefaults.js Defines default config.xp values.
src/modules/reputationDefaults.js Removes XP-specific fields moved to xpDefaults.
src/modules/reputation.js Switches level-up side effects to executeLevelUpPipeline() (fire-and-forget).
src/modules/levelUpActions.js Adds pipeline executor + action registry + action resolution across level skips.
src/modules/actions/roleUtils.js Adds role permission checks, in-memory rate limiter, and level-down enforcement.
src/modules/actions/grantRole.js Adds grantRole handler with stack/replace behavior and template-context role info.
src/modules/actions/removeRole.js Adds removeRole handler.
src/utils/templateEngine.js Adds template renderer, context builder, and length validator.
src/commands/rank.js Reads thresholds from config.xp defaults instead of reputation defaults.
src/api/utils/configAllowlist.js Allows xp to be written via API/dashboard.
src/api/routes/community.js Reads thresholds from config.xp.
src/api/routes/members.js Reads thresholds from config.xp; enforces role level-down on XP reduction.
tests/utils/templateEngine.test.js New tests for template rendering/context/length validation.
tests/modules/levelUpActions.test.js New tests for action resolution + sequential execution + error isolation.
tests/modules/actions/roleUtils.test.js New tests for permission checks, rate limiting, role collection, and level-down enforcement.
tests/modules/actions/grantRole.test.js New tests for grantRole behavior including stack/replace.
tests/modules/actions/removeRole.test.js New tests for removeRole behavior.
tests/modules/reputationDefaults.test.js Updates expectations for moved XP fields.
tests/modules/reputation.test.js Updates to assert pipeline invocation and new config shape.

Comment thread src/modules/actions/roleUtils.js
Comment thread src/modules/actions/grantRole.js
Comment thread src/modules/levelUpActions.js Outdated
Comment thread src/modules/reputation.js Outdated
Comment thread src/utils/templateEngine.js Outdated
Comment thread src/modules/actions/roleUtils.js
Comment thread src/modules/actions/grantRole.js
Comment thread src/utils/templateEngine.js
Copy link
Copy Markdown
Contributor

@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: 13

Caution

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

⚠️ Outside diff range comments (1)
src/api/routes/members.js (1)

994-1037: ⚠️ Potential issue | 🟠 Major

Successful XP adjustments leave cached reads stale.

This route updates reputation, but it never evicts the per-user enrichment cache populated earlier in this file, and src/api/routes/community.js also caches XP-based reads. After an admin adjustment, /members and community endpoints can keep serving the old XP/level until TTL expiry. Please invalidate the affected member and community cache keys before returning success.

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

In `@src/api/routes/members.js` around lines 994 - 1037, This route updates a
member's XP/level but doesn't evict cached reads—before sending the success
response (before the res.json call), invalidate the per-user enrichment cache
key populated earlier in this file and the XP/community cache key(s) used by
src/api/routes/community.js for the same guildId/userId so subsequent reads
return the updated newXp/newLevel; use the same cache helper/functions those
modules use (the same cache client or cache.del/cache.invalidate functions) and
reference the current guildId and userId when building the cache keys to remove
the entries prior to returning from this handler.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@config.json`:
- Around line 286-295: Existing guild configs using legacy keys
reputation.levelThresholds and reputation.roleRewards are being ignored because
getXpConfig() only reads cfg.xp and falls back to XP_DEFAULTS; implement a fix
that either migrates stored configs into the new xp shape on load/save or adds a
compatibility branch inside getXpConfig() in src/modules/reputation.js: when
cfg.xp is missing/empty, detect and map legacy keys reputation.levelThresholds
-> xp.levelThresholds and reputation.roleRewards -> xp.roleRewards (preserving
stackRoles/removeOnLevelDown semantics) before returning (or persist the
migrated xp back to the DB), ensuring getXpConfig() returns the
migrated/compatible object instead of defaulting to XP_DEFAULTS.

In `@src/api/routes/community.js`:
- Around line 203-206: The code sets nextLevelXp to the last threshold for
max-level users, making currentLevelXp === nextLevelXp; change the logic in the
block that computes currentLevelXp and nextLevelXp (referencing
xpConfig.levelThresholds and level) so that when level is at or beyond the
highest threshold you assign nextLevelXp = null (matching members.js's
nextThreshold sentinel) instead of mirroring the last threshold; also ensure any
response schema or docs that describe nextLevelXp are updated to allow null for
max-level users.

In `@src/api/routes/members.js`:
- Around line 663-666: The current code trusts rep?.level which may be stale;
change the member-detail logic to always recompute level from XP by calling
computeLevel(xp, xpConfig.levelThresholds) (use getXpConfig and xp from rep?.xp
?? 0 as before) and then derive nextThreshold from xpConfig.levelThresholds
using that computed level; replace any use of rep?.level with the computed value
so nextThreshold = xpConfig.levelThresholds[computedLevel] ?? null.

In `@src/modules/actions/grantRole.js`:
- Around line 22-35: The per-call rate limiter in grantRole.js (calls to
checkRoleRateLimit and recordRoleChange around member.roles.add/remove) causes
skipped-level rewards when executeLevelUpPipeline invokes the handler multiple
times; change the flow so rate limiting is applied once per pipeline run rather
than per crossed level: add a boolean parameter (e.g., bypassRateLimit or
pipelineRun) to the grantRole handler or to checkRoleRateLimit so
executeLevelUpPipeline can call it once before iterating levels, and ensure
recordRoleChange is invoked only once after the final role mutation; update
canManageRole, checkRoleRateLimit, and the call sites in executeLevelUpPipeline
to accept/propagate the new flag and move/remove per-call
checkRoleRateLimit/recordRoleChange invocations so role removals/additions
(member.roles.remove/add) are batched for the pipeline run.

In `@src/modules/actions/removeRole.js`:
- Around line 21-25: This file currently consumes a rate-limit slot and records
a role change on every individual removal (calls to checkRoleRateLimit and
recordRoleChange) which prevents multi-role removals from completing; remove the
per-removal rate-limit and recording from the remove path by deleting the
checkRoleRateLimit(guild.id, member.user?.id) guard and the
recordRoleChange(guild.id, member.user?.id) call surrounding
member.roles.remove(roleId), and instead have the batched caller perform one
checkRoleRateLimit and one recordRoleChange for the entire set of removals so
the batch is treated as a single logical operation.

In `@src/modules/actions/roleUtils.js`:
- Around line 129-145: The enforceRoleLevelDown function has high cognitive
complexity due to nested loops and multiple continue guards; extract the inner
action-processing loop into a new helper (e.g., processLevelEntryActions or
handleEntryActions) that accepts (guild, member, entry, newLevel) and contains
the per-action checks (action.type==='grantRole', action.roleId,
member.roles.cache.has, canManageRole, checkRoleRateLimit) and the await
member.roles.remove + recordRoleChange call; then simplify enforceRoleLevelDown
to iterate xpConfig.levelActions, skip entries where entry.level <= newLevel,
and call the helper for each remaining entry, preserving current logic and async
behavior.
- Around line 139-142: In enforceRoleLevelDown, avoid calling
member.roles.remove(action.roleId) repeatedly; instead gather all action.roleId
values that should be removed for a given member into a single array, call
checkRoleRateLimit(guild.id, member.user.id) once before the batch, then call
member.roles.remove([...roleIds]) once and invoke recordRoleChange(guild.id,
member.user.id) a single time after the batch removal so the operation counts as
one rate-limit change rather than N individual removals.

In `@src/utils/templateEngine.js`:
- Around line 82-84: The template token `nextLevel` is currently set to the XP
threshold (via `nextThreshold`) instead of the numeric next level; update the
token mapping so `nextLevel` returns the next level number (e.g., `level + 1` or
null if no next level) and keep `xpToNext` as the XP delta; if the XP threshold
itself is required, expose it under a new token name (e.g., `nextThreshold`)
rather than reusing `nextLevel`, and ensure any code that constructs tokens uses
these corrected identifiers (`nextLevel`, `nextThreshold`, `xpToNext`).
- Line 138: The buildTemplateContext function currently dereferences
message.channel unguarded (see the line setting channel: message.channel?.name ?
`#${message.channel.name}` : ''), which throws when buildTemplateContext is
called without a message; fix it by guarding access to message first (e.g., use
message?.channel?.name or check message exists before reading channel) so
channel is computed only when message is present and otherwise set to ''. Ensure
the change only affects the channel expression in buildTemplateContext so other
callers remain unaffected.

In `@tests/modules/actions/grantRole.test.js`:
- Around line 57-121: Add a regression test that simulates three sequential
grants for the same user to exercise the 2/60s rate limiter and ensure the
highest reward still lands: using makeContext(), call handleGrantRole() three
times in order with relevant roleIds (e.g., lower, mid, highest), mock or let
checkRoleRateLimit behave normally so the limiter can block the later calls, and
then assert that the final member state contains the highest role
(ctx._mocks.rolesAdd called with highest roleId) and recordRoleChange was
invoked for the user; reference handleGrantRole, makeContext,
checkRoleRateLimit, ctx._mocks.rolesAdd and recordRoleChange to locate code to
test.

In `@tests/modules/actions/roleUtils.test.js`:
- Around line 68-97: Tests share in-memory rate limit state stored in the
roleLimits Map which can leak between tests; add a reset step for isolation by
either exporting a new clearRoleLimits() helper from the module that empties the
roleLimits Map, or call an existing reset function, and invoke that helper in
the test file's beforeEach (or afterEach) to clear roleLimits prior to each
test; reference the roleLimits Map and the checkRoleRateLimit and
recordRoleChange functions so reviewers can locate where to add the
clearRoleLimits() implementation and where to call it in the test setup.
- Around line 38-66: Add a beforeEach that calls vi.clearAllMocks() to ensure
warn mock assertions in canManageRole tests are isolated; update the test suite
for describe('canManageRole') so beforeEach invokes vi.clearAllMocks(), leaving
the existing test cases and references to canManageRole and warn unchanged so
each it-block verifies warn calls reliably.

In `@tests/utils/templateEngine.test.js`:
- Around line 99-204: Add two tests to tests/utils/templateEngine.test.js for
buildTemplateContext: (1) mock the DB query function that buildTemplateContext
uses (e.g., db.query or the module's query helper) to return populated rows for
rank, messages, voiceHours, and daysActive and assert that ctx.rank,
ctx.messages, ctx.voiceHours, and ctx.daysActive match the expected formatted
values; (2) add a test where that same query mock rejects (throws) to exercise
the rejected-query fallback and assert the fallback values (e.g., '0' or empty
string) and that no exception bubbles up. Ensure you restore/reset the mock
between tests.

---

Outside diff comments:
In `@src/api/routes/members.js`:
- Around line 994-1037: This route updates a member's XP/level but doesn't evict
cached reads—before sending the success response (before the res.json call),
invalidate the per-user enrichment cache key populated earlier in this file and
the XP/community cache key(s) used by src/api/routes/community.js for the same
guildId/userId so subsequent reads return the updated newXp/newLevel; use the
same cache helper/functions those modules use (the same cache client or
cache.del/cache.invalidate functions) and reference the current guildId and
userId when building the cache keys to remove the entries prior to returning
from this handler.
🪄 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

Run ID: 09b64035-4730-4b17-812a-4437d10e9503

📥 Commits

Reviewing files that changed from the base of the PR and between 6e95a3d and 49a887d.

📒 Files selected for processing (20)
  • config.json
  • src/api/routes/community.js
  • src/api/routes/members.js
  • src/api/utils/configAllowlist.js
  • src/commands/rank.js
  • src/modules/actions/grantRole.js
  • src/modules/actions/removeRole.js
  • src/modules/actions/roleUtils.js
  • src/modules/levelUpActions.js
  • src/modules/reputation.js
  • src/modules/reputationDefaults.js
  • src/modules/xpDefaults.js
  • src/utils/templateEngine.js
  • tests/modules/actions/grantRole.test.js
  • tests/modules/actions/removeRole.test.js
  • tests/modules/actions/roleUtils.test.js
  • tests/modules/levelUpActions.test.js
  • tests/modules/reputation.test.js
  • tests/modules/reputationDefaults.test.js
  • tests/utils/templateEngine.test.js
💤 Files with no reviewable changes (1)
  • src/modules/reputationDefaults.js
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Cursor Bugbot
  • GitHub Check: Agent
  • GitHub Check: Greptile Review
  • GitHub Check: Test
  • GitHub Check: Docker Build Validation
🧰 Additional context used
📓 Path-based instructions (13)
**/*.js

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use ESM-only syntax: import/export, never require()/module.exports

Files:

  • src/commands/rank.js
  • src/api/utils/configAllowlist.js
  • src/modules/xpDefaults.js
  • tests/modules/reputationDefaults.test.js
  • src/api/routes/community.js
  • tests/modules/actions/removeRole.test.js
  • tests/modules/actions/grantRole.test.js
  • src/modules/reputation.js
  • src/modules/actions/removeRole.js
  • tests/modules/levelUpActions.test.js
  • tests/utils/templateEngine.test.js
  • src/modules/actions/grantRole.js
  • tests/modules/reputation.test.js
  • tests/modules/actions/roleUtils.test.js
  • src/utils/templateEngine.js
  • src/modules/levelUpActions.js
  • src/modules/actions/roleUtils.js
  • src/api/routes/members.js
**/*.{js,ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{js,ts,tsx}: Use single quotes for strings (except in JSON files); no double quotes
Always include semicolons at the end of statements
Use 2-space indentation (spaces, not tabs)
Always include trailing commas in multi-line arrays, objects, and function parameters
Maintain a maximum line width of 100 characters

Files:

  • src/commands/rank.js
  • src/api/utils/configAllowlist.js
  • src/modules/xpDefaults.js
  • tests/modules/reputationDefaults.test.js
  • src/api/routes/community.js
  • tests/modules/actions/removeRole.test.js
  • tests/modules/actions/grantRole.test.js
  • src/modules/reputation.js
  • src/modules/actions/removeRole.js
  • tests/modules/levelUpActions.test.js
  • tests/utils/templateEngine.test.js
  • src/modules/actions/grantRole.js
  • tests/modules/reputation.test.js
  • tests/modules/actions/roleUtils.test.js
  • src/utils/templateEngine.js
  • src/modules/levelUpActions.js
  • src/modules/actions/roleUtils.js
  • src/api/routes/members.js
src/**/*.js

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

src/**/*.js: Never use console.* methods; use the Winston logger instead via import logger from '../logger.js' (adjust path as needed), then call logger.info(), logger.warn(), logger.error(), or logger.debug()
Always use safeReply(), safeSend(), or safeEditReply() instead of raw Discord.js methods for safe Discord messaging that handles errors gracefully

Files:

  • src/commands/rank.js
  • src/api/utils/configAllowlist.js
  • src/modules/xpDefaults.js
  • src/api/routes/community.js
  • src/modules/reputation.js
  • src/modules/actions/removeRole.js
  • src/modules/actions/grantRole.js
  • src/utils/templateEngine.js
  • src/modules/levelUpActions.js
  • src/modules/actions/roleUtils.js
  • src/api/routes/members.js
src/commands/**/*.js

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Create slash command definitions in src/commands/, exporting a slash command builder and an execute function

Files:

  • src/commands/rank.js
**/*.{js,mjs,ts}

📄 CodeRabbit inference engine (AGENTS.md)

Use ESM only

Files:

  • src/commands/rank.js
  • src/api/utils/configAllowlist.js
  • src/modules/xpDefaults.js
  • tests/modules/reputationDefaults.test.js
  • src/api/routes/community.js
  • tests/modules/actions/removeRole.test.js
  • tests/modules/actions/grantRole.test.js
  • src/modules/reputation.js
  • src/modules/actions/removeRole.js
  • tests/modules/levelUpActions.test.js
  • tests/utils/templateEngine.test.js
  • src/modules/actions/grantRole.js
  • tests/modules/reputation.test.js
  • tests/modules/actions/roleUtils.test.js
  • src/utils/templateEngine.js
  • src/modules/levelUpActions.js
  • src/modules/actions/roleUtils.js
  • src/api/routes/members.js
**/*.{js,ts}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{js,ts}: Use src/logger.js; do not use console.*
Use the safe Discord messaging helpers in src/utils/safeSend.js instead of raw reply/send/edit calls
Use parameterized SQL only
Community features should be gated behind config..enabled. Moderation commands are the exception

Files:

  • src/commands/rank.js
  • src/api/utils/configAllowlist.js
  • src/modules/xpDefaults.js
  • tests/modules/reputationDefaults.test.js
  • src/api/routes/community.js
  • tests/modules/actions/removeRole.test.js
  • tests/modules/actions/grantRole.test.js
  • src/modules/reputation.js
  • src/modules/actions/removeRole.js
  • tests/modules/levelUpActions.test.js
  • tests/utils/templateEngine.test.js
  • src/modules/actions/grantRole.js
  • tests/modules/reputation.test.js
  • tests/modules/actions/roleUtils.test.js
  • src/utils/templateEngine.js
  • src/modules/levelUpActions.js
  • src/modules/actions/roleUtils.js
  • src/api/routes/members.js
src/api/utils/configAllowlist.js

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

When adding a new config section, add it to SAFE_CONFIG_KEYS to enable saves via the API/dashboard, and to READABLE_CONFIG_KEYS for API read access

Files:

  • src/api/utils/configAllowlist.js
{config.json,**/configAllowlist.{js,ts}}

📄 CodeRabbit inference engine (AGENTS.md)

Config-backed features must be added to config.json and src/api/utils/configAllowlist.js. If a key is missing from SAFE_CONFIG_KEYS, the dashboard cannot save it

Files:

  • src/api/utils/configAllowlist.js
  • config.json
src/modules/**/*.js

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Create feature modules in src/modules/ and add corresponding config sections to config.json

Files:

  • src/modules/xpDefaults.js
  • src/modules/reputation.js
  • src/modules/actions/removeRole.js
  • src/modules/actions/grantRole.js
  • src/modules/levelUpActions.js
  • src/modules/actions/roleUtils.js
tests/**/*.test.js

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

tests/**/*.test.js: Write bot tests using Vitest 4 with the node environment, matching the src/ structure in the tests/ directory
Maintain test coverage thresholds: statements 85%, branches 82%, functions 85%, lines 85%; never lower thresholds—add tests to cover new code instead

Files:

  • tests/modules/reputationDefaults.test.js
  • tests/modules/actions/removeRole.test.js
  • tests/modules/actions/grantRole.test.js
  • tests/modules/levelUpActions.test.js
  • tests/utils/templateEngine.test.js
  • tests/modules/reputation.test.js
  • tests/modules/actions/roleUtils.test.js
**/*.json

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use double quotes in JSON files (standard JSON format); this exception applies only to JSON files, not JavaScript/TypeScript

Files:

  • config.json
config.json

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

All community features in config.json should be gated behind a config.<feature>.enabled flag; moderation commands are always available regardless of config settings

Files:

  • config.json
src/api/routes/**/*.js

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Create API route handlers in src/api/routes/ and mount them in src/api/server.js; add auth middleware if the endpoint requires authentication

Files:

  • src/api/routes/community.js
  • src/api/routes/members.js
🧠 Learnings (11)
📚 Learning: 2026-03-12T02:03:36.493Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-12T02:03:36.493Z
Learning: Applies to src/utils/cache.js : Use `src/utils/cache.js` for generic caching with Redis primary + in-memory fallback; use `src/utils/discordCache.js` for Discord API response caching (channels, roles, members); use `src/utils/reputationCache.js` for leaderboard and user reputation data; all caches auto-invalidate when config changes

Applied to files:

  • src/commands/rank.js
  • src/api/routes/community.js
  • src/modules/reputation.js
  • src/api/routes/members.js
📚 Learning: 2026-03-12T02:03:36.493Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-12T02:03:36.493Z
Learning: Applies to src/api/utils/configAllowlist.js : When adding a new config section, add it to `SAFE_CONFIG_KEYS` to enable saves via the API/dashboard, and to `READABLE_CONFIG_KEYS` for API read access

Applied to files:

  • src/api/utils/configAllowlist.js
📚 Learning: 2026-03-22T01:55:35.229Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-22T01:55:35.229Z
Learning: Applies to {config.json,**/configAllowlist.{js,ts}} : Config-backed features must be added to config.json and src/api/utils/configAllowlist.js. If a key is missing from SAFE_CONFIG_KEYS, the dashboard cannot save it

Applied to files:

  • src/api/utils/configAllowlist.js
📚 Learning: 2026-03-12T02:03:36.493Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-12T02:03:36.493Z
Learning: Applies to config.json : All community features in `config.json` should be gated behind a `config.<feature>.enabled` flag; moderation commands are always available regardless of config settings

Applied to files:

  • src/api/utils/configAllowlist.js
📚 Learning: 2026-03-22T01:55:35.229Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-22T01:55:35.229Z
Learning: Applies to **/*.{js,ts} : Community features should be gated behind config.<feature>.enabled. Moderation commands are the exception

Applied to files:

  • src/api/utils/configAllowlist.js
📚 Learning: 2026-03-12T02:03:36.493Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-12T02:03:36.493Z
Learning: Applies to web/tests/**/*.test.{ts,tsx} : Write web dashboard tests using Vitest 4 with the `jsdom` environment and React Testing Library, matching the `web/src/` structure

Applied to files:

  • tests/modules/actions/removeRole.test.js
  • tests/modules/actions/grantRole.test.js
  • tests/modules/levelUpActions.test.js
  • tests/utils/templateEngine.test.js
  • tests/modules/actions/roleUtils.test.js
📚 Learning: 2026-03-12T02:03:36.493Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-12T02:03:36.493Z
Learning: Applies to tests/**/*.test.js : Write bot tests using Vitest 4 with the `node` environment, matching the `src/` structure in the `tests/` directory

Applied to files:

  • tests/modules/actions/removeRole.test.js
  • tests/modules/actions/grantRole.test.js
  • tests/modules/levelUpActions.test.js
  • tests/utils/templateEngine.test.js
  • tests/modules/actions/roleUtils.test.js
📚 Learning: 2026-03-11T06:42:38.728Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-11T06:42:38.728Z
Learning: Applies to src/commands/**/*.js : Guild owners should be explicitly exempted from role hierarchy restrictions in command handlers to match Discord permission model expectations

Applied to files:

  • tests/modules/actions/grantRole.test.js
  • src/modules/actions/removeRole.js
  • src/modules/actions/grantRole.js
  • tests/modules/actions/roleUtils.test.js
  • src/modules/actions/roleUtils.js
📚 Learning: 2026-03-10T23:21:49.730Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-10T23:21:49.730Z
Learning: Applies to src/api/routes/*.js : Add adaptDeleteGuildIdParam in API route handlers to enforce guild moderation checks on DELETE requests without losing record id

Applied to files:

  • src/modules/actions/removeRole.js
  • src/api/routes/members.js
📚 Learning: 2026-03-10T23:29:51.063Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-10T23:29:51.063Z
Learning: Applies to src/api/routes/*.js : Apply requireGuildModerator authorization check to DELETE endpoints that modify guild-scoped resources

Applied to files:

  • src/modules/actions/removeRole.js
  • src/api/routes/members.js
📚 Learning: 2026-03-11T06:42:38.728Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-11T06:42:38.728Z
Learning: Applies to src/commands/reactionrole.js : Enforce invoker role hierarchy check in /reactionrole add command to prevent non-owner users from configuring roles at or above their highest role

Applied to files:

  • src/modules/actions/removeRole.js
  • src/modules/actions/grantRole.js
  • tests/modules/actions/roleUtils.test.js
  • src/modules/actions/roleUtils.js
🪛 GitHub Check: SonarCloud Code Analysis
tests/utils/templateEngine.test.js

[warning] 9-9: 'vitest' imported multiple times.

See more on https://sonarcloud.io/project/issues?id=VolvoxLLC_volvox-bot&issues=AZ0nXmHvA2WZ1HihRe1V&open=AZ0nXmHvA2WZ1HihRe1V&pullRequest=373


[warning] 1-1: 'vitest' imported multiple times.

See more on https://sonarcloud.io/project/issues?id=VolvoxLLC_volvox-bot&issues=AZ0nXmHvA2WZ1HihRe1U&open=AZ0nXmHvA2WZ1HihRe1U&pullRequest=373

src/utils/templateEngine.js

[warning] 134-134: Unexpected negated condition.

See more on https://sonarcloud.io/project/issues?id=VolvoxLLC_volvox-bot&issues=AZ0nXmJ3A2WZ1HihRe1Z&open=AZ0nXmJ3A2WZ1HihRe1Z&pullRequest=373


[warning] 134-134: Remove this "!==" check; it will always be true. Did you mean to use "!="?

See more on https://sonarcloud.io/project/issues?id=VolvoxLLC_volvox-bot&issues=AZ0nXmJ3A2WZ1HihRe1a&open=AZ0nXmJ3A2WZ1HihRe1a&pullRequest=373


[warning] 83-83: Remove this "!==" check; it will always be true. Did you mean to use "!="?

See more on https://sonarcloud.io/project/issues?id=VolvoxLLC_volvox-bot&issues=AZ0nXmJ3A2WZ1HihRe1Y&open=AZ0nXmJ3A2WZ1HihRe1Y&pullRequest=373


[warning] 26-26: Prefer String#replaceAll() over String#replace().

See more on https://sonarcloud.io/project/issues?id=VolvoxLLC_volvox-bot&issues=AZ0nXmJ3A2WZ1HihRe1W&open=AZ0nXmJ3A2WZ1HihRe1W&pullRequest=373


[warning] 83-83: Unexpected negated condition.

See more on https://sonarcloud.io/project/issues?id=VolvoxLLC_volvox-bot&issues=AZ0nXmJ3A2WZ1HihRe1X&open=AZ0nXmJ3A2WZ1HihRe1X&pullRequest=373

src/modules/actions/roleUtils.js

[failure] 129-129: Refactor this function to reduce its Cognitive Complexity from 17 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=VolvoxLLC_volvox-bot&issues=AZ0nXmKNA2WZ1HihRe1b&open=AZ0nXmKNA2WZ1HihRe1b&pullRequest=373

🔇 Additional comments (11)
src/api/utils/configAllowlist.js (1)

34-37: Good allowlist update for the new xp section.

READABLE_CONFIG_KEYS inherits from SAFE_CONFIG_KEYS, so adding xp here keeps the dashboard/API read-write path aligned with the new top-level config. Based on learnings, "When adding a new config section, add it to SAFE_CONFIG_KEYS to enable saves via the API/dashboard, and to READABLE_CONFIG_KEYS for API read access".

src/commands/rank.js (1)

13-13: Nice switch to the shared XP config.

Using XP_DEFAULTS + cfg.xp keeps /rank aligned with the new top-level XP settings.

Also applies to: 48-49

src/modules/levelUpActions.js (3)

1-34: LGTM! Well-structured action registry pattern.

The registry pattern with registerAction provides clean extensibility for Phase 2 additions. The built-in handler registration is appropriately placed at module load time.


44-64: LGTM! Correct level-skip handling.

The resolveActions function properly handles level skips by iterating from previousLevel + 1 through newLevel, collecting actions for each crossed threshold. The fallback to defaultActions when no level-specific actions exist matches the PR requirements.


79-149: LGTM! Robust pipeline execution with proper error isolation.

The implementation correctly:

  • Returns early when no actions need execution
  • Builds shared context once for efficiency
  • Uses per-action try/catch to ensure failures don't abort the pipeline
  • Logs structured warnings for unknown action types and handler errors

The optional chaining on member.user?.id provides good defensive coding against edge cases.

tests/modules/actions/roleUtils.test.js (2)

116-141: LGTM! Comprehensive coverage of collectXpManagedRoles.

Tests correctly verify that only grantRole and removeRole action types contribute role IDs, and other action types like sendDm and addReaction are ignored.


143-208: LGTM! Good coverage of enforceRoleLevelDown edge cases.

Tests cover the key scenarios: removing roles above new level, keeping roles at/below new level, and skipping roles the member doesn't have. The mock setup correctly simulates Discord.js member/guild structures.

src/modules/actions/roleUtils.js (4)

1-60: LGTM! Solid permission and hierarchy checks.

The canManageRole function correctly implements Discord's permission model:

  • Verifies bot has ManageRoles permission
  • Ensures role exists in cache
  • Enforces role hierarchy (role must be strictly below bot's highest)

Warning logs provide good observability for debugging permission issues.


70-103: LGTM! Clean separation of check vs. record for rate limiting.

The two-step pattern (checkRoleRateLimit then recordRoleChange) allows callers to verify before attempting changes, and only record on success. The sliding window implementation correctly filters expired timestamps.


147-148: LGTM! Good use of .unref() for the sweep interval.

Using .unref() ensures the timer doesn't prevent Node.js from exiting when the process should terminate. This matches the pattern used elsewhere (reputation.js cooldowns per the comment).


157-167: LGTM! Correctly extracts XP-managed role IDs.

The function properly iterates through levelActions and collects role IDs from both grantRole and removeRole action types, which is needed for the stack/replace logic in grant handlers.

Comment thread config.json
Comment thread src/api/routes/community.js Outdated
Comment thread src/modules/actions/grantRole.js
Comment thread src/modules/actions/removeRole.js
Comment thread src/modules/actions/roleUtils.js Outdated
Comment thread src/utils/templateEngine.js
Comment thread src/utils/templateEngine.js Outdated
Comment thread tests/modules/actions/grantRole.test.js
Comment thread tests/modules/actions/roleUtils.test.js
Comment thread tests/modules/actions/roleUtils.test.js
@github-project-automation github-project-automation Bot moved this from Backlog to In Review in Volvox.Bot Mar 25, 2026
Copilot AI review requested due to automatic review settings March 25, 2026 23:58
Comment thread src/modules/reputation.js
Comment thread src/modules/reputation.js
Comment thread src/modules/actions/grantRole.js
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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 31 out of 31 changed files in this pull request and generated 6 comments.

Comment thread src/modules/reputation.js
Comment thread src/modules/levelUpActions.js Outdated
Comment on lines +116 to +121
const templateContext = await buildTemplateContext({
member,
message,
guild,
level,
previousLevel,
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

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

previousLevel passed into buildTemplateContext() is always the original pre-jump level. During level skips (e.g. 4→12), intermediate levels (5, 6, …) will render {{previousLevel}} incorrectly. Consider passing level - 1 (or otherwise tracking the per-step previous level) when building the context for each intermediate level.

Suggested change
const templateContext = await buildTemplateContext({
member,
message,
guild,
level,
previousLevel,
// For templates, previousLevel should reflect the level immediately before this step.
const stepPreviousLevel = level > 0 ? level - 1 : 0;
const templateContext = await buildTemplateContext({
member,
message,
guild,
level,
previousLevel: stepPreviousLevel,

Copilot uses AI. Check for mistakes.
Comment thread src/modules/levelUpActions.js
Comment thread src/modules/actions/roleUtils.js
Comment thread src/modules/actions/roleUtils.js Outdated
Comment on lines +142 to +143
await member.roles.remove(action.roleId);
recordRoleChange(guild.id, member.user.id);
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

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

enforceRoleLevelDown() awaits member.roles.remove() without handling failures. A single Discord API error (missing perms, unknown role, transient failure) will throw and abort the loop, potentially leaving other above-level roles intact. Wrap each removal in a try/catch (log + continue) so level-down enforcement is best-effort like the rest of the pipeline.

Suggested change
await member.roles.remove(action.roleId);
recordRoleChange(guild.id, member.user.id);
try {
await member.roles.remove(action.roleId);
recordRoleChange(guild.id, member.user.id);
} catch (error) {
warn('Failed to remove XP level-down role', {
guildId: guild.id,
userId: member.user.id,
roleId: action.roleId,
error,
});
}

Copilot uses AI. Check for mistakes.
Comment thread src/utils/templateEngine.js Outdated
}

return {
username: member.user.displayName ?? '',
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

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

username is derived from member.user.displayName, which won’t reflect a guild nickname. Since this context builder receives a GuildMember, using member.displayName (with a fallback to the user name) will better match how the rest of the codebase surfaces display names and what users expect in server-scoped templates.

Suggested change
username: member.user.displayName ?? '',
username: member.displayName ?? member.user.username ?? '',

Copilot uses AI. Check for mistakes.
Comment thread src/modules/actions/roleUtils.js
Comment thread src/modules/actions/grantRole.js
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 26, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Comment thread src/modules/levelUpActions.js
Comment thread src/api/routes/community.js
Copilot AI review requested due to automatic review settings March 26, 2026 04:15
Comment thread web/src/components/landing/bento/BentoChart.tsx Outdated
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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 33 out of 33 changed files in this pull request and generated 3 comments.

Comment thread web/src/components/landing/bento/BentoChart.tsx
Comment thread src/modules/actions/roleUtils.js
Comment thread src/api/routes/members.js
Comment on lines 975 to 979
newXp = rows[0].xp;
// Calculate oldLevel from pre-update XP (newXp - amount)
const oldXp = newXp - amount;
oldLevel = computeLevel(oldXp, xpConfig.levelThresholds);

Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

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

oldXp is inferred as newXp - amount, but the upsert floors XP at 0 via GREATEST(0, ...). When the floor is hit (e.g., existing XP=10, amount=-20 → newXp=0), newXp - amount produces 20, which is not the pre-update XP. That makes oldLevel incorrect and can trigger incorrect level-down enforcement logic. To compute oldLevel reliably, fetch the current XP/level inside the same transaction (e.g., SELECT xp, level ... FOR UPDATE) before applying the adjustment, then compute oldLevel from that value.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

@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: 11

♻️ Duplicate comments (6)
src/modules/actions/removeRole.js (1)

24-25: ⚠️ Potential issue | 🟠 Major

Don't record limiter usage inside each removeRole action.

The pipeline can hit this handler several times in one skipped-level run.
Recording here turns one logical batch into N limiter entries, which defeats the
upstream "check once per pipeline" design. Record the successful batch once in
the caller instead.

♻️ Minimal fix
   await member.roles.remove(roleId);
-  recordRoleChange(guild.id, member.user?.id);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modules/actions/removeRole.js` around lines 24 - 25, The removeRole
action currently calls recordRoleChange(guild.id, member.user?.id) after
awaiting member.roles.remove(roleId), which records limiter usage per individual
removal; remove that call from this handler (i.e., delete the recordRoleChange
invocation in the removeRole flow) so the handler only performs the role removal
and lets the caller/batch processor record a single limiter entry for the whole
pipeline run.
tests/modules/actions/removeRole.test.js (1)

9-13: ⚠️ Potential issue | 🟡 Minor

These tests don't actually cover pipeline rate limiting.

handleRemoveRole() never calls checkRoleRateLimit(), so the mock at
Lines 9-13 is dead. The case at Lines 50-58 duplicates the earlier
canManageRole === false path, and the "pipeline" case at Lines 60-73 just
rechecks the happy path without invoking the pipeline. Move rate-limit coverage
to levelUpActions.test.js and trim the duplicate assertions here.

Also applies to: 50-73

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

In `@tests/modules/actions/removeRole.test.js` around lines 9 - 13, The test file
mocks checkRoleRateLimit but handleRemoveRole never calls it, so remove the dead
mock of checkRoleRateLimit from tests/modules/actions/removeRole.test.js (keep
canManageRole and recordRoleChange as needed), delete the duplicated
canManageRole===false case and the duplicated "pipeline" happy-path assertions,
and instead add a dedicated rate-limit test in levelUpActions.test.js that
exercises the codepath which calls checkRoleRateLimit (invoke the
pipeline-related function used in level-up flow and assert rate-limit behavior),
ensuring you reference handleRemoveRole for trimming and checkRoleRateLimit for
where to add coverage.
src/modules/actions/grantRole.js (1)

25-49: ⚠️ Potential issue | 🟠 Major

Replace mode still mutates roles one by one.

Despite the comment at Line 23, Lines 31-34 still call
checkRoleRateLimit() and recordRoleChange() for every old XP role. On
skipped levels, cleanup can stop after the second mutation, and because the add
happens afterward, a later roles.add() failure can leave the member with no XP
reward. Compute the replace-mode diff and apply/record it as one logical
operation.

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

In `@src/modules/actions/grantRole.js` around lines 25 - 49, The replace-mode flow
mutates roles one-by-one (loop using member.roles.remove, calling
checkRoleRateLimit and recordRoleChange per removed role) which can leave users
in an inconsistent state if the later member.roles.add fails; instead compute
the set of roles to remove (using xpManagedRoles and roleId), batch the intended
changes, perform the removes and the add as a single logical operation (e.g.,
collect removal IDs, ensure rate limit once via checkRoleRateLimit(guild.id,
member.user?.id) before applying, then call member.roles.remove with the full
array and member.roles.add(roleId) and only then call recordRoleChange once),
and update logging/permission checks (canManageRole) to run during diff
computation rather than per removal so the operation is atomic from the
recordRoleChange perspective.
web/src/components/dashboard/config-sections/CommunitySettingsSection.tsx (1)

528-550: ⚠️ Potential issue | 🟠 Major

Keep raw threshold text in local state.

Because this field renders from levelThresholds.join(', '), the onChange
handler reparses and re-sorts every keystroke. Partial edits get rewritten
mid-entry, and the nums.length > 0 guard still drops the clear/all-invalid
case instead of persisting it. Parse on blur/save, not while the user is typing.

src/modules/actions/roleUtils.js (1)

152-179: ⚠️ Potential issue | 🟠 Major

Count a level-down batch once in the limiter.

After the single check at Line 193, both the batch path and the fallback path
still record one change per removed role. A large level drop can burn the entire
2/60s budget in one logical operation, which defeats the batching goal for
level-down cleanup. Record the whole removal batch once instead.

Also applies to: 217-235

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

In `@src/modules/actions/roleUtils.js` around lines 152 - 179, remove the per-role
counting from inside removeSingleRole by deleting the recordRoleChange(guild.id,
member.user.id) call and have removeSingleRole only attempt removal and return
success/failure; instead, in the higher-level level-down batch handlers (the
batch path and the fallback path referenced around the other block at lines
217-235), call recordRoleChange once per logical level-down operation (once per
member/guild batch) after all role removals complete so the limiter is
incremented a single time for the whole batch rather than once per removed role.
src/modules/reputation.js (1)

164-180: ⚠️ Potential issue | 🟠 Major

Add announcement action handler or document required configuration.

The level-up pipeline now delegates all effects to registered action handlers, but only grantRole and removeRole are implemented. Without an announcement handler, level-ups produce silent success—users receive no feedback on their progression.

Guilds need either:

  1. A built-in announcement action handler (e.g., src/modules/actions/announce.js) registered and wired into the pipeline, or
  2. Clear documentation requiring admins to configure levelActions with custom handlers to restore visible feedback.

Without this, deployments will have broken user experience.

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

In `@src/modules/reputation.js` around lines 164 - 180, The level-up pipeline
(executeLevelUpPipeline) currently invokes action handlers but only has
grantRole/removeRole implemented, so no announcement is made; add a built-in
announcement action handler (e.g., create src/modules/actions/announce.js
exporting the handler and register it where other actions are
collected/registered) and ensure executeLevelUpPipeline looks up and invokes the
"announce" action when levelActions include an announcement, or alternatively
update docs to require guilds include a custom "announce" handler in their
levelActions configuration; reference executeLevelUpPipeline, grantRole,
removeRole, and levelActions when wiring/ documenting the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/api/routes/members.js`:
- Around line 1002-1005: Replace the dynamic import with a static import: add a
top-of-file import for enforceRoleLevelDown from roleUtils.js in members.js,
then remove the await import('../../modules/actions/roleUtils.js') line and call
enforceRoleLevelDown(member, newLevel, xpConfig) directly where it’s currently
used to avoid per-call latency.

In `@src/modules/levelUpActions.js`:
- Around line 79-192: The executeLevelUpPipeline function is too complex;
extract the template-context resolution and per-action dispatch into helpers:
create a resolveTemplateContext(level) helper that uses templateContextCache,
calls buildTemplateContext with ({member,message,guild,level,previousLevel:
level-1,xp,levelThresholds:config.levelThresholds,roleName:null,roleId:null}),
catches errors, logs via warn and returns the cached or empty context; and
create a dispatchLevelAction(action, pipelineContext, roleChangesRemainingRef)
helper that contains rate-limit checks for grantRole/removeRole, retrieves the
handler via actionRegistry.get(action.type), logs unknown types, decrements
roleChangesRemainingRef when consuming quota, calls
handler(action,pipelineContext) inside try/catch and logs failures; replace the
in-loop templateContext construction and action handling with calls to these two
helpers, passing necessary values
(member,message,guild,config,xp,templateContextCache,roleChangesRemaining) so
cognitive complexity is reduced.

In `@src/modules/reputation.js`:
- Around line 48-55: getXpConfig currently only reads cfg.xp and will drop
legacy reputation.* keys; add a rollout fallback that reads
cfg.reputation.levelThresholds and cfg.reputation.roleRewards when cfg.xp.* are
missing and merge them into the returned config (preserving XP_DEFAULTS and
XP_DEFAULTS.roleRewards), or run a one-time migration of stored guild configs to
move reputation.* into cfg.xp before shipping; specifically update getXpConfig
to check cfg.xp.levelThresholds and cfg.xp.roleRewards and if absent copy/merge
from cfg.reputation.levelThresholds and cfg.reputation.roleRewards into the
merged object so legacy customizations are preserved.

In `@tests/api/routes/members.test.js`:
- Around line 179-180: The test fixture for getConfig() still places thresholds
under reputation, but the route now reads xp.levelThresholds, so update the
mocked config returned by getConfig() in the tests to include an xp object with
levelThresholds (distinct values from XP_DEFAULTS) so the expectation on
alice.level is deterministic; locate the getConfig() mock used by
tests/api/routes/members.test.js and replace reputation.thresholds (or similar)
with xp: { levelThresholds: [...] } ensuring values differ from
XP_DEFAULTS.levelThresholds so the assertion against alice.level exercises the
new code path.

In `@tests/modules/actions/grantRole.test.js`:
- Around line 119-142: The test uses roles ('role-l2'...'role-l5') that aren't
in the default guild cache; extend makeContext to accept a guildRoles parameter
(or add these roles to its existing guild.roles.cache) so the guild role cache
contains those role objects, then update the test to call makeContext with a
guildRoles Map containing entries for 'role-l2'...'role-l5'; this ensures
handleGrantRole sees real guild roles in the guild.roles.cache when it runs and
keeps the rest of the test (xpManagedRoles, memberRoles, stackRoles) unchanged.

In `@web/src/components/dashboard/config-categories/onboarding-growth.tsx`:
- Line 136: Replace the double-quoted string literals in this .tsx file with
single-quoted strings per the style guide: locate the onboarding messages (the
array/const containing strings such as "Things are moving at a healthy pace in
`#general`, so you'll fit right in.") and change them to use single quotes and
escape any internal apostrophes (e.g., you'll -> you\'ll). Apply the same change
to the other occurrences referenced (the strings at the other spots noted) so
all string literals in onboarding-growth.tsx use single quotes.

In `@web/src/components/landing/bento/BentoChart.tsx`:
- Around line 257-258: The day label rendering in BentoChart uses non-unique
keys (dayLabels) which repeat across weeks; update the map where dayLabels is
rendered (the JSX mapping that produces <span key={label}>{label}</span>) to use
a unique key per item—e.g., combine the weekday label with the source date or
index (such as `${label}-${i}` or the original dailyActivity date string) so
each key is globally unique and avoids React reconciliation collisions.

In `@web/src/components/landing/Footer.tsx`:
- Line 37: The long JSX className string in the Footer component (the className
prop on the rounded button element) exceeds the 100-char width; split the
classes into multiple shorter lines by turning the single string into an array
of class tokens and using .join(' ') (e.g., replace className="rounded-full h-14
px-12 font-bold text-sm tracking-widest uppercase shadow-lg
shadow-[var(--color-discord)]/20" with className={[ 'rounded-full', 'h-14',
'px-12', 'font-bold', 'text-sm', 'tracking-widest', 'uppercase', 'shadow-lg',
'shadow-[var(--color-discord)]/20' ].join(' ')}), and do the same for the other
long className on the same file to keep each source line under 100 characters.

In `@web/src/components/ui/neo-minimal-footer.tsx`:
- Line 27: Extract the repeated external URLs (Docs/GitHub/Discord) into shared
constants (e.g., DOCS_URL, GITHUB_URL, DISCORD_URL) in a central module and
replace hard-coded strings in NeoMinimalFooter (the menu items like the object
with label 'Self-Host') and other components that reference the same links by
importing these constants; update any arrays or href usages in the footer
component and corresponding header/navigation components to use the constants so
all places reference a single source of truth.
- Around line 50-52: The long className strings in the neo-minimal-footer
component exceed the 100-char line limit; split/wrap these className values into
multiple concatenated strings or template literals so each line stays ≤100
chars. Specifically update the footer element's className (the long string
starting with "max-w-7xl..."), the absolute background grid div's className (the
long bg-[linear-gradient(...)] string), and the other oversized className props
referenced around lines 78, 103-105, 116, and 158 in this file (search for
multiple long className="..." occurrences in neo-minimal-footer.tsx) so each
class segment is on its own line but retains the same final combined className
at runtime.

---

Duplicate comments:
In `@src/modules/actions/grantRole.js`:
- Around line 25-49: The replace-mode flow mutates roles one-by-one (loop using
member.roles.remove, calling checkRoleRateLimit and recordRoleChange per removed
role) which can leave users in an inconsistent state if the later
member.roles.add fails; instead compute the set of roles to remove (using
xpManagedRoles and roleId), batch the intended changes, perform the removes and
the add as a single logical operation (e.g., collect removal IDs, ensure rate
limit once via checkRoleRateLimit(guild.id, member.user?.id) before applying,
then call member.roles.remove with the full array and member.roles.add(roleId)
and only then call recordRoleChange once), and update logging/permission checks
(canManageRole) to run during diff computation rather than per removal so the
operation is atomic from the recordRoleChange perspective.

In `@src/modules/actions/removeRole.js`:
- Around line 24-25: The removeRole action currently calls
recordRoleChange(guild.id, member.user?.id) after awaiting
member.roles.remove(roleId), which records limiter usage per individual removal;
remove that call from this handler (i.e., delete the recordRoleChange invocation
in the removeRole flow) so the handler only performs the role removal and lets
the caller/batch processor record a single limiter entry for the whole pipeline
run.

In `@src/modules/actions/roleUtils.js`:
- Around line 152-179: remove the per-role counting from inside removeSingleRole
by deleting the recordRoleChange(guild.id, member.user.id) call and have
removeSingleRole only attempt removal and return success/failure; instead, in
the higher-level level-down batch handlers (the batch path and the fallback path
referenced around the other block at lines 217-235), call recordRoleChange once
per logical level-down operation (once per member/guild batch) after all role
removals complete so the limiter is incremented a single time for the whole
batch rather than once per removed role.

In `@src/modules/reputation.js`:
- Around line 164-180: The level-up pipeline (executeLevelUpPipeline) currently
invokes action handlers but only has grantRole/removeRole implemented, so no
announcement is made; add a built-in announcement action handler (e.g., create
src/modules/actions/announce.js exporting the handler and register it where
other actions are collected/registered) and ensure executeLevelUpPipeline looks
up and invokes the "announce" action when levelActions include an announcement,
or alternatively update docs to require guilds include a custom "announce"
handler in their levelActions configuration; reference executeLevelUpPipeline,
grantRole, removeRole, and levelActions when wiring/ documenting the change.

In `@tests/modules/actions/removeRole.test.js`:
- Around line 9-13: The test file mocks checkRoleRateLimit but handleRemoveRole
never calls it, so remove the dead mock of checkRoleRateLimit from
tests/modules/actions/removeRole.test.js (keep canManageRole and
recordRoleChange as needed), delete the duplicated canManageRole===false case
and the duplicated "pipeline" happy-path assertions, and instead add a dedicated
rate-limit test in levelUpActions.test.js that exercises the codepath which
calls checkRoleRateLimit (invoke the pipeline-related function used in level-up
flow and assert rate-limit behavior), ensuring you reference handleRemoveRole
for trimming and checkRoleRateLimit for where to add coverage.
🪄 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

Run ID: 7902ff0c-512a-4e89-91e1-640fb125688f

📥 Commits

Reviewing files that changed from the base of the PR and between 0925535 and 445a047.

📒 Files selected for processing (18)
  • src/api/routes/community.js
  • src/api/routes/members.js
  • src/modules/actions/grantRole.js
  • src/modules/actions/removeRole.js
  • src/modules/actions/roleUtils.js
  • src/modules/levelUpActions.js
  • src/modules/reputation.js
  • tests/api/routes/members.test.js
  • tests/modules/actions/grantRole.test.js
  • tests/modules/actions/removeRole.test.js
  • web/src/components/dashboard/config-categories/onboarding-growth.tsx
  • web/src/components/dashboard/config-sections/CommunitySettingsSection.tsx
  • web/src/components/landing/DashboardShowcase.tsx
  • web/src/components/landing/Footer.tsx
  • web/src/components/landing/bento/BentoChart.tsx
  • web/src/components/ui/neo-minimal-footer.tsx
  • web/src/components/ui/simple-icon.tsx
  • web/tests/components/landing/footer.test.tsx
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Greptile Review
  • GitHub Check: Cursor Bugbot
🧰 Additional context used
📓 Path-based instructions (11)
**/*.{js,ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{js,ts,tsx}: Use single quotes for strings (except in JSON files); no double quotes
Always include semicolons at the end of statements
Use 2-space indentation (spaces, not tabs)
Always include trailing commas in multi-line arrays, objects, and function parameters
Maintain a maximum line width of 100 characters

Files:

  • web/src/components/landing/DashboardShowcase.tsx
  • web/src/components/landing/Footer.tsx
  • web/tests/components/landing/footer.test.tsx
  • src/modules/actions/removeRole.js
  • web/src/components/landing/bento/BentoChart.tsx
  • web/src/components/dashboard/config-categories/onboarding-growth.tsx
  • src/modules/reputation.js
  • src/api/routes/community.js
  • web/src/components/ui/simple-icon.tsx
  • tests/modules/actions/removeRole.test.js
  • tests/modules/actions/grantRole.test.js
  • web/src/components/ui/neo-minimal-footer.tsx
  • src/api/routes/members.js
  • src/modules/actions/grantRole.js
  • web/src/components/dashboard/config-sections/CommunitySettingsSection.tsx
  • src/modules/levelUpActions.js
  • tests/api/routes/members.test.js
  • src/modules/actions/roleUtils.js
web/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Never use console.* methods in web dashboard code; use appropriate logging mechanisms for React applications

Files:

  • web/src/components/landing/DashboardShowcase.tsx
  • web/src/components/landing/Footer.tsx
  • web/src/components/landing/bento/BentoChart.tsx
  • web/src/components/dashboard/config-categories/onboarding-growth.tsx
  • web/src/components/ui/simple-icon.tsx
  • web/src/components/ui/neo-minimal-footer.tsx
  • web/src/components/dashboard/config-sections/CommunitySettingsSection.tsx
**/*.{js,ts,jsx,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{js,ts,jsx,tsx}: ESM only - do not use CommonJS modules
Use src/logger.js; do not use console.*

Files:

  • web/src/components/landing/DashboardShowcase.tsx
  • web/src/components/landing/Footer.tsx
  • web/tests/components/landing/footer.test.tsx
  • src/modules/actions/removeRole.js
  • web/src/components/landing/bento/BentoChart.tsx
  • web/src/components/dashboard/config-categories/onboarding-growth.tsx
  • src/modules/reputation.js
  • src/api/routes/community.js
  • web/src/components/ui/simple-icon.tsx
  • tests/modules/actions/removeRole.test.js
  • tests/modules/actions/grantRole.test.js
  • web/src/components/ui/neo-minimal-footer.tsx
  • src/api/routes/members.js
  • src/modules/actions/grantRole.js
  • web/src/components/dashboard/config-sections/CommunitySettingsSection.tsx
  • src/modules/levelUpActions.js
  • tests/api/routes/members.test.js
  • src/modules/actions/roleUtils.js
web/tests/**/*.test.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

web/tests/**/*.test.{ts,tsx}: Write web dashboard tests using Vitest 4 with the jsdom environment and React Testing Library, matching the web/src/ structure
Maintain test coverage thresholds of 85% across all metrics (statements, branches, functions, lines) for web dashboard tests

Files:

  • web/tests/components/landing/footer.test.tsx
**/*.js

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use ESM-only syntax: import/export, never require()/module.exports

Files:

  • src/modules/actions/removeRole.js
  • src/modules/reputation.js
  • src/api/routes/community.js
  • tests/modules/actions/removeRole.test.js
  • tests/modules/actions/grantRole.test.js
  • src/api/routes/members.js
  • src/modules/actions/grantRole.js
  • src/modules/levelUpActions.js
  • tests/api/routes/members.test.js
  • src/modules/actions/roleUtils.js
src/**/*.js

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

src/**/*.js: Never use console.* methods; use the Winston logger instead via import logger from '../logger.js' (adjust path as needed), then call logger.info(), logger.warn(), logger.error(), or logger.debug()
Always use safeReply(), safeSend(), or safeEditReply() instead of raw Discord.js methods for safe Discord messaging that handles errors gracefully

Files:

  • src/modules/actions/removeRole.js
  • src/modules/reputation.js
  • src/api/routes/community.js
  • src/api/routes/members.js
  • src/modules/actions/grantRole.js
  • src/modules/levelUpActions.js
  • src/modules/actions/roleUtils.js
src/modules/**/*.js

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Create feature modules in src/modules/ and add corresponding config sections to config.json

Files:

  • src/modules/actions/removeRole.js
  • src/modules/reputation.js
  • src/modules/actions/grantRole.js
  • src/modules/levelUpActions.js
  • src/modules/actions/roleUtils.js
src/**/*.{js,ts}

📄 CodeRabbit inference engine (AGENTS.md)

src/**/*.{js,ts}: Use the safe Discord messaging helpers in src/utils/safeSend.js instead of raw reply/send/edit calls
Community features should be gated behind config..enabled. Moderation commands are the exception.

Files:

  • src/modules/actions/removeRole.js
  • src/modules/reputation.js
  • src/api/routes/community.js
  • src/api/routes/members.js
  • src/modules/actions/grantRole.js
  • src/modules/levelUpActions.js
  • src/modules/actions/roleUtils.js
**/*.{js,ts}

📄 CodeRabbit inference engine (AGENTS.md)

Use parameterized SQL only; do not use string concatenation for SQL queries

Files:

  • src/modules/actions/removeRole.js
  • src/modules/reputation.js
  • src/api/routes/community.js
  • tests/modules/actions/removeRole.test.js
  • tests/modules/actions/grantRole.test.js
  • src/api/routes/members.js
  • src/modules/actions/grantRole.js
  • src/modules/levelUpActions.js
  • tests/api/routes/members.test.js
  • src/modules/actions/roleUtils.js
src/api/routes/**/*.js

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Create API route handlers in src/api/routes/ and mount them in src/api/server.js; add auth middleware if the endpoint requires authentication

Files:

  • src/api/routes/community.js
  • src/api/routes/members.js
tests/**/*.test.js

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

tests/**/*.test.js: Write bot tests using Vitest 4 with the node environment, matching the src/ structure in the tests/ directory
Maintain test coverage thresholds: statements 85%, branches 82%, functions 85%, lines 85%; never lower thresholds—add tests to cover new code instead

Files:

  • tests/modules/actions/removeRole.test.js
  • tests/modules/actions/grantRole.test.js
  • tests/api/routes/members.test.js
🧠 Learnings (18)
📚 Learning: 2026-03-12T02:03:36.493Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-12T02:03:36.493Z
Learning: Applies to web/src/app/dashboard/**/*.tsx : For dashboard routes, add a matcher entry to `dashboardTitleMatchers` in `web/src/lib/page-titles.ts`: use exact equality for leaf routes (`pathname === '/dashboard/my-route'`) and subtree checks (`pathname.startsWith('/dashboard/my-route/')`); export `metadata` using `createPageMetadata(title)` for SSR entry points

Applied to files:

  • web/src/components/landing/DashboardShowcase.tsx
  • web/src/components/dashboard/config-categories/onboarding-growth.tsx
📚 Learning: 2026-03-11T06:42:38.728Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-11T06:42:38.728Z
Learning: Applies to web/src/pages/dashboard/**/*.{ts,tsx} : Use shared title helpers from web/src/lib/page-titles.ts for setting browser titles in dashboard pages

Applied to files:

  • web/src/components/landing/DashboardShowcase.tsx
  • web/src/components/dashboard/config-categories/onboarding-growth.tsx
📚 Learning: 2026-03-10T23:21:49.730Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-10T23:21:49.730Z
Learning: Applies to web/src/components/layout/dashboard-shell.tsx : Dashboard page titles should sync with route changes using DashboardTitleSync component mounted in dashboard-shell.tsx and canonical title string 'Volvox.Bot - AI Powered Discord Bot'

Applied to files:

  • web/src/components/landing/DashboardShowcase.tsx
  • web/src/components/dashboard/config-categories/onboarding-growth.tsx
📚 Learning: 2026-03-10T23:21:49.730Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-10T23:21:49.730Z
Learning: Applies to web/src/components/dashboard/config-workspace/**/*.{ts,tsx} : Web dashboard config editor should use category workspace navigation with reusable SettingsFeatureCard pattern (header + master toggle + Basic/Advanced blocks)

Applied to files:

  • web/src/components/landing/DashboardShowcase.tsx
  • web/src/components/dashboard/config-categories/onboarding-growth.tsx
  • web/src/components/dashboard/config-sections/CommunitySettingsSection.tsx
📚 Learning: 2026-03-26T00:04:14.673Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-26T00:04:14.673Z
Learning: Applies to {web/src/lib/page-titles.ts,web/src/**/+page.ts,web/src/**/+page.svelte} : New dashboard routes need title wiring in web/src/lib/page-titles.ts: use createPageMetadata() for SSR and keep DashboardTitleSync aligned for client navigation.

Applied to files:

  • web/src/components/landing/DashboardShowcase.tsx
  • web/src/components/dashboard/config-categories/onboarding-growth.tsx
📚 Learning: 2026-03-11T05:32:46.325Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-11T05:32:46.325Z
Learning: Applies to web/src/app/**/*.{ts,tsx} : Apply static metadata to server-rendered dashboard entry pages and use title template format for root app metadata

Applied to files:

  • web/src/components/landing/DashboardShowcase.tsx
  • web/src/components/dashboard/config-categories/onboarding-growth.tsx
📚 Learning: 2026-03-10T23:21:49.730Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-10T23:21:49.730Z
Learning: Applies to src/api/routes/*.js : Add adaptDeleteGuildIdParam in API route handlers to enforce guild moderation checks on DELETE requests without losing record id

Applied to files:

  • src/modules/actions/removeRole.js
  • src/api/routes/community.js
  • src/api/routes/members.js
  • src/modules/actions/grantRole.js
  • src/modules/actions/roleUtils.js
📚 Learning: 2026-03-11T06:42:38.728Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-11T06:42:38.728Z
Learning: Applies to src/commands/**/*.js : Guild owners should be explicitly exempted from role hierarchy restrictions in command handlers to match Discord permission model expectations

Applied to files:

  • src/modules/actions/removeRole.js
  • src/modules/reputation.js
  • tests/modules/actions/removeRole.test.js
  • tests/modules/actions/grantRole.test.js
  • src/modules/actions/grantRole.js
  • src/modules/levelUpActions.js
  • src/modules/actions/roleUtils.js
📚 Learning: 2026-03-11T06:42:38.728Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-11T06:42:38.728Z
Learning: Applies to src/commands/reactionrole.js : Enforce invoker role hierarchy check in /reactionrole add command to prevent non-owner users from configuring roles at or above their highest role

Applied to files:

  • src/modules/actions/removeRole.js
  • src/modules/reputation.js
  • tests/modules/actions/grantRole.test.js
  • src/api/routes/members.js
  • src/modules/actions/grantRole.js
  • src/modules/levelUpActions.js
  • src/modules/actions/roleUtils.js
📚 Learning: 2026-03-10T23:29:51.063Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-10T23:29:51.063Z
Learning: Applies to src/api/routes/*.js : Apply requireGuildModerator authorization check to DELETE endpoints that modify guild-scoped resources

Applied to files:

  • src/modules/actions/removeRole.js
  • src/api/routes/members.js
📚 Learning: 2026-03-12T02:03:36.493Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-12T02:03:36.493Z
Learning: Applies to **/*.{js,ts,tsx} : Use single quotes for strings (except in JSON files); no double quotes

Applied to files:

  • web/src/components/dashboard/config-categories/onboarding-growth.tsx
📚 Learning: 2026-03-12T02:03:36.493Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-12T02:03:36.493Z
Learning: Applies to src/**/*.js : Never use `console.*` methods; use the Winston logger instead via `import logger from '../logger.js'` (adjust path as needed), then call `logger.info()`, `logger.warn()`, `logger.error()`, or `logger.debug()`

Applied to files:

  • src/modules/reputation.js
  • src/modules/actions/roleUtils.js
📚 Learning: 2026-03-12T02:03:36.493Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-12T02:03:36.493Z
Learning: Applies to src/utils/cache.js : Use `src/utils/cache.js` for generic caching with Redis primary + in-memory fallback; use `src/utils/discordCache.js` for Discord API response caching (channels, roles, members); use `src/utils/reputationCache.js` for leaderboard and user reputation data; all caches auto-invalidate when config changes

Applied to files:

  • src/modules/reputation.js
  • src/api/routes/community.js
📚 Learning: 2026-03-12T02:03:36.493Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-12T02:03:36.493Z
Learning: Applies to web/tests/**/*.test.{ts,tsx} : Write web dashboard tests using Vitest 4 with the `jsdom` environment and React Testing Library, matching the `web/src/` structure

Applied to files:

  • tests/modules/actions/removeRole.test.js
  • tests/modules/actions/grantRole.test.js
📚 Learning: 2026-03-12T02:03:36.493Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-12T02:03:36.493Z
Learning: Applies to tests/**/*.test.js : Write bot tests using Vitest 4 with the `node` environment, matching the `src/` structure in the `tests/` directory

Applied to files:

  • tests/modules/actions/removeRole.test.js
  • tests/modules/actions/grantRole.test.js
📚 Learning: 2026-03-26T00:04:14.673Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-26T00:04:14.673Z
Learning: Applies to src/**/*.{js,ts} : Community features should be gated behind config.<feature>.enabled. Moderation commands are the exception.

Applied to files:

  • web/src/components/dashboard/config-sections/CommunitySettingsSection.tsx
📚 Learning: 2026-03-12T02:03:36.493Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-12T02:03:36.493Z
Learning: Applies to src/**/*.js : Always use `safeReply()`, `safeSend()`, or `safeEditReply()` instead of raw Discord.js methods for safe Discord messaging that handles errors gracefully

Applied to files:

  • src/modules/actions/roleUtils.js
📚 Learning: 2026-03-26T00:04:14.673Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-26T00:04:14.673Z
Learning: Applies to **/*.{js,ts,jsx,tsx} : Use src/logger.js; do not use console.*

Applied to files:

  • src/modules/actions/roleUtils.js
🪛 GitHub Check: SonarCloud Code Analysis
src/modules/actions/grantRole.js

[failure] 18-18: Refactor this function to reduce its Cognitive Complexity from 20 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=VolvoxLLC_volvox-bot&issues=AZ0ncCwpTj4M-vqtfnLl&open=AZ0ncCwpTj4M-vqtfnLl&pullRequest=373

src/modules/levelUpActions.js

[failure] 79-79: Refactor this function to reduce its Cognitive Complexity from 17 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=VolvoxLLC_volvox-bot&issues=AZ0oMVMM7Lq6732fYobX&open=AZ0oMVMM7Lq6732fYobX&pullRequest=373

src/modules/actions/roleUtils.js

[failure] 191-191: Refactor this function to reduce its Cognitive Complexity from 30 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=VolvoxLLC_volvox-bot&issues=AZ0oMPRFTiLYW-eK644w&open=AZ0oMPRFTiLYW-eK644w&pullRequest=373


[warning] 221-221: Remove the declaration of the unused '_roleId' variable.

See more on https://sonarcloud.io/project/issues?id=VolvoxLLC_volvox-bot&issues=AZ0oWE6ThIRfcnuOu43b&open=AZ0oWE6ThIRfcnuOu43b&pullRequest=373

🔇 Additional comments (26)
web/src/components/landing/DashboardShowcase.tsx (1)

42-44: LGTM! Clean import/re-export pattern.

The reorganization correctly imports the type for internal use (line 54) while also re-exporting it for consumers. The blank line separator improves readability between import and export declarations.

web/src/components/dashboard/config-categories/onboarding-growth.tsx (1)

331-345: Good accessibility improvement.

The refactoring from aria-labelledby (which ChannelSelector never forwarded to its internal Button) to implicit label association via wrapping is correct. The nested <Button role="combobox"> is a labelable element, so the browser will properly associate the label with it.

The biome-ignore comments are appropriate and well-documented.

Also applies to: 347-361

web/src/components/ui/simple-icon.tsx (1)

1-15: Clean, reusable icon helper implementation.

This abstraction is concise and integrates well for footer social icons.

web/src/components/landing/Footer.tsx (1)

61-63: Good separation of concerns with footer extraction.

Delegating footer UI to NeoMinimalFooter keeps this component focused on the CTA section.

web/tests/components/landing/footer.test.tsx (1)

73-97: Solid coverage additions for the new footer UX.

These tests validate the new newsletter and social/status link behavior with good, user-facing selectors.

web/src/components/landing/bento/BentoChart.tsx (2)

71-72: Date parsing refactor is clean and safe.

The template-literal form keeps behavior intact and improves readability in both label paths.

Also applies to: 87-93


215-218: Hit-zone key change is a good improvement.

Using coordinate-based keys is stronger than index-only keys for these rendered SVG hit areas.

web/src/components/dashboard/config-sections/CommunitySettingsSection.tsx (2)

58-60: No changes needed - variable names are correct.

The variables at lines 61-63 are already named tldrDefaultMessages, tldrMaxMessages, and tldrCooldownSeconds (without leading underscores), and they are correctly referenced at lines 646, 666, and 686. There are no unresolved identifiers or compilation issues. The suggested diff does not apply to the current code state.

			> Likely an incorrect or invalid review comment.

252-255: Fix TypeScript error and key instability in badge rendering.

badge.emoji does not exist on the Badge type (only days and label are defined), causing a TypeScript error. Additionally, using badge.days in the key causes the key to change whenever the value is edited, which remounts the element mid-edit.

Change the key to a stable identifier:

key={`badge-${index}`}
src/api/routes/members.js (6)

11-11: LGTM!

Clean import of computeLevel and getXpConfig from the reputation module, following ESM conventions.


86-86: LGTM!

The CSV export correctly computes level from XP using computeLevel(xp, xpConfig.levelThresholds) instead of relying on the potentially stale rep.level database column. This ensures consistency after threshold configuration changes.

Also applies to: 138-150


383-406: LGTM!

The member list enrichment properly computes level from XP for both cached and freshly-fetched data, ensuring consistent level computation across the codebase.


656-660: LGTM!

Good addition of the explanatory comment at line 658 clarifying why level is recomputed from XP rather than trusting the stored reputation.level column.


950-957: LGTM!

The xpConfig variable is now properly hoisted to the outer scope (line 950), and oldLevel is correctly computed from oldXp = newXp - amount (lines 976-978). This addresses the previous review concerns about config drift and stale level detection.

Also applies to: 976-981


999-1015: LGTM on the level-down enforcement logic.

The error isolation with try/catch ensures that failures in role removal don't affect the API response. The response is correctly sent after this block completes, so even if enforceRoleLevelDown throws, the client still receives the updated XP/level data.

tests/modules/actions/grantRole.test.js (4)

1-15: LGTM!

Clean test setup with proper mocking of dependencies. The mocks for logger.js and roleUtils.js are minimal and focused on the functions actually used by the handler.


17-50: LGTM!

The makeContext helper is well-structured and provides sensible defaults while allowing customization of memberRoles, xpManagedRoles, and stackRoles for different test scenarios.


57-89: LGTM!

Good coverage of the core grant path: verifying roles.add is called, recordRoleChange is invoked, and templateContext is populated with role metadata.


91-117: LGTM!

The stackRoles tests correctly verify that when stackRoles: false, other XP-managed roles are removed (but unrelated roles are preserved), and when stackRoles: true, no roles are removed.

src/modules/levelUpActions.js (7)

1-13: LGTM!

Clean module setup with clear documentation and proper ESM imports. The issue link in the JSDoc is helpful for traceability.


14-34: LGTM!

The action registry pattern provides good extensibility for Phase 2 additions. Registration of built-in handlers (grantRole, removeRole) at module load time ensures they're available before any pipeline execution.


44-64: LGTM!

resolveActions correctly handles:

  • Early return when no level-up occurred (newLevel <= previousLevel)
  • Level skips by iterating from previousLevel + 1 to newLevel
  • Per-level overrides via config.levelActions with fallback to config.defaultActions

99-102: LGTM on rate limit quota tracking.

The approach of checking rate limit once and tracking a per-pipeline quota of 2 role changes addresses the previous review concern. Role actions now decrement this quota and are skipped when exhausted, while non-role actions continue executing.


129-152: LGTM on template context error handling.

The try/catch around buildTemplateContext with fallback to an empty object ensures per-action error isolation as required by #365. The warning log includes sufficient context for debugging.


167-179: LGTM on per-action rate limit enforcement.

Role actions (grantRole, removeRole) are correctly gated by the remaining quota, while other action types bypass this check entirely. This addresses the concern about the rate limiter being a pipeline-wide kill switch.


181-191: LGTM on per-action error isolation.

The try/catch around handler execution ensures failures don't abort the pipeline, satisfying the error isolation requirement from #365. The warning log includes action type, level, and error details.

Comment thread src/api/routes/members.js
Comment on lines +1002 to +1005
try {
const member = await req.guild.members.fetch(userId);
const { enforceRoleLevelDown } = await import('../../modules/actions/roleUtils.js');
await enforceRoleLevelDown(member, newLevel, xpConfig);
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.

🧹 Nitpick | 🔵 Trivial

Consider using a static import for enforceRoleLevelDown.

The dynamic import() here adds latency on each XP reduction that triggers level-down. Since roleUtils.js is already imported elsewhere in the codebase and this code path is not conditionally loaded, a static import at the top of the file would be more efficient.

♻️ Proposed fix — use static import

Add at the top with other imports:

 import { computeLevel, getXpConfig } from '../../modules/reputation.js';
+import { enforceRoleLevelDown } from '../../modules/actions/roleUtils.js';

Then simplify the usage:

           try {
             const member = await req.guild.members.fetch(userId);
-            const { enforceRoleLevelDown } = await import('../../modules/actions/roleUtils.js');
             await enforceRoleLevelDown(member, newLevel, xpConfig);
           } catch (err) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/api/routes/members.js` around lines 1002 - 1005, Replace the dynamic
import with a static import: add a top-of-file import for enforceRoleLevelDown
from roleUtils.js in members.js, then remove the await
import('../../modules/actions/roleUtils.js') line and call
enforceRoleLevelDown(member, newLevel, xpConfig) directly where it’s currently
used to avoid per-call latency.

Comment on lines +131 to +149
function collectRolesToRemove(member, newLevel, xpConfig) {
const rolesToRemove = [];
const seenRoleIds = new Set();

// Collect roles from levelActions (above newLevel)
for (const entry of xpConfig.levelActions ?? []) {
if (entry.level <= newLevel) continue;

for (const action of entry.actions ?? []) {
if (action.type !== 'grantRole' || !action.roleId) continue;
if (!member.roles.cache.has(action.roleId)) continue;
if (seenRoleIds.has(action.roleId)) continue;

seenRoleIds.add(action.roleId);
rolesToRemove.push({ roleId: action.roleId, entry });
}
}

return rolesToRemove;
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.

⚠️ Potential issue | 🟠 Major

defaultActions are ignored during level-down reconciliation.

This code only computes removals/restores from levelActions, but
defaultActions are part of the resolved XP config and are also treated as
XP-managed roles later in this module. A role granted only via defaultActions
will survive a drop to level 0, and replace mode will not restore it when a
member falls below a higher per-level reward.

Also applies to: 238-251

Comment on lines +79 to +192
export async function executeLevelUpPipeline({
member,
message,
guild,
previousLevel,
newLevel,
xp,
config,
}) {
const actions = resolveActions(previousLevel, newLevel, config);
if (actions.length === 0) return;

info('Executing level-up pipeline', {
guildId: guild.id,
userId: member.user?.id,
previousLevel,
newLevel,
actionCount: actions.length,
});

// Check rate limit and track remaining quota (2 changes per pipeline)
// Note: We don't return early here - rate limit only skips role actions, not the whole pipeline
const rateLimitOk = checkRoleRateLimit(guild.id, member.user?.id);
let roleChangesRemaining = rateLimitOk ? 2 : 0;

// Compute XP-managed roles once for stack/replace logic
const xpManagedRoles = collectXpManagedRoles(config);

// Build base pipeline context
const basePipelineContext = {
member,
message,
guild,
previousLevel,
newLevel,
xp,
config,
xpManagedRoles,
};

// Cache template contexts per level to avoid duplicate DB queries
const templateContextCache = new Map();

for (const { level, action } of actions) {
// Track previousLevel incrementally for correct intermediate level context
const levelPreviousLevel = level - 1;

// Rebuild template context for each intermediate level during level-skip
// Cache per level to avoid duplicate DB queries
let templateContext = templateContextCache.get(level);
if (!templateContext) {
try {
templateContext = await buildTemplateContext({
member,
message,
guild,
level,
previousLevel: levelPreviousLevel,
xp,
levelThresholds: config.levelThresholds ?? [],
roleName: null,
roleId: null,
});
templateContextCache.set(level, templateContext);
} catch (err) {
warn('Template context build failed — continuing with empty context', {
level,
guildId: guild.id,
userId: member.user?.id,
error: err.message,
});
templateContext = {};
templateContextCache.set(level, templateContext);
}
}

const pipelineContext = { ...basePipelineContext, templateContext, currentLevel: level };

const handler = actionRegistry.get(action.type);
if (!handler) {
warn('Unknown action type — skipping', {
actionType: action.type,
level,
guildId: guild.id,
});
continue;
}

// Skip role-related actions if rate limit quota is exhausted
if (action.type === 'grantRole' || action.type === 'removeRole') {
if (roleChangesRemaining <= 0) {
warn('Role action skipped due to rate limit quota exhausted', {
actionType: action.type,
level,
guildId: guild.id,
userId: member.user?.id,
});
continue;
}
roleChangesRemaining--;
}

try {
await handler(action, pipelineContext);
} catch (err) {
warn('Action failed in level-up pipeline — continuing', {
actionType: action.type,
level,
guildId: guild.id,
userId: member.user?.id,
error: err.message,
});
}
}
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.

🧹 Nitpick | 🔵 Trivial

Reduce cognitive complexity by extracting helper functions.

SonarCloud flags this function with cognitive complexity 17 (limit 15). Consider extracting the template context resolution and the action dispatch logic into helper functions.

♻️ Suggested refactor — extract helpers
+/**
+ * Get or build template context for a level, caching to avoid duplicate DB queries.
+ */
+async function getTemplateContext(cache, params) {
+  const { level, member, message, guild, xp, config } = params;
+  if (cache.has(level)) return cache.get(level);
+
+  try {
+    const ctx = await buildTemplateContext({
+      member,
+      message,
+      guild,
+      level,
+      previousLevel: level - 1,
+      xp,
+      levelThresholds: config.levelThresholds ?? [],
+      roleName: null,
+      roleId: null,
+    });
+    cache.set(level, ctx);
+    return ctx;
+  } catch (err) {
+    warn('Template context build failed — continuing with empty context', {
+      level,
+      guildId: guild.id,
+      userId: member.user?.id,
+      error: err.message,
+    });
+    cache.set(level, {});
+    return {};
+  }
+}
+
+const ROLE_ACTION_TYPES = new Set(['grantRole', 'removeRole']);
+
 export async function executeLevelUpPipeline({ ... }) {
   // ... setup code ...
 
   for (const { level, action } of actions) {
-    const levelPreviousLevel = level - 1;
-
-    let templateContext = templateContextCache.get(level);
-    if (!templateContext) {
-      try {
-        templateContext = await buildTemplateContext({ ... });
-        templateContextCache.set(level, templateContext);
-      } catch (err) {
-        warn(...);
-        templateContext = {};
-        templateContextCache.set(level, templateContext);
-      }
-    }
+    const templateContext = await getTemplateContext(templateContextCache, {
+      level, member, message, guild, xp, config,
+    });
 
     // ... rest of loop ...
 
-    if (action.type === 'grantRole' || action.type === 'removeRole') {
+    if (ROLE_ACTION_TYPES.has(action.type)) {
       if (roleChangesRemaining <= 0) {
         // ... warn and skip ...
       }
       roleChangesRemaining--;
     }
🧰 Tools
🪛 GitHub Check: SonarCloud Code Analysis

[failure] 79-79: Refactor this function to reduce its Cognitive Complexity from 17 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=VolvoxLLC_volvox-bot&issues=AZ0oMVMM7Lq6732fYobX&open=AZ0oMVMM7Lq6732fYobX&pullRequest=373

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

In `@src/modules/levelUpActions.js` around lines 79 - 192, The
executeLevelUpPipeline function is too complex; extract the template-context
resolution and per-action dispatch into helpers: create a
resolveTemplateContext(level) helper that uses templateContextCache, calls
buildTemplateContext with ({member,message,guild,level,previousLevel:
level-1,xp,levelThresholds:config.levelThresholds,roleName:null,roleId:null}),
catches errors, logs via warn and returns the cached or empty context; and
create a dispatchLevelAction(action, pipelineContext, roleChangesRemainingRef)
helper that contains rate-limit checks for grantRole/removeRole, retrieves the
handler via actionRegistry.get(action.type), logs unknown types, decrements
roleChangesRemainingRef when consuming quota, calls
handler(action,pipelineContext) inside try/catch and logs failures; replace the
in-loop templateContext construction and action handling with calls to these two
helpers, passing necessary values
(member,message,guild,config,xp,templateContextCache,roleChangesRemaining) so
cognitive complexity is reduced.

Comment thread src/modules/reputation.js
Comment on lines +48 to +55
export function getXpConfig(guildId) {
const cfg = getConfig(guildId);
const merged = { ...XP_DEFAULTS, ...cfg.xp };
// Deep merge roleRewards to preserve defaults for missing nested properties
if (cfg.xp?.roleRewards) {
merged.roleRewards = { ...XP_DEFAULTS.roleRewards, ...cfg.xp.roleRewards };
}
return merged;
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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's understand the structure of the src/modules directory
find src/modules -type f -name "*.js" | head -20

Repository: VolvoxLLC/volvox-bot

Length of output: 634


🏁 Script executed:

# Read the reputation.js file to see the full implementation
wc -l src/modules/reputation.js

Repository: VolvoxLLC/volvox-bot

Length of output: 93


🏁 Script executed:

# Get the complete reputation.js file
cat -n src/modules/reputation.js

Repository: VolvoxLLC/volvox-bot

Length of output: 7047


🏁 Script executed:

# Search for config migration logic and how old config keys are handled
rg "levelThresholds|roleRewards" --type js -B 2 -A 2

Repository: VolvoxLLC/volvox-bot

Length of output: 17913


🏁 Script executed:

# Look for executeLevelUpPipeline implementation and action handlers
ast-grep --pattern 'function executeLevelUpPipeline($_) { $$$ }'

Repository: VolvoxLLC/volvox-bot

Length of output: 7196


🏁 Script executed:

# Search for action handlers, particularly announcement handlers
rg "action|handler|announce" src/modules/ --type js -i

Repository: VolvoxLLC/volvox-bot

Length of output: 50376


🏁 Script executed:

# Check the config.json or config defaults to understand the old vs new structure
fd config.json | head -5

Repository: VolvoxLLC/volvox-bot

Length of output: 93


🏁 Script executed:

# Check if there's migration logic in the config module
cat -n src/modules/config.js | head -100

Repository: VolvoxLLC/volvox-bot

Length of output: 4725


🏁 Script executed:

# Look for any backwards compatibility or migration comments in the codebase
rg "migration|migrate|backward|compat|legacy" --type js -i

Repository: VolvoxLLC/volvox-bot

Length of output: 12825


🏁 Script executed:

# Check the config.json structure
cat config.json | head -100

Repository: VolvoxLLC/volvox-bot

Length of output: 3296


🏁 Script executed:

# Search for any mention of old reputation config keys still being used
rg "reputation\.levelThresholds|reputation\.roleRewards" --type js

Repository: VolvoxLLC/volvox-bot

Length of output: 46


Add a rollout fallback for legacy XP config keys, or migrate configs before shipping.

Existing guild configs storing custom reputation.levelThresholds or reputation.roleRewards will silently lose these customizations because getXpConfig() only reads from cfg.xp. Unlike other backward compatibility patterns in the codebase (e.g., adminRoleId fallback), there is no fallback to the old reputation keys. Additionally, if guilds rely on automatic level-up announcements, these will be missing unless an action handler is explicitly registered, since no announcement/message action type is implemented.

Either migrate stored configs before this ships, or add temporary fallback logic in getXpConfig() to read legacy keys during a transition period.

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

In `@src/modules/reputation.js` around lines 48 - 55, getXpConfig currently only
reads cfg.xp and will drop legacy reputation.* keys; add a rollout fallback that
reads cfg.reputation.levelThresholds and cfg.reputation.roleRewards when
cfg.xp.* are missing and merge them into the returned config (preserving
XP_DEFAULTS and XP_DEFAULTS.roleRewards), or run a one-time migration of stored
guild configs to move reputation.* into cfg.xp before shipping; specifically
update getXpConfig to check cfg.xp.levelThresholds and cfg.xp.roleRewards and if
absent copy/merge from cfg.reputation.levelThresholds and
cfg.reputation.roleRewards into the merged object so legacy customizations are
preserved.

Comment on lines +179 to +180
// 250 XP with thresholds [100, 300, 600, 1000] = level 1 (0-indexed)
expect(alice.level).toBe(1);
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.

⚠️ Potential issue | 🟡 Minor

Mock the new config shape here.

This expectation is right, but the getConfig() fixture above still places
thresholds under reputation. The route now reads xp.levelThresholds, so this
passes only because XP_DEFAULTS.levelThresholds currently match the old
fixture. Move the test data to xp and make it distinct from the defaults.

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

In `@tests/api/routes/members.test.js` around lines 179 - 180, The test fixture
for getConfig() still places thresholds under reputation, but the route now
reads xp.levelThresholds, so update the mocked config returned by getConfig() in
the tests to include an xp object with levelThresholds (distinct values from
XP_DEFAULTS) so the expectation on alice.level is deterministic; locate the
getConfig() mock used by tests/api/routes/members.test.js and replace
reputation.thresholds (or similar) with xp: { levelThresholds: [...] } ensuring
values differ from XP_DEFAULTS.levelThresholds so the assertion against
alice.level exercises the new code path.

.replace(
/{vibeLine}/g,
'Things are moving at a healthy pace in #general, so you\'ll fit right in.',
"Things are moving at a healthy pace in #general, so you'll fit right in.",
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.

⚠️ Potential issue | 🟡 Minor

Use single quotes per coding guidelines.

These strings use double quotes to avoid escaping the apostrophes, but the coding guidelines require single quotes for all strings in .tsx files.

Proposed fix
@@ -133,7 +133,7 @@
         .replace(
           /{vibeLine}/g,
-          "Things are moving at a healthy pace in `#general`, so you'll fit right in.",
+          'Things are moving at a healthy pace in `#general`, so you\'ll fit right in.',
         )
@@ -228,9 +228,9 @@
                     <div>
                       <code>{'{vibeLine}'}</code> — Activity description{' '}
                       <span className="italic text-muted-foreground/70">
-                        {
-                          "You're catching us in a quiet window - perfect time to introduce yourself."
-                        }
+                        {'You\'re catching us in a quiet window - perfect time to introduce yourself.'}
                       </span>
                     </div>

As per coding guidelines: "Use single quotes for strings (except in JSON files); no double quotes".

Also applies to: 231-233

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

In `@web/src/components/dashboard/config-categories/onboarding-growth.tsx` at line
136, Replace the double-quoted string literals in this .tsx file with
single-quoted strings per the style guide: locate the onboarding messages (the
array/const containing strings such as "Things are moving at a healthy pace in
`#general`, so you'll fit right in.") and change them to use single quotes and
escape any internal apostrophes (e.g., you'll -> you\'ll). Apply the same change
to the other occurrences referenced (the strings at the other spots noted) so
all string literals in onboarding-growth.tsx use single quotes.

Comment thread web/src/components/landing/bento/BentoChart.tsx Outdated
<GetStartedButton
variant="discord"
href={botInviteUrl}
className="rounded-full h-14 px-12 font-bold text-sm tracking-widest uppercase shadow-lg shadow-[var(--color-discord)]/20"
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.

⚠️ Potential issue | 🟡 Minor

Split long className values to stay within max width.

Line 37 and Line 44 exceed the 100-character limit.

As per coding guidelines **/*.{js,ts,tsx}: Maintain a maximum line width of 100 characters.

Also applies to: 44-44

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

In `@web/src/components/landing/Footer.tsx` at line 37, The long JSX className
string in the Footer component (the className prop on the rounded button
element) exceeds the 100-char width; split the classes into multiple shorter
lines by turning the single string into an array of class tokens and using
.join(' ') (e.g., replace className="rounded-full h-14 px-12 font-bold text-sm
tracking-widest uppercase shadow-lg shadow-[var(--color-discord)]/20" with
className={[ 'rounded-full', 'h-14', 'px-12', 'font-bold', 'text-sm',
'tracking-widest', 'uppercase', 'shadow-lg', 'shadow-[var(--color-discord)]/20'
].join(' ')}), and do the same for the other long className on the same file to
keep each source line under 100 characters.

{ label: 'Features', href: '#features' },
{ label: 'Pricing', href: '#pricing' },
{ label: 'Dashboard', href: '/login' },
{ label: 'Self-Host', href: 'https://docs.volvox.bot' },
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.

🧹 Nitpick | 🔵 Trivial

Extract repeated external URLs into shared constants.

Docs/GitHub/Discord links are duplicated in multiple places (including header). Centralizing them reduces drift and update mistakes.

Also applies to: 33-35, 125-125, 134-134, 143-143, 155-155

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

In `@web/src/components/ui/neo-minimal-footer.tsx` at line 27, Extract the
repeated external URLs (Docs/GitHub/Discord) into shared constants (e.g.,
DOCS_URL, GITHUB_URL, DISCORD_URL) in a central module and replace hard-coded
strings in NeoMinimalFooter (the menu items like the object with label
'Self-Host') and other components that reference the same links by importing
these constants; update any arrays or href usages in the footer component and
corresponding header/navigation components to use the constants so all places
reference a single source of truth.

Comment on lines +50 to +52
<footer className="max-w-7xl mx-auto bg-card/10 border-t rounded-t-lg border-card/10 flex flex-wrap pt-16 pb-8 relative overflow-hidden">
{/* Background grid pattern */}
<div className="absolute inset-0 bg-[linear-gradient(hsl(var(--foreground)/0.02)_1px,transparent_1px),linear-gradient(90deg,hsl(var(--foreground)/0.02)_1px,transparent_1px)] bg-[size:40px_40px] [mask-image:radial-gradient(circle_at_center,black,transparent_80%)]" />
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.

⚠️ Potential issue | 🟡 Minor

Wrap oversized class strings to satisfy line-width rule.

Line 50, Line 52, Line 78, Line 103, Line 105, Line 116, and Line 158 exceed 100 chars.

Example fix pattern (Line 50)
-    <footer className="max-w-7xl mx-auto bg-card/10 border-t rounded-t-lg border-card/10 flex flex-wrap pt-16 pb-8 relative overflow-hidden">
+    <footer
+      className={[
+        'max-w-7xl mx-auto bg-card/10 border-t rounded-t-lg border-card/10',
+        'flex flex-wrap pt-16 pb-8 relative overflow-hidden',
+      ].join(' ')}
+    >

As per coding guidelines **/*.{js,ts,tsx}: Maintain a maximum line width of 100 characters.

Also applies to: 78-78, 103-105, 116-116, 158-158

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

In `@web/src/components/ui/neo-minimal-footer.tsx` around lines 50 - 52, The long
className strings in the neo-minimal-footer component exceed the 100-char line
limit; split/wrap these className values into multiple concatenated strings or
template literals so each line stays ≤100 chars. Specifically update the footer
element's className (the long string starting with "max-w-7xl..."), the absolute
background grid div's className (the long bg-[linear-gradient(...)] string), and
the other oversized className props referenced around lines 78, 103-105, 116,
and 158 in this file (search for multiple long className="..." occurrences in
neo-minimal-footer.tsx) so each class segment is on its own line but retains the
same final combined className at runtime.

Copilot AI review requested due to automatic review settings March 26, 2026 04:54
Copy link
Copy Markdown
Contributor

@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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/modules/actions/grantRole.test.js`:
- Around line 16-17: The test named "no internal rate-limit" currently only
verifies roles.add() ran but doesn't confirm checkRoleRateLimit wasn't invoked;
update the test to import the mocked checkRoleRateLimit from the same module
(roleUtils.js) and add an assertion that checkRoleRateLimit was not called after
calling handleGrantRole(), so the test fails if internal rate-limit checks are
executed; reference handleGrantRole, roles.add, and checkRoleRateLimit when
locating where to add the import and the expect(...).not.toHaveBeenCalled()
assertion.

In `@web/src/components/dashboard/config-sections/CommunitySettingsSection.tsx`:
- Around line 255-258: The current key for each badge element uses value-derived
parts (`badge.days`/`badge.label`) which causes React to remount inputs and lose
focus while typing; update the key in the badge mapping inside
CommunitySettingsSection (the JSX that renders each badge with
key={`badge-${badge.days}-${badge.label}`}) to use a stable index-based key
(e.g., `badge-${index}`) or a composite that includes the index (e.g.,
`badge-${index}-${badge.label}`) so elements are not remounted on input changes.
🪄 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

Run ID: 01281080-9242-4f95-b50c-0f46fc3ef81c

📥 Commits

Reviewing files that changed from the base of the PR and between 445a047 and d0fbe58.

📒 Files selected for processing (3)
  • tests/modules/actions/grantRole.test.js
  • web/src/components/dashboard/config-sections/CommunitySettingsSection.tsx
  • web/src/components/landing/bento/BentoChart.tsx
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Greptile Review
  • GitHub Check: Cursor Bugbot
🧰 Additional context used
📓 Path-based instructions (6)
**/*.{js,ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{js,ts,tsx}: Use single quotes for strings (except in JSON files); no double quotes
Always include semicolons at the end of statements
Use 2-space indentation (spaces, not tabs)
Always include trailing commas in multi-line arrays, objects, and function parameters
Maintain a maximum line width of 100 characters

Files:

  • web/src/components/landing/bento/BentoChart.tsx
  • web/src/components/dashboard/config-sections/CommunitySettingsSection.tsx
  • tests/modules/actions/grantRole.test.js
web/src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Never use console.* methods in web dashboard code; use appropriate logging mechanisms for React applications

Files:

  • web/src/components/landing/bento/BentoChart.tsx
  • web/src/components/dashboard/config-sections/CommunitySettingsSection.tsx
**/*.{js,ts,jsx,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{js,ts,jsx,tsx}: ESM only - do not use CommonJS modules
Use src/logger.js; do not use console.*

Files:

  • web/src/components/landing/bento/BentoChart.tsx
  • web/src/components/dashboard/config-sections/CommunitySettingsSection.tsx
  • tests/modules/actions/grantRole.test.js
**/*.js

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use ESM-only syntax: import/export, never require()/module.exports

Files:

  • tests/modules/actions/grantRole.test.js
tests/**/*.test.js

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

tests/**/*.test.js: Write bot tests using Vitest 4 with the node environment, matching the src/ structure in the tests/ directory
Maintain test coverage thresholds: statements 85%, branches 82%, functions 85%, lines 85%; never lower thresholds—add tests to cover new code instead

Files:

  • tests/modules/actions/grantRole.test.js
**/*.{js,ts}

📄 CodeRabbit inference engine (AGENTS.md)

Use parameterized SQL only; do not use string concatenation for SQL queries

Files:

  • tests/modules/actions/grantRole.test.js
🧠 Learnings (5)
📚 Learning: 2026-03-10T23:21:49.730Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-10T23:21:49.730Z
Learning: Applies to web/src/components/dashboard/config-workspace/**/*.{ts,tsx} : Web dashboard config editor should use category workspace navigation with reusable SettingsFeatureCard pattern (header + master toggle + Basic/Advanced blocks)

Applied to files:

  • web/src/components/dashboard/config-sections/CommunitySettingsSection.tsx
📚 Learning: 2026-03-26T00:04:14.673Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-26T00:04:14.673Z
Learning: Applies to src/**/*.{js,ts} : Community features should be gated behind config.<feature>.enabled. Moderation commands are the exception.

Applied to files:

  • web/src/components/dashboard/config-sections/CommunitySettingsSection.tsx
📚 Learning: 2026-03-11T06:42:38.728Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-11T06:42:38.728Z
Learning: Applies to src/commands/**/*.js : Guild owners should be explicitly exempted from role hierarchy restrictions in command handlers to match Discord permission model expectations

Applied to files:

  • tests/modules/actions/grantRole.test.js
📚 Learning: 2026-03-12T02:03:36.493Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-12T02:03:36.493Z
Learning: Applies to web/tests/**/*.test.{ts,tsx} : Write web dashboard tests using Vitest 4 with the `jsdom` environment and React Testing Library, matching the `web/src/` structure

Applied to files:

  • tests/modules/actions/grantRole.test.js
📚 Learning: 2026-03-11T06:42:38.728Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-11T06:42:38.728Z
Learning: Applies to src/commands/reactionrole.js : Enforce invoker role hierarchy check in /reactionrole add command to prevent non-owner users from configuring roles at or above their highest role

Applied to files:

  • tests/modules/actions/grantRole.test.js
🔇 Additional comments (6)
web/src/components/landing/bento/BentoChart.tsx (3)

71-73: Date parsing update looks correct and consistent.

Using template literals for ISO date construction is clean and preserves UTC weekday formatting behavior.

Also applies to: 87-93


215-225: SVG hit-zone interaction handling is acceptable here.

The lint suppression is narrowly scoped and the hover hit areas remain clear and functional for chart tooltip behavior.


217-217: Key updates are good and address reconciliation stability.

Both key changes improve sibling key specificity in mapped elements and avoid the previously flagged collision risk.

Also applies to: 258-258

web/src/components/dashboard/config-sections/CommunitySettingsSection.tsx (3)

536-553: Sorting still happens on every keystroke.

The input value is derived from levelThresholds.join(', ') and onChange immediately parses, filters, and sorts. This causes partial edits to be rewritten mid-entry (e.g., typing 100, 5 instantly becomes 5, 100), making thresholds hard to edit and moving the cursor unexpectedly. Consider storing raw text in local state and normalizing on blur.


466-528: LGTM!

The new Level-Up Actions feature card follows the SettingsFeatureCard pattern correctly, gates the feature behind draftConfig.xp?.enabled, and properly wires toggles to draftConfig.xp.roleRewards.stackRoles and removeOnLevelDown. Category/feature visibility checks align with the registered xp-level-actions feature in config-categories.ts.


383-383: LGTM!

Description update accurately reflects the new XP behavior.

Comment on lines +16 to +17
import { canManageRole, recordRoleChange } from '../../../src/modules/actions/roleUtils.js';

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.

⚠️ Potential issue | 🟠 Major

The “no internal rate-limit” test doesn’t validate its claim.

At Line 85, the test name says internal rate limits are not checked, but it only checks that roles.add() ran. If handleGrantRole() starts calling checkRoleRateLimit() (mocked true), this test still passes. Import the mock and assert it was not called.

✅ Suggested patch
-import { canManageRole, recordRoleChange } from '../../../src/modules/actions/roleUtils.js';
+import {
+  canManageRole,
+  checkRoleRateLimit,
+  recordRoleChange,
+} from '../../../src/modules/actions/roleUtils.js';
@@
   it('should not check rate limit internally (handled at pipeline level)', async () => {
     const ctx = makeContext();
     await handleGrantRole({ type: 'grantRole', roleId: 'role-a' }, ctx);
 
+    expect(checkRoleRateLimit).not.toHaveBeenCalled();
     expect(ctx._mocks.rolesAdd).toHaveBeenCalledWith('role-a');
   });

Also applies to: 85-90

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

In `@tests/modules/actions/grantRole.test.js` around lines 16 - 17, The test named
"no internal rate-limit" currently only verifies roles.add() ran but doesn't
confirm checkRoleRateLimit wasn't invoked; update the test to import the mocked
checkRoleRateLimit from the same module (roleUtils.js) and add an assertion that
checkRoleRateLimit was not called after calling handleGrantRole(), so the test
fails if internal rate-limit checks are executed; reference handleGrantRole,
roles.add, and checkRoleRateLimit when locating where to add the import and the
expect(...).not.toHaveBeenCalled() assertion.

Comment on lines +255 to +258
<div
key={`badge-${badge.days}-${badge.label}`}
className="flex items-center gap-2"
>
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.

⚠️ Potential issue | 🟡 Minor

Value-derived key causes input focus loss during edits.

Using badge-${badge.days}-${badge.label} as the key means that whenever the user types in the "days" or "label" input, the key changes, React remounts the element, and the input loses focus. Since badges are edited in place and not reordered externally, an index-based key is actually stable for this use case.

🔧 Proposed fix: revert to index-based key or use a composite
-                  <div
-                    key={`badge-${badge.days}-${badge.label}`}
-                    className="flex items-center gap-2"
-                  >
+                  <div key={index} className="flex items-center gap-2">

Alternatively, if you want to guard against future reordering, use a composite that includes index:

-                    key={`badge-${badge.days}-${badge.label}`}
+                    key={`badge-${index}`}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div
key={`badge-${badge.days}-${badge.label}`}
className="flex items-center gap-2"
>
<div key={index} className="flex items-center gap-2">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/dashboard/config-sections/CommunitySettingsSection.tsx`
around lines 255 - 258, The current key for each badge element uses
value-derived parts (`badge.days`/`badge.label`) which causes React to remount
inputs and lose focus while typing; update the key in the badge mapping inside
CommunitySettingsSection (the JSX that renders each badge with
key={`badge-${badge.days}-${badge.label}`}) to use a stable index-based key
(e.g., `badge-${index}`) or a composite that includes the index (e.g.,
`badge-${index}-${badge.label}`) so elements are not remounted on input changes.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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 33 out of 33 changed files in this pull request and generated 1 comment.

const xpToNext = nextThreshold !== null ? nextThreshold - xp : 0;

// DB queries for rank, messages, voiceHours, daysActive — all best-effort
let rank = '?';
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

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

buildTemplateContext initializes rank to '?' and the catch block leaves it unchanged when DB queries fail. This conflicts with the documented fallback behavior and the new tests that expect rank to be '#0' when the DB is unavailable. Set a deterministic fallback value for rank (and keep it on failure) so rendering is consistent.

Suggested change
let rank = '?';
let rank = '#0';

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Comment thread src/commands/rank.js
return safeEditReply(interaction, { content: 'Reputation system is not enabled.' });
}

const xpCfg = { ...XP_DEFAULTS, ...cfg.xp };
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Rank command uses shallow merge unlike getXpConfig

Low Severity

rank.js merges XP config with { ...XP_DEFAULTS, ...cfg.xp } (shallow), while all other consumers use getXpConfig() from reputation.js which performs a deep merge of nested roleRewards. This inconsistency means rank.js could get a different resolved config shape than the rest of the system if a guild partially overrides roleRewards. Using the shared getXpConfig helper would keep behavior consistent.

Fix in Cursor Fix in Web

@sonarqubecloud
Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
5.5% Duplication on New Code (required ≤ 3%)
C Reliability Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

Copy link
Copy Markdown
Contributor

@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: 3

♻️ Duplicate comments (1)
tests/modules/actions/roleUtils.test.js (1)

72-78: ⚠️ Potential issue | 🟠 Major

sweepRoleLimits() is not a true test-state reset.

beforeEach says “clear” state, but sweepRoleLimits() only removes stale entries. Recent entries can leak across tests (especially with reused keys like guild1:user1), making rate-limit assertions order-dependent.

💡 Suggested fix
 import {
   canManageRole,
   checkRoleRateLimit,
+  clearRoleLimits,
   collectXpManagedRoles,
   enforceRoleLevelDown,
   recordRoleChange,
   sweepRoleLimits,
 } from '../../../src/modules/actions/roleUtils.js';

 describe('checkRoleRateLimit / recordRoleChange', () => {
   beforeEach(() => {
     vi.useFakeTimers();
     vi.clearAllMocks();
-    // Clear rate limit state between tests
-    sweepRoleLimits();
+    clearRoleLimits();
   });
 describe('enforceRoleLevelDown', () => {
   beforeEach(() => {
     vi.clearAllMocks();
-    // Clear rate limit state between tests
-    sweepRoleLimits();
+    clearRoleLimits();
   });

And in src/modules/actions/roleUtils.js, export a test helper:

export function clearRoleLimits() {
  roleLimits.clear();
}

Also applies to: 170-175

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

In `@tests/modules/actions/roleUtils.test.js` around lines 72 - 78, The tests leak
recent rate-limit state because sweepRoleLimits() only prunes stale entries; add
and export a true reset helper in the module (e.g., export function
clearRoleLimits() { roleLimits.clear(); }) inside
src/modules/actions/roleUtils.js and update the tests
(describe('checkRoleRateLimit / recordRoleChange', ...) and the other block at
lines ~170-175) to call clearRoleLimits() in beforeEach instead of
sweepRoleLimits() so each test gets a clean roleLimits Map.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/utils/templateEngine.test.js`:
- Around line 99-269: Tests for buildTemplateContext don't exercise
optional-chaining fallbacks (e.g., guild.iconURL?.(), message?.channel?.name,
member.user.displayAvatarURL?.()), so add a test named "should handle missing
optional properties gracefully" that calls buildTemplateContext with a member
lacking displayAvatarURL and joinedAt, a guild lacking iconURL, and message set
to null, then assert ctx.avatar === '', ctx.serverIcon === '', ctx.channel ===
'', and ctx.joinDate === '' (optionally assert other fields like
roleName/roleMention remain sensible); this targets the buildTemplateContext
behavior and ensures the empty-string fallback paths are covered.
- Around line 100-146: Add a new unit test that covers the primary
member.displayName path: call buildTemplateContext with a member object that
includes displayName (e.g., member.displayName = 'GuildNickname') plus
member.user.displayName different from it, a simple message.channel, and a
minimal guild object, then assert that ctx.username equals 'GuildNickname'; this
verifies the templateEngine logic that prefers member.displayName over
member.user.displayName.
- Around line 1-9: Consolidate the two separate Vitest imports into a single
import to remove duplication: replace the separate vi import and the later
"import { describe, expect, it } from 'vitest'" with one line importing vi,
describe, expect, and it together; keep the vi.mock(...) call (and its mock
factory) in place and ensure vi.mock is executed before any imports of modules
that depend on the mocked db (reference symbols: vi, vi.mock, describe, expect,
it).

---

Duplicate comments:
In `@tests/modules/actions/roleUtils.test.js`:
- Around line 72-78: The tests leak recent rate-limit state because
sweepRoleLimits() only prunes stale entries; add and export a true reset helper
in the module (e.g., export function clearRoleLimits() { roleLimits.clear(); })
inside src/modules/actions/roleUtils.js and update the tests
(describe('checkRoleRateLimit / recordRoleChange', ...) and the other block at
lines ~170-175) to call clearRoleLimits() in beforeEach instead of
sweepRoleLimits() so each test gets a clean roleLimits Map.
🪄 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

Run ID: ca915fae-dd71-4062-98d5-482453e4fcd3

📥 Commits

Reviewing files that changed from the base of the PR and between d0fbe58 and aa0a8fa.

📒 Files selected for processing (2)
  • tests/modules/actions/roleUtils.test.js
  • tests/utils/templateEngine.test.js
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Greptile Review
  • GitHub Check: Cursor Bugbot
  • GitHub Check: Test
🧰 Additional context used
📓 Path-based instructions (5)
**/*.js

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use ESM-only syntax: import/export, never require()/module.exports

Files:

  • tests/modules/actions/roleUtils.test.js
  • tests/utils/templateEngine.test.js
**/*.{js,ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/*.{js,ts,tsx}: Use single quotes for strings (except in JSON files); no double quotes
Always include semicolons at the end of statements
Use 2-space indentation (spaces, not tabs)
Always include trailing commas in multi-line arrays, objects, and function parameters
Maintain a maximum line width of 100 characters

Files:

  • tests/modules/actions/roleUtils.test.js
  • tests/utils/templateEngine.test.js
tests/**/*.test.js

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

tests/**/*.test.js: Write bot tests using Vitest 4 with the node environment, matching the src/ structure in the tests/ directory
Maintain test coverage thresholds: statements 85%, branches 82%, functions 85%, lines 85%; never lower thresholds—add tests to cover new code instead

Files:

  • tests/modules/actions/roleUtils.test.js
  • tests/utils/templateEngine.test.js
**/*.{js,ts,jsx,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{js,ts,jsx,tsx}: ESM only - do not use CommonJS modules
Use src/logger.js; do not use console.*

Files:

  • tests/modules/actions/roleUtils.test.js
  • tests/utils/templateEngine.test.js
**/*.{js,ts}

📄 CodeRabbit inference engine (AGENTS.md)

Use parameterized SQL only; do not use string concatenation for SQL queries

Files:

  • tests/modules/actions/roleUtils.test.js
  • tests/utils/templateEngine.test.js
🧠 Learnings (6)
📚 Learning: 2026-03-12T02:03:36.493Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-12T02:03:36.493Z
Learning: Applies to tests/**/*.test.js : Write bot tests using Vitest 4 with the `node` environment, matching the `src/` structure in the `tests/` directory

Applied to files:

  • tests/modules/actions/roleUtils.test.js
  • tests/utils/templateEngine.test.js
📚 Learning: 2026-03-11T06:42:38.728Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-11T06:42:38.728Z
Learning: Applies to src/commands/reactionrole.js : Enforce invoker role hierarchy check in /reactionrole add command to prevent non-owner users from configuring roles at or above their highest role

Applied to files:

  • tests/modules/actions/roleUtils.test.js
📚 Learning: 2026-03-12T02:03:36.493Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-12T02:03:36.493Z
Learning: Applies to web/tests/**/*.test.{ts,tsx} : Write web dashboard tests using Vitest 4 with the `jsdom` environment and React Testing Library, matching the `web/src/` structure

Applied to files:

  • tests/modules/actions/roleUtils.test.js
  • tests/utils/templateEngine.test.js
📚 Learning: 2026-03-11T06:42:38.728Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-11T06:42:38.728Z
Learning: Applies to src/commands/**/*.js : Guild owners should be explicitly exempted from role hierarchy restrictions in command handlers to match Discord permission model expectations

Applied to files:

  • tests/modules/actions/roleUtils.test.js
📚 Learning: 2026-03-12T02:03:36.493Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-12T02:03:36.493Z
Learning: Applies to tests/**/*.test.js : Maintain test coverage thresholds: statements 85%, branches 82%, functions 85%, lines 85%; never lower thresholds—add tests to cover new code instead

Applied to files:

  • tests/modules/actions/roleUtils.test.js
  • tests/utils/templateEngine.test.js
📚 Learning: 2026-03-12T02:03:36.493Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-12T02:03:36.493Z
Learning: Applies to web/tests/**/*.test.{ts,tsx} : Maintain test coverage thresholds of 85% across all metrics (statements, branches, functions, lines) for web dashboard tests

Applied to files:

  • tests/utils/templateEngine.test.js
🪛 GitHub Check: SonarCloud Code Analysis
tests/utils/templateEngine.test.js

[warning] 9-9: 'vitest' imported multiple times.

See more on https://sonarcloud.io/project/issues?id=VolvoxLLC_volvox-bot&issues=AZ0nXmHvA2WZ1HihRe1V&open=AZ0nXmHvA2WZ1HihRe1V&pullRequest=373


[warning] 1-1: 'vitest' imported multiple times.

See more on https://sonarcloud.io/project/issues?id=VolvoxLLC_volvox-bot&issues=AZ0nXmHvA2WZ1HihRe1U&open=AZ0nXmHvA2WZ1HihRe1U&pullRequest=373

🔇 Additional comments (3)
tests/modules/actions/roleUtils.test.js (3)

38-70: canManageRole coverage is solid and focused.

Good matrix across permission, hierarchy, equality edge, and missing-role cache with logger assertions.


126-168: collectXpManagedRoles tests correctly cover both action sources.

Nice inclusion of defaultActions alongside levelActions, plus empty-config behavior.


282-334: Great regression coverage for level-down edge paths.

The fallback-to-individual removal, replace-mode restore, and rate-limit skip scenarios are high-value tests.

Also applies to: 336-362

Comment on lines +1 to +9
import { vi } from 'vitest';

vi.mock('../../src/db.js', () => ({
getPool: vi.fn(() => ({
query: vi.fn().mockResolvedValue({ rows: [] }),
})),
}));

import { describe, expect, it } from 'vitest';
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.

🧹 Nitpick | 🔵 Trivial

Consider consolidating the Vitest imports.

The file imports from 'vitest' twice (lines 1 and 9), which triggers a SonarCloud warning. While this pattern is valid for separating mock utilities from test utilities, you can consolidate them into a single import for cleaner code.

♻️ Proposed consolidation
-import { vi } from 'vitest';
+import { describe, expect, it, vi } from 'vitest';

 vi.mock('../../src/db.js', () => ({
   getPool: vi.fn(() => ({
     query: vi.fn().mockResolvedValue({ rows: [] }),
   })),
 }));

-import { describe, expect, it } from 'vitest';
-
 import {
   buildTemplateContext,
   renderTemplate,
   validateLength,
 } from '../../src/utils/templateEngine.js';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { vi } from 'vitest';
vi.mock('../../src/db.js', () => ({
getPool: vi.fn(() => ({
query: vi.fn().mockResolvedValue({ rows: [] }),
})),
}));
import { describe, expect, it } from 'vitest';
import { describe, expect, it, vi } from 'vitest';
vi.mock('../../src/db.js', () => ({
getPool: vi.fn(() => ({
query: vi.fn().mockResolvedValue({ rows: [] }),
})),
}));
import {
buildTemplateContext,
renderTemplate,
validateLength,
} from '../../src/utils/templateEngine.js';
🧰 Tools
🪛 GitHub Check: SonarCloud Code Analysis

[warning] 9-9: 'vitest' imported multiple times.

See more on https://sonarcloud.io/project/issues?id=VolvoxLLC_volvox-bot&issues=AZ0nXmHvA2WZ1HihRe1V&open=AZ0nXmHvA2WZ1HihRe1V&pullRequest=373


[warning] 1-1: 'vitest' imported multiple times.

See more on https://sonarcloud.io/project/issues?id=VolvoxLLC_volvox-bot&issues=AZ0nXmHvA2WZ1HihRe1U&open=AZ0nXmHvA2WZ1HihRe1U&pullRequest=373

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

In `@tests/utils/templateEngine.test.js` around lines 1 - 9, Consolidate the two
separate Vitest imports into a single import to remove duplication: replace the
separate vi import and the later "import { describe, expect, it } from 'vitest'"
with one line importing vi, describe, expect, and it together; keep the
vi.mock(...) call (and its mock factory) in place and ensure vi.mock is executed
before any imports of modules that depend on the mocked db (reference symbols:
vi, vi.mock, describe, expect, it).

Comment on lines +99 to +269
describe('buildTemplateContext', () => {
it('should populate all Discord-derived variables from member/guild/message', async () => {
const member = {
user: {
id: '123456789',
displayName: 'TestUser',
displayAvatarURL: () => 'https://cdn.example.com/avatar.png',
},
joinedAt: new Date('2025-01-15T12:00:00Z'),
};
const message = {
channel: { name: 'general' },
};
const guild = {
id: 'guild1',
name: 'Test Server',
iconURL: () => 'https://cdn.example.com/icon.png',
memberCount: 1234,
};

const ctx = await buildTemplateContext({
member,
message,
guild,
level: 5,
previousLevel: 4,
xp: 1500,
levelThresholds: [100, 300, 600, 1000, 1500, 2500],
roleName: null,
roleId: null,
});

expect(ctx.username).toBe('TestUser');
expect(ctx.mention).toBe('<@123456789>');
expect(ctx.avatar).toBe('https://cdn.example.com/avatar.png');
expect(ctx.level).toBe('5');
expect(ctx.previousLevel).toBe('4');
expect(ctx.xp).toBe('1,500');
expect(ctx.xpToNext).toBe('1,000');
expect(ctx.nextLevel).toBe('6');
expect(ctx.server).toBe('Test Server');
expect(ctx.serverIcon).toBe('https://cdn.example.com/icon.png');
expect(ctx.memberCount).toBe('1,234');
expect(ctx.channel).toBe('#general');
expect(ctx.joinDate).toMatch(/Jan/);
expect(ctx.roleName).toBe('');
expect(ctx.roleMention).toBe('');
});

it('should populate roleName and roleMention when roleId is provided', async () => {
const member = {
user: {
id: '123',
displayName: 'User',
displayAvatarURL: () => '',
},
joinedAt: new Date(),
};
const message = { channel: { name: 'ch' } };
const guild = {
id: 'guild1',
name: 'S',
iconURL: () => '',
memberCount: 1,
};

const ctx = await buildTemplateContext({
member,
message,
guild,
level: 1,
previousLevel: 0,
xp: 100,
levelThresholds: [100],
roleName: 'Regular',
roleId: '999888777',
});

expect(ctx.roleName).toBe('Regular');
expect(ctx.roleMention).toBe('<@&999888777>');
});

it('should return "0" for xpToNext when at max level', async () => {
const member = {
user: { id: '1', displayName: 'U', displayAvatarURL: () => '' },
joinedAt: new Date(),
};
const message = { channel: { name: 'ch' } };
const guild = { id: 'guild1', name: 'S', iconURL: () => '', memberCount: 1 };

const ctx = await buildTemplateContext({
member,
message,
guild,
level: 3,
previousLevel: 2,
xp: 700,
levelThresholds: [100, 300, 600],
roleName: null,
roleId: null,
});

expect(ctx.xpToNext).toBe('0');
expect(ctx.nextLevel).toBe('0');
});

it('should populate DB-derived variables when query returns rows', async () => {
const { getPool } = await import('../../src/db.js');
getPool.mockReturnValueOnce({
query: vi
.fn()
.mockResolvedValueOnce({ rows: [{ rank: 3 }] })
.mockResolvedValueOnce({
rows: [{ messages_count: 42, days_active: 7, voice_seconds: 3600 }],
}),
});

const member = {
user: { id: 'u1', displayName: 'U', displayAvatarURL: () => '' },
joinedAt: new Date(),
};
const guild = { id: 'g1', name: 'S', iconURL: () => '', memberCount: 1 };

const ctx = await buildTemplateContext({
member,
message: { channel: { name: 'ch' } },
guild,
level: 1,
previousLevel: 0,
xp: 100,
levelThresholds: [100, 300],
roleName: null,
roleId: null,
});

expect(ctx.rank).toBe('#3');
expect(ctx.messages).toBe('42');
expect(ctx.daysActive).toBe('7');
expect(ctx.voiceHours).toBe('1');
});

it('should use fallback values when DB query rejects', async () => {
const { getPool } = await import('../../src/db.js');
getPool.mockReturnValueOnce({
query: vi.fn().mockRejectedValue(new Error('DB down')),
});

const member = {
user: { id: 'u1', displayName: 'U', displayAvatarURL: () => '' },
joinedAt: new Date(),
};
const guild = { id: 'g1', name: 'S', iconURL: () => '', memberCount: 1 };

const ctx = await buildTemplateContext({
member,
message: { channel: { name: 'ch' } },
guild,
level: 1,
previousLevel: 0,
xp: 100,
levelThresholds: [100, 300],
roleName: null,
roleId: null,
});

expect(ctx.rank).toBe('?');
expect(ctx.messages).toBe('0');
expect(ctx.daysActive).toBe('0');
expect(ctx.voiceHours).toBe('0');
});
});
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.

🧹 Nitpick | 🔵 Trivial

Consider adding tests for optional property fallbacks.

The implementation uses optional chaining for properties like guild.iconURL?.(), message?.channel?.name, and member.user.displayAvatarURL?.(). All current test fixtures provide these properties, so the fallback paths ('' empty string) are not exercised.

🧪 Proposed edge-case test
it('should handle missing optional properties gracefully', async () => {
  const member = {
    user: { id: '123', displayName: 'User' },
    // No displayAvatarURL method, no joinedAt
  };
  const guild = {
    id: 'g1',
    name: 'Server',
    // No iconURL method
    memberCount: 10,
  };
  // message is null

  const ctx = await buildTemplateContext({
    member,
    message: null,
    guild,
    level: 1,
    previousLevel: 0,
    xp: 100,
    levelThresholds: [100],
    roleName: null,
    roleId: null,
  });

  expect(ctx.avatar).toBe('');
  expect(ctx.serverIcon).toBe('');
  expect(ctx.channel).toBe('');
  expect(ctx.joinDate).toBe('');
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/utils/templateEngine.test.js` around lines 99 - 269, Tests for
buildTemplateContext don't exercise optional-chaining fallbacks (e.g.,
guild.iconURL?.(), message?.channel?.name, member.user.displayAvatarURL?.()), so
add a test named "should handle missing optional properties gracefully" that
calls buildTemplateContext with a member lacking displayAvatarURL and joinedAt,
a guild lacking iconURL, and message set to null, then assert ctx.avatar === '',
ctx.serverIcon === '', ctx.channel === '', and ctx.joinDate === '' (optionally
assert other fields like roleName/roleMention remain sensible); this targets the
buildTemplateContext behavior and ensures the empty-string fallback paths are
covered.

Comment on lines +100 to +146
it('should populate all Discord-derived variables from member/guild/message', async () => {
const member = {
user: {
id: '123456789',
displayName: 'TestUser',
displayAvatarURL: () => 'https://cdn.example.com/avatar.png',
},
joinedAt: new Date('2025-01-15T12:00:00Z'),
};
const message = {
channel: { name: 'general' },
};
const guild = {
id: 'guild1',
name: 'Test Server',
iconURL: () => 'https://cdn.example.com/icon.png',
memberCount: 1234,
};

const ctx = await buildTemplateContext({
member,
message,
guild,
level: 5,
previousLevel: 4,
xp: 1500,
levelThresholds: [100, 300, 600, 1000, 1500, 2500],
roleName: null,
roleId: null,
});

expect(ctx.username).toBe('TestUser');
expect(ctx.mention).toBe('<@123456789>');
expect(ctx.avatar).toBe('https://cdn.example.com/avatar.png');
expect(ctx.level).toBe('5');
expect(ctx.previousLevel).toBe('4');
expect(ctx.xp).toBe('1,500');
expect(ctx.xpToNext).toBe('1,000');
expect(ctx.nextLevel).toBe('6');
expect(ctx.server).toBe('Test Server');
expect(ctx.serverIcon).toBe('https://cdn.example.com/icon.png');
expect(ctx.memberCount).toBe('1,234');
expect(ctx.channel).toBe('#general');
expect(ctx.joinDate).toMatch(/Jan/);
expect(ctx.roleName).toBe('');
expect(ctx.roleMention).toBe('');
});
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.

🧹 Nitpick | 🔵 Trivial

Add test coverage for the primary member.displayName path.

The test fixture provides user.displayName but not member.displayName. The implementation in src/utils/templateEngine.js line 139 prioritizes member.displayName (the guild-specific nickname) and only falls back to member.user.displayName. This test currently exercises only the fallback path.

🧪 Proposed test addition

Add a test case that verifies guild nicknames take precedence:

it('should use member.displayName (guild nickname) over user.displayName', async () => {
  const member = {
    displayName: 'GuildNickname',
    user: {
      id: '123',
      displayName: 'UserDisplayName',
      displayAvatarURL: () => '',
    },
    joinedAt: new Date(),
  };
  const guild = { id: 'g1', name: 'S', iconURL: () => '', memberCount: 1 };

  const ctx = await buildTemplateContext({
    member,
    message: { channel: { name: 'ch' } },
    guild,
    level: 1,
    previousLevel: 0,
    xp: 100,
    levelThresholds: [100],
    roleName: null,
    roleId: null,
  });

  expect(ctx.username).toBe('GuildNickname');
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/utils/templateEngine.test.js` around lines 100 - 146, Add a new unit
test that covers the primary member.displayName path: call buildTemplateContext
with a member object that includes displayName (e.g., member.displayName =
'GuildNickname') plus member.user.displayName different from it, a simple
message.channel, and a minimal guild object, then assert that ctx.username
equals 'GuildNickname'; this verifies the templateEngine logic that prefers
member.displayName over member.user.displayName.

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

Labels

None yet

Projects

Status: Done

2 participants