release(v5.51.0): config RMW atomicity, plugin fixes (B-006/B-018), public API exports, per-plugin test floor#72
Conversation
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>
There was a problem hiding this comment.
Sorry @rolling-codes, your pull request is larger than the review limit of 150000 diff characters
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
Warning Review limit reached
Next review available in: 48 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the 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 configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughThis 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. ChangesCore Runtime & Plugin Behavior Fixes
Release Engineering, CI, and Config
Documentation, Changelog, and Planning Content
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related PRs
Suggested labels: Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
PR Summary by Qodov5.51.0 stability: atomic per-guild config writes, hot-reload gating, permission preflight
AI Description
Diagram
High-Level Assessment
Files changed (85)
|
Code Review by Qodo
Context used✅ Compliance rules (platform):
28 rules 1.
|
- 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.
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>
|
✅ Performance benchmarks completed. All thresholds maintained. See workflow artifacts for detailed results. |
…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>
…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>
|
✅ 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>
|
✅ 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.
|
Heads-up on the CodeQL autofix suggestions: the ten auto-applied "Potential fix" commits were reverted in 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 |
|
✅ 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>
|
✅ Performance benchmarks completed. All thresholds maintained. See workflow artifacts for detailed results. |
…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
|
✅ Performance benchmarks completed. All thresholds maintained. See workflow artifacts for detailed results. |
There was a problem hiding this comment.
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 winUpdate the upgrade note to reflect the API migration.
ctx.authornow raises andslash_commandis 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 winMake 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 liftSlash 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/**/*.pymust useLocalizationManagerlookups 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
LocalizationManagerlookups; 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 winConsider extracting the shared reaction-guard logic.
_on_reaction_addand_on_reaction_removeduplicate the same self-reaction/enabled/emoji guard sequence. This exact duplicatedenabledcheck 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 winHardcoded 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 winAdd 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 winUpdate 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 winRemove the already-shipped items from the deferred list.
LocalizationManagermetrics atomicity and the hot-reload dispatch lock are already described in this PR as implemented fixes, so keeping them underDeferred to v5.52.0will 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
📒 Files selected for processing (42)
.github/workflows/nightly.yml.github/workflows/tests.ymlAGENTS.mdCHANGELOG.mdCLAUDE.mdCONTRIBUTING.mdREADME.mdbugs.mdcontext/implementation-plan.mddocs/builtin-plugins.mdeasycord/__init__.pyeasycord/_command_registration.pyeasycord/_context_base.pyeasycord/decorators.pyeasycord/helpers/__init__.pyeasycord/helpers/channel.pyeasycord/plugin.pyeasycord/plugin_creator.pyeasycord/plugins/_utils.pyeasycord/plugins/ai_moderator.pyeasycord/plugins/invite_tracker.pyeasycord/plugins/member_logging.pyeasycord/plugins/moderation.pyeasycord/plugins/polls.pyeasycord/plugins/starboard.pyeasycord/testing.pyexamples/with-builtin-plugins.pyproject.mdpyproject.tomlpyrightconfig.jsonrelease_v5.51.0/notes.mdscripts/patch_tests.pyscripts/stress_test_db.pyscripts/verify_plugin_tests.pytests/test_guild_sync_timeout.pytests/test_hot_reload.pytests/test_plugin_creator.pytests/test_pr71_concurrency.pytests/test_starboard.pytests/test_suggestions.pytests/test_tags.pytests/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
##[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
##[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
##[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
##[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
##[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
##[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
##[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/checkouttov7andactions/setup-pythontov5.
Files:
.github/workflows/tests.yml.github/workflows/nightly.yml
easycord/**/*.py
📄 CodeRabbit inference engine (AGENTS.md)
easycord/**/*.py: Usectx.user/ctx.member;ctx.authordoes not exist.
ctx.is_adminis a property, not a method; do not call it asctx.is_admin().
Cooldown sentinels should default tofloat("-inf"), not0.0, so first-message events pass on fresh runners.
Files:
easycord/helpers/channel.pyeasycord/plugins/invite_tracker.pyeasycord/helpers/__init__.pyeasycord/plugin.pyeasycord/plugins/_utils.pyeasycord/plugins/polls.pyeasycord/_context_base.pyeasycord/__init__.pyeasycord/plugin_creator.pyeasycord/testing.pyeasycord/decorators.pyeasycord/plugins/moderation.pyeasycord/plugins/ai_moderator.pyeasycord/_command_registration.pyeasycord/plugins/starboard.pyeasycord/plugins/member_logging.py
easycord/plugins/**/*.py
📄 CodeRabbit inference engine (AGENTS.md)
Plugin responses must use
LocalizationManagerlookups; hardcoded strings should not be used in plugin responses.
Files:
easycord/plugins/invite_tracker.pyeasycord/plugins/_utils.pyeasycord/plugins/polls.pyeasycord/plugins/moderation.pyeasycord/plugins/ai_moderator.pyeasycord/plugins/starboard.pyeasycord/plugins/member_logging.py
tests/**/*.py
📄 CodeRabbit inference engine (AGENTS.md)
Async tests use
pytest-asynciowithasyncio_mode = "auto", so they should not set up a manual event loop.
Files:
tests/test_v52_interactions.pytests/test_plugin_creator.pytests/test_hot_reload.pytests/test_suggestions.pytests/test_tags.pytests/test_starboard.py
easycord/plugin.py
📄 CodeRabbit inference engine (CLAUDE.md)
Subclass
Plugin, attach behavior with@slash/@on, and register instances withbot.add_plugin(plugin_instance); keep per-guild state out ofselfand 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.pyor the_context_<area>.pycontext mixin files.
Files:
easycord/_context_base.py
easycord/_*.py
📄 CodeRabbit inference engine (CLAUDE.md)
Treat every
_-prefixed module ineasycordas internal implementation detail; these modules may change without notice and should not be imported from outside the package boundary.
Files:
easycord/_context_base.pyeasycord/__init__.pyeasycord/_command_registration.py
easycord/_context_*.py
📄 CodeRabbit inference engine (CLAUDE.md)
In internal context helper modules, preserve the
ContextAPI contract: usectx.user/ctx.member, neverctx.author, and treatctx.is_adminas a property.
Files:
easycord/_context_base.py
easycord/__init__.py
📄 CodeRabbit inference engine (CLAUDE.md)
Treat
easycord/__init__.pyas 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.pydefines 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.pybuilds callback wrappers, while_command_registration.pyhandles 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 QualityPreviously 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 = Noneguard plusfinallyblock now ensuresdb.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:testsexecutionEnvironment now correctly consolidated here.This resolves the prior review finding that the
[[tool.pyright.executionEnvironments]]block inpyproject.tomlwas being silently ignored becausepyrightconfig.jsontakes 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 againstself.bot.userbeingNone.Also applies to: 224-234
264-276: LGTM! Theassert ctx.guild is not Noneguards are consistent with the existingguild_only=Trueconvention 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 newenableddefault 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 thesuggestions()implementation's filtering/sorting/truncation behavior.
354-401: LGTM! Good concurrency regression coverage for the atomicsuggest/suggestion_approvemutate paths.tests/test_tags.py (2)
165-201: LGTM! Correctly validates the atomicdelete_if_authorizedreason-code contract that fixes the tags TOCTOU issue.
203-226: LGTM!tests/test_v52_interactions.py (1)
17-26: 🎯 Functional CorrectnessCodeQL "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 covercomment) and was already flagged by CodeQL in a prior review pass on this file.easycord/_context_base.py (1)
35-45: 🎯 Functional CorrectnessNo instance-level
__getattr__fallback here.Contextand its mixins do not define__getattr__or__getattribute__, so this property’sAttributeErrorwon’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 aDeprecationWarningand correct forwarding toslash(*args, **kwargs).easycord/_command_registration.py (4)
84-118: LGTM!
bot_permissionsis correctly threaded through tocallback_builder, consistent with the upstream_bot_commands.py/_plugin_scanner.pypropagation shown in the provided snippets.
176-184: LGTM!
246-249: 🩺 Stability & AvailabilityConfirm
register_context_menu'sbot: "_BotBase"typing doesn't reintroduce the cyclic-import CodeQL finding.
register_slashand_register_autocomplete_handlerwere switched to the new_BotBaseLikeProtocol specifically to avoid importing_BotBaseand 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 typedbot: "_BotBase"rather than"_BotBaseLike". If_BotBaseis imported (even guarded byTYPE_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. becauseregister_context_menuneeds a nominal type not covered by the Protocol) or align it with_BotBaseLikefor consistency.
[medium_effort_and_high_reward]
7-19: 🎯 Functional CorrectnessNo issue here.
Contextis imported underif TYPE_CHECKING,_BotBaseLikeis defined locally, andUnionis used later inregister_context_menu.> Likely an incorrect or invalid review comment.easycord/plugins/moderation.py (3)
11-11: LGTM!
127-127: LGTM!
bot_permissionscorrectly matches the privileged Discord API each command exercises (kick_members,ban_members,moderate_members,manage_roles), and is enforced pre-execution bybuild_slash_callback'sctx.bot_permissionscheck 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-/warnlost-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_guildfor moderation config commands (mod_enable,mod_threshold,mod_action_level,mod_add_rule,mod_remove_rule) is a sound tightening — previously onlyguild_only=Truegated 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 keepeasycord/__init__.pyas 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!
| # 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, | ||
| ) |
There was a problem hiding this comment.
🎯 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' easycordRepository: 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:
- 1: https://github.com/Rapptz/discord.py/blob/2fbed936/docs/whats_new.rst
- 2: https://github.com/Rapptz/discord.py/blob/master/discord/abc.py
- 3: StageChannel is not Messagable Rapptz/discord.py#9248
- 4: https://discordpy.readthedocs.io/en/v2.3.1/whats_new.html
- 5: https://discordpy.readthedocs.io/en/stable/whats_new.html
- 6: https://github.com/Rapptz/discord.py/blob/master/discord/channel.py
- 7: https://discordpy.readthedocs.io/en/v2.7.1/api.html
🏁 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)
PYRepository: 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.
| 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 |
There was a problem hiding this comment.
🎯 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.
| 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.
| 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, | ||
| ) |
There was a problem hiding this comment.
🎯 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:
- 1: https://stackoverflow.com/questions/70676150/discord-py-get-channel-permissions-of-user
- 2: https://deepwiki.com/Rapptz/discord.py/3.5-roles-and-permissions
🏁 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.
| print(f"✓ Loaded built-in plugins: welcome, tags, polls, levels") | ||
| print(f"✓ Loaded opt-in plugins: moderation, economy, reminders, starboard, admin") |
There was a problem hiding this comment.
📐 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.
| 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
| | 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. |
There was a problem hiding this comment.
📐 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.
| ## 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) | ||
|
|
There was a problem hiding this comment.
🎯 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: ... |
|
✅ Performance benchmarks completed. All thresholds maintained. See workflow artifacts for detailed results. |
There was a problem hiding this comment.
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 winChannel guard runs after the giveaway is already publicly posted.
ctx.respond(embed=embed)andctx.interaction.original_response()execute before the newSENDABLE_CHANNEL_TYPEScheck. 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.pyandpolls.pyperform the identical guard before responding; aligngiveaway()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 winTimed-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_handlesor_background_tasks. On timeout the task keeps running in the background, butclose()(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; theasyncio.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
📒 Files selected for processing (22)
easycord/_command_registration.pyeasycord/_plugin_scanner.pyeasycord/audit.pyeasycord/bot.pyeasycord/plugins/economy.pyeasycord/plugins/giveaway.pyeasycord/plugins/polls.pyeasycord/plugins/reminder.pyeasycord/plugins/verification.pyproject.mdpyproject.tomlpyrightconfig.jsontests/test_context_ui.pytests/test_core_gaps.pytests/test_giveaway_commands.pytests/test_levels_plugin.pytests/test_plugin_commands.pytests/test_registry_component.pytests/test_reminder.pytests/test_tickets_commands.pytests/test_v52_interactions.pytests/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
##[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
##[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
##[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
##[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
##[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
##[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
##[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
##[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 usepytest-asynciowithasyncio_mode = "auto", so async tests need no manual event loop setup.
The@ai_tooldecorator must register a function intoToolRegistryand requires an explicitToolSafetypermission annotation.
Usectx.user/ctx.member;ctx.authordoes not exist.
ctx.is_adminis a property, not a method; do not call it asctx.is_admin().
Cooldown sentinels must default tofloat("-inf"), not0.0, so first-message events pass on fresh runners.
**/*.py: Import fromeasycord’s public API only; do not import from anyeasycord._*internal module.
When working withContext, usectx.user/ctx.member;ctx.authordoes not exist, andctx.is_adminis a property, not a method.
When implementing plugins, subclassPlugin, decorate handlers with@slash/@on, register them withbot.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 withctx.t(...).
Any function that usesToolLimiter.check_limit,reset_user, orreset_toolmustawaitthem; these methods are async.
Never call.send()on a channel obtained fromctxor Discord until its type is narrowed first; useSENDABLE_CHANNEL_TYPESor an equivalentisinstanceguard.
Files:
tests/test_core_gaps.pytests/test_verification.pyeasycord/plugins/verification.pytests/test_v52_interactions.pytests/test_registry_component.pytests/test_plugin_commands.pyeasycord/audit.pyeasycord/plugins/giveaway.pyeasycord/plugins/reminder.pyeasycord/plugins/polls.pytests/test_giveaway_commands.pytests/test_levels_plugin.pyeasycord/plugins/economy.pytests/test_tickets_commands.pytests/test_context_ui.pytests/test_reminder.pyeasycord/_plugin_scanner.pyeasycord/bot.pyeasycord/_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.pyeasycord/plugins/giveaway.pyeasycord/plugins/reminder.pyeasycord/plugins/polls.pyeasycord/plugins/economy.py
easycord/**/*.py
📄 CodeRabbit inference engine (CLAUDE.md)
easycord/**/*.py: Do not usebot.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_toolfunctions with an explicitToolSafetyvalue;@ai_toolwithout a safety annotation must not register.
Files:
easycord/plugins/verification.pyeasycord/audit.pyeasycord/plugins/giveaway.pyeasycord/plugins/reminder.pyeasycord/plugins/polls.pyeasycord/plugins/economy.pyeasycord/_plugin_scanner.pyeasycord/bot.pyeasycord/_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; raiseValueErrorbefore 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 unusedpytestimport appears resolved — this file no longer importspytest.
34-243: LGTM! Good coverage of confirm/paginate/choose/ask_form/prompt success and timeout paths.easycord/bot.py (3)
388-391: 🗄️ Data Integrity & IntegrationRe-verify:
setup_hookstill triggers guild sync before the gateway connects.
self.guildsis empty insetup_hook(discord.py'ssetup_hook()runs after login but before the gateway connects, so guildGUILD_CREATEevents 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 thatdb.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 inon_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 withasyncio.shield()correctly avoids interrupting the DB task mid-lock, andon_ready's use of the same helper is correctly placed after guilds are populated.Also applies to: 392-528
9-12: 🎯 Functional CorrectnessNo runtime
NameErrorherefrom __future__ import annotationsdefers annotation evaluation, so theAIProviderProtocolhint underTYPE_CHECKINGis 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 viaSENDABLE_CHANNEL_TYPESis a stricter, more correct guard than the previoushasattrcheck.
As per coding guidelines, "Never call.send()on a channel obtained fromctxor Discord until its type is narrowed first; useSENDABLE_CHANNEL_TYPESor an equivalentisinstanceguard."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 thecreate_taskpatch 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 CorrectnessNo issue:
easycord.registryimports thetimemodule, somonkeypatch.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 beforechannel.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 viactx.t(...)— this is the patterngiveaway.py/reminder.pyshould match.Also applies to: 302-308
tests/test_tickets_commands.py (1)
1-308: LGTM! Previously flagged unusedasyncio/pytestimports are no longer present.easycord/_plugin_scanner.py (2)
26-30: 📐 Maintainability & Code QualityCodeQL "statement has no effect" on the Protocol stub is a known false positive.
...is the standard body for aProtocolmethod 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 QualityFull-suite count still inconsistent with the release record.
1248 passeddoesn'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_lockacquisition, 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 & AvailabilityNo action needed —
from __future__ import annotationsalready makes theseTYPE_CHECKING-only annotations safe.> Likely an incorrect or invalid review comment.
| if not isinstance(channel, SENDABLE_CHANNEL_TYPES): | ||
| await ctx.respond("This command must be used in a channel.", ephemeral=True) | ||
| return |
There was a problem hiding this comment.
🔒 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.
| 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
| 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 |
There was a problem hiding this comment.
🔒 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.
| 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
| def _future(value=None): | ||
| """Return a future already resolved with *value*.""" | ||
| f = asyncio.get_event_loop().create_future() | ||
| f.set_result(value) | ||
| return f |
There was a problem hiding this comment.
📐 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.
| 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.
| 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 |
There was a problem hiding this comment.
🎯 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.
| 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.
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
ServerConfigStore.mutate()with per-guild locks: suggestions, starboard, reaction_roles, moderation, role_persistence.asyncio.Lock, asyncset/delete, TOCTOU-safedelete_if_authorized.Plugin bug fixes
cfg.get("enabled")without a default treated a missing key as disabled — a guild that only ran/starboard_channelhad a starboard that never fired. Nowcfg.get("enabled", True)in both reaction handlers and the config display.discord.Forbidden/HTTPExceptionon send.ctx.channel is Noneguards; the polls guard response is localized viactx.t(...).self.suggestion_counterfield removed.Public API
SENDABLE_CHANNEL_TYPES,EventBus,HookRegistrynow importable fromeasycord;slash_commandis a real deprecation shim;ctx.authorraises with actx.user/ctx.membermigration 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.Docs & triage
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.main(v5.50.2 + dependabot action bumps) to clear the conflicting state; CLAUDE.md/AGENTS.md action-pin invariant synced tocheckout@v7.Test plan
--select E9,F63,F7,F82) — passscripts/check_release_metadata.py— passscripts/verify_plugin_tests.py— all plugins ≥20 — pass-vlog — a machine-local interpreter-exit hang is documented in project.md and does not affect the CI matrix)Deferred (tracked, out of scope)
🤖 Generated with Claude Code