Skip to content

release(v5.51.0): config RMW atomicity, plugin fixes (B-006/B-018), public API exports, per-plugin test floor#72

Merged
rolling-codes merged 49 commits into
mainfrom
fix/config-concurrency-rmw
Jul 3, 2026
Merged

release(v5.51.0): config RMW atomicity, plugin fixes (B-006/B-018), public API exports, per-plugin test floor#72
rolling-codes merged 49 commits into
mainfrom
fix/config-concurrency-rmw

Conversation

@rolling-codes

@rolling-codes rolling-codes commented Jul 1, 2026

Copy link
Copy Markdown
Owner

Summary

v5.51.0 Stability Release — serializes all plugin config read-modify-write paths under per-guild locks, fixes 10+ live plugin bugs (including HIGH B-018: starboard dead-on-arrival), exposes missing public API, and raises the CI per-plugin test floor to a flat 20. 1335 tests (up from 1301), green on the 3.10/3.11/3.12 matrix.

Motivation

Racing config writes dropped data (duplicate suggestion IDs, lost warnings, clobbered role-persistence records); starboard silently never fired for guilds configured via a single command; and audit debt from the PR #70/#71 review cycles needed closure before release.

Key changes

Config concurrency

  • All load→modify→save paths routed through atomic ServerConfigStore.mutate() with per-guild locks: suggestions, starboard, reaction_roles, moderation, role_persistence.
  • Tags storage (B-010): per-guild asyncio.Lock, async set/delete, TOCTOU-safe delete_if_authorized.

Plugin bug fixes

  • B-018 (HIGH, starboard): cfg.get("enabled") without a default treated a missing key as disabled — a guild that only ran /starboard_channel had a starboard that never fired. Now cfg.get("enabled", True) in both reaction handlers and the config display.
  • B-008 (openclaw): 7 sites of unguarded Optional member access narrowed with asserts/guards; pyright-clean.
  • B-009 (scheduled_announcements): loop survives discord.Forbidden/HTTPException on send.
  • B-011 (tickets): guild guard in persistent view button for DM interactions.
  • B-012 (giveaway, polls, reminder): ctx.channel is None guards; the polls guard response is localized via ctx.t(...).
  • B-017 (suggestions): dead self.suggestion_counter field removed.
  • Cherry-pick c60c8b6: birthday Feb 29 crash, tickets transcript order, levels role rewards.

Public API

  • SENDABLE_CHANNEL_TYPES, EventBus, HookRegistry now importable from easycord; slash_command is a real deprecation shim; ctx.author raises with a ctx.user/ctx.member migration hint.

CI / test gates

  • verify_plugin_tests.py: flat ≥20 tests per plugin (was complex ≥20 / simple ≥8); async-test counting and filename-alias fixes (B-001/B-002). Suggestions 13→21, tags 15→21, starboard 11→25 tests.
  • Release-metadata consistency gate.

Docs & triage

  • bugs.md: B-019 closed as verified benign — the B-018 pattern requires an update()-only section-creation path, and none of the five flagged sibling plugins (auto_responder, economy, member_logging, role_persistence, ai_moderator) have one. B-008 record reconciled with the release section that already documented its fix.
  • Merged main (v5.50.2 + dependabot action bumps) to clear the conflicting state; CLAUDE.md/AGENTS.md action-pin invariant synced to checkout@v7.
  • CHANGELOG + release notes refreshed (B-018, public exports, 1335 test count).

Test plan

  • Blocking ruff gate (--select E9,F63,F7,F82) — pass
  • scripts/check_release_metadata.py — pass
  • scripts/verify_plugin_tests.py — all plugins ≥20 — pass
  • Full pytest: 1335/1335 PASSED, 0 FAILED/ERROR (local py3.14; verified via streamed -v log — a machine-local interpreter-exit hang is documented in project.md and does not affect the CI matrix)
  • CI matrix (3.10/3.11/3.12) on this PR

Deferred (tracked, out of scope)

  • v5.52.0: B-013 (auto_responder TOCTOU), B-014 (on_ready exception logging), B-015/B-016 (exception narrowing), plugin i18n sweep, hot-reload dispatch race.
  • Improvement-plan Phases 2–4: PostgreSQL backend, StatefulPlugin/state persistence, plugin dependency resolver, plugin registry — multi-week roadmap items.

🤖 Generated with Claude Code

tee1339 and others added 17 commits June 23, 2026 22:10
The prose still described warn/timeout as governed after the timeout branch was removed; corrected to warn-only for internal consistency.
…lity)

- i18n: guard LocalizationManager metrics with a threading.Lock so sharded
  bots no longer lose counter updates on concurrent access (audit #1)
- hot-reload: serialize the plugin swap with command dispatch via a bot-wide
  reload lock, gated on the dev watcher being active so production stays
  lock-free (audit #2)
- permissions: opt-in bot_permissions= validated at dispatch via
  ctx.bot_permissions; destructive moderation commands now declare them so
  they fail fast with a clear message instead of a mid-execution Forbidden
  (audit #3)
- registry: strict component TTL boundary (expires_at > now) so a ttl=0
  component is inactive within the same coarse-clock tick
- economy: bind daily-reward response fields on every reachable path
  (possibly-unbound fix)

Tests: i18n metrics thread-safety, hot-reload lock idempotency/gating,
bot-permission dispatch block/allow, component regex+TTL regression guards.
- add a runtime-checkable AIProviderProtocol structural type and thread it
  through bot.py / orchestrator.py provider typing in place of the concrete
  AIProvider, replacing ad-hoc `# type: ignore` with setattr/getattr
- mark optional AI-SDK imports with `# pyright: ignore[reportMissingImports]`
- expose event_bus on _BotBase for the mixin typing surface
- pyrightconfig: reportFunctionMemberAccess -> warning (decorators stamp
  attributes onto functions by design)

No runtime behavior change; suite stays at 1213 passing.
- new guides: troubleshooting (day-one friction points), database-guide
  (concurrency model, guild sync, backends, production), plugin-ecosystem-health
  (profiling at 50+ plugins); wired into docs/README index
- context/implementation-plan.md: durable record of the v5.51.0 critical-bug
  round and the corrections made to the audit's suggested fixes
- RELEASE.md: pre-release checklist and sign-off playbook
- refresh architecture/conventions, conversation-memory, CONTRIBUTING,
  SECURITY, and CLAUDE.md to match the hardened behavior
- sync __version__ to 5.50.2 across pyproject/__init__ and update the
  release/download URLs (was lagging at 5.50.1)
- add the tests execution-environment block to pyproject so test files are
  type-relaxed centrally (replaces the per-file pyright headers)
- CI: add the verify_plugin_tests.py coverage-threshold gate
- dev scripts: verify_plugin_tests, stress_test_db, patch_tests
- add OpenClaude/AIPlugin test coverage
…c_timeout)

`sync_guilds` loops over every joined guild with no bound; a bot in many
guilds or against a slow backend can stall startup forever.  This adds a
`guild_sync_timeout: float | None = 30.0` parameter to both `Bot` and
`BotConfig` so the startup sync can be abandoned after a configurable
number of seconds instead of hanging indefinitely.

* `Bot.__init__` accepts `guild_sync_timeout` and stores it as
  `self._guild_sync_timeout`.
* A new private helper `_sync_guilds_with_timeout()` wraps the
  `await self.db.sync_guilds(...)` call in `asyncio.wait_for` when
  a positive timeout is set; on `asyncio.TimeoutError` it logs a WARNING
  (naming the timeout and guild count) and returns so startup continues.
  Passing `None` or a value `<= 0` opts out and restores the previous
  unbounded behavior.
* Both call sites in `setup_hook` and `on_ready` are replaced with the
  single helper, eliminating the duplication.
* `BotConfig` gains a `guild_sync_timeout: float | None = 30.0` field,
  env-var `EASYCORD_GUILD_SYNC_TIMEOUT`, and `build_bot` threads it
  through via `kwargs.setdefault`.
* 15 new tests cover: constructor storage, timeout/no-timeout paths for
  both call sites, elapsed-time bound, and BotConfig env plumbing.
The per-guild asyncio.Lock only made an individual load() or save() atomic,
not a load -> modify -> save sequence, so concurrent updates to the same guild
could lose each other's writes (last-save-wins). The class docstring claimed a
guarantee it did not provide.

Add ServerConfigStore.mutate(guild_id, fn): a synchronous-callback read-modify-
write that holds the per-guild lock across the whole load/modify/save span
(load() and save() are split into lock-free _load_unlocked/_save_unlocked since
asyncio.Lock is not reentrant). Route every unguarded read-modify-write through
it:

- _config_manager: update / set_default / get(default-creation). get() stays a
  pure read when the section already exists (hot per-event callers don't write).
- suggestions: _get_next_id (fixes duplicate IDs), suggestion storage, approve,
  reject.
- starboard: archived-message set/remove.
- reaction_roles: mapping set/remove plus the message-delete and role-delete
  cleanup handlers (message-delete keeps a read-first guard so it doesn't write
  on every deletion).
- moderation: /warn warning append.

role_persistence also gets three logic fixes alongside the locking:
- save records roles by identity (exclude only @everyone + managed), not by the
  bot's current hierarchy, so a role above the bot at leave time is remembered;
- assignability is gated at restore time;
- the saved record is cleared only on a successful restore or when every saved
  role no longer exists in the guild, so a Forbidden/HTTP failure is retryable
  and stale entries don't leak.

Tests: TestAtomicMutate (incl. a test documenting the naive lost-write race),
tests/test_suggestions.py, tests/test_role_persistence.py.
From the PR #70 review triage (verified against current code):

- ai_moderator: the config mutators (mod_enable, mod_threshold, mod_action_level,
  mod_add_rule, mod_remove_rule) were guild_only with no permission gate, letting
  any member enable moderation or flip it to auto_delete. Add
  permissions=["manage_guild"] (enforced server-side in _command_callbacks),
  matching the project convention used by auto_role/levels/starboard. Adds a
  parametrized regression test.
- tags.delete: use the ctx.is_admin property instead of
  ctx.guild.get_member(ctx.user.id) + administrator, which returns None for an
  uncached member and falsely denies a legitimate admin.
- project.md: living decisions / bug-fix / per-PR-triage log so review findings
  stay consistent across PRs.
- CLAUDE.md / AGENTS.md: document the ruff lint gate, verify_plugin_tests
  threshold check, the CI PR-gate order, and the registry/tools/middleware
  modules that were previously undocumented.
…stion parsing

- ServerConfigStore.mutate logs a warning when a callback exceeds 50ms, enforcing
  the "fn must be fast / no I/O while the lock is held" contract.
- suggestions list view skips non-dict entries defensively.
- Add tests/test_pr71_concurrency.py and expand the ai_moderator / suggestions /
  server_config tests around the per-guild-lock read-modify-write paths.
- starboard: guard self.bot.user in the reaction handlers and assert ctx.guild in
  the guild_only slash commands so pyright reports the file clean
  (reportOptionalMemberAccess) — no behavior change.
- CLAUDE.md: document that all config read-modify-write must go through
  ServerConfigStore.mutate (per-guild lock held across load/modify/save).
The coverage gate counted only ast.FunctionDef, so every `async def test_`
(nearly all plugin tests) was invisible — failing PR #71's gate on all
Python versions. Count AsyncFunctionDef too, and add a plugin->test-file
alias map (levels->test_levels_plugin, reminders->test_reminder).

Close the genuine gaps the fix exposed: +2 reminder tests (->20), and new
test_tags.py (15) and test_starboard.py (11) covering store CRUD, mutate-
guarded archived-map RMW, archive/unarchive, Optional/None guards, and
admin/permission gating.

Also drop the invalid `assignees: ['tee']` from ci-failure-reporter,
nightly, and triage workflows (no such GitHub user; broke the reporter).
Record B-001..B-005 (coverage-gate async/filename bugs, invalid CI assignee,
and two test typing issues) with root cause and lesson per entry, so they are
not repeated. Narrow Optional access in test_starboard/test_reminder that
Pyright flagged (configure mock bot on a local, assert footer.text not None).
The XP cooldown map bounded memory with self._cooldowns.clear() at 10k
entries, which reset every user's cooldown at once — a server-wide XP-gate
bypass. Prune only entries older than the cooldown window, drop emptied
guild dicts, keep active cooldowns. Threshold named _COOLDOWN_PRUNE_THRESHOLD;
regression test added.

Also records the bug audit outcome in bugs.md: mutate-contract, destructive-
action isolation, and Optional-narrowing domains verified clean; openclaw
lock-cleanup flagged as a false positive (finally block already pops);
invite_tracker guild-keyed cache noted as bounded (B-007, won't fix).
…(B-008)

Replace the empty pass-stubs in test_restart_resilience.py with real tests
exercising each plugin's on_ready restore path: Polls (view custom_id
re-registration, vote persistence, timer resume, corrupted-payload skip),
Reminders (re-schedule pending, deliver idempotency via done flag, missed-
window reschedule), Birthday (single role-drop schedule, Forbidden handled
gracefully, no re-announce on restart). 10 tests, all passing.

Document B-008 in bugs.md: openclaw.py guild_only commands access ctx.guild.id
without the `assert ctx.guild is not None` guard (plus Optional orchestrator/
source), for a follow-up agent to fix using starboard's bdd0c22 pattern.
… rewards)

- birthday: _days_until no longer raises an uncaught ValueError for a Feb 29
  birthday when the following year is also non-leap (e.g. 2026 -> 2027). The
  first except branch uses a guarded loop instead of an unguarded
  datetime.date(this_year + 1, ...). Fixes /birthday_list throwing for the whole
  guild, since _days_until is the _sort_upcoming sort key.
- tickets: _finish_close fetches the most recent 100 messages
  (oldest_first=False) rather than the oldest 100, so long-ticket transcripts
  capture the resolution instead of the opening. _format_transcript already
  re-sorts chronologically, so the [-3800:] tail keeps the end.
- levels: extract _grant_level_reward and call it from both _award_xp and
  /give_xp, so manually leveling a member into a reward tier applies the role
  (previously only organic message-XP leveling did).

Tests cover all three; full suite 1234 passed.
Major fixes:
- B-008: openclaw Optional member access (7 sites narrowed)
- B-009: scheduled_announcements loop resilience (discord.Forbidden handling)
- B-012: ctx.channel guards in giveaway, polls, reminder
- B-011: tickets button view guild guard for DM context
- Cherry-pick c60c8b6: birthday Feb 29 fix, tickets transcript order, levels role rewards
- B-017: Remove dead suggestion_counter field

Version: 5.50.2 → 5.51.0
Tests: 1301 → 1307 passing
CI gates: All passing (ruff, pytest, plugin coverage, release metadata)

Deferred to v5.52.0:
- B-010: tags concurrent write safety
- B-013: auto_responder TOCTOU
- LocalizationManager thread-safety
- Hot-reload dispatch race

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Sorry @rolling-codes, your pull request is larger than the review limit of 150000 diff characters

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

@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

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

Next review available in: 48 minutes

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

How can I continue?

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

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

How do review limits work?

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

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

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 1d2e16da-17d4-4a1d-b2bc-66b6884ed705

📥 Commits

Reviewing files that changed from the base of the PR and between c44a67c and 789c103.

📒 Files selected for processing (1)
  • .github/workflows/perf-regression.yml
📝 Walkthrough

Walkthrough

This PR bumps EasyCord to v5.51.0 and updates slash-command bot permission gating, hot-reload locking, i18n metric locking, atomic config mutation, guild sync timeout handling, sendable-channel typing, CI/test tooling, and extensive documentation and planning content.

Changes

Core Runtime & Plugin Behavior Fixes

Layer / File(s) Summary
Slash bot permissions and dispatch
easycord/decorators.py, easycord/_bot_commands.py, easycord/_command_registration.py, easycord/_plugin_scanner.py, easycord/_command_callbacks.py, easycord/_bot_plugins.py, easycord/plugins/moderation.py, easycord/plugins/ai_moderator.py, tests/test_permission_validator.py, tests/test_ai_moderator.py
Adds bot_permissions propagation from @slash through registration/scanning to dispatch, validates against ctx.bot_permissions with ephemeral errors, applies it to moderation commands, and deprecates slash_command.
Hot-reload and i18n locking
easycord/_bot_plugins.py, easycord/_command_callbacks.py, easycord/i18n.py, tests/test_hot_reload.py, tests/test_i18n.py
Adds a bot-wide reload lock used during plugin swaps and dispatch, and wraps localization metric reads and writes in an internal lock.
Atomic config storage core
easycord/server_config.py, easycord/plugins/_config_manager.py, tests/test_server_config.py, context/implementation-plan.md
Refactors ServerConfigStore to use locked helpers and atomic writes, rewrites PluginConfigManager around mutate, and adds concurrency coverage and plan notes.
Plugin storage and guard updates
easycord/plugins/{moderation,reaction_roles,starboard,suggestions,role_persistence,tags}.py, related tests
Moves multiple plugin storage paths onto atomic mutate or locked async writes, with updated tests and permission gating.
Guild sync timeout
easycord/bot.py, easycord/config.py, tests/test_guild_sync_timeout.py
Adds guild_sync_timeout bounding startup db.sync_guilds via a timeout-aware helper.
Sendable channel helper
easycord/helpers/channel.py, easycord/helpers/__init__.py, easycord/plugins/_utils.py, easycord/audit.py, easycord/plugins/{giveaway,polls,reminder,verification,ai_moderator,invite_tracker,member_logging,moderation,starboard}.py
Extracts SENDABLE_CHANNEL_TYPES into a shared helper module and updates import sites and runtime checks.
Defensive guards and behavior fixes
easycord/plugins/{openclaw,giveaway,polls,reminder,tickets,birthday,economy,levels,scheduled_announcements}.py, related tests
Adds guild/channel/orchestrator guards, adjusts cooldown pruning and role rewards, fixes birthday leap-day logic, and catches scheduled announcement send errors.
AI provider typing
easycord/orchestrator.py, easycord/plugins/_ai_providers.py, easycord/_bot_base.py, tests/test_openclaude.py
Introduces AIProviderProtocol, updates orchestrator typing around it, and extends AI provider and OpenClaude test coverage.
Restart resilience tests
tests/test_restart_resilience.py
Adds tests for Polls, Reminders, and Birthday restart behavior and state restoration.
API cleanup and scaffold updates
easycord/_context_base.py, easycord/plugin_creator.py, easycord/testing.py, easycord/plugin.py, related tests
Raises on ctx.author, deprecates slash_command, updates plugin scaffolds, and adjusts PluginTestSuite.make_plugin.
Built-in plugins example
examples/with-builtin-plugins.py, docs/builtin-plugins.md
Adds an example bot using built-in and opt-in plugins and documents the same setup.

Release Engineering, CI, and Config

Layer / File(s) Summary
CI workflow changes
.github/workflows/*.yml
Removes invalid issue assignees, adjusts triage comment text, and adds plugin test-threshold verification to CI.
Verification and stress-test scripts
scripts/*.py
Adds the plugin test verifier, the test patcher, and the SQLite stress test script.
Version and static-check config
pyproject.toml, pyrightconfig.json, README.md, docs/getting-started.md, easycord/__init__.py
Bumps package/docs versions to v5.51.0 and adjusts Pyright diagnostic settings.

Documentation, Changelog, and Planning Content

Layer / File(s) Summary
Changelog and release notes
CHANGELOG.md, release_v5.51.0/notes.md
Adds v5.51.0 changelog entries and release notes.
Agent and contributor guidance
AGENTS.md, CLAUDE.md, CONTRIBUTING.md
Documents lint/test gates and plugin test thresholds.
Security policy and release checklist
SECURITY.md, RELEASE.md
Rewrites support/reporting policy and adds a release checklist.
Bug and project logs
bugs.md, project.md
Adds incident and decision/triage logs.
Architecture, conventions, implementation plan
context/*.md
Documents the v5.51.0 fixes and invariants.
User-facing documentation
docs/*.md
Reorganizes docs navigation and adds new guides for commands, organizing code, request lifecycle, conversation memory, database readiness, plugin health, and troubleshooting.
Improvement plans and critiques
EasyCord_Critiques_and_Solutions.md, EasyCord_Improvement_Plan/*
Adds long-form critique and roadmap documents.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

Suggested labels: bug, documentation, tests, ci, plugin, release:minor

Poem

A rabbit hops through locks and gates,
Permissions checked before it waits,
Configs mutate, safe and neat,
Hot-reloads swap without defeat,
🐇 v5.51.0 — hop, hop, complete!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main release changes: atomic config updates, plugin fixes, public API exports, and the per-plugin test floor.
Description check ✅ Passed The description is directly aligned with the changeset, covering the config locking, plugin bug fixes, API exports, and test-threshold update.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/config-concurrency-rmw

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.

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

v5.51.0 stability: atomic per-guild config writes, hot-reload gating, permission preflight

🐞 Bug fix ✨ Enhancement 🧪 Tests 📝 Documentation ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Add atomic per-guild config mutation APIs to prevent concurrent write loss.
• Serialize dev hot-reload with dispatch; add bot permission preflight at dispatch.
• Fix multiple plugin crash/edge cases and expand regression/stress test coverage.
Diagram

graph TD
  A["Discord Interaction"] --> B["Command Dispatch"] --> C["Plugin Callback"]
  B --> D["Reload Lock"]
  B --> E["Bot Perm Check"]
  C --> F["ServerConfigStore.mutate"]
  C --> G["TagsStore Locks"]
  C --> H["Localization Metrics"]
  subgraph Legend
    direction LR
    _evt["Event/Request"] ~~~ _core["Core Runtime"] ~~~ _store[("Storage")]
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Move guild configs/tags to transactional DB
  • ➕ Eliminates file-level race conditions without per-guild locks
  • ➕ Enables atomic updates and better scalability across processes
  • ➖ Higher operational complexity and migration cost
  • ➖ Overkill for simple single-process deployments
2. Centralize all per-guild locks in a shared LockManager
  • ➕ Avoids duplicated lock maps (TagsStore vs config store vs plugins)
  • ➕ Easier instrumentation and leak prevention for lock lifecycles
  • ➖ Requires new shared dependency/initialization path
  • ➖ Doesn’t materially improve correctness vs current targeted locks

Recommendation: The PR’s approach (explicit per-guild locks + an atomic mutate API) is the right near-term fix for a single-process asyncio bot: it prevents lost writes with minimal behavioral change and keeps production dispatch lock-free. Consider a DB-backed store only if multi-process or high-scale persistence becomes a requirement.

Files changed (85) +6784 / -564

Enhancement (4) +109 / -32
_bot_commands.pyThread bot_permissions through slash registration pipeline +8/-0

Thread bot_permissions through slash registration pipeline

• Extends slash command registration to carry bot_permissions metadata into callbacks.

easycord/_bot_commands.py

_plugin_scanner.pyPropagate bot_permissions from decorators during plugin scanning +11/-6

Propagate bot_permissions from decorators during plugin scanning

• Enhances plugin method scanning to pass bot_permissions into slash registrations and improves typing.

easycord/_plugin_scanner.py

decorators.pyAdd bot_permissions parameter to @slash decorator +10/-0

Add bot_permissions parameter to @slash decorator

• Extends the public slash decorator API to declare required bot permissions, storing metadata on the function.

easycord/decorators.py

server_config.pyAdd atomic mutate API and refactor load/save internals +80/-26

Add atomic mutate API and refactor load/save internals

• Adds _load_unlocked/_save_unlocked helpers and a new mutate() method that holds the per-guild lock across load-modify-save. Adds timing instrumentation that warns on slow mutation callbacks.

easycord/server_config.py

Bug fix (22) +482 / -236
_bot_plugins.pySerialize hot-reload swaps and validate bot_permissions at startup +35/-5

Serialize hot-reload swaps and validate bot_permissions at startup

• Introduces a bot-wide reload lock to prevent dispatch during plugin swap windows, gated to dev hot-reload mode. Extends plugin permission validation to include explicitly declared bot_permissions.

easycord/_bot_plugins.py

_command_callbacks.pyAdd dispatch-time bot permission checks and hot-reload gating +38/-1

Add dispatch-time bot permission checks and hot-reload gating

• Adds bot_permissions preflight validation for guild commands to fail fast with a clear ephemeral message. When dev hot-reload is active, wraps dispatch in the reload lock to avoid half-unregistered execution.

easycord/_command_callbacks.py

bot.pyAdd guild sync startup timeout and improve AI provider typing +35/-4

Add guild sync startup timeout and improve AI provider typing

• Adds a configurable guild sync timeout to prevent startup hangs, logging and continuing on timeout. Improves typing for ai_provider via a protocol type.

easycord/bot.py

i18n.pyMake LocalizationManager metrics thread-safe +46/-33

Make LocalizationManager metrics thread-safe

• Guards metrics snapshot/reset/increments with a threading.Lock and centralizes counter updates in _record_metric to prevent lost increments under multi-threaded access.

easycord/i18n.py

_config_manager.pyUse atomic ServerConfigStore.mutate for plugin config operations +34/-19

Use atomic ServerConfigStore.mutate for plugin config operations

• Avoids load→save lost updates by routing get/update/default creation through atomic mutate operations while keeping pure reads write-free.

easycord/plugins/_config_manager.py

ai_moderator.pyRequire manage_guild for AI moderator configuration commands +5/-5

Require manage_guild for AI moderator configuration commands

• Adds permissions gating to moderation configuration slash commands to prevent unauthorized config changes.

easycord/plugins/ai_moderator.py

birthday.pyFix Feb 29 handling in non-leap years +8/-7

Fix Feb 29 handling in non-leap years

• Correctly searches forward for the next valid leap year date without raising ValueError on consecutive non-leap years.

easycord/plugins/birthday.py

economy.pyFix possibly-unbound daily reward response fields +6/-0

Fix possibly-unbound daily reward response fields

• Initializes response fields on all reachable paths to avoid unbound variable errors in /daily handling.

easycord/plugins/economy.py

giveaway.pyGuard against missing ctx.channel before accessing channel.id +4/-1

Guard against missing ctx.channel before accessing channel.id

• Adds a ctx.channel None check and responds ephemerally when invoked without a channel context.

easycord/plugins/giveaway.py

levels.pyFix cooldown pruning and apply role rewards consistently +54/-14

Fix cooldown pruning and apply role rewards consistently

• Replaces global cooldown reset with targeted pruning of expired entries and factors role reward granting into a shared helper used by leveling and admin XP grants.

easycord/plugins/levels.py

moderation.pyAdd bot_permissions to destructive commands; make warnings atomic +16/-20

Add bot_permissions to destructive commands; make warnings atomic

• Declares required bot permissions for kick/ban/timeout/mute/unmute to fail fast at dispatch. Updates /warn storage to use an atomic mutate to avoid concurrent lost warnings.

easycord/plugins/moderation.py

openclaw.pyHarden guild-only optional access and orchestrator/tool registry paths +8/-0

Harden guild-only optional access and orchestrator/tool registry paths

• Adds asserts/guards to prevent Optional attribute crashes in guild-only commands and handles missing tool registry gracefully.

easycord/plugins/openclaw.py

polls.pyGuard against missing ctx.channel before storing channel_id +4/-1

Guard against missing ctx.channel before storing channel_id

• Adds a ctx.channel None check and responds ephemerally when invoked without a channel context.

easycord/plugins/polls.py

reaction_roles.pyMake reaction-role config updates atomic and avoid writes on no-op deletes +44/-36

Make reaction-role config updates atomic and avoid writes on no-op deletes

• Moves mapping add/remove/cleanup operations to ServerConfigStore.mutate to prevent lost updates and keeps common no-mapping delete events read-only.

easycord/plugins/reaction_roles.py

reminder.pyGuard against missing ctx.channel before storing channel_id +4/-1

Guard against missing ctx.channel before storing channel_id

• Adds a ctx.channel None check and responds ephemerally when invoked without a channel context.

easycord/plugins/reminder.py

role_persistence.pyFix role persistence correctness and make writes atomic +33/-20

Fix role persistence correctness and make writes atomic

• Saves roles without filtering by current bot hierarchy and persists via atomic mutate. Cleans up saved records only after successful restore (or when roles are stale).

easycord/plugins/role_persistence.py

scheduled_announcements.pyMake announcement loop resilient to send failures +4/-1

Make announcement loop resilient to send failures

• Catches Forbidden/HTTPException on channel send and logs a warning instead of killing the loop.

easycord/plugins/scheduled_announcements.py

starboard.pyMake archive mapping atomic and guard bot user Optional access +24/-14

Make archive mapping atomic and guard bot user Optional access

• Writes archived message mappings via atomic mutate and avoids None dereference of bot.user. Adds asserts for guild-only config commands.

easycord/plugins/starboard.py

suggestions.pyRemove dead field and make suggestion ID/state updates atomic +47/-36

Remove dead field and make suggestion ID/state updates atomic

• Eliminates unused suggestion_counter field and uses atomic mutate for ID allocation and suggestion status updates to prevent concurrent collisions/lost writes.

easycord/plugins/suggestions.py

tags.pyAdd per-guild async locks for tag storage and align admin checks +23/-15

Add per-guild async locks for tag storage and align admin checks

• Serializes tag JSON writes with a per-guild asyncio.Lock, making set/delete async, and uses ctx.is_admin for delete authorization.

easycord/plugins/tags.py

tickets.pyGuard persistent view against DM context and fix transcript capture order +9/-2

Guard persistent view against DM context and fix transcript capture order

• Adds a guild guard for claim button interactions and captures the most recent messages for transcripts while keeping chronological output.

easycord/plugins/tickets.py

registry.pyFix TTL boundary for interaction registry entries +1/-1

Fix TTL boundary for interaction registry entries

• Changes expiry check to strict 'expires_at > now' so ttl=0 entries are inactive immediately.

easycord/registry.py

Refactor (4) +39 / -25
_bot_base.pyTighten core bot base typing (EventBus attribute) +2/-0

Tighten core bot base typing (EventBus attribute)

• Updates type-only imports/attributes to better reflect bot base capabilities.

easycord/_bot_base.py

_command_registration.pyImprove typing and pass bot_permissions to callback builder +13/-7

Improve typing and pass bot_permissions to callback builder

• Tightens type signatures with TYPE_CHECKING and threads bot_permissions through registration helpers.

easycord/_command_registration.py

orchestrator.pyRefine orchestrator typing for provider protocol and tool schemas +12/-12

Refine orchestrator typing for provider protocol and tool schemas

• Uses an AIProviderProtocol structural type and strengthens typing for messages/tools schemas while avoiding unsafe attribute access via setattr/getattr.

easycord/orchestrator.py

_ai_providers.pyIntroduce AIProviderProtocol and quiet optional import type errors +12/-6

Introduce AIProviderProtocol and quiet optional import type errors

• Adds a runtime-checkable protocol for AI providers and annotates optional third-party imports for static checking.

easycord/plugins/_ai_providers.py

Tests (20) +2179 / -29
test_ai_moderator.pyAdd AI moderator plugin tests +135/-0

Add AI moderator plugin tests

• Adds tests for AI moderator configuration and behavior, including permission gating.

tests/test_ai_moderator.py

test_birthday.pyAdd birthday plugin regression tests +22/-0

Add birthday plugin regression tests

• Adds tests covering edge cases like Feb 29 scheduling behavior.

tests/test_birthday.py

test_guild_sync_timeout.pyAdd tests for guild sync timeout behavior +206/-0

Add tests for guild sync timeout behavior

• Adds coverage for bounded startup guild sync and timeout logging/continuation behavior.

tests/test_guild_sync_timeout.py

test_hot_reload.pyAdd tests for reload lock behavior and gating +55/-0

Add tests for reload lock behavior and gating

• Adds regression tests ensuring reload lock is idempotent, held during swap, and gated to dev watcher activity.

tests/test_hot_reload.py

test_i18n.pyAdd thread-safety regression tests for localization metrics +44/-0

Add thread-safety regression tests for localization metrics

• Adds multi-threaded contention tests ensuring metrics counters and locale frequency are not lost.

tests/test_i18n.py

test_levels_plugin.pyAdd levels plugin tests +43/-0

Add levels plugin tests

• Adds tests for levels behaviors including XP progression and role rewards.

tests/test_levels_plugin.py

test_openclaude.pyAdd OpenClaw/OpenClaude plugin tests +171/-0

Add OpenClaw/OpenClaude plugin tests

• Adds tests covering OpenClaw/OpenClaude command flows and edge conditions.

tests/test_openclaude.py

test_permission_validator.pyAdd tests for bot_permissions dispatch-time blocking +73/-0

Add tests for bot_permissions dispatch-time blocking

• Adds tests verifying bot_permissions preflight blocks execution when missing and allows when present.

tests/test_permission_validator.py

test_plugins.pyUpdate plugin tests for new behaviors/async paths +21/-15

Update plugin tests for new behaviors/async paths

• Adjusts existing plugin test expectations to match updated runtime behavior and APIs.

tests/test_plugins.py

test_plugins_new.pyUpdate new plugin tests for new behaviors/async paths +17/-13

Update new plugin tests for new behaviors/async paths

• Adjusts tests to match updated registration/dispatch behaviors.

tests/test_plugins_new.py

test_pr71_concurrency.pyAdd concurrency stress tests for atomic config mutation and plugins +219/-0

Add concurrency stress tests for atomic config mutation and plugins

• Adds stress tests validating ServerConfigStore.mutate correctness under high concurrency, plus plugin-level concurrency tests for suggestions and role persistence.

tests/test_pr71_concurrency.py

test_registry_component.pyAdd interaction registry TTL/component tests +70/-0

Add interaction registry TTL/component tests

• Adds regression coverage for component TTL boundary and related behaviors.

tests/test_registry_component.py

test_reminder.pyAdd reminder plugin tests +12/-0

Add reminder plugin tests

• Adds tests for reminder scheduling and channel-context guarding.

tests/test_reminder.py

test_restart_resilience.pyAdd restart resilience tests +245/-0

Add restart resilience tests

• Adds coverage ensuring bot/plugin behavior remains correct across restart-like scenarios.

tests/test_restart_resilience.py

test_role_persistence.pyAdd role persistence plugin tests +146/-0

Add role persistence plugin tests

• Adds tests covering save/restore behavior and cleanup semantics.

tests/test_role_persistence.py

test_server_config.pyAdd server config store tests +80/-0

Add server config store tests

• Adds tests for config store behaviors including persistence and mutation semantics.

tests/test_server_config.py

test_starboard.pyAdd starboard plugin tests +197/-0

Add starboard plugin tests

• Adds tests for starboard configuration and archive/unarchive behaviors.

tests/test_starboard.py

test_suggestions.pyAdd suggestions plugin tests +223/-0

Add suggestions plugin tests

• Adds tests for suggestion submission, ID allocation, and status transitions.

tests/test_suggestions.py

test_tags.pyAdd tags store and tags plugin tests +162/-0

Add tags store and tags plugin tests

• Adds unit and command-flow tests covering async set/delete, per-guild isolation, and admin/author delete authorization.

tests/test_tags.py

test_tickets.pyUpdate tickets plugin tests +38/-1

Update tickets plugin tests

• Updates tests to cover DM guild-guard behavior and transcript ordering expectations.

tests/test_tickets.py

Documentation (24) +3747 / -212
AGENTS.mdDocument agent/review automation guidance +6/-0

Document agent/review automation guidance

• Adds additional guidance for agents/review automation usage and expectations.

AGENTS.md

CHANGELOG.mdAdd v5.51.0 changelog entry +39/-0

Add v5.51.0 changelog entry

• Adds a new v5.51.0 section documenting the stability fixes and changes.

CHANGELOG.md

CLAUDE.mdUpdate Claude project instructions +18/-2

Update Claude project instructions

• Refines project instructions and expectations for automated change generation.

CLAUDE.md

CONTRIBUTING.mdExpand contributor guidance +10/-0

Expand contributor guidance

• Adds new contributor notes/process guidance aligned with current workflows.

CONTRIBUTING.md

EasyCord_Critiques_and_Solutions.mdAdd audit critiques and solutions document +1412/-0

Add audit critiques and solutions document

• Introduces a large audit write-up capturing critiques and proposed fixes.

EasyCord_Critiques_and_Solutions.md

AUDIT_FINDINGS.mdAdd audit findings document +264/-0

Add audit findings document

• Adds an audit findings document to track issues and remediation notes.

EasyCord_Improvement_Plan/AUDIT_FINDINGS.md

EASYCORD_IMPROVEMENT_PLAN.mdAdd long-form improvement plan +957/-0

Add long-form improvement plan

• Adds a comprehensive improvement plan describing intended follow-up work.

EasyCord_Improvement_Plan/EASYCORD_IMPROVEMENT_PLAN.md

IMPLEMENTATION_SUMMARY.txtAdd implementation summary for audit work +334/-0

Add implementation summary for audit work

• Adds an implementation summary capturing what was done and why.

EasyCord_Improvement_Plan/IMPLEMENTATION_SUMMARY.txt

README.mdRefresh README for v5.51.0 messaging +3/-3

Refresh README for v5.51.0 messaging

• Updates README content to align with current behavior and release info.

README.md

RELEASE.mdAdd release process details for v5.51.x +60/-0

Add release process details for v5.51.x

• Adds additional release guidance and validation steps for maintainers.

RELEASE.md

SECURITY.mdUpdate security policy and reporting details +22/-9

Update security policy and reporting details

• Refreshes security guidance, reporting expectations, and policy details.

SECURITY.md

bugs.mdRecord new audited bugs and status +181/-0

Record new audited bugs and status

• Adds entries for newly identified bugs and tracks deferred items.

bugs.md

architecture.mdUpdate architecture context notes +5/-3

Update architecture context notes

• Refines architecture documentation to reflect current components and patterns.

context/architecture.md

conventions.mdAdd/clarify project conventions +5/-0

Add/clarify project conventions

• Adds conventions guidance to improve consistency across contributions.

context/conventions.md

implementation-plan.mdAdd implementation planning guidance +90/-0

Add implementation planning guidance

• Documents implementation planning approach and sequencing for changes.

context/implementation-plan.md

README.mdUpdate docs landing page +3/-0

Update docs landing page

• Adds small documentation updates to the docs index/README.

docs/README.md

conversation-memory.mdStreamline conversation memory documentation +24/-194

Streamline conversation memory documentation

• Removes large sections and keeps updated, focused guidance for conversation memory.

docs/conversation-memory.md

database-guide.mdAdd database usage guide +44/-0

Add database usage guide

• Adds guidance for database configuration, operation, and troubleshooting.

docs/database-guide.md

getting-started.mdMinor getting-started correction +1/-1

Minor getting-started correction

• Updates a small getting-started detail for correctness.

docs/getting-started.md

plugin-ecosystem-health.mdAdd plugin ecosystem health documentation +39/-0

Add plugin ecosystem health documentation

• Introduces documentation describing plugin health checks and expectations.

docs/plugin-ecosystem-health.md

troubleshooting.mdAdd expanded troubleshooting guide +60/-0

Add expanded troubleshooting guide

• Adds troubleshooting steps for common runtime and configuration issues.

docs/troubleshooting.md

project.mdUpdate project documentation and planning notes +97/-0

Update project documentation and planning notes

• Adds/updates project-level documentation describing goals, scope, and planning.

project.md

notes.mdAdd v5.50.2 release notes +32/-0

Add v5.50.2 release notes

• Adds release notes documenting installation/upgrade guidance for v5.50.2.

release_v5.50.2/notes.md

notes.mdAdd v5.51.0 release notes +41/-0

Add v5.51.0 release notes

• Adds v5.51.0 notes describing the stability release, upgrade steps, and test plan.

release_v5.51.0/notes.md

Other (11) +228 / -30
ci-failure-reporter.ymlTweak CI failure reporter workflow configuration +0/-1

Tweak CI failure reporter workflow configuration

• Removes a single workflow setting/line to adjust CI failure reporting behavior.

.github/workflows/ci-failure-reporter.yml

nightly.ymlAdjust nightly workflow configuration +0/-1

Adjust nightly workflow configuration

• Removes a single nightly workflow setting/line to reflect updated automation needs.

.github/workflows/nightly.yml

tests.ymlAdd plugin test-coverage gate to CI +2/-0

Add plugin test-coverage gate to CI

• Adds a CI step to verify plugin test coverage thresholds before running pytest.

.github/workflows/tests.yml

triage.ymlSimplify issue triage automation messaging +1/-8

Simplify issue triage automation messaging

• Removes auto-assignment behavior and updates the triage comment copy.

.github/workflows/triage.yml

__init__.pyBump package version to 5.51.0 +1/-1

Bump package version to 5.51.0

• Updates the exported __version__ constant for the v5.51.0 release.

easycord/init.py

config.pyAdd guild_sync_timeout config/env support +34/-11

Add guild_sync_timeout config/env support

• Adds BotConfig.guild_sync_timeout with environment variable parsing and passes it into bot construction.

easycord/config.py

pyproject.tomlBump version to 5.51.0 and tune Pyright behavior for tests +14/-3

Bump version to 5.51.0 and tune Pyright behavior for tests

• Updates project version/links for v5.51.0 and adds a Pyright execution environment to relax diagnostics in tests.

pyproject.toml

pyrightconfig.jsonTighten Pyright configuration and path resolution +14/-5

Tighten Pyright configuration and path resolution

• Adds extraPaths, refines excludes, and adjusts report severities to improve static checking signal.

pyrightconfig.json

patch_tests.pyAdd test patching utility script +16/-0

Add test patching utility script

• Introduces a script to help update/patch tests as part of maintenance workflows.

scripts/patch_tests.py

stress_test_db.pyAdd database stress test script +71/-0

Add database stress test script

• Introduces a stress testing script for DB/backing store behaviors under load.

scripts/stress_test_db.py

verify_plugin_tests.pyAdd plugin test coverage/threshold verifier +75/-0

Add plugin test coverage/threshold verifier

• Adds a script used in CI to enforce plugin test coverage thresholds.

scripts/verify_plugin_tests.py

@coderabbitai coderabbitai Bot added bug Something isn't working ci documentation Improvements or additions to documentation plugin release:minor tests labels Jul 1, 2026
@qodo-code-review

qodo-code-review Bot commented Jul 1, 2026

Copy link
Copy Markdown

Code Review by Qodo

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

Context used
✅ Compliance rules (platform): 28 rules

Grey Divider


Action required

1. Tag delete TOCTOU ✓ Resolved 🐞 Bug ⛨ Security
Description
TagsPlugin.delete performs the authorization check using TagsStore.get() without holding the
per-guild lock, then later deletes under a lock. A concurrent update to the same tag name between
the check and delete can let a user delete a tag they no longer own.
Code

easycord/plugins/tags.py[R78-88]

        if entry is None:
            await ctx.respond(ctx.t("tags.not_found", default="Tag `{name}` not found.", name=name), ephemeral=True)
            return
-        member = ctx.guild.get_member(ctx.user.id)
-        is_admin = member is not None and member.guild_permissions.administrator
-        if ctx.user.id != entry["author_id"] and not is_admin:
+        if ctx.user.id != entry["author_id"] and not ctx.is_admin:
            await ctx.respond(
                ctx.t("tags.cannot_delete", default="You can only delete your own tags (or be an admin)."),
                ephemeral=True,
            )
            return
-        self._store.delete(ctx.guild_id, name)
+        await self._store.delete(ctx.guild_id, name)
        await ctx.respond(ctx.t("tags.deleted", default="Tag `{name}` deleted.", name=name), ephemeral=True)
Relevance

⭐⭐ Medium

No prior accepted/rejected history found for TOCTOU auth-check vs locked delete in tags store.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
TagsStore.get() reads from disk without the per-guild lock, while delete acquires the lock only for
the write; the plugin uses the unlocked read for author_id authorization before doing the locked
delete.

easycord/plugins/tags.py[35-49]
easycord/plugins/tags.py[76-88]

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

### Issue description
The delete flow is a read-check-write sequence split across an unlocked `get()` and a locked `delete()`. This is a TOCTOU race: ownership can change after the check but before deletion.

### Issue Context
You added per-guild locks to prevent concurrent write issues, but delete authorization depends on `entry["author_id"]`, so correctness/security requires that the check and deletion are performed atomically under the same lock.

### Fix Focus Areas
- easycord/plugins/tags.py[35-49]
- easycord/plugins/tags.py[76-88]

### Suggested fix
Make delete authorization + deletion atomic under the per-guild lock, e.g.:
- Add a new `TagsStore` method like `async def delete_if_allowed(guild_id, name, *, requester_id, is_admin) -> bool` that:
 - acquires the guild lock
 - loads the tag
 - checks author/admin
 - deletes and saves
 - returns True/False
- Update `TagsPlugin.delete` to call that method and respond based on its result.

This keeps the auth decision tied to the exact state being deleted.

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


2. Timeout cancels DB sync ✓ Resolved 🐞 Bug ☼ Reliability
Description
Bot._sync_guilds_with_timeout uses asyncio.wait_for() around db.sync_guilds(), which cancels the
coroutine on timeout. If cancellation happens while SQLiteDatabase is awaiting asyncio.to_thread()
inside an async lock, the lock can be released while the thread continues, allowing overlapping
sqlite access and unstable DB behavior.
Code

easycord/bot.py[R359-375]

+    async def _sync_guilds_with_timeout(self) -> None:
+        """Run ``db.sync_guilds`` bounded by ``_guild_sync_timeout``.
+
+        When a positive timeout is configured the sync is wrapped in
+        ``asyncio.wait_for``; on expiry a warning is logged and startup
+        continues uninterrupted.  Pass ``guild_sync_timeout=None`` (or
+        ``<=0``) to opt out and restore the original unbounded behaviour.
+        """
+        guild_ids = [guild.id for guild in getattr(self, "guilds", [])]
+        timeout = self._guild_sync_timeout
+        if timeout is not None and timeout > 0:
+            try:
+                await asyncio.wait_for(
+                    self.db.sync_guilds(guild_ids),
+                    timeout=timeout,
+                )
+            except asyncio.TimeoutError:
Relevance

⭐⭐ Medium

No direct historical evidence on wait_for cancellation/SQLite lock issues; reliability hardening
often rejected in past PRs.

PR-#29
PR-#58

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The new startup/on_ready sync wrapper cancels db.sync_guilds() on timeout, and the sqlite backend
performs work in background threads while holding an async lock; cancellation does not stop the
thread, so the lock can be released before the thread finishes.

easycord/bot.py[359-383]
easycord/database.py[140-151]

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

### Issue description
`asyncio.wait_for()` cancels `db.sync_guilds()` on timeout. Because the sqlite backend uses `asyncio.to_thread()` inside `async with self._lock`, cancellation can release the lock while the worker thread keeps running, allowing concurrent access to the same sqlite connection.

### Issue Context
We want startup to proceed if sync takes too long, but we must avoid cancellation that can break the DB backend’s lock/thread-safety assumptions.

### Fix Focus Areas
- easycord/bot.py[359-383]
- easycord/database.py[140-151]

### Suggested fix
In `Bot._sync_guilds_with_timeout`, avoid cancellation by:
- creating a task for `self.db.sync_guilds(guild_ids)`
- using `asyncio.wait(..., timeout=...)` or `asyncio.wait_for(asyncio.shield(task), ...)`
- if the timeout expires, log warning and let the task continue in background (attach a done-callback to log exceptions), instead of cancelling it.

Alternative (more invasive): make DB backend cancellation-safe by shielding `asyncio.to_thread(...)` calls (or otherwise ensuring the lock isn’t released until the thread completes), but this is broader than needed for the startup-time timeout behavior.

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



Informational

3. Bot logic added in bot.py 📘 Rule violation ⌂ Architecture
Description
Bot._sync_guilds_with_timeout() adds new bot-level guild-sync behavior directly in
easycord/bot.py instead of the appropriate _bot_*.py mixin file. This breaks the project’s
mixin-only behavior rule for bot.py, increasing coupling and making future extensions harder to
manage.
Code

easycord/bot.py[R359-384]

+    async def _sync_guilds_with_timeout(self) -> None:
+        """Run ``db.sync_guilds`` bounded by ``_guild_sync_timeout``.
+
+        When a positive timeout is configured the sync is wrapped in
+        ``asyncio.wait_for``; on expiry a warning is logged and startup
+        continues uninterrupted.  Pass ``guild_sync_timeout=None`` (or
+        ``<=0``) to opt out and restore the original unbounded behaviour.
+        """
+        guild_ids = [guild.id for guild in getattr(self, "guilds", [])]
+        timeout = self._guild_sync_timeout
+        if timeout is not None and timeout > 0:
+            try:
+                await asyncio.wait_for(
+                    self.db.sync_guilds(guild_ids),
+                    timeout=timeout,
+                )
+            except asyncio.TimeoutError:
+                logger.warning(
+                    "Guild sync timed out after %.1fs for %d guild(s); "
+                    "startup will continue without a complete sync.",
+                    timeout,
+                    len(guild_ids),
+                )
+        else:
+            await self.db.sync_guilds(guild_ids)
+
Relevance

⭐ Low

Team previously rejected moving new bot behavior out of bot.py into _bot_ mixins (PR #39).

PR-#39

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 526288 requires new bot-level behavior to be implemented in _bot_*.py mixins
rather than directly in bot.py. The diff adds a new concrete method _sync_guilds_with_timeout()
inside easycord/bot.py and uses it from lifecycle hooks, which is substantial behavior beyond
wiring.

Rule 526288: Add new bot-level behavior via mixin files, not directly in bot.py
easycord/bot.py[359-384]

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

## Issue description
New bot behavior (`_sync_guilds_with_timeout`) was implemented directly in `easycord/bot.py`, but policy requires bot behavior to live in mixin modules and keep `bot.py` as wiring/thin delegation.

## Issue Context
A `_bot_guild.py` mixin exists and is the appropriate home for guild-sync related behavior.

## Fix Focus Areas
- easycord/bot.py[359-384]
- easycord/_bot_guild.py[1-200]

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


4. Starboard test uses normal constructor 📘 Rule violation ▣ Testability
Description
The new Starboard and restart-resilience plugin tests create plugin instances by calling the normal
constructors (e.g., StarboardPlugin() / PollsPlugin(...)) and then manually setting
plugin._bot, instead of constructing via PluginSubclass.__new__ and setting _bot directly as
required. This can trigger avoidable __init__ side effects, making tests slower and less isolated.
Code

tests/test_starboard.py[R14-18]

+def _make_plugin(tmp_path) -> StarboardPlugin:
+    plugin = StarboardPlugin()
+    plugin.config = PluginConfigManager(str(tmp_path / "starboard"))
+    plugin._bot = MagicMock()
+    return plugin
Relevance

⭐ Low

Test guideline to set plugin._bot / construct plugins specially was rejected previously (PR #60).

PR-#60

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 1073283 specifies that tests must construct plugin instances using
PluginSubclass.__new__ and then set plugin._bot directly, rather than invoking the normal
constructor. In the added code, the Starboard helper _make_plugin uses StarboardPlugin() before
assigning _bot, and the restart-resilience tests similarly instantiate plugins via their
constructors (e.g., PollsPlugin(store_path=...)) and then assign plugin._bot, which is the
non-compliant pattern the rule is meant to prevent.

Rule 1073283: Construct Plugin instances in tests via new and set _bot directly
tests/test_starboard.py[14-18]
tests/test_restart_resilience.py[88-91]

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

## Issue description
Update the new tests so that any time they only need a plugin instance with an attached bot, they construct the plugin via `PluginSubclass.__new__` and set `plugin._bot` directly, instead of calling the plugin’s normal constructor.

## Issue Context
PR Compliance ID 1073283 requires `__new__`-based construction to keep tests lightweight and avoid triggering plugin `__init__` initialization behaviors and side effects when they aren’t needed, improving test speed and isolation.

## Fix Focus Areas
- tests/test_starboard.py[14-18]
- tests/test_restart_resilience.py[88-91]
- tests/test_restart_resilience.py[103-106]
- tests/test_restart_resilience.py[115-118]
- tests/test_restart_resilience.py[126-129]
- tests/test_restart_resilience.py[143-146]
- tests/test_restart_resilience.py[187-196]

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


5. Test imports easycord.server_config submodule 📘 Rule violation ⌂ Architecture
Description
New tests outside the easycord/ package import EasyCord types from internal submodules (including
underscored modules like easycord.plugins._config_manager) such as ServerConfigStore from
easycord.server_config and InteractionRegistry from easycord.registry instead of using the
public root easycord API. This violates the public-import-only rule and makes tests (and any
external code) brittle to internal refactors and module layout changes.
Code

tests/test_pr71_concurrency.py[R12-15]

+from easycord.plugins._config_manager import PluginConfigManager
+from easycord.plugins.role_persistence import RolePersistencePlugin
+from easycord.plugins.suggestions import SuggestionsPlugin
+from easycord.server_config import ServerConfigStore
Relevance

⭐ Low

Public-API-only test import boundary suggestions were repeatedly rejected (e.g., PRs #45, #46, #39).

PR-#45
PR-#46
PR-#39

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 526296 requires any code outside easycord/ to import only from the public
EasyCord API (the root package re-exports) rather than internal submodules. The cited tests import
ServerConfigStore via easycord.server_config, import InteractionRegistry from (and also
import) easycord.registry for monkeypatching, and import PluginConfigManager from
easycord.plugins._config_manager, which contains an underscore path component indicating an
internal module; together these citations demonstrate direct reliance on internal module structure
rather than the public easycord surface.

Rule 526296: Only import public easycord API from outside the package
tests/test_pr71_concurrency.py[12-15]
tests/test_starboard.py[10-10]
tests/test_registry_component.py[14-14]

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

## Issue description
Tests outside the `easycord/` package are importing EasyCord classes from internal submodules (including underscored internal modules) such as `easycord.server_config`, `easycord.registry`, and `easycord.plugins._config_manager` instead of importing from the public root `easycord` API. Update the tests to use only the public `easycord` imports, and if needed, expose the required symbols via an explicit public API (preferably re-exports from `easycord/__init__.py` or another clearly public, non-underscored module).

## Issue Context
- PR Compliance ID 526296 requires code outside `easycord/` (including tests) to import only from the public root API surface.
- `ServerConfigStore` is already re-exported by `easycord/__init__.py`, so tests should import it via `from easycord import ServerConfigStore`.
- For `InteractionRegistry` and `PluginConfigManager`, either:
 - Re-export a public name from `easycord/__init__.py` (preferred) and update tests to import from `easycord`, or
 - Provide an explicitly public, non-underscored module path intended for consumers/tests, or
 - Replace usage in tests with a test double.

## Fix Focus Areas
- tests/test_pr71_concurrency.py[12-15]
- tests/test_starboard.py[10-10]
- tests/test_registry_component.py[14-14]
- tests/test_registry_component.py[54-54]
- easycord/__init__.py[21-60]
- easycord/__init__.py[45-55]

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


View more (1)
6. Bot perms None-channel crash 🐞 Bug ≡ Correctness
Description
build_slash_callback now calls ctx.bot_permissions when bot_permissions are specified, but
ctx.channel can be None even in guild contexts. ctx.bot_permissions dereferences
self.channel.permissions_for(...), which will raise when channel is None, causing commands to crash
instead of returning a clean error.
Code

easycord/_command_callbacks.py[R151-168]

+            if bot_permissions:
+                # Validate the *bot's* own permissions before running, so a
+                # command that calls a privileged API (member.kick(), etc.) fails
+                # fast with a clear message instead of executing and raising
+                # Forbidden partway through. Distinct from the user check above.
+                if not ctx.guild:
+                    await ctx.respond(
+                        ctx.t(
+                            "errors.guild_only",
+                            default="This command can only be used inside a server.",
+                        ),
+                        ephemeral=True,
+                    )
+                    return
+                bot_perms = ctx.bot_permissions
+                missing_bot = [
+                    p for p in bot_permissions if not getattr(bot_perms, p, False)
+                ]
Relevance

⭐ Low

Similar request to guard Context.bot_permissions against None channel was rejected in PR #1.

PR-#1

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The new bot-permissions dispatch path dereferences ctx.bot_permissions; the context implementation
allows ctx.channel to be None, and this PR itself adds guards for that condition, proving it occurs
in real execution paths.

easycord/_command_callbacks.py[151-168]
easycord/_context_base.py[41-43]
easycord/_context_base.py[430-440]
easycord/plugins/giveaway.py[296-304]

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

### Issue description
The new `bot_permissions` gate assumes `ctx.channel` is always present when `ctx.guild` is present. That’s not true in this codebase (several plugins now guard `ctx.channel is None`). Calling `ctx.bot_permissions` can therefore raise at dispatch time.

### Issue Context
`BaseContext.channel` is optional and directly returns `interaction.channel`. `BaseContext.bot_permissions` uses `self.channel.permissions_for(...)` without a None check.

### Fix Focus Areas
- easycord/_command_callbacks.py[151-178]
- easycord/_context_base.py[41-43]
- easycord/_context_base.py[430-440]

### Suggested fix
In `build_slash_callback`, before reading `ctx.bot_permissions`, add:
- if `ctx.channel is None`: respond ephemerally with a clear message (e.g., "This command must be used in a channel.") and return.

Additionally (recommended), harden `BaseContext.bot_permissions`:
- if `self.channel is None`: raise a `RuntimeError("bot_permissions requires a channel context")`

This ensures the dispatch-time check cannot crash on a missing channel object.

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


Grey Divider

Qodo Logo

Comment thread easycord/bot.py
Comment thread easycord/plugins/tags.py Outdated
- Fix asyncio.wait_for() cancellation race in _sync_guilds_with_timeout by using
  asyncio.shield() to let DB sync complete in background without cancellation.
  Prevents SQLite lock corruption when timeout interrupts mid-lock.

- Fix TOCTOU in tags.delete() by adding atomic delete_if_authorized() method that
  performs authorization check and deletion under a single per-guild lock.
  Prevents race where tag ownership changes between check and delete.

Fixes Qodo security/reliability findings in PR #72.
All 1307 tests passing.
rolling-codes and others added 2 commits July 2, 2026 17:15
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
…import'

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown

✅ Performance benchmarks completed. All thresholds maintained.

See workflow artifacts for detailed results.

rolling-codes and others added 2 commits July 2, 2026 17:16
…tiple times'

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
…import'

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Comment thread easycord/_plugin_scanner.py Fixed
Comment thread easycord/_command_registration.py Fixed
Comment thread easycord/_command_registration.py Fixed
rolling-codes and others added 2 commits July 2, 2026 17:18
…import'

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
…ect'

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown

✅ Performance benchmarks completed. All thresholds maintained.

See workflow artifacts for detailed results.

Comment thread easycord/_plugin_scanner.py Fixed
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown

✅ Performance benchmarks completed. All thresholds maintained.

See workflow artifacts for detailed results.

The auto-applied 'Potential fix' commits fixed findings already triaged
as false positives (see PR #72 triage comment) and introduced real
breakage:
- Deleted TYPE_CHECKING imports while leaving the string annotations
  behind -> six F821 undefined-name errors in the blocking ruff gate
  (_BotBase x5, AIProviderProtocol x1).
- Replaced _BotBase with an empty _BotLike Protocol (matches anything)
  and a guessed _PluginLike, violating the documented _MixinBase
  architecture (CLAUDE.md key invariants).
- Removed two of the three deliberate seed assignments in economy.py
  that the in-code comment explicitly defends.
- Rewrote the levels prune test to use _COOLDOWN_PRUNE_THRESHOLD.__module__
  on an int, which raises AttributeError at runtime.

Restores the verified state: blocking ruff green, 1335-test suite green.
@rolling-codes

Copy link
Copy Markdown
Owner Author

Heads-up on the CodeQL autofix suggestions: the ten auto-applied "Potential fix" commits were reverted in d01715a — they broke the blocking ruff gate (six F821s: TYPE_CHECKING imports deleted while their string annotations remained), replaced _BotBase with an empty Protocol (violating the _MixinBase architecture documented in CLAUDE.md), removed deliberate seed assignments in economy.py, and rewrote a levels test to call .__module__ on an int (runtime AttributeError).

Please don't re-apply these suggestions — the underlying alerts are triaged as false positives in the comment above. If the alert noise is a problem, the durable options are dismissing the alerts in the Security tab or adding a CodeQL config that excludes py/unsafe-cyclic-import for the mixin modules.

@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown

✅ Performance benchmarks completed. All thresholds maintained.

See workflow artifacts for detailed results.

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown

✅ Performance benchmarks completed. All thresholds maintained.

See workflow artifacts for detailed results.

Comment thread easycord/_command_registration.py Fixed
rolling-codes and others added 2 commits July 3, 2026 10:52
…import'

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
…tests

- _command_registration.py: restore TYPE_CHECKING for Context/InteractionRegistry,
  introduce _BotBaseLike Protocol with tree/registry/_plugins/_dispatch_framework_error
  to satisfy pyright without creating a cyclic import
- pyrightconfig.json: reportFunctionMemberAccess = "warning" (audit finding)
- pyproject.toml: fill authors field
- tests/test_reminder.py: fix three unawaited-coroutine RuntimeWarnings
- tests/test_context_ui.py: 23 tests for UIMixin (confirm/paginate/choose/ask_form/prompt)
- tests/test_tickets_commands.py: 23 tests for TicketsPlugin
- tests/test_giveaway_commands.py: 20 tests for GiveawayPlugin
@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown

✅ Performance benchmarks completed. All thresholds maintained.

See workflow artifacts for detailed results.

Comment thread tests/test_context_ui.py Fixed
Comment thread tests/test_giveaway_commands.py Fixed
Comment thread tests/test_tickets_commands.py Fixed
Comment thread tests/test_tickets_commands.py Fixed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Caution

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

⚠️ Outside diff range comments (4)
release_v5.51.0/notes.md (1)

40-42: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Update the upgrade note to reflect the API migration.

ctx.author now raises and slash_command is only a deprecation shim, so “No breaking changes” is misleading. Call out the migration impact here instead.

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

In `@release_v5.51.0/notes.md` around lines 40 - 42, The upgrade note under
Upgrade Notes is misleading because the API migration does introduce breaking
behavior around ctx.author and the slash_command deprecation shim. Update the
release note text in notes.md to explicitly mention the migration impact and
warn users that ctx.author now raises while slash_command is only a deprecation
shim, instead of claiming there are no breaking changes.
context/implementation-plan.md (1)

58-67: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Make the verification counts consistent.

This plan still says the suite “stays at 1213 passed,” while the verification block says 1307 passed, and the current PR status reports 1335 passing tests. Please collapse this into one final figure, or explicitly label the earlier numbers as intermediate checkpoints.

Also applies to: 83-87

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

In `@context/implementation-plan.md` around lines 58 - 67, The verification
numbers in this plan are inconsistent across the summary and the verification
block. Update the “Type-checking cleanup” section to use one final test count,
or clearly mark the older 1213-passed and 1307-passed values as intermediate
checkpoints, while aligning the final stated result with the current 1335
passing tests. Refer to the verification summary text in this document and keep
the counts internally consistent everywhere they appear.
easycord/plugins/starboard.py (2)

264-297: 📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

Slash command responses use hardcoded strings instead of LocalizationManager.

Lines 266, 272, 278-279, 281, and 294 respond with hardcoded English text. Per coding guidelines, plugin responses in easycord/plugins/**/*.py must use LocalizationManager lookups instead of hardcoded strings (the PR's commit log shows this convention being applied elsewhere, e.g., localizing a polls guard response).

As per coding guidelines, "Plugin responses must use LocalizationManager lookups; hardcoded strings should not be used in plugin responses."

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

In `@easycord/plugins/starboard.py` around lines 264 - 297, The slash command
responses in starboard are still hardcoded English strings, which violates the
plugin localization rule. Update the response paths in starboard_config-related
commands and the other slash handlers in Starboard to fetch messages through
LocalizationManager instead of embedding literals directly in ctx.respond. Use
the existing localization pattern used elsewhere in the plugin, and keep the
command methods’ behavior the same while swapping the response text sources.

Source: Coding guidelines


181-193: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider extracting the shared reaction-guard logic.

_on_reaction_add and _on_reaction_remove duplicate the same self-reaction/enabled/emoji guard sequence. This exact duplicated enabled check was where the B-018 bug originated (one copy had the default, the pattern needed a fix in two places). Extracting a small helper (e.g. _should_process_reaction(guild, payload) -> dict | None) would prevent the two copies from drifting again.

Also applies to: 224-234

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

In `@easycord/plugins/starboard.py` around lines 181 - 193, The reaction handling
in _on_reaction_add and _on_reaction_remove repeats the same self-reaction,
guild lookup, config fetch, enabled default, and emoji guard logic, which can
drift and reintroduce bugs. Extract that shared sequence into a small helper
such as _should_process_reaction(...) and have both handlers call it before
continuing, so the enabled/default handling and early returns live in one place.
♻️ Duplicate comments (4)
README.md (1)

83-97: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Hardcoded bot token example still unresolved.

Line 96 still shows bot.run("YOUR_DISCORD_BOT_TOKEN"), the same pattern flagged in a previous review as encouraging committing secrets into source. Unlike other files in this PR, this comment lacks an "Addressed" marker, indicating the fix was not carried into this revision.

🔒 Proposed fix
+import os
 from easycord import Bot, Plugin, slash

 class GreetingPlugin(Plugin):
     `@slash`(description="Greet a user")
     async def greet(self, ctx, user: str):
         """Say hello to someone."""
         await ctx.respond(f"Hello, {user}! 👋")

 bot = Bot()
 bot.add_plugin(GreetingPlugin())

 if __name__ == "__main__":
-    bot.run("YOUR_DISCORD_BOT_TOKEN")
+    bot.run(os.environ["DISCORD_TOKEN"])
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` around lines 83 - 97, The README example still hardcodes a Discord
bot token in Bot.run, so update the Bot/GreetingPlugin usage snippet to show
loading the token from an environment variable or configuration instead of a
literal secret. Keep the example runnable by referencing Bot.run and the
__main__ block, but replace the placeholder token pattern with a safe
secret-handling approach consistent with the rest of the docs.
CONTRIBUTING.md (1)

28-29: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add the blank line Markdownlint expects.

This heading needs a blank line after it to satisfy the docs lint rule.

🛠️ Proposed fix
 ### Plugin Test-Count Thresholds
+
 We enforce minimum test counts for plugins using `scripts/verify_plugin_tests.py` (counts test_* functions per plugin).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CONTRIBUTING.md` around lines 28 - 29, The markdown heading “Plugin
Test-Count Thresholds” in CONTRIBUTING.md is missing the blank line required by
markdownlint. Insert a blank line after that heading before the following
sentence, keeping the surrounding section formatting consistent.

Source: Linters/SAST tools

project.md (1)

14-15: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Update the snapshot to the current release.

This still reads as v5.50.2 / the old branch, which is stale for a v5.51.0 release log.

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

In `@project.md` around lines 14 - 15, Update the Snapshot line in project.md so
it reflects the current v5.51.0 release metadata instead of the stale
v5.50.2/old-branch text. Edit the snapshot entry to match the latest release
version and the correct active branch naming, keeping the rest of the
release-log context consistent.
bugs.md (1)

226-232: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Remove the already-shipped items from the deferred list.

LocalizationManager metrics atomicity and the hot-reload dispatch lock are already described in this PR as implemented fixes, so keeping them under Deferred to v5.52.0 will send future triage in the wrong direction.

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

In `@bugs.md` around lines 226 - 232, The deferred list still includes items that
are already implemented in this PR, specifically the LocalizationManager metrics
atomicity fix and the hot-reload command dispatch race lock. Update the deferred
section in bugs.md to remove those shipped items and keep only the unresolved
entries, using the existing bullet list near the “Deferred to v5.52.0” heading.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@easycord/helpers/channel.py`:
- Around line 6-12: `SENDABLE_CHANNEL_TYPES` is missing a valid messageable
channel type, causing stage-channel configs to be rejected in
announcement/logging flows. Update the tuple in `easycord/helpers/channel.py` to
include `discord.StageChannel` alongside `discord.TextChannel`,
`discord.Thread`, and `discord.VoiceChannel`. Keep the existing
`SENDABLE_CHANNEL_TYPES` symbol as the single source of truth so any validation
using it accepts stage channels under the pinned discord.py range.

In `@easycord/testing.py`:
- Around line 579-604: `make_plugin()` is skipping subclass initialization for
zero-argument plugin construction, so any state set in a plugin’s own `__init__`
is lost. Update `make_plugin` to detect whether `cls` overrides `__init__` and
call the subclass constructor in that case, only falling back to
`object.__new__(cls)` plus `_Plugin.__init__` for true no-override/legacy cases.
Keep the fix inside `make_plugin` and preserve the existing
`self.bot.add_plugin(plugin)` registration flow.

In `@examples/with-builtin-plugins.py`:
- Around line 99-100: The two print statements in the built-in plugin loading
example use f-strings without any placeholders, which triggers ruff F541. Update
the prints in the example so they are plain string literals instead of
f-strings, keeping the same messages while removing the unnecessary prefix from
those literal-only lines.
- Around line 62-69: The Bot Permissions example is reaching into the private
_values mapping on ctx.bot_permissions instead of using the public
discord.Permissions iterator. Update the permissions display logic in the
with-builtin-plugins example to iterate directly over ctx.bot_permissions using
the (perm, has_it) pairs it yields, and keep the same filtering/formatting
behavior without referencing private internals.

In `@project.md`:
- Line 64: The full-suite test count in the project log is stale and
inconsistent with the release record. Update the test summary text in the
project documentation to match the current release result, and ensure the
wording around the full suite reflects the correct passing count alongside the
referenced new tests.

In `@release_v5.51.0/notes.md`:
- Around line 23-29: The “Known Deferred Issues (v5.52.0)” list still includes
items already shipped in v5.51.0, so update the release notes to keep them
accurate. Remove or move the `LocalizationManager thread-safety` and `Hot-reload
command dispatch race` bullets out of the deferred section, and place them in
the fixed/shipped notes if that section exists, leaving only unresolved items
like `Auto-responder TOCTOU refactor` and `on_ready exception logging` under
deferred issues.

---

Outside diff comments:
In `@context/implementation-plan.md`:
- Around line 58-67: The verification numbers in this plan are inconsistent
across the summary and the verification block. Update the “Type-checking
cleanup” section to use one final test count, or clearly mark the older
1213-passed and 1307-passed values as intermediate checkpoints, while aligning
the final stated result with the current 1335 passing tests. Refer to the
verification summary text in this document and keep the counts internally
consistent everywhere they appear.

In `@easycord/plugins/starboard.py`:
- Around line 264-297: The slash command responses in starboard are still
hardcoded English strings, which violates the plugin localization rule. Update
the response paths in starboard_config-related commands and the other slash
handlers in Starboard to fetch messages through LocalizationManager instead of
embedding literals directly in ctx.respond. Use the existing localization
pattern used elsewhere in the plugin, and keep the command methods’ behavior the
same while swapping the response text sources.
- Around line 181-193: The reaction handling in _on_reaction_add and
_on_reaction_remove repeats the same self-reaction, guild lookup, config fetch,
enabled default, and emoji guard logic, which can drift and reintroduce bugs.
Extract that shared sequence into a small helper such as
_should_process_reaction(...) and have both handlers call it before continuing,
so the enabled/default handling and early returns live in one place.

In `@release_v5.51.0/notes.md`:
- Around line 40-42: The upgrade note under Upgrade Notes is misleading because
the API migration does introduce breaking behavior around ctx.author and the
slash_command deprecation shim. Update the release note text in notes.md to
explicitly mention the migration impact and warn users that ctx.author now
raises while slash_command is only a deprecation shim, instead of claiming there
are no breaking changes.

---

Duplicate comments:
In `@bugs.md`:
- Around line 226-232: The deferred list still includes items that are already
implemented in this PR, specifically the LocalizationManager metrics atomicity
fix and the hot-reload command dispatch race lock. Update the deferred section
in bugs.md to remove those shipped items and keep only the unresolved entries,
using the existing bullet list near the “Deferred to v5.52.0” heading.

In `@CONTRIBUTING.md`:
- Around line 28-29: The markdown heading “Plugin Test-Count Thresholds” in
CONTRIBUTING.md is missing the blank line required by markdownlint. Insert a
blank line after that heading before the following sentence, keeping the
surrounding section formatting consistent.

In `@project.md`:
- Around line 14-15: Update the Snapshot line in project.md so it reflects the
current v5.51.0 release metadata instead of the stale v5.50.2/old-branch text.
Edit the snapshot entry to match the latest release version and the correct
active branch naming, keeping the rest of the release-log context consistent.

In `@README.md`:
- Around line 83-97: The README example still hardcodes a Discord bot token in
Bot.run, so update the Bot/GreetingPlugin usage snippet to show loading the
token from an environment variable or configuration instead of a literal secret.
Keep the example runnable by referencing Bot.run and the __main__ block, but
replace the placeholder token pattern with a safe secret-handling approach
consistent with the rest of the docs.
🪄 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: 465b0c95-08e9-4799-801a-a9e74043c1fb

📥 Commits

Reviewing files that changed from the base of the PR and between 4d3d94d and 9f18d33.

📒 Files selected for processing (42)
  • .github/workflows/nightly.yml
  • .github/workflows/tests.yml
  • AGENTS.md
  • CHANGELOG.md
  • CLAUDE.md
  • CONTRIBUTING.md
  • README.md
  • bugs.md
  • context/implementation-plan.md
  • docs/builtin-plugins.md
  • easycord/__init__.py
  • easycord/_command_registration.py
  • easycord/_context_base.py
  • easycord/decorators.py
  • easycord/helpers/__init__.py
  • easycord/helpers/channel.py
  • easycord/plugin.py
  • easycord/plugin_creator.py
  • easycord/plugins/_utils.py
  • easycord/plugins/ai_moderator.py
  • easycord/plugins/invite_tracker.py
  • easycord/plugins/member_logging.py
  • easycord/plugins/moderation.py
  • easycord/plugins/polls.py
  • easycord/plugins/starboard.py
  • easycord/testing.py
  • examples/with-builtin-plugins.py
  • project.md
  • pyproject.toml
  • pyrightconfig.json
  • release_v5.51.0/notes.md
  • scripts/patch_tests.py
  • scripts/stress_test_db.py
  • scripts/verify_plugin_tests.py
  • tests/test_guild_sync_timeout.py
  • tests/test_hot_reload.py
  • tests/test_plugin_creator.py
  • tests/test_pr71_concurrency.py
  • tests/test_starboard.py
  • tests/test_suggestions.py
  • tests/test_tags.py
  • tests/test_v52_interactions.py
💤 Files with no reviewable changes (3)
  • scripts/patch_tests.py
  • tests/test_pr71_concurrency.py
  • tests/test_guild_sync_timeout.py
📜 Review details
⚠️ CI failures not shown inline (7)

GitHub Actions: Performance Regression Detection / benchmark: release(v5.51.0): config RMW atomicity, plugin fixes (B-006/B-018), public API exports, per-plugin test floor

Conclusion: failure

View job details

##[group]Run python << 'EOF'
 �[36;1mpython << 'EOF'�[0m
 �[36;1mimport json�[0m
 �[36;1mimport sys�[0m
 �[36;1mfrom pathlib import Path�[0m
 �[36;1m�[0m
 �[36;1m# Performance thresholds (hard limits)�[0m
 �[36;1mTHRESHOLDS = {�[0m
 �[36;1m    "Cold Cache (100 lookups)": {�[0m
 �[36;1m        "total_time_ms": 100,�[0m
 �[36;1m        "avg_lookup_ms": 1.0,�[0m
 �[36;1m    },�[0m
 �[36;1m    "Warm Cache (10k lookups)": {�[0m
 �[36;1m        "total_time_ms": 500,�[0m
 �[36;1m        "avg_lookup_ms": 0.05,�[0m
 �[36;1m    },�[0m
 �[36;1m    "Diagnostics Overhead": {�[0m
 �[36;1m        "overhead_percent": 50,�[0m
 �[36;1m    },�[0m
 �[36;1m    "Metrics Overhead": {�[0m
 �[36;1m        "overhead_percent": 30,�[0m
 �[36;1m    },�[0m
 �[36;1m    "Validator Scaling (20 locales)": {�[0m
 �[36;1m        "validation_time_ms": 1000,�[0m
 �[36;1m    },�[0m
 �[36;1m}�[0m
 �[36;1m�[0m
 �[36;1m# Load current results�[0m
 �[36;1mcurrent_file = Path("benchmark-results.json")�[0m
 �[36;1mif not current_file.exists():�[0m
 �[36;1m    print("[!] ERROR: benchmark-results.json not found")�[0m
 �[36;1m    sys.exit(1)�[0m
 �[36;1m�[0m
 �[36;1mwith open(current_file) as f:�[0m
 �[36;1m    current_results = json.load(f)�[0m
 �[36;1m�[0m
 �[36;1m# Load baseline results if available�[0m
 �[36;1mbaseline_file = Path("baseline/benchmark-results.json")�[0m
 �[36;1mbaseline_results = {}�[0m
 �[36;1mif baseline_file.exists():�[0m
 �[36;1m    with open(baseline_file) as f:�[0m
 �[36;1m        baseline_results = json.load(f)�[0m
 �[36;1m�[0m
 �[36;1mviolations = []�[0m
 �[36;1mprint("[*] Checking performance thresholds:\n")�[0m
 �[36;1m�[0m
 �[36;1mfor benchmark_name, thresholds in THRESHOLDS.items():�[0m
 �[36;1m    if benchmark_name not in current_results:�[0m
 �[36;1m        print(f"[!] {benchmark_name}: NOT FOUND in results")�[0m
 �[36;1m        violations.append(f"{benchmark_name}: missing from results")�[0m
 �[36;1m        continue�[0m
 �[36;1m�[0m
 �[36;1m    current = current_results[benchm...

GitHub Actions: Performance Regression Detection / 0_benchmark.txt: release(v5.51.0): config RMW atomicity, plugin fixes (B-006/B-018), public API exports, per-plugin test floor

Conclusion: failure

View job details

##[group]Run python << 'EOF'
 �[36;1mpython << 'EOF'�[0m
 �[36;1mimport json�[0m
 �[36;1mimport sys�[0m
 �[36;1mfrom pathlib import Path�[0m
 �[36;1m�[0m
 �[36;1m# Performance thresholds (hard limits)�[0m
 �[36;1mTHRESHOLDS = {�[0m
 �[36;1m    "Cold Cache (100 lookups)": {�[0m
 �[36;1m        "total_time_ms": 100,�[0m
 �[36;1m        "avg_lookup_ms": 1.0,�[0m
 �[36;1m    },�[0m
 �[36;1m    "Warm Cache (10k lookups)": {�[0m
 �[36;1m        "total_time_ms": 500,�[0m
 �[36;1m        "avg_lookup_ms": 0.05,�[0m
 �[36;1m    },�[0m
 �[36;1m    "Diagnostics Overhead": {�[0m
 �[36;1m        "overhead_percent": 50,�[0m
 �[36;1m    },�[0m
 �[36;1m    "Metrics Overhead": {�[0m
 �[36;1m        "overhead_percent": 30,�[0m
 �[36;1m    },�[0m
 �[36;1m    "Validator Scaling (20 locales)": {�[0m
 �[36;1m        "validation_time_ms": 1000,�[0m
 �[36;1m    },�[0m
 �[36;1m}�[0m
 �[36;1m�[0m
 �[36;1m# Load current results�[0m
 �[36;1mcurrent_file = Path("benchmark-results.json")�[0m
 �[36;1mif not current_file.exists():�[0m
 �[36;1m    print("[!] ERROR: benchmark-results.json not found")�[0m
 �[36;1m    sys.exit(1)�[0m
 �[36;1m�[0m
 �[36;1mwith open(current_file) as f:�[0m
 �[36;1m    current_results = json.load(f)�[0m
 �[36;1m�[0m
 �[36;1m# Load baseline results if available�[0m
 �[36;1mbaseline_file = Path("baseline/benchmark-results.json")�[0m
 �[36;1mbaseline_results = {}�[0m
 �[36;1mif baseline_file.exists():�[0m
 �[36;1m    with open(baseline_file) as f:�[0m
 �[36;1m        baseline_results = json.load(f)�[0m
 �[36;1m�[0m
 �[36;1mviolations = []�[0m
 �[36;1mprint("[*] Checking performance thresholds:\n")�[0m
 �[36;1m�[0m
 �[36;1mfor benchmark_name, thresholds in THRESHOLDS.items():�[0m
 �[36;1m    if benchmark_name not in current_results:�[0m
 �[36;1m        print(f"[!] {benchmark_name}: NOT FOUND in results")�[0m
 �[36;1m        violations.append(f"{benchmark_name}: missing from results")�[0m
 �[36;1m        continue�[0m
 �[36;1m�[0m
 �[36;1m    current = current_results[benchm...

GitHub Actions: PR Quality / lint-and-test (3.11): release(v5.51.0): config RMW atomicity, plugin fixes (B-006/B-018), public API exports, per-plugin test floor

Conclusion: failure

View job details

##[group]Run ruff check .
 �[36;1mruff check .�[0m
 shell: /usr/bin/bash -e {0}
 env:
   pythonLocation: /opt/hostedtoolcache/Python/3.11.15/x64
   PKG_CONFIG_PATH: /opt/hostedtoolcache/Python/3.11.15/x64/lib/pkgconfig
   Python_ROOT_DIR: /opt/hostedtoolcache/Python/3.11.15/x64
   Python2_ROOT_DIR: /opt/hostedtoolcache/Python/3.11.15/x64
   Python3_ROOT_DIR: /opt/hostedtoolcache/Python/3.11.15/x64
   LD_LIBRARY_PATH: /opt/hostedtoolcache/Python/3.11.15/x64/lib
 ##[endgroup]
 F821 Undefined name `Context`
   --> easycord/_command_registration.py:88:55
    |
 86 |     *,
 87 |     callback_builder: Callable[..., Callable],
 88 |     context_factory: Callable[[discord.Interaction], "Context"],
    |                                                       ^^^^^^^
 89 |     name: str,
 90 |     description: str,
    |
 F821 Undefined name `Context`
    --> easycord/_command_registration.py:184:55
     |
 182 |     source_plugin: str | None,
 183 |     guild_id: int | None,
 184 |     context_factory: Callable[[discord.Interaction], "Context"],
     |                                                       ^^^^^^^
 185 | ) -> None:
 186 |     sig = inspect.signature(handler)
     |
 F821 Undefined name `_BotBase`
    --> easycord/_command_registration.py:246:11
     |
 245 | def register_context_menu(
 246 |     bot: "_BotBase",
     |           ^^^^^^^^
 247 |     func: Callable,
 248 |     *,
     |
 F821 Undefined name `Context`
    --> easycord/_command_registration.py:249:55
     |
 247 |     func: Callable,
 248 |     *,
 249 |     context_factory: Callable[[discord.Interaction], "Context"],
     |                                                       ^^^^^^^
 250 |     chain_builder: Callable,
 251 |     name: str,
     |
 F401 [*] `asyncio` imported but unused
  --> easycord/hooks.py:3:8
   |
 1 | from __future__ import annotations
 2 |
 3 | import asyncio
   |        ^^^^^^^
 4 | import inspect
 5 | import logging
   |
 help: Remove unused import: `asyncio`
 F401 ...

GitHub Actions: PR Quality / lint-and-test (3.12): release(v5.51.0): config RMW atomicity, plugin fixes (B-006/B-018), public API exports, per-plugin test floor

Conclusion: failure

View job details

##[group]Run ruff check .
 �[36;1mruff check .�[0m
 shell: /usr/bin/bash -e {0}
 env:
   pythonLocation: /opt/hostedtoolcache/Python/3.12.13/x64
   PKG_CONFIG_PATH: /opt/hostedtoolcache/Python/3.12.13/x64/lib/pkgconfig
   Python_ROOT_DIR: /opt/hostedtoolcache/Python/3.12.13/x64
   Python2_ROOT_DIR: /opt/hostedtoolcache/Python/3.12.13/x64
   Python3_ROOT_DIR: /opt/hostedtoolcache/Python/3.12.13/x64
   LD_LIBRARY_PATH: /opt/hostedtoolcache/Python/3.12.13/x64/lib
 ##[endgroup]
 F821 Undefined name `Context`
   --> easycord/_command_registration.py:88:55
    |
 86 |     *,
 87 |     callback_builder: Callable[..., Callable],
 88 |     context_factory: Callable[[discord.Interaction], "Context"],
    |                                                       ^^^^^^^
 89 |     name: str,
 90 |     description: str,
    |
 F821 Undefined name `Context`
    --> easycord/_command_registration.py:184:55
     |
 182 |     source_plugin: str | None,
 183 |     guild_id: int | None,
 184 |     context_factory: Callable[[discord.Interaction], "Context"],
     |                                                       ^^^^^^^
 185 | ) -> None:
 186 |     sig = inspect.signature(handler)
     |
 F821 Undefined name `_BotBase`
    --> easycord/_command_registration.py:246:11
     |
 245 | def register_context_menu(
 246 |     bot: "_BotBase",
     |           ^^^^^^^^
 247 |     func: Callable,
 248 |     *,
     |
 F821 Undefined name `Context`
    --> easycord/_command_registration.py:249:55
     |
 247 |     func: Callable,
 248 |     *,
 249 |     context_factory: Callable[[discord.Interaction], "Context"],
     |                                                       ^^^^^^^
 250 |     chain_builder: Callable,
 251 |     name: str,
     |
 F401 [*] `asyncio` imported but unused
  --> easycord/hooks.py:3:8
   |
 1 | from __future__ import annotations
 2 |
 3 | import asyncio
   |        ^^^^^^^
 4 | import inspect
 5 | import logging
   |
 help: Remove unused import: `asyncio`
 F401 ...

GitHub Actions: PR Quality / lint-and-test (3.10): release(v5.51.0): config RMW atomicity, plugin fixes (B-006/B-018), public API exports, per-plugin test floor

Conclusion: failure

View job details

##[group]Run ruff check .
 �[36;1mruff check .�[0m
 shell: /usr/bin/bash -e {0}
 env:
   pythonLocation: /opt/hostedtoolcache/Python/3.10.20/x64
   PKG_CONFIG_PATH: /opt/hostedtoolcache/Python/3.10.20/x64/lib/pkgconfig
   Python_ROOT_DIR: /opt/hostedtoolcache/Python/3.10.20/x64
   Python2_ROOT_DIR: /opt/hostedtoolcache/Python/3.10.20/x64
   Python3_ROOT_DIR: /opt/hostedtoolcache/Python/3.10.20/x64
   LD_LIBRARY_PATH: /opt/hostedtoolcache/Python/3.10.20/x64/lib
 ##[endgroup]
 F821 Undefined name `Context`
   --> easycord/_command_registration.py:88:55
    |
 86 |     *,
 87 |     callback_builder: Callable[..., Callable],
 88 |     context_factory: Callable[[discord.Interaction], "Context"],
    |                                                       ^^^^^^^
 89 |     name: str,
 90 |     description: str,
    |
 F821 Undefined name `Context`
    --> easycord/_command_registration.py:184:55
     |
 182 |     source_plugin: str | None,
 183 |     guild_id: int | None,
 184 |     context_factory: Callable[[discord.Interaction], "Context"],
     |                                                       ^^^^^^^
 185 | ) -> None:
 186 |     sig = inspect.signature(handler)
     |
 F821 Undefined name `_BotBase`
    --> easycord/_command_registration.py:246:11
     |
 245 | def register_context_menu(
 246 |     bot: "_BotBase",
     |           ^^^^^^^^
 247 |     func: Callable,
 248 |     *,
     |
 F821 Undefined name `Context`
    --> easycord/_command_registration.py:249:55
     |
 247 |     func: Callable,
 248 |     *,
 249 |     context_factory: Callable[[discord.Interaction], "Context"],
     |                                                       ^^^^^^^
 250 |     chain_builder: Callable,
 251 |     name: str,
     |
 F401 [*] `asyncio` imported but unused
  --> easycord/hooks.py:3:8
   |
 1 | from __future__ import annotations
 2 |
 3 | import asyncio
   |        ^^^^^^^
 4 | import inspect
 5 | import logging
   |
 help: Remove unused import: `asyncio`
 F401 ...

GitHub Actions: PR Quality / lint-and-test (3.10): release(v5.51.0): config RMW atomicity, plugin fixes (B-006/B-018), public API exports, per-plugin test floor

Conclusion: failure

View job details

##[group]Run ruff check easycord tests --select E9,F63,F7,F82
 �[36;1mruff check easycord tests --select E9,F63,F7,F82�[0m
 shell: /usr/bin/bash -e {0}
 env:
   pythonLocation: /opt/hostedtoolcache/Python/3.10.20/x64
   PKG_CONFIG_PATH: /opt/hostedtoolcache/Python/3.10.20/x64/lib/pkgconfig
   Python_ROOT_DIR: /opt/hostedtoolcache/Python/3.10.20/x64
   Python2_ROOT_DIR: /opt/hostedtoolcache/Python/3.10.20/x64
   Python3_ROOT_DIR: /opt/hostedtoolcache/Python/3.10.20/x64
   LD_LIBRARY_PATH: /opt/hostedtoolcache/Python/3.10.20/x64/lib
 ##[endgroup]
 F821 Undefined name `Context`
   --> easycord/_command_registration.py:88:55
    |
 86 |     *,
 87 |     callback_builder: Callable[..., Callable],
 88 |     context_factory: Callable[[discord.Interaction], "Context"],
    |                                                       ^^^^^^^
 89 |     name: str,
 90 |     description: str,
    |
 F821 Undefined name `Context`
    --> easycord/_command_registration.py:184:55
     |
 182 |     source_plugin: str | None,
 183 |     guild_id: int | None,
 184 |     context_factory: Callable[[discord.Interaction], "Context"],
     |                                                       ^^^^^^^
 185 | ) -> None:
 186 |     sig = inspect.signature(handler)
     |
 F821 Undefined name `_BotBase`
    --> easycord/_command_registration.py:246:11
     |
 245 | def register_context_menu(
 246 |     bot: "_BotBase",
     |           ^^^^^^^^
 247 |     func: Callable,
 248 |     *,
     |
 F821 Undefined name `Context`
    --> easycord/_command_registration.py:249:55
     |
 247 |     func: Callable,
 248 |     *,
 249 |     context_factory: Callable[[discord.Interaction], "Context"],
     |                                                       ^^^^^^^
 250 |     chain_builder: Callable,
 251 |     name: str,
     |
 Found 4 errors.
 ##[error]Process completed with exit code 1.

GitHub Actions: PR Quality / 1_lint-and-test (3.11).txt: release(v5.51.0): config RMW atomicity, plugin fixes (B-006/B-018), public API exports, per-plugin test floor

Conclusion: failure

View job details

##[group]Run ruff check easycord tests --select E9,F63,F7,F82
 �[36;1mruff check easycord tests --select E9,F63,F7,F82�[0m
 shell: /usr/bin/bash -e {0}
 env:
   pythonLocation: /opt/hostedtoolcache/Python/3.11.15/x64
   PKG_CONFIG_PATH: /opt/hostedtoolcache/Python/3.11.15/x64/lib/pkgconfig
   Python_ROOT_DIR: /opt/hostedtoolcache/Python/3.11.15/x64
   Python2_ROOT_DIR: /opt/hostedtoolcache/Python/3.11.15/x64
   Python3_ROOT_DIR: /opt/hostedtoolcache/Python/3.11.15/x64
   LD_LIBRARY_PATH: /opt/hostedtoolcache/Python/3.11.15/x64/lib
 ##[endgroup]
 F821 Undefined name `Context`
   --> easycord/_command_registration.py:88:55
    |
 86 |     *,
 87 |     callback_builder: Callable[..., Callable],
 88 |     context_factory: Callable[[discord.Interaction], "Context"],
    |                                                       ^^^^^^^
 89 |     name: str,
 90 |     description: str,
    |
 F821 Undefined name `Context`
    --> easycord/_command_registration.py:184:55
     |
 182 |     source_plugin: str | None,
 183 |     guild_id: int | None,
 184 |     context_factory: Callable[[discord.Interaction], "Context"],
     |                                                       ^^^^^^^
 185 | ) -> None:
 186 |     sig = inspect.signature(handler)
     |
 F821 Undefined name `_BotBase`
    --> easycord/_command_registration.py:246:11
     |
 245 | def register_context_menu(
 246 |     bot: "_BotBase",
     |           ^^^^^^^^
 247 |     func: Callable,
 248 |     *,
     |
 F821 Undefined name `Context`
    --> easycord/_command_registration.py:249:55
     |
 247 |     func: Callable,
 248 |     *,
 249 |     context_factory: Callable[[discord.Interaction], "Context"],
     |                                                       ^^^^^^^
 250 |     chain_builder: Callable,
 251 |     name: str,
     |
 Found 4 errors.
 ##[error]Process completed with exit code 1.
🧰 Additional context used
📓 Path-based instructions (11)
.github/workflows/**/*.yml

📄 CodeRabbit inference engine (AGENTS.md)

GitHub Actions workflows must pin actions/checkout to v7 and actions/setup-python to v5.

Files:

  • .github/workflows/tests.yml
  • .github/workflows/nightly.yml
easycord/**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

easycord/**/*.py: Use ctx.user / ctx.member; ctx.author does not exist.
ctx.is_admin is a property, not a method; do not call it as ctx.is_admin().
Cooldown sentinels should default to float("-inf"), not 0.0, so first-message events pass on fresh runners.

Files:

  • easycord/helpers/channel.py
  • easycord/plugins/invite_tracker.py
  • easycord/helpers/__init__.py
  • easycord/plugin.py
  • easycord/plugins/_utils.py
  • easycord/plugins/polls.py
  • easycord/_context_base.py
  • easycord/__init__.py
  • easycord/plugin_creator.py
  • easycord/testing.py
  • easycord/decorators.py
  • easycord/plugins/moderation.py
  • easycord/plugins/ai_moderator.py
  • easycord/_command_registration.py
  • easycord/plugins/starboard.py
  • easycord/plugins/member_logging.py
easycord/plugins/**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

Plugin responses must use LocalizationManager lookups; hardcoded strings should not be used in plugin responses.

Files:

  • easycord/plugins/invite_tracker.py
  • easycord/plugins/_utils.py
  • easycord/plugins/polls.py
  • easycord/plugins/moderation.py
  • easycord/plugins/ai_moderator.py
  • easycord/plugins/starboard.py
  • easycord/plugins/member_logging.py
tests/**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

Async tests use pytest-asyncio with asyncio_mode = "auto", so they should not set up a manual event loop.

Files:

  • tests/test_v52_interactions.py
  • tests/test_plugin_creator.py
  • tests/test_hot_reload.py
  • tests/test_suggestions.py
  • tests/test_tags.py
  • tests/test_starboard.py
easycord/plugin.py

📄 CodeRabbit inference engine (CLAUDE.md)

Subclass Plugin, attach behavior with @slash / @on, and register instances with bot.add_plugin(plugin_instance); keep per-guild state out of self and in the database layer instead.

Files:

  • easycord/plugin.py
easycord/{context.py,_context_*.py}

📄 CodeRabbit inference engine (AGENTS.md)

User-facing command-handler behavior belongs in context.py or the _context_<area>.py context mixin files.

Files:

  • easycord/_context_base.py
easycord/_*.py

📄 CodeRabbit inference engine (CLAUDE.md)

Treat every _-prefixed module in easycord as internal implementation detail; these modules may change without notice and should not be imported from outside the package boundary.

Files:

  • easycord/_context_base.py
  • easycord/__init__.py
  • easycord/_command_registration.py
easycord/_context_*.py

📄 CodeRabbit inference engine (CLAUDE.md)

In internal context helper modules, preserve the Context API contract: use ctx.user / ctx.member, never ctx.author, and treat ctx.is_admin as a property.

Files:

  • easycord/_context_base.py
easycord/__init__.py

📄 CodeRabbit inference engine (CLAUDE.md)

Treat easycord/__init__.py as the stable public API surface; do not import from internal _-prefixed modules, and rely on top-level re-exports/lazy __getattr__ instead.

Files:

  • easycord/__init__.py
easycord/decorators.py

📄 CodeRabbit inference engine (AGENTS.md)

decorators.py defines the primary extension decorators such as @slash, @autocomplete, @on, @component, @modal, @message_command, @user_command, @task, @ai_tool, @cooldown, @require_permissions, @install_type, and @premium_required.

Authoring decorators live in decorators.py; use the provided decorator set (@slash, @on, @autocomplete, @component, @modal, @message_command, @user_command, @task, @ai_tool, @cooldown, @require_permissions, @install_type, @premium_required) rather than inventing ad hoc equivalents.

Files:

  • easycord/decorators.py
easycord/_command_*.py

📄 CodeRabbit inference engine (CLAUDE.md)

Keep command registration split correctly: _command_callbacks.py builds callback wrappers, while _command_registration.py handles option injection, choice population, and context-menu registration.

Files:

  • easycord/_command_registration.py
🪛 GitHub Actions: PR Quality / 0_lint-and-test (3.10).txt
easycord/_command_registration.py

[error] 88-90: ruff (E9/F821) Undefined name Context.


[error] 184-186: ruff (E9/F821) Undefined name Context.


[error] 246-248: ruff (E9/F821) Undefined name _BotBase.


[error] 249-251: ruff (E9/F821) Undefined name Context.

🪛 GitHub Actions: PR Quality / 1_lint-and-test (3.11).txt
easycord/_command_registration.py

[error] 88-88: ruff check (F821) Undefined name Context.


[error] 184-184: ruff check (F821) Undefined name Context.


[error] 246-246: ruff check (F821) Undefined name _BotBase.


[error] 249-249: ruff check (F821) Undefined name Context.

🪛 GitHub Actions: PR Quality / 2_lint-and-test (3.12).txt
easycord/_command_registration.py

[error] 88-88: ruff (E9/F821) Undefined name Context


[error] 184-184: ruff (E9/F821) Undefined name Context


[error] 246-246: ruff (E9/F821) Undefined name _BotBase


[error] 249-249: ruff (E9/F821) Undefined name Context

🪛 GitHub Actions: PR Quality / lint-and-test (3.10)
scripts/stress_test_db.py

[error] 47-47: F541 f-string without any placeholders (Remove extraneous f prefix).

examples/with-builtin-plugins.py

[error] 99-99: F541 f-string without any placeholders (Remove extraneous f prefix).


[error] 100-100: F541 f-string without any placeholders (Remove extraneous f prefix).

easycord/_command_registration.py

[error] 88-88: F821 Undefined name Context.


[error] 184-184: F821 Undefined name Context.


[error] 246-246: F821 Undefined name _BotBase.


[error] 249-249: F821 Undefined name Context.

🪛 GitHub Actions: PR Quality / lint-and-test (3.11)
scripts/stress_test_db.py

[error] 47-47: ruff F541: f-string without any placeholders. Remove extraneous f prefix

examples/with-builtin-plugins.py

[error] 99-99: ruff F541: f-string without any placeholders. Remove extraneous f prefix


[error] 100-100: ruff F541: f-string without any placeholders. Remove extraneous f prefix

easycord/_command_registration.py

[error] 88-88: ruff F821: Undefined name Context


[error] 184-184: ruff F821: Undefined name Context


[error] 246-246: ruff F821: Undefined name _BotBase


[error] 249-249: ruff F821: Undefined name Context

🪛 GitHub Actions: PR Quality / lint-and-test (3.12)
scripts/stress_test_db.py

[error] 47-47: ruff (F541): f-string without any placeholders. Remove extraneous f prefix.

examples/with-builtin-plugins.py

[error] 99-99: ruff (F541): f-string without any placeholders. Remove extraneous f prefix.


[error] 100-100: ruff (F541): f-string without any placeholders. Remove extraneous f prefix.

easycord/_command_registration.py

[error] 88-88: ruff (F821): Undefined name Context.


[error] 184-184: ruff (F821): Undefined name Context.


[error] 246-246: ruff (F821): Undefined name _BotBase.


[error] 249-249: ruff (F821): Undefined name Context.

🪛 GitHub Check: CodeQL
tests/test_v52_interactions.py

[notice] 22-22: Statement has no effect
This statement has no effect.

🪛 LanguageTool
AGENTS.md

[uncategorized] ~62-~62: The official name of this software platform is spelled with a capital “H”.
Context: ... manages major bumps; keep in sync with .github/workflows/). ## Branch / repo state ...

(GITHUB)

CLAUDE.md

[uncategorized] ~23-~23: The official name of this software platform is spelled with a capital “H”.
Context: ...ent loop setup needed. CI PR gate (.github/workflows/tests.yml, Python 3.10/3.11/...

(GITHUB)


[uncategorized] ~124-~124: The official name of this software platform is spelled with a capital “H”.
Context: ...ajor bumps; keep this line in sync with .github/workflows/. - sync_commands() raises...

(GITHUB)

🪛 markdownlint-cli2 (0.22.1)
CONTRIBUTING.md

[warning] 28-28: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)

bugs.md

[warning] 21-21: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


[warning] 42-42: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


[warning] 156-156: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


[warning] 221-221: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


[warning] 226-226: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)

🪛 zizmor (1.26.1)
.github/workflows/tests.yml

[warning] 16-17: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[error] 17-17: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

.github/workflows/nightly.yml

[warning] 16-16: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[error] 16-16: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🔇 Additional comments (45)
tests/test_hot_reload.py (1)

404-418: 📐 Maintainability & Code Quality

Previously requested refactor applied.

The pytest.raises(asyncio.CancelledError) refactor addresses the earlier nitpick about using try/except.

easycord/plugins/polls.py (1)

12-12: LGTM!

Also applies to: 300-307

.github/workflows/nightly.yml (1)

92-97: LGTM!

.github/workflows/tests.yml (1)

30-35: LGTM!

scripts/verify_plugin_tests.py (1)

38-39: LGTM!

Also applies to: 49-65

scripts/stress_test_db.py (1)

18-69: Prior try/finally cleanup fix applied correctly.

db = None guard plus finally block now ensures db.close() and temp-file removal always run, addressing the earlier review comment.

pyproject.toml (1)

7-7: LGTM!

Also applies to: 51-52

pyrightconfig.json (1)

24-36: tests executionEnvironment now correctly consolidated here.

This resolves the prior review finding that the [[tool.pyright.executionEnvironments]] block in pyproject.toml was being silently ignored because pyrightconfig.json takes precedence when both exist.

README.md (1)

2-56: LGTM!

Also applies to: 126-140, 1029-1029

examples/with-builtin-plugins.py (1)

1-108: LGTM!

docs/builtin-plugins.md (1)

222-254: LGTM!

CHANGELOG.md (1)

31-41: LGTM!

release_v5.51.0/notes.md (1)

1-22: LGTM!

Also applies to: 30-39

AGENTS.md (1)

12-14: LGTM!

Also applies to: 23-30, 62-62

CLAUDE.md (1)

12-14: LGTM!

Also applies to: 23-30, 124-124

easycord/plugins/starboard.py (4)

10-10: LGTM!


69-85: LGTM!

Both helpers correctly funnel the archived-message map update through store.mutate, matching the atomic RMW contract.


181-193: LGTM! Correctly fixes the B-018 default-enabled bug and guards against self.bot.user being None.

Also applies to: 224-234


264-276: LGTM! The assert ctx.guild is not None guards are consistent with the existing guild_only=True convention used elsewhere in the codebase (e.g. suggestions.py).

Also applies to: 285-285

tests/test_starboard.py (3)

198-227: LGTM!


232-398: LGTM! Thorough coverage of the reaction-add/remove guard paths, including the new enabled default behavior and bot-user guard.


400-459: LGTM!

tests/test_suggestions.py (3)

226-260: LGTM!


261-353: LGTM! Listing cap, exclusion-of-resolved, truncation, and malformed-entry skip tests all correctly match the suggestions() implementation's filtering/sorting/truncation behavior.


354-401: LGTM! Good concurrency regression coverage for the atomic suggest/suggestion_approve mutate paths.

tests/test_tags.py (2)

165-201: LGTM! Correctly validates the atomic delete_if_authorized reason-code contract that fixes the tags TOCTOU issue.


203-226: LGTM!

tests/test_v52_interactions.py (1)

17-26: 🎯 Functional Correctness

CodeQL "no effect" alert on the ... stub body is a pre-existing false positive.

The ... placeholder is intentional (function body never executes, per the # pragma: no cover comment) and was already flagged by CodeQL in a prior review pass on this file.

easycord/_context_base.py (1)

35-45: 🎯 Functional Correctness

No instance-level __getattr__ fallback here. Context and its mixins do not define __getattr__ or __getattribute__, so this property’s AttributeError won’t be intercepted.

			> Likely an incorrect or invalid review comment.
easycord/decorators.py (2)

289-289: LGTM!

Also applies to: 320-327, 362-362


382-393: LGTM!

Matches the downstream test (test_slash_command_is_deprecated_alias_for_slash) expecting a DeprecationWarning and correct forwarding to slash(*args, **kwargs).

easycord/_command_registration.py (4)

84-118: LGTM!

bot_permissions is correctly threaded through to callback_builder, consistent with the upstream _bot_commands.py/_plugin_scanner.py propagation shown in the provided snippets.


176-184: LGTM!


246-249: 🩺 Stability & Availability

Confirm register_context_menu's bot: "_BotBase" typing doesn't reintroduce the cyclic-import CodeQL finding.

register_slash and _register_autocomplete_handler were switched to the new _BotBaseLike Protocol specifically to avoid importing _BotBase and creating a cycle (per the summary and the pre-existing CodeQL "Module-level cyclic import" finding on this file). register_context_menu, however, is described as still typed bot: "_BotBase" rather than "_BotBaseLike". If _BotBase is imported (even guarded by TYPE_CHECKING) anywhere in this module to make that forward reference resolvable, the same cyclic-import risk CodeQL previously flagged could resurface for this function. Please confirm this was intentional (e.g. because register_context_menu needs a nominal type not covered by the Protocol) or align it with _BotBaseLike for consistency.
[medium_effort_and_high_reward]


7-19: 🎯 Functional Correctness

No issue here. Context is imported under if TYPE_CHECKING, _BotBaseLike is defined locally, and Union is used later in register_context_menu.

			> Likely an incorrect or invalid review comment.
easycord/plugins/moderation.py (3)

11-11: LGTM!


127-127: LGTM!

bot_permissions correctly matches the privileged Discord API each command exercises (kick_members, ban_members, moderate_members, manage_roles), and is enforced pre-execution by build_slash_callback's ctx.bot_permissions check per the referenced snippet.

Also applies to: 153-153, 180-180, 203-203, 314-314, 349-349


255-267: LGTM!

Replacing the load/mutate/save flow with store.mutate(guild.id, _apply) closes the concurrent-/warn lost-update race the previous non-atomic flow had, consistent with the PR's RMW-atomicity objective.

easycord/plugins/ai_moderator.py (2)

17-18: LGTM!


223-223: LGTM!

Requiring manage_guild for moderation config commands (mod_enable, mod_threshold, mod_action_level, mod_add_rule, mod_remove_rule) is a sound tightening — previously only guild_only=True gated these, so any guild member could reconfigure moderation settings.

Also applies to: 243-251, 261-261, 275-275

easycord/__init__.py (1)

19-19: LGTM!

New public exports (EventBus, HookRegistry, SENDABLE_CHANNEL_TYPES) are sourced from non-underscore-prefixed modules (event_bus.py, hooks.py, helpers/channel.py), consistent with the guideline to keep easycord/__init__.py as the stable surface without reaching into internal _-prefixed modules.

Also applies to: 31-32, 57-57, 117-117, 129-129, 157-157

easycord/helpers/__init__.py (1)

2-2: LGTM!

Also applies to: 15-15

easycord/plugins/_utils.py (1)

4-6: LGTM!

easycord/plugins/invite_tracker.py (1)

10-11: LGTM!

easycord/plugins/member_logging.py (1)

11-11: LGTM!

Comment on lines +6 to +12
# Channel types that support .send() in discord.py.
# StageChannel is intentionally excluded — it has no Messageable interface.
SENDABLE_CHANNEL_TYPES = (
discord.TextChannel,
discord.Thread,
discord.VoiceChannel,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'pyproject.toml version pins:\n'
rg -n 'discord\.py|discordpy|discord' pyproject.toml

printf '\nRelevant helper file:\n'
sed -n '1,80p' easycord/helpers/channel.py

printf '\nSearch for StageChannel usage / exclusion rationale:\n'
rg -n 'StageChannel|SENDABLE_CHANNEL_TYPES|Messageable' easycord

Repository: rolling-codes/EasyCord

Length of output: 3473


🌐 Web query:

discord.py 2.7.1 StageChannel abc.Messageable send docs or source

💡 Result:

In discord.py 2.7.1, the StageChannel class inherits from discord.abc.Messageable [1][2]. This implementation allows StageChannel objects to utilize the standard messaging methods defined in the Messageable interface, such as send [2]. The implementation of this feature was tracked in issue #9248 and is officially included in the library's recent versions [1][3][4]. Because StageChannel now inherits from abc.Messageable, you can send messages to a stage channel using the same syntax as you would for a TextChannel: await stage_channel.send("Hello, this is a message to the stage channel.") The Messageable interface provides several asynchronous methods for interacting with channels, including: - send: To send a message to the channel [2]. - pins: To retrieve pinned messages [5][2]. - purge: To bulk delete messages (depending on permissions). You can verify the source code for this inheritance in the discord.abc module where Messageable is defined, and the discord.channel module where StageChannel is defined [6][2]. For complete details on the available parameters for the send method (such as file, embeds, view, or poll), refer to the official discord.py API reference for discord.abc.Messageable [7][2].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
text = Path('pyproject.toml').read_text()
for line in text.splitlines():
    if 'discord.py' in line:
        print(line)
PY

Repository: rolling-codes/EasyCord

Length of output: 288


Include discord.StageChannel in SENDABLE_CHANNEL_TYPES. This project pins discord.py>=2.7.1,<3, and StageChannel implements abc.Messageable in that range, so excluding it will reject valid stage-channel configs in the logging/announcement paths.

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

In `@easycord/helpers/channel.py` around lines 6 - 12, `SENDABLE_CHANNEL_TYPES` is
missing a valid messageable channel type, causing stage-channel configs to be
rejected in announcement/logging flows. Update the tuple in
`easycord/helpers/channel.py` to include `discord.StageChannel` alongside
`discord.TextChannel`, `discord.Thread`, and `discord.VoiceChannel`. Keep the
existing `SENDABLE_CHANNEL_TYPES` symbol as the single source of truth so any
validation using it accepts stage channels under the pinned discord.py range.

Comment thread easycord/testing.py
Comment on lines +579 to 604
def make_plugin(self, cls: "type[Any]", *init_args: Any, **init_kwargs: Any) -> Any:
"""Construct *cls* and register it with the test bot.

Calls ``cls.__new__(cls)``, runs ``Plugin.__init__`` to populate
base attributes, then calls ``self.bot.add_plugin()`` which wires
``plugin._bot``. Any *attrs* are applied via ``setattr`` after
base init but before registration.
If the plugin's __init__ accepts arguments, pass them as *init_args / **init_kwargs.
For zero-argument plugins, uses ``cls.__new__(cls)`` + ``Plugin.__init__`` for direct control.
The plugin is registered via ``self.bot.add_plugin()``, which wires ``_bot``.

Returns the registered plugin instance.
Example::

# Simple plugin with no constructor args
plugin = self.make_plugin(MyPlugin)

# Plugin with constructor args
plugin = self.make_plugin(WelcomePlugin, data_dir=".easycord/welcome")
"""
from .plugin import Plugin as _Plugin

plugin: _Plugin = object.__new__(cls) # type: ignore[arg-type]
_Plugin.__init__(plugin)
for key, value in attrs.items():
setattr(plugin, key, value)
if init_args or init_kwargs:
# Plugin has custom constructor args — call normally
plugin = cls(*init_args, **init_kwargs)
else:
# Zero-arg plugin — use __new__ + Plugin.__init__ for clarity
plugin: _Plugin = object.__new__(cls) # type: ignore[arg-type]
_Plugin.__init__(plugin)
self.bot.add_plugin(plugin) # type: ignore[arg-type]
return plugin

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

make_plugin() bypasses subclass __init__ for zero-arg calls, dropping instance state.

The branch choice is based on whether callers pass init_args/init_kwargs to make_plugin, not on whether cls actually overrides __init__. For any plugin whose constructor is optional-arg-only (e.g. StarboardPlugin.__init__(self) sets self.config = PluginConfigManager(...), PollsPlugin.__init__ sets self._store/self._locks/self._timers), calling self.make_plugin(StarboardPlugin) with no args takes the else branch, which calls _Plugin.__init__(plugin) directly — the base Plugin.__init__ only, never the subclass's override. Any subsequent access to attributes the subclass constructor would have set raises AttributeError.

A safer discriminator is whether cls actually overrides __init__, not whether the caller passed args:

🐛 Proposed fix
-        if init_args or init_kwargs:
-            # Plugin has custom constructor args — call normally
-            plugin = cls(*init_args, **init_kwargs)
-        else:
-            # Zero-arg plugin — use __new__ + Plugin.__init__ for clarity
-            plugin: _Plugin = object.__new__(cls)  # type: ignore[arg-type]
-            _Plugin.__init__(plugin)
+        if cls.__init__ is _Plugin.__init__:
+            # No custom constructor — use __new__ + Plugin.__init__ directly
+            plugin: _Plugin = object.__new__(cls)  # type: ignore[arg-type]
+            _Plugin.__init__(plugin)
+        else:
+            # Custom constructor — always call it, even with zero extra args,
+            # so subclass-specific initialization actually runs.
+            plugin = cls(*init_args, **init_kwargs)
📝 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
def make_plugin(self, cls: "type[Any]", *init_args: Any, **init_kwargs: Any) -> Any:
"""Construct *cls* and register it with the test bot.
Calls ``cls.__new__(cls)``, runs ``Plugin.__init__`` to populate
base attributes, then calls ``self.bot.add_plugin()`` which wires
``plugin._bot``. Any *attrs* are applied via ``setattr`` after
base init but before registration.
If the plugin's __init__ accepts arguments, pass them as *init_args / **init_kwargs.
For zero-argument plugins, uses ``cls.__new__(cls)`` + ``Plugin.__init__`` for direct control.
The plugin is registered via ``self.bot.add_plugin()``, which wires ``_bot``.
Returns the registered plugin instance.
Example::
# Simple plugin with no constructor args
plugin = self.make_plugin(MyPlugin)
# Plugin with constructor args
plugin = self.make_plugin(WelcomePlugin, data_dir=".easycord/welcome")
"""
from .plugin import Plugin as _Plugin
plugin: _Plugin = object.__new__(cls) # type: ignore[arg-type]
_Plugin.__init__(plugin)
for key, value in attrs.items():
setattr(plugin, key, value)
if init_args or init_kwargs:
# Plugin has custom constructor args — call normally
plugin = cls(*init_args, **init_kwargs)
else:
# Zero-arg plugin — use __new__ + Plugin.__init__ for clarity
plugin: _Plugin = object.__new__(cls) # type: ignore[arg-type]
_Plugin.__init__(plugin)
self.bot.add_plugin(plugin) # type: ignore[arg-type]
return plugin
def make_plugin(self, cls: "type[Any]", *init_args: Any, **init_kwargs: Any) -> Any:
"""Construct *cls* and register it with the test bot.
If the plugin's __init__ accepts arguments, pass them as *init_args / **init_kwargs.
For zero-argument plugins, uses ``cls.__new__(cls)`` + ``Plugin.__init__`` for direct control.
The plugin is registered via ``self.bot.add_plugin()``, which wires ``_bot``.
Example::
# Simple plugin with no constructor args
plugin = self.make_plugin(MyPlugin)
# Plugin with constructor args
plugin = self.make_plugin(WelcomePlugin, data_dir=".easycord/welcome")
"""
from .plugin import Plugin as _Plugin
if cls.__init__ is _Plugin.__init__:
# No custom constructor — use __new__ + Plugin.__init__ directly
plugin: _Plugin = object.__new__(cls) # type: ignore[arg-type]
_Plugin.__init__(plugin)
else:
# Custom constructor — always call it, even with zero extra args,
# so subclass-specific initialization actually runs.
plugin = cls(*init_args, **init_kwargs)
self.bot.add_plugin(plugin) # type: ignore[arg-type]
return plugin
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@easycord/testing.py` around lines 579 - 604, `make_plugin()` is skipping
subclass initialization for zero-argument plugin construction, so any state set
in a plugin’s own `__init__` is lost. Update `make_plugin` to detect whether
`cls` overrides `__init__` and call the subclass constructor in that case, only
falling back to `object.__new__(cls)` plus `_Plugin.__init__` for true
no-override/legacy cases. Keep the fix inside `make_plugin` and preserve the
existing `self.bot.add_plugin(plugin)` registration flow.

Comment on lines +62 to +69
name="Bot Permissions",
value=", ".join(
perm.replace("_", " ").title()
for perm, has_it in ctx.bot_permissions._values.items()
if has_it
)[:100] or "None",
inline=False,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Does discord.py's Permissions class support iteration returning (name, value) tuples in the latest version?

💡 Result:

Yes, discord.py's Permissions class supports iteration, which yields (name, value) tuples [1]. In discord.py, the Permissions class implements the iter method, allowing it to be used in loops or comprehensions [1]. When you iterate over a Permissions object, it returns tuples where the first element is the string name of the permission (e.g., 'send_messages') and the second element is a boolean (True if the permission is enabled, False otherwise) [1]. Example usage: for name, value in my_permissions: if value: print(f"Permission {name} is enabled.") This functionality is a standard feature of the library's flags-based system [2][1].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the target file around the reported lines
file="examples/with-builtin-plugins.py"
wc -l "$file"
sed -n '1,140p' "$file" | cat -n

# Find any other Permissions usages in the repo for context
rg -n "Permissions|_values" .

Repository: rolling-codes/EasyCord

Length of output: 8243


Use the public Permissions iterator instead of _values discord.Permissions already yields (name, value) pairs, so this example should iterate directly (for perm, has_it in ctx.bot_permissions) rather than reaching into the private _values mapping.

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

In `@examples/with-builtin-plugins.py` around lines 62 - 69, The Bot Permissions
example is reaching into the private _values mapping on ctx.bot_permissions
instead of using the public discord.Permissions iterator. Update the permissions
display logic in the with-builtin-plugins example to iterate directly over
ctx.bot_permissions using the (perm, has_it) pairs it yields, and keep the same
filtering/formatting behavior without referencing private internals.

Comment on lines +99 to +100
print(f"✓ Loaded built-in plugins: welcome, tags, polls, levels")
print(f"✓ Loaded opt-in plugins: moderation, economy, reminders, starboard, admin")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔴 Critical | ⚡ Quick win

Fix ruff F541: remove unnecessary f-string prefixes.

CI pipeline (3.10/3.11/3.12) fails on these lines because the f-strings contain no placeholders.

🐛 Proposed fix
-    print(f"✓ Loaded built-in plugins: welcome, tags, polls, levels")
-    print(f"✓ Loaded opt-in plugins: moderation, economy, reminders, starboard, admin")
+    print("✓ Loaded built-in plugins: welcome, tags, polls, levels")
+    print("✓ Loaded opt-in plugins: moderation, economy, reminders, starboard, admin")
📝 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
print(f"✓ Loaded built-in plugins: welcome, tags, polls, levels")
print(f"✓ Loaded opt-in plugins: moderation, economy, reminders, starboard, admin")
print("✓ Loaded built-in plugins: welcome, tags, polls, levels")
print("✓ Loaded opt-in plugins: moderation, economy, reminders, starboard, admin")
🧰 Tools
🪛 GitHub Actions: PR Quality / lint-and-test (3.10)

[error] 99-99: F541 f-string without any placeholders (Remove extraneous f prefix).


[error] 100-100: F541 f-string without any placeholders (Remove extraneous f prefix).

🪛 GitHub Actions: PR Quality / lint-and-test (3.11)

[error] 99-99: ruff F541: f-string without any placeholders. Remove extraneous f prefix


[error] 100-100: ruff F541: f-string without any placeholders. Remove extraneous f prefix

🪛 GitHub Actions: PR Quality / lint-and-test (3.12)

[error] 99-99: ruff (F541): f-string without any placeholders. Remove extraneous f prefix.


[error] 100-100: ruff (F541): f-string without any placeholders. Remove extraneous f prefix.

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

In `@examples/with-builtin-plugins.py` around lines 99 - 100, The two print
statements in the built-in plugin loading example use f-strings without any
placeholders, which triggers ruff F541. Update the prints in the example so they
are plain string literals instead of f-strings, keeping the same messages while
removing the unnecessary prefix from those literal-only lines.

Source: Pipeline failures

Comment thread project.md
| 2026-06-28 | Med (data integrity + logic) | `plugins/role_persistence.py` | Routed save/restore through `mutate`; plus 3 logic fixes — save by identity (not bot hierarchy), gate assignability at restore, and only delete the saved record on successful restore or when all saved roles are gone (failed restore now retryable). | Code-read audit (user) |
| 2026-07-02 | High (feature dead on arrival) | `plugins/starboard.py` | B-018: `cfg.get("enabled")` without default treated a missing key as disabled — any guild whose config section was created by a single config command (e.g. `/starboard_channel`) had a starboard that never fired. Fixed with `cfg.get("enabled", True)` in both reaction handlers + config display. B-019 closed 2026-07-02 — the five sibling plugins verified benign (see bugs.md). | New ≥20-test gate work (test-first exposure) |

New tests: `tests/test_server_config.py::TestAtomicMutate`, `tests/test_suggestions.py`, `tests/test_role_persistence.py`. Full suite: 1248 passed.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Align the full-suite count with the release record.

1248 passed looks stale relative to this PR's stated release result (1335 passing tests), so the project log is internally inconsistent.

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

In `@project.md` at line 64, The full-suite test count in the project log is stale
and inconsistent with the release record. Update the test summary text in the
project documentation to match the current release result, and ensure the
wording around the full suite reflects the correct passing count alongside the
referenced new tests.

Comment thread release_v5.51.0/notes.md
Comment on lines +23 to +29
## Known Deferred Issues (v5.52.0)

- Auto-responder TOCTOU refactor (requires mutate contract rework)
- LocalizationManager thread-safety (metrics atomicity with threading.Lock)
- Hot-reload command dispatch race (architectural: requires dispatch-side lock)
- on_ready exception logging (4 plugins: exception swallowing silently)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Prune shipped fixes from the deferred list.

LocalizationManager thread-safety and the hot-reload dispatch lock are already part of v5.51.0, so leaving them under “Known Deferred Issues (v5.52.0)” makes the release notes stale. Remove or move them to the fixed section.

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

In `@release_v5.51.0/notes.md` around lines 23 - 29, The “Known Deferred Issues
(v5.52.0)” list still includes items already shipped in v5.51.0, so update the
release notes to keep them accurate. Remove or move the `LocalizationManager
thread-safety` and `Hot-reload command dispatch race` bullets out of the
deferred section, and place them in the fixed/shipped notes if that section
exists, leaving only unresolved items like `Auto-responder TOCTOU refactor` and
`on_ready exception logging` under deferred issues.

Errors (3) — cyclic import chain broken:
- _plugin_scanner.py: replace TYPE_CHECKING _BotBase/Plugin imports with
  inline _BotBaseLike/_PluginLike Protocol classes; eliminates the
  _bot_plugins → _plugin_scanner → _bot_base → _bot_plugins cycle
- _command_registration.py: use setattr/getattr instead of dunder attr
  direct-write to avoid unsafe dynamic-attribute patterns

Warnings (3) — economy.py daily reward refactor:
- Remove redundant seed assignments (reward/currency/symbol) that were
  immediately overwritten; use claimed tuple + None sentinel instead

Notes (8) — test hygiene:
- Remove unused imports (asyncio, pytest) from test_context_ui,
  test_giveaway_commands, test_tickets_commands
- Replace module-level import + monkeypatch.setattr(mod, attr) with
  string-form monkeypatch in test_levels_plugin, test_registry_component
- Replace ... with pass in test_v52_interactions (statement-has-no-effect)
- Spec channel mocks as discord.TextChannel in test_core_gaps,
  test_plugin_commands, test_reminder, test_verification so isinstance
  guard in audit.py and plugins passes
class _PluginLike(Protocol):
"""Structural plugin type used to avoid importing Plugin and creating cycles."""

def id(self, raw: str) -> str: ...
@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown

✅ Performance benchmarks completed. All thresholds maintained.

See workflow artifacts for detailed results.

@rolling-codes rolling-codes merged commit a21d904 into main Jul 3, 2026
12 of 13 checks passed
@rolling-codes rolling-codes deleted the fix/config-concurrency-rmw branch July 3, 2026 21:00

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

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

⚠️ Outside diff range comments (1)
easycord/plugins/giveaway.py (1)

296-325: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Channel guard runs after the giveaway is already publicly posted.

ctx.respond(embed=embed) and ctx.interaction.original_response() execute before the new SENDABLE_CHANNEL_TYPES check. If the check fails, a public giveaway embed has already been sent (and the response consumed) with no entry button attached and no giveaway persisted — a broken "ghost" announcement, followed by an ephemeral error. reminder.py and polls.py perform the identical guard before responding; align giveaway() with that pattern.

🐛 Proposed fix: validate channel before responding
-        end_dt = datetime.now(timezone.utc) + timedelta(seconds=seconds)
-        end_ts = int(end_dt.timestamp())
-        embed = _build_embed(prize, end_ts, winners, 0)
-
-        await ctx.respond(embed=embed)
-        message = await ctx.interaction.original_response()
-        message_id = message.id
-        guild_id = ctx.guild.id
-        channel = ctx.channel
-        if not isinstance(channel, SENDABLE_CHANNEL_TYPES):
-            await ctx.respond("This command must be used in a channel.", ephemeral=True)
-            return
-        channel_id: int = channel.id
+        channel = ctx.channel
+        if not isinstance(channel, SENDABLE_CHANNEL_TYPES):
+            await ctx.respond("This command must be used in a channel.", ephemeral=True)
+            return
+        channel_id: int = channel.id
+
+        end_dt = datetime.now(timezone.utc) + timedelta(seconds=seconds)
+        end_ts = int(end_dt.timestamp())
+        embed = _build_embed(prize, end_ts, winners, 0)
+
+        guild_id = ctx.guild.id
+        await ctx.respond(embed=embed)
+        message = await ctx.interaction.original_response()
+        message_id = message.id
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@easycord/plugins/giveaway.py` around lines 296 - 325, The giveaway command
currently posts the public embed before validating the channel, so move the
SENDABLE_CHANNEL_TYPES guard in giveaway() to run before ctx.respond/embed
creation and before ctx.interaction.original_response() is called. Follow the
same ordering used in reminder.py and polls.py: reject non-sendable channels
first, then proceed with creating the giveaway message, saving the giveaway in
the store, adding the _GiveawayView, and scheduling the timer.
♻️ Duplicate comments (1)
easycord/bot.py (1)

373-386: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Timed-out guild-sync task still isn't tracked for shutdown/exception observation.

asyncio.create_task(self.db.sync_guilds(guild_ids)) is not added to _task_handles or _background_tasks. On timeout the task keeps running in the background, but close() (lines 479-491) only drains those two collections, so shutdown can proceed (and close the DB) while the sync is still active, and any exception raised by the task afterward is never observed. This is the same gap flagged in a prior review pass; the asyncio.shield() fix addressed cancellation-safety but not task tracking.

🔧 Proposed fix
         if timeout is not None and timeout > 0:
             task = asyncio.create_task(self.db.sync_guilds(guild_ids))
+            self._background_tasks.add(task)
+            task.add_done_callback(self._background_tasks.discard)
+            task.add_done_callback(self._log_task_exception)
             try:
                 await asyncio.wait_for(asyncio.shield(task), timeout=timeout)
             except asyncio.TimeoutError:
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@easycord/bot.py` around lines 373 - 386, The timed-out guild sync task
created in bot.py’s guild-sync startup flow is left untracked, so it can outlive
shutdown and hide exceptions. Update the async path around
self.db.sync_guilds(guild_ids) so the created task is registered with the same
tracking used by close() (_task_handles or _background_tasks), and make sure it
is removed/awaited appropriately after completion or timeout. Keep the existing
asyncio.shield/asyncio.wait_for behavior, but ensure the tracked task is still
observed during shutdown and any later exception is surfaced through the bot’s
task-management helpers.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@easycord/plugins/giveaway.py`:
- Around line 302-304: The channel-type guard in giveaway flow uses a hardcoded
user-facing response, which bypasses localization. Update the `ctx.respond` call
in the `giveaway.py` command handler to use `ctx.t(...)` the same way the
equivalent guard in `polls.py` does, so the message is retrieved from
translations instead of embedded text. Keep the existing
`SENDABLE_CHANNEL_TYPES` check and `ctx.respond(..., ephemeral=True)` flow, but
replace the literal string with the appropriate translation key and context
arguments.

In `@easycord/plugins/reminder.py`:
- Around line 210-214: The guard in the reminder command uses a hardcoded
user-facing message instead of the localization helper, so update the
`ctx.respond` call in the channel-type check to use `ctx.t(...)` like the
equivalent guard in `polls.py`. Keep the existing `isinstance(channel,
SENDABLE_CHANNEL_TYPES)` validation and `channel.id` flow intact, but replace
the literal string with the appropriate translation key and context arguments
used by the plugin’s localized responses.

In `@tests/test_context_ui.py`:
- Around line 12-16: The helper `_future()` currently uses
`asyncio.get_event_loop()`, but it is only called from running coroutines, so
update it to use `asyncio.get_running_loop()` instead. Make this change in
`_future()` in `tests/test_context_ui.py` and keep the rest of the future
creation and result-setting logic unchanged.

In `@tests/test_giveaway_commands.py`:
- Around line 23-30: The _ctx fixture uses an unspec’d MagicMock for
ctx.channel, which will fail the new giveaway() isinstance(ctx.channel,
SENDABLE_CHANNEL_TYPES) guard. Update the channel mock in _ctx to use a sendable
channel spec, matching the approach used in test_verification.py and
test_plugin_commands.py, so giveaway() can be exercised through its success
path.

---

Outside diff comments:
In `@easycord/plugins/giveaway.py`:
- Around line 296-325: The giveaway command currently posts the public embed
before validating the channel, so move the SENDABLE_CHANNEL_TYPES guard in
giveaway() to run before ctx.respond/embed creation and before
ctx.interaction.original_response() is called. Follow the same ordering used in
reminder.py and polls.py: reject non-sendable channels first, then proceed with
creating the giveaway message, saving the giveaway in the store, adding the
_GiveawayView, and scheduling the timer.

---

Duplicate comments:
In `@easycord/bot.py`:
- Around line 373-386: The timed-out guild sync task created in bot.py’s
guild-sync startup flow is left untracked, so it can outlive shutdown and hide
exceptions. Update the async path around self.db.sync_guilds(guild_ids) so the
created task is registered with the same tracking used by close() (_task_handles
or _background_tasks), and make sure it is removed/awaited appropriately after
completion or timeout. Keep the existing asyncio.shield/asyncio.wait_for
behavior, but ensure the tracked task is still observed during shutdown and any
later exception is surfaced through the bot’s task-management helpers.
🪄 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: bd17e475-be84-4247-81a7-89f956ebc035

📥 Commits

Reviewing files that changed from the base of the PR and between 9f18d33 and c44a67c.

📒 Files selected for processing (22)
  • easycord/_command_registration.py
  • easycord/_plugin_scanner.py
  • easycord/audit.py
  • easycord/bot.py
  • easycord/plugins/economy.py
  • easycord/plugins/giveaway.py
  • easycord/plugins/polls.py
  • easycord/plugins/reminder.py
  • easycord/plugins/verification.py
  • project.md
  • pyproject.toml
  • pyrightconfig.json
  • tests/test_context_ui.py
  • tests/test_core_gaps.py
  • tests/test_giveaway_commands.py
  • tests/test_levels_plugin.py
  • tests/test_plugin_commands.py
  • tests/test_registry_component.py
  • tests/test_reminder.py
  • tests/test_tickets_commands.py
  • tests/test_v52_interactions.py
  • tests/test_verification.py
📜 Review details
⚠️ CI failures not shown inline (8)

GitHub Actions: Performance Regression Detection / benchmark: release(v5.51.0): config RMW atomicity, plugin fixes (B-006/B-018), public API exports, per-plugin test floor

Conclusion: failure

View job details

##[group]Run python << 'EOF'
 �[36;1mpython << 'EOF'�[0m
 �[36;1mimport json�[0m
 �[36;1mimport sys�[0m
 �[36;1mfrom pathlib import Path�[0m
 �[36;1m�[0m
 �[36;1m# Performance thresholds (hard limits)�[0m
 �[36;1mTHRESHOLDS = {�[0m
 �[36;1m    "Cold Cache (100 lookups)": {�[0m
 �[36;1m        "total_time_ms": 100,�[0m
 �[36;1m        "avg_lookup_ms": 1.0,�[0m
 �[36;1m    },�[0m
 �[36;1m    "Warm Cache (10k lookups)": {�[0m
 �[36;1m        "total_time_ms": 500,�[0m
 �[36;1m        "avg_lookup_ms": 0.05,�[0m
 �[36;1m    },�[0m
 �[36;1m    "Diagnostics Overhead": {�[0m
 �[36;1m        "overhead_percent": 50,�[0m
 �[36;1m    },�[0m
 �[36;1m    "Metrics Overhead": {�[0m
 �[36;1m        "overhead_percent": 30,�[0m
 �[36;1m    },�[0m
 �[36;1m    "Validator Scaling (20 locales)": {�[0m
 �[36;1m        "validation_time_ms": 1000,�[0m
 �[36;1m    },�[0m
 �[36;1m}�[0m
 �[36;1m�[0m
 �[36;1m# Load current results�[0m
 �[36;1mcurrent_file = Path("benchmark-results.json")�[0m
 �[36;1mif not current_file.exists():�[0m
 �[36;1m    print("[!] ERROR: benchmark-results.json not found")�[0m
 �[36;1m    sys.exit(1)�[0m
 �[36;1m�[0m
 �[36;1mwith open(current_file) as f:�[0m
 �[36;1m    current_results = json.load(f)�[0m
 �[36;1m�[0m
 �[36;1m# Load baseline results if available�[0m
 �[36;1mbaseline_file = Path("baseline/benchmark-results.json")�[0m
 �[36;1mbaseline_results = {}�[0m
 �[36;1mif baseline_file.exists():�[0m
 �[36;1m    with open(baseline_file) as f:�[0m
 �[36;1m        baseline_results = json.load(f)�[0m
 �[36;1m�[0m
 �[36;1mviolations = []�[0m
 �[36;1mprint("[*] Checking performance thresholds:\n")�[0m
 �[36;1m�[0m
 �[36;1mfor benchmark_name, thresholds in THRESHOLDS.items():�[0m
 �[36;1m    if benchmark_name not in current_results:�[0m
 �[36;1m        print(f"[!] {benchmark_name}: NOT FOUND in results")�[0m
 �[36;1m        violations.append(f"{benchmark_name}: missing from results")�[0m
 �[36;1m        continue�[0m
 �[36;1m�[0m
 �[36;1m    current = current_results[benchm...

GitHub Actions: Performance Regression Detection / 0_benchmark.txt: release(v5.51.0): config RMW atomicity, plugin fixes (B-006/B-018), public API exports, per-plugin test floor

Conclusion: failure

View job details

##[group]Run python << 'EOF'
 �[36;1mpython << 'EOF'�[0m
 �[36;1mimport json�[0m
 �[36;1mimport sys�[0m
 �[36;1mfrom pathlib import Path�[0m
 �[36;1m�[0m
 �[36;1m# Performance thresholds (hard limits)�[0m
 �[36;1mTHRESHOLDS = {�[0m
 �[36;1m    "Cold Cache (100 lookups)": {�[0m
 �[36;1m        "total_time_ms": 100,�[0m
 �[36;1m        "avg_lookup_ms": 1.0,�[0m
 �[36;1m    },�[0m
 �[36;1m    "Warm Cache (10k lookups)": {�[0m
 �[36;1m        "total_time_ms": 500,�[0m
 �[36;1m        "avg_lookup_ms": 0.05,�[0m
 �[36;1m    },�[0m
 �[36;1m    "Diagnostics Overhead": {�[0m
 �[36;1m        "overhead_percent": 50,�[0m
 �[36;1m    },�[0m
 �[36;1m    "Metrics Overhead": {�[0m
 �[36;1m        "overhead_percent": 30,�[0m
 �[36;1m    },�[0m
 �[36;1m    "Validator Scaling (20 locales)": {�[0m
 �[36;1m        "validation_time_ms": 1000,�[0m
 �[36;1m    },�[0m
 �[36;1m}�[0m
 �[36;1m�[0m
 �[36;1m# Load current results�[0m
 �[36;1mcurrent_file = Path("benchmark-results.json")�[0m
 �[36;1mif not current_file.exists():�[0m
 �[36;1m    print("[!] ERROR: benchmark-results.json not found")�[0m
 �[36;1m    sys.exit(1)�[0m
 �[36;1m�[0m
 �[36;1mwith open(current_file) as f:�[0m
 �[36;1m    current_results = json.load(f)�[0m
 �[36;1m�[0m
 �[36;1m# Load baseline results if available�[0m
 �[36;1mbaseline_file = Path("baseline/benchmark-results.json")�[0m
 �[36;1mbaseline_results = {}�[0m
 �[36;1mif baseline_file.exists():�[0m
 �[36;1m    with open(baseline_file) as f:�[0m
 �[36;1m        baseline_results = json.load(f)�[0m
 �[36;1m�[0m
 �[36;1mviolations = []�[0m
 �[36;1mprint("[*] Checking performance thresholds:\n")�[0m
 �[36;1m�[0m
 �[36;1mfor benchmark_name, thresholds in THRESHOLDS.items():�[0m
 �[36;1m    if benchmark_name not in current_results:�[0m
 �[36;1m        print(f"[!] {benchmark_name}: NOT FOUND in results")�[0m
 �[36;1m        violations.append(f"{benchmark_name}: missing from results")�[0m
 �[36;1m        continue�[0m
 �[36;1m�[0m
 �[36;1m    current = current_results[benchm...

GitHub Actions: PR Quality / lint-and-test (3.11): release(v5.51.0): config RMW atomicity, plugin fixes (B-006/B-018), public API exports, per-plugin test floor

Conclusion: failure

View job details

##[group]Run ruff check .
 �[36;1mruff check .�[0m
 shell: /usr/bin/bash -e {0}
 env:
   pythonLocation: /opt/hostedtoolcache/Python/3.11.15/x64
   PKG_CONFIG_PATH: /opt/hostedtoolcache/Python/3.11.15/x64/lib/pkgconfig
   Python_ROOT_DIR: /opt/hostedtoolcache/Python/3.11.15/x64
   Python2_ROOT_DIR: /opt/hostedtoolcache/Python/3.11.15/x64
   Python3_ROOT_DIR: /opt/hostedtoolcache/Python/3.11.15/x64
   LD_LIBRARY_PATH: /opt/hostedtoolcache/Python/3.11.15/x64/lib
 ##[endgroup]
 F401 [*] `asyncio` imported but unused
  --> easycord/hooks.py:3:8
   |
 1 | from __future__ import annotations
 2 |
 3 | import asyncio
   |        ^^^^^^^
 4 | import inspect
 5 | import logging
   |
 help: Remove unused import: `asyncio`
 F401 [*] `re` imported but unused
  --> easycord/plugins/security_lab.py:3:8
   |
 1 | """SecurityLabPlugin — Educational security demonstrations for Discord bots."""
 2 |
 3 | import re
   |        ^^
 4 | import threading
 5 | from typing import Any, Dict
   |
 help: Remove unused import: `re`
 F401 [*] `threading` imported but unused
  --> easycord/plugins/security_lab.py:4:8
   |
 3 | import re
 4 | import threading
   |        ^^^^^^^^^
 5 | from typing import Any, Dict
   |
 help: Remove unused import: `threading`
 F401 [*] `easycord.security.truncate` imported but unused
   --> easycord/plugins/security_lab.py:10:86
    |
  9 | from easycord import EmbedBuilder, Plugin, slash
 10 | from easycord.security import escape_mentions, safe_regex, strip_injection_prefixes, truncate
    |                                                                                      ^^^^^^^^
    |
 help: Remove unused import: `easycord.security.truncate`
 F541 [*] f-string without any placeholders
   --> easycord/plugins/security_lab.py:50:34
    |
 48 |             .field("What Happened", f"Accepted {len(max_discord_str)} chars with no truncation.", inline=False)
 49 |             .field("Why It Works", "Most EasyCord plugins have no hard limit on free-text slash par...

GitHub Actions: PR Quality / lint-and-test (3.12): release(v5.51.0): config RMW atomicity, plugin fixes (B-006/B-018), public API exports, per-plugin test floor

Conclusion: failure

View job details

##[group]Run ruff check .
 �[36;1mruff check .�[0m
 shell: /usr/bin/bash -e {0}
 env:
   pythonLocation: /opt/hostedtoolcache/Python/3.12.13/x64
   PKG_CONFIG_PATH: /opt/hostedtoolcache/Python/3.12.13/x64/lib/pkgconfig
   Python_ROOT_DIR: /opt/hostedtoolcache/Python/3.12.13/x64
   Python2_ROOT_DIR: /opt/hostedtoolcache/Python/3.12.13/x64
   Python3_ROOT_DIR: /opt/hostedtoolcache/Python/3.12.13/x64
   LD_LIBRARY_PATH: /opt/hostedtoolcache/Python/3.12.13/x64/lib
 ##[endgroup]
 F401 [*] `asyncio` imported but unused
  --> easycord/hooks.py:3:8
   |
 1 | from __future__ import annotations
 2 |
 3 | import asyncio
   |        ^^^^^^^
 4 | import inspect
 5 | import logging
   |
 help: Remove unused import: `asyncio`
 F401 [*] `re` imported but unused
  --> easycord/plugins/security_lab.py:3:8
   |
 1 | """SecurityLabPlugin — Educational security demonstrations for Discord bots."""
 2 |
 3 | import re
   |        ^^
 4 | import threading
 5 | from typing import Any, Dict
   |
 help: Remove unused import: `re`
 F401 [*] `threading` imported but unused
  --> easycord/plugins/security_lab.py:4:8
   |
 3 | import re
 4 | import threading
   |        ^^^^^^^^^
 5 | from typing import Any, Dict
   |
 help: Remove unused import: `threading`
 F401 [*] `easycord.security.truncate` imported but unused
   --> easycord/plugins/security_lab.py:10:86
    |
  9 | from easycord import EmbedBuilder, Plugin, slash
 10 | from easycord.security import escape_mentions, safe_regex, strip_injection_prefixes, truncate
    |                                                                                      ^^^^^^^^
    |
 help: Remove unused import: `easycord.security.truncate`
 F541 [*] f-string without any placeholders
   --> easycord/plugins/security_lab.py:50:34
    |
 48 |             .field("What Happened", f"Accepted {len(max_discord_str)} chars with no truncation.", inline=False)
 49 |             .field("Why It Works", "Most EasyCord plugins have no hard limit on free-text slash par...

GitHub Actions: PR Quality / 2_lint-and-test (3.12).txt: release(v5.51.0): config RMW atomicity, plugin fixes (B-006/B-018), public API exports, per-plugin test floor

Conclusion: failure

View job details

##[group]Run ruff check .
 �[36;1mruff check .�[0m
 shell: /usr/bin/bash -e {0}
 env:
   pythonLocation: /opt/hostedtoolcache/Python/3.12.13/x64
   PKG_CONFIG_PATH: /opt/hostedtoolcache/Python/3.12.13/x64/lib/pkgconfig
   Python_ROOT_DIR: /opt/hostedtoolcache/Python/3.12.13/x64
   Python2_ROOT_DIR: /opt/hostedtoolcache/Python/3.12.13/x64
   Python3_ROOT_DIR: /opt/hostedtoolcache/Python/3.12.13/x64
   LD_LIBRARY_PATH: /opt/hostedtoolcache/Python/3.12.13/x64/lib
 ##[endgroup]
 F401 [*] `asyncio` imported but unused
  --> easycord/hooks.py:3:8
   |
 1 | from __future__ import annotations
 2 |
 3 | import asyncio
   |        ^^^^^^^
 4 | import inspect
 5 | import logging
   |
 help: Remove unused import: `asyncio`
 F401 [*] `re` imported but unused
  --> easycord/plugins/security_lab.py:3:8
   |
 1 | """SecurityLabPlugin — Educational security demonstrations for Discord bots."""
 2 |
 3 | import re
   |        ^^
 4 | import threading
 5 | from typing import Any, Dict
   |
 help: Remove unused import: `re`
 F401 [*] `threading` imported but unused
  --> easycord/plugins/security_lab.py:4:8
   |
 3 | import re
 4 | import threading
   |        ^^^^^^^^^
 5 | from typing import Any, Dict
   |
 help: Remove unused import: `threading`
 F401 [*] `easycord.security.truncate` imported but unused
   --> easycord/plugins/security_lab.py:10:86
    |
  9 | from easycord import EmbedBuilder, Plugin, slash
 10 | from easycord.security import escape_mentions, safe_regex, strip_injection_prefixes, truncate
    |                                                                                      ^^^^^^^^
    |
 help: Remove unused import: `easycord.security.truncate`
 F541 [*] f-string without any placeholders
   --> easycord/plugins/security_lab.py:50:34
    |
 48 |             .field("What Happened", f"Accepted {len(max_discord_str)} chars with no truncation.", inline=False)
 49 |             .field("Why It Works", "Most EasyCord plugins have no hard limit on free-text slash par...

GitHub Actions: PR Quality / lint-and-test (3.10): release(v5.51.0): config RMW atomicity, plugin fixes (B-006/B-018), public API exports, per-plugin test floor

Conclusion: failure

View job details

##[group]Run ruff check .
 �[36;1mruff check .�[0m
 shell: /usr/bin/bash -e {0}
 env:
   pythonLocation: /opt/hostedtoolcache/Python/3.10.20/x64
   PKG_CONFIG_PATH: /opt/hostedtoolcache/Python/3.10.20/x64/lib/pkgconfig
   Python_ROOT_DIR: /opt/hostedtoolcache/Python/3.10.20/x64
   Python2_ROOT_DIR: /opt/hostedtoolcache/Python/3.10.20/x64
   Python3_ROOT_DIR: /opt/hostedtoolcache/Python/3.10.20/x64
   LD_LIBRARY_PATH: /opt/hostedtoolcache/Python/3.10.20/x64/lib
 ##[endgroup]
 F401 [*] `asyncio` imported but unused
  --> easycord/hooks.py:3:8
   |
 1 | from __future__ import annotations
 2 |
 3 | import asyncio
   |        ^^^^^^^
 4 | import inspect
 5 | import logging
   |
 help: Remove unused import: `asyncio`
 F401 [*] `re` imported but unused
  --> easycord/plugins/security_lab.py:3:8
   |
 1 | """SecurityLabPlugin — Educational security demonstrations for Discord bots."""
 2 |
 3 | import re
   |        ^^
 4 | import threading
 5 | from typing import Any, Dict
   |
 help: Remove unused import: `re`
 F401 [*] `threading` imported but unused
  --> easycord/plugins/security_lab.py:4:8
   |
 3 | import re
 4 | import threading
   |        ^^^^^^^^^
 5 | from typing import Any, Dict
   |
 help: Remove unused import: `threading`
 F401 [*] `easycord.security.truncate` imported but unused
   --> easycord/plugins/security_lab.py:10:86
    |
  9 | from easycord import EmbedBuilder, Plugin, slash
 10 | from easycord.security import escape_mentions, safe_regex, strip_injection_prefixes, truncate
    |                                                                                      ^^^^^^^^
    |
 help: Remove unused import: `easycord.security.truncate`
 F541 [*] f-string without any placeholders
   --> easycord/plugins/security_lab.py:50:34
    |
 48 |             .field("What Happened", f"Accepted {len(max_discord_str)} chars with no truncation.", inline=False)
 49 |             .field("Why It Works", "Most EasyCord plugins have no hard limit on free-text slash par...

GitHub Actions: PR Quality / 1_lint-and-test (3.10).txt: release(v5.51.0): config RMW atomicity, plugin fixes (B-006/B-018), public API exports, per-plugin test floor

Conclusion: failure

View job details

##[group]Run ruff check .
 �[36;1mruff check .�[0m
 shell: /usr/bin/bash -e {0}
 env:
   pythonLocation: /opt/hostedtoolcache/Python/3.10.20/x64
   PKG_CONFIG_PATH: /opt/hostedtoolcache/Python/3.10.20/x64/lib/pkgconfig
   Python_ROOT_DIR: /opt/hostedtoolcache/Python/3.10.20/x64
   Python2_ROOT_DIR: /opt/hostedtoolcache/Python/3.10.20/x64
   Python3_ROOT_DIR: /opt/hostedtoolcache/Python/3.10.20/x64
   LD_LIBRARY_PATH: /opt/hostedtoolcache/Python/3.10.20/x64/lib
 ##[endgroup]
 F401 [*] `asyncio` imported but unused
  --> easycord/hooks.py:3:8
   |
 1 | from __future__ import annotations
 2 |
 3 | import asyncio
   |        ^^^^^^^
 4 | import inspect
 5 | import logging
   |
 help: Remove unused import: `asyncio`
 F401 [*] `re` imported but unused
  --> easycord/plugins/security_lab.py:3:8
   |
 1 | """SecurityLabPlugin — Educational security demonstrations for Discord bots."""
 2 |
 3 | import re
   |        ^^
 4 | import threading
 5 | from typing import Any, Dict
   |
 help: Remove unused import: `re`
 F401 [*] `threading` imported but unused
  --> easycord/plugins/security_lab.py:4:8
   |
 3 | import re
 4 | import threading
   |        ^^^^^^^^^
 5 | from typing import Any, Dict
   |
 help: Remove unused import: `threading`
 F401 [*] `easycord.security.truncate` imported but unused
   --> easycord/plugins/security_lab.py:10:86
    |
  9 | from easycord import EmbedBuilder, Plugin, slash
 10 | from easycord.security import escape_mentions, safe_regex, strip_injection_prefixes, truncate
    |                                                                                      ^^^^^^^^
    |
 help: Remove unused import: `easycord.security.truncate`
 F541 [*] f-string without any placeholders
   --> easycord/plugins/security_lab.py:50:34
    |
 48 |             .field("What Happened", f"Accepted {len(max_discord_str)} chars with no truncation.", inline=False)
 49 |             .field("Why It Works", "Most EasyCord plugins have no hard limit on free-text slash par...

GitHub Actions: PR Quality / 0_lint-and-test (3.11).txt: release(v5.51.0): config RMW atomicity, plugin fixes (B-006/B-018), public API exports, per-plugin test floor

Conclusion: failure

View job details

##[group]Run ruff check .
 �[36;1mruff check .�[0m
 shell: /usr/bin/bash -e {0}
 env:
   pythonLocation: /opt/hostedtoolcache/Python/3.11.15/x64
   PKG_CONFIG_PATH: /opt/hostedtoolcache/Python/3.11.15/x64/lib/pkgconfig
   Python_ROOT_DIR: /opt/hostedtoolcache/Python/3.11.15/x64
   Python2_ROOT_DIR: /opt/hostedtoolcache/Python/3.11.15/x64
   Python3_ROOT_DIR: /opt/hostedtoolcache/Python/3.11.15/x64
   LD_LIBRARY_PATH: /opt/hostedtoolcache/Python/3.11.15/x64/lib
 ##[endgroup]
 F401 [*] `asyncio` imported but unused
  --> easycord/hooks.py:3:8
   |
 1 | from __future__ import annotations
 2 |
 3 | import asyncio
   |        ^^^^^^^
 4 | import inspect
 5 | import logging
   |
 help: Remove unused import: `asyncio`
 F401 [*] `re` imported but unused
  --> easycord/plugins/security_lab.py:3:8
   |
 1 | """SecurityLabPlugin — Educational security demonstrations for Discord bots."""
 2 |
 3 | import re
   |        ^^
 4 | import threading
 5 | from typing import Any, Dict
   |
 help: Remove unused import: `re`
 F401 [*] `threading` imported but unused
  --> easycord/plugins/security_lab.py:4:8
   |
 3 | import re
 4 | import threading
   |        ^^^^^^^^^
 5 | from typing import Any, Dict
   |
 help: Remove unused import: `threading`
 F401 [*] `easycord.security.truncate` imported but unused
   --> easycord/plugins/security_lab.py:10:86
    |
  9 | from easycord import EmbedBuilder, Plugin, slash
 10 | from easycord.security import escape_mentions, safe_regex, strip_injection_prefixes, truncate
    |                                                                                      ^^^^^^^^
    |
 help: Remove unused import: `easycord.security.truncate`
 F541 [*] f-string without any placeholders
   --> easycord/plugins/security_lab.py:50:34
    |
 48 |             .field("What Happened", f"Accepted {len(max_discord_str)} chars with no truncation.", inline=False)
 49 |             .field("Why It Works", "Most EasyCord plugins have no hard limit on free-text slash par...
🧰 Additional context used
📓 Path-based instructions (4)
**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

**/*.py: Tests use pytest-asyncio with asyncio_mode = "auto", so async tests need no manual event loop setup.
The @ai_tool decorator must register a function into ToolRegistry and requires an explicit ToolSafety permission annotation.
Use ctx.user / ctx.member; ctx.author does not exist.
ctx.is_admin is a property, not a method; do not call it as ctx.is_admin().
Cooldown sentinels must default to float("-inf"), not 0.0, so first-message events pass on fresh runners.

**/*.py: Import from easycord’s public API only; do not import from any easycord._* internal module.
When working with Context, use ctx.user / ctx.member; ctx.author does not exist, and ctx.is_admin is a property, not a method.
When implementing plugins, subclass Plugin, decorate handlers with @slash / @on, register them with bot.add_plugin(plugin_instance), and keep per-guild state in the database layer rather than on the plugin instance.
Never hardcode user-facing response strings in plugins; always look them up with ctx.t(...).
Any function that uses ToolLimiter.check_limit, reset_user, or reset_tool must await them; these methods are async.
Never call .send() on a channel obtained from ctx or Discord until its type is narrowed first; use SENDABLE_CHANNEL_TYPES or an equivalent isinstance guard.

Files:

  • tests/test_core_gaps.py
  • tests/test_verification.py
  • easycord/plugins/verification.py
  • tests/test_v52_interactions.py
  • tests/test_registry_component.py
  • tests/test_plugin_commands.py
  • easycord/audit.py
  • easycord/plugins/giveaway.py
  • easycord/plugins/reminder.py
  • easycord/plugins/polls.py
  • tests/test_giveaway_commands.py
  • tests/test_levels_plugin.py
  • easycord/plugins/economy.py
  • tests/test_tickets_commands.py
  • tests/test_context_ui.py
  • tests/test_reminder.py
  • easycord/_plugin_scanner.py
  • easycord/bot.py
  • easycord/_command_registration.py
easycord/plugins/**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

Localization keys must be looked up via LocalizationManager; plugin responses must not hardcode strings.

Files:

  • easycord/plugins/verification.py
  • easycord/plugins/giveaway.py
  • easycord/plugins/reminder.py
  • easycord/plugins/polls.py
  • easycord/plugins/economy.py
easycord/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

easycord/**/*.py: Do not use bot.run(reload=True) in production code; the hot-reload watcher is dev-only.
Event-path plugins (@on("message") and similar) must route every destructive action through one governed method that owns rate limiting, channel narrowing, and Discord error handling, and must not let Discord exceptions escape into the dispatcher.
When defining AI tools, annotate @ai_tool functions with an explicit ToolSafety value; @ai_tool without a safety annotation must not register.

Files:

  • easycord/plugins/verification.py
  • easycord/audit.py
  • easycord/plugins/giveaway.py
  • easycord/plugins/reminder.py
  • easycord/plugins/polls.py
  • easycord/plugins/economy.py
  • easycord/_plugin_scanner.py
  • easycord/bot.py
  • easycord/_command_registration.py
easycord/_command_*.py

📄 CodeRabbit inference engine (CLAUDE.md)

Validate Discord command constraints up front: names must be ≤ 32 characters and match [-_a-z0-9], descriptions must be ≤ 100 characters, and commands/options must not exceed Discord’s option/choice limits; raise ValueError before syncing to the tree.

Files:

  • easycord/_command_registration.py
🧠 Learnings (1)
📓 Common learnings
Learnt from: CR
Repo: rolling-codes/EasyCord

Timestamp: 2026-07-03T20:53:33.038Z
Learning: Per-guild state must go through the database layer and must never be stored directly on the `Bot` instance.
🪛 GitHub Actions: PR Quality / 0_lint-and-test (3.11).txt
tests/test_reminder.py

[error] 8-8: ruff (F401): pytest imported but unused. Help: Remove unused import: pytest.

🪛 GitHub Actions: PR Quality / 1_lint-and-test (3.10).txt
tests/test_reminder.py

[error] 8-8: ruff check reported F401: pytest imported but unused. Help: Remove unused import: pytest.

🪛 GitHub Actions: PR Quality / 2_lint-and-test (3.12).txt
tests/test_reminder.py

[error] 8-8: ruff check failed (F401). pytest imported but unused. Remove unused import: pytest.

🪛 GitHub Actions: PR Quality / lint-and-test (3.10)
tests/test_reminder.py

[error] 8-8: ruff (F401): pytest imported but unused. Remove unused import.

🪛 GitHub Actions: PR Quality / lint-and-test (3.11)
tests/test_reminder.py

[error] 8-8: Ruff (F401) pytest imported but unused. Remove unused import.

🪛 GitHub Actions: PR Quality / lint-and-test (3.12)
tests/test_reminder.py

[error] 8-8: Ruff F401: pytest imported but unused. Remove unused import: pytest.

🪛 GitHub Check: CodeQL
easycord/_plugin_scanner.py

[notice] 29-29: Statement has no effect
This statement has no effect.

🔇 Additional comments (25)
tests/test_context_ui.py (2)

1-30: Past CodeQL alert about the unused pytest import appears resolved — this file no longer imports pytest.


34-243: LGTM! Good coverage of confirm/paginate/choose/ask_form/prompt success and timeout paths.

easycord/bot.py (3)

388-391: 🗄️ Data Integrity & Integration

Re-verify: setup_hook still triggers guild sync before the gateway connects.

self.guilds is empty in setup_hook (discord.py's setup_hook() runs after login but before the gateway connects, so guild GUILD_CREATE events haven't arrived yet). A prior review flagged this exact call as a harmless "no-op." Given this PR's stated focus on guild-config RMW atomicity, it's worth re-confirming that db.sync_guilds([]) at this point doesn't treat the empty list as authoritative and prune/delete existing per-guild configs — that would turn a "no-op" into data loss on every restart, whereas the equivalent call in on_ready (line 437) is safe since guilds are populated there.

#!/bin/bash
rg -n "async def sync_guilds" -A 30 easycord/database.py

70-138: LGTM! Timeout-bounded guild sync with asyncio.shield() correctly avoids interrupting the DB task mid-lock, and on_ready's use of the same helper is correctly placed after guilds are populated.

Also applies to: 392-528


9-12: 🎯 Functional Correctness

No runtime NameError here from __future__ import annotations defers annotation evaluation, so the AIProviderProtocol hint under TYPE_CHECKING is safe.

			> Likely an incorrect or invalid review comment.
tests/test_core_gaps.py (1)

354-354: LGTM!

pyrightconfig.json (1)

2-36: LGTM!

easycord/audit.py (1)

10-11: LGTM! Narrowing via SENDABLE_CHANNEL_TYPES is a stricter, more correct guard than the previous hasattr check.
As per coding guidelines, "Never call .send() on a channel obtained from ctx or Discord until its type is narrowed first; use SENDABLE_CHANNEL_TYPES or an equivalent isinstance guard."

Also applies to: 114-119

Source: Coding guidelines

tests/test_reminder.py (1)

7-7: LGTM! Solid hardening — typed channel mocks, closing the real coroutine in the create_task patch to avoid unawaited-coroutine warnings, and added duration-parsing edge cases.

Also applies to: 24-24, 84-85, 104-114, 226-226, 271-271, 294-294

tests/test_registry_component.py (1)

54-66: 🎯 Functional Correctness

No issue: easycord.registry imports the time module, so monkeypatch.setattr("time.time", ...) affects the clock reads used here.

			> Likely an incorrect or invalid review comment.
tests/test_verification.py (1)

271-274: LGTM!

easycord/plugins/verification.py (1)

11-11: LGTM! Correctly narrows the channel type before channel.send() is invoked, per project guidelines on sendable-channel guards.

Also applies to: 288-294

tests/test_plugin_commands.py (1)

8-8: LGTM!

Also applies to: 36-36

easycord/plugins/polls.py (1)

12-12: LGTM! Correct ordering (guard before responding) and proper localization via ctx.t(...) — this is the pattern giveaway.py/reminder.py should match.

Also applies to: 302-308

tests/test_tickets_commands.py (1)

1-308: LGTM! Previously flagged unused asyncio/pytest imports are no longer present.

easycord/_plugin_scanner.py (2)

26-30: 📐 Maintainability & Code Quality

CodeQL "statement has no effect" on the Protocol stub is a known false positive.

... is the standard body for a Protocol method stub; this alert was already raised in a prior review round on this file and CodeQL alerts related to this typing architecture were noted as triaged in the PR history. No action needed.

Source: Linters/SAST tools


4-34: LGTM!

Also applies to: 105-142, 145-204

project.md (2)

64-64: 📐 Maintainability & Code Quality

Full-suite count still inconsistent with the release record.

1248 passed doesn't match this PR's own stated result of 1335 passing tests. This was already flagged in a prior review round without an "Addressed" resolution recorded — still appears open in the current text.


1-49: LGTM!

Also applies to: 52-63, 68-92, 95-101

tests/test_v52_interactions.py (1)

17-26: LGTM!

pyproject.toml (1)

7-7: LGTM!

Also applies to: 29-31, 51-52

tests/test_levels_plugin.py (1)

3-3: LGTM!

Also applies to: 272-293

easycord/plugins/economy.py (1)

236-278: LGTM!

Atomic claim-decide-and-persist under a single _balance_lock acquisition, with all Discord I/O deferred until after the lock releases — matches the RMW-atomicity goal for this release and eliminates the earlier reward/currency/symbol re-assignment CodeQL noise.

easycord/_command_registration.py (2)

296-300: LGTM!

setattr-based signature stamping and the getattr/setattr choices-init are functionally equivalent, safer refactors.

Also applies to: 325-334


7-27: 🩺 Stability & Availability

No action needed — from __future__ import annotations already makes these TYPE_CHECKING-only annotations safe.

			> Likely an incorrect or invalid review comment.

Comment on lines +302 to +304
if not isinstance(channel, SENDABLE_CHANNEL_TYPES):
await ctx.respond("This command must be used in a channel.", ephemeral=True)
return

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Hardcoded error string bypasses ctx.t(...).

Unlike the equivalent guard added to polls.py (ctx.t("polls.channel_required", ...)), this string is hardcoded. As per coding guidelines, "Never hardcode user-facing response strings in plugins; always look them up with ctx.t(...)."

🌐 Proposed fix
-            await ctx.respond("This command must be used in a channel.", ephemeral=True)
+            await ctx.respond(
+                ctx.t("giveaway.channel_required", default="This command must be used in a channel."),
+                ephemeral=True,
+            )
📝 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
if not isinstance(channel, SENDABLE_CHANNEL_TYPES):
await ctx.respond("This command must be used in a channel.", ephemeral=True)
return
if not isinstance(channel, SENDABLE_CHANNEL_TYPES):
await ctx.respond(
ctx.t("giveaway.channel_required", default="This command must be used in a channel."),
ephemeral=True,
)
return
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@easycord/plugins/giveaway.py` around lines 302 - 304, The channel-type guard
in giveaway flow uses a hardcoded user-facing response, which bypasses
localization. Update the `ctx.respond` call in the `giveaway.py` command handler
to use `ctx.t(...)` the same way the equivalent guard in `polls.py` does, so the
message is retrieved from translations instead of embedded text. Keep the
existing `SENDABLE_CHANNEL_TYPES` check and `ctx.respond(..., ephemeral=True)`
flow, but replace the literal string with the appropriate translation key and
context arguments.

Source: Coding guidelines

Comment on lines +210 to +214
channel = ctx.channel
if not isinstance(channel, SENDABLE_CHANNEL_TYPES):
await ctx.respond("This command must be used in a channel.", ephemeral=True)
return
channel_id: int = channel.id

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Hardcoded error string bypasses ctx.t(...).

Ordering is correct here (guard runs before ctx.respond), but the message itself is hardcoded. The equivalent guard in polls.py was localized via ctx.t("polls.channel_required", ...); this one should follow the same pattern. As per coding guidelines, "Never hardcode user-facing response strings in plugins; always look them up with ctx.t(...)."

🌐 Proposed fix
-            await ctx.respond("This command must be used in a channel.", ephemeral=True)
+            await ctx.respond(
+                ctx.t("reminder.channel_required", default="This command must be used in a channel."),
+                ephemeral=True,
+            )
📝 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
channel = ctx.channel
if not isinstance(channel, SENDABLE_CHANNEL_TYPES):
await ctx.respond("This command must be used in a channel.", ephemeral=True)
return
channel_id: int = channel.id
channel = ctx.channel
if not isinstance(channel, SENDABLE_CHANNEL_TYPES):
await ctx.respond(
ctx.t("reminder.channel_required", default="This command must be used in a channel."),
ephemeral=True,
)
return
channel_id: int = channel.id
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@easycord/plugins/reminder.py` around lines 210 - 214, The guard in the
reminder command uses a hardcoded user-facing message instead of the
localization helper, so update the `ctx.respond` call in the channel-type check
to use `ctx.t(...)` like the equivalent guard in `polls.py`. Keep the existing
`isinstance(channel, SENDABLE_CHANNEL_TYPES)` validation and `channel.id` flow
intact, but replace the literal string with the appropriate translation key and
context arguments used by the plugin’s localized responses.

Source: Coding guidelines

Comment thread tests/test_context_ui.py
Comment on lines +12 to +16
def _future(value=None):
"""Return a future already resolved with *value*."""
f = asyncio.get_event_loop().create_future()
f.set_result(value)
return f

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Prefer asyncio.get_running_loop() over get_event_loop().

_future() is only ever called from inside running coroutines; get_running_loop() is the more correct, non-deprecated call here.

♻️ Proposed tweak
 def _future(value=None):
     """Return a future already resolved with *value*."""
-    f = asyncio.get_event_loop().create_future()
+    f = asyncio.get_running_loop().create_future()
     f.set_result(value)
     return f
📝 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
def _future(value=None):
"""Return a future already resolved with *value*."""
f = asyncio.get_event_loop().create_future()
f.set_result(value)
return f
def _future(value=None):
"""Return a future already resolved with *value*."""
f = asyncio.get_running_loop().create_future()
f.set_result(value)
return f
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_context_ui.py` around lines 12 - 16, The helper `_future()`
currently uses `asyncio.get_event_loop()`, but it is only called from running
coroutines, so update it to use `asyncio.get_running_loop()` instead. Make this
change in `_future()` in `tests/test_context_ui.py` and keep the rest of the
future creation and result-setting logic unchanged.

Comment on lines +23 to +30
def _ctx(guild_id: int = 100) -> MagicMock:
ctx = MagicMock()
ctx.guild = MagicMock()
ctx.guild.id = guild_id
ctx.respond = AsyncMock()
ctx.channel = MagicMock()
ctx.channel.id = 55
return ctx

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

ctx.channel mock isn't spec'd to a sendable channel type.

giveaway() now guards with isinstance(ctx.channel, SENDABLE_CHANNEL_TYPES). A plain MagicMock() fails that check, so this fixture would defeat any future happy-path test of giveaway()'s success flow. Sibling fixtures in test_verification.py/test_plugin_commands.py were updated to MagicMock(spec=discord.TextChannel) in this same PR; recommend the same here for consistency and to unblock coverage of the new guard's success path.

♻️ Proposed fix
+import discord
+
 def _ctx(guild_id: int = 100) -> MagicMock:
     ctx = MagicMock()
     ctx.guild = MagicMock()
     ctx.guild.id = guild_id
     ctx.respond = AsyncMock()
-    ctx.channel = MagicMock()
+    ctx.channel = MagicMock(spec=discord.TextChannel)
     ctx.channel.id = 55
     return ctx
📝 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
def _ctx(guild_id: int = 100) -> MagicMock:
ctx = MagicMock()
ctx.guild = MagicMock()
ctx.guild.id = guild_id
ctx.respond = AsyncMock()
ctx.channel = MagicMock()
ctx.channel.id = 55
return ctx
import discord
def _ctx(guild_id: int = 100) -> MagicMock:
ctx = MagicMock()
ctx.guild = MagicMock()
ctx.guild.id = guild_id
ctx.respond = AsyncMock()
ctx.channel = MagicMock(spec=discord.TextChannel)
ctx.channel.id = 55
return ctx
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_giveaway_commands.py` around lines 23 - 30, The _ctx fixture uses
an unspec’d MagicMock for ctx.channel, which will fail the new giveaway()
isinstance(ctx.channel, SENDABLE_CHANNEL_TYPES) guard. Update the channel mock
in _ctx to use a sendable channel spec, matching the approach used in
test_verification.py and test_plugin_commands.py, so giveaway() can be exercised
through its success path.

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

Labels

bug Something isn't working ci dependencies documentation Improvements or additions to documentation plugin release:minor tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants