feat: add offline 'easycord try' command runner - #128
Conversation
Add an 'easycord try <target> <command>' subcommand that invokes a registered slash command through the existing offline harness (easycord.testing.invoke) and prints the captured responses — no token, no network. - Reuses _load_bot() and testing.invoke(); no new core logic. - --set key=value (repeatable) with int/float/bool/str coercion. - --user/--guild/--dm/--no-admin control the invocation context. - --json for structured output, matching inspect/sync-plan. - Exits non-zero on unknown command (lists available) or command error. - New format_try_result() formatter and tests/test_cli_try.py (10 tests).
Reviewer's GuideAdds a new Sequence diagram for the new easycord try offline commandsequenceDiagram
actor User
participant CLI as cli_cmd_try
participant Bot
participant Testing as testing_invoke
participant Formatter as format_try_result
User->>CLI: easycord try target command [args]
CLI->>CLI: _load_bot(target)
CLI-->>Bot: Bot instance
CLI->>CLI: _parse_set_args(set)
CLI->>Testing: invoke(Bot, command, user_id, guild_id, is_admin, kwargs)
alt command not found
Testing-->>CLI: raise LookupError
CLI-->>User: stderr "unknown command" (exit 1)
else command raises
Testing-->>CLI: raise Exception
CLI-->>User: stderr "Command ... raised" (exit 1)
else success
Testing-->>CLI: ctx (responses)
CLI->>CLI: _try_result(command, ctx)
alt --json
CLI-->>User: print json.dumps(result)
else text output
CLI->>Formatter: format_try_result(result)
Formatter-->>CLI: formatted text
CLI-->>User: print formatted text
end
CLI-->>User: exit 0
end
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
📝 WalkthroughSummary by CodeRabbit
WalkthroughThe pull request adds an offline ChangesOffline command testing
Estimated code review effort: 3 (Moderate) | ~20 minutes Possibly related PRs
Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 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 |
There was a problem hiding this comment.
Hey - I've found 1 issue
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location path="easycord/formatters.py" line_range="70-75" />
<code_context>
+ if embed:
+ title = embed.get("title")
+ description = embed.get("description")
+ field_count = embed.get("fields")
+ if title:
+ lines.append(f" embed.title: {title}")
+ if description:
+ lines.append(f" embed.description: {description}")
+ if field_count:
+ lines.append(f" embed.fields: {field_count}")
+ return "\n".join(lines)
</code_context>
<issue_to_address>
**issue:** Zero embed field counts are skipped due to truthiness check.
Because `if field_count:` treats `0` as false, a valid count of zero will not be shown. If you want to render any provided count, check explicitly for `None` instead, e.g. `if field_count is not None:`.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
PR Summary by QodoAdd offline
AI Description
Diagram
High-Level Assessment
Files changed (4)
|
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
🤖 Augment PR SummarySummary: Adds an offline Changes:
🤖 Was this summary useful? React with 👍 or 👎 |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@easycord/formatters.py`:
- Around line 70-76: Update the field_count condition in the embed formatting
logic to append embed.fields whenever the value is not None, including zero,
while continuing to omit it when the count is unavailable.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 320eea9e-1fa6-4423-af79-69a060d58e75
📒 Files selected for processing (4)
docs/developer-toolkit.mdeasycord/cli.pyeasycord/formatters.pytests/test_cli_try.py
📜 Review details
⏰ Context from checks skipped due to timeout. (2)
- GitHub Check: Sourcery review
- GitHub Check: Analyze (Python)
🧰 Additional context used
📓 Path-based instructions (5)
**/*
📄 CodeRabbit inference engine (CLAUDE.md)
For any question about the codebase, query
graphifyagainstgraphify-out/graph.jsonbefore opening source, documentation, or context files; do not read the deprecated Wiki or rebuild the graph unless explicitly asked.
Files:
easycord/formatters.pydocs/developer-toolkit.mdeasycord/cli.pytests/test_cli_try.py
easycord/**/*.py
📄 CodeRabbit inference engine (CLAUDE.md)
easycord/**/*.py: Treateasycord/__init__.pyas the stable public API. Import consumers fromeasycord, never from_-prefixed internal modules or helpers.
Usectx.userorctx.member, notctx.author; accessctx.is_adminas a property and never call it asctx.is_admin().
Store per-guild plugin state in the database layer using per-guild namespaced records, never on thePlugininstance itself.
Never hardcode plugin response strings; retrieve localized text throughctx.t(...).
Always await asynchronousToolLimitermethods, includingcheck_limit,reset_user, andreset_tool.
Require an explicitToolSafetyannotation when registering an@ai_tool; keepRESTRICTEDtools unexposed.
Preserve Discord command-registration constraints before synchronization: names must be at most 32 characters and match[-_a-z0-9], descriptions at most 100 characters, and commands/options at most 25 choices or options as applicable.
UseServerConfigStore.mutate(guild_id, fn)for every per-guild load-modify-save operation. The callback must be synchronous and local; perform Discord or network I/O outside the lock.
Never perform an unguarded per-guild configuration read-modify-write. Equivalent plugin-specific per-guild locks are acceptable when held across the entire operation, but Discord or network I/O must not occur while the configuration lock is held.
For event-path plugins such as@on("message"), route every destructive action through one governed method that owns rate limiting, channel narrowing, and Discord error handling; Discord exceptions must not escape into the dispatcher.
Before calling.send()on a channel obtained from context or Discord, narrow its type withSENDABLE_CHANNEL_TYPESusingisinstance; do not call.send()on an unnarrowed channel.
InitializeLevelsPlugin._cooldownssentinels tofloat("-inf"), not0.0, so the first message always passes.
sync_commands()must raise on removals unlessconfirm_removals=Trueis exp...
Files:
easycord/formatters.pyeasycord/cli.py
**/*.{py,md}
📄 CodeRabbit inference engine (AGENTS.md)
Before answering questions about EasyCord architecture, module relationships, data flows, or cross-cutting patterns, query
graphify-out/graph.jsonwith/graphify; do not manually read source first.
Files:
easycord/formatters.pydocs/developer-toolkit.mdeasycord/cli.pytests/test_cli_try.py
tests/**/*.py
📄 CodeRabbit inference engine (CLAUDE.md)
tests/**/*.py: Useeasycord.testinghelpers such asinvoke,FakeContextBuilder, andPluginTestSuitefor command and plugin tests instead of requiring a live Discord connection.
When constructing plugins manually in tests, useMyPlugin.__new__, assignplugin._bot = bot, and then callPlugin.__init__(plugin); do not assignplugin.bot.
tests/**/*.py: Usepytestandpytest-asynciowithasyncio_mode = "auto"; asynchronous tests do not need manual event-loop setup.
Run the full test suite withpytest tests/; individual tests may be run using their test node IDs.
Files:
tests/test_cli_try.py
**/test*.py
📄 CodeRabbit inference engine (CLAUDE.md)
Maintain at least 20 tests per plugin, as verified by
scripts/verify_plugin_tests.py.
Files:
tests/test_cli_try.py
🪛 ast-grep (0.45.1)
easycord/cli.py
[info] 483-483: use jsonify instead of json.dumps for JSON output
Context: json.dumps(result, indent=2, default=str)
Note: [CWE-116] Improper Encoding or Escaping of Output.
(use-jsonify)
🔇 Additional comments (8)
easycord/formatters.py (1)
53-69: LGTM!Also applies to: 77-77, 115-115
easycord/cli.py (5)
5-5: LGTM!Also applies to: 16-24
404-427: LGTM!
430-452: LGTM!
455-487: LGTM!
819-842: LGTM!tests/test_cli_try.py (1)
1-130: LGTM!docs/developer-toolkit.md (1)
155-178: LGTM!Also applies to: 281-281
Code Review by Qodo
1. LookupError masks command errors
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 903224c0f1
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Release **v5.61.0**, bundling everything merged since v5.60.0. ## Included - **#128** — offline `easycord try` CLI (run a slash command without Discord) - **#129** — plugin ergonomics helpers: `PluginConfigHelper` (atomic config via `store.mutate`), decorator stacks (`slash_admin_command` etc.), `TaskManager`/`TimerManager` - **#130** — `discord_errors()` middleware + `server_stats` `store.mutate` migration - **#131** — `slash_mod_command` now uses `moderate_members` (real perm) instead of the nonexistent `timeout_members` ## Version bump `5.60.0 → 5.61.0` (minor: additive features + fixes, no breaking changes) across `pyproject.toml`, `easycord/__init__.py`, `README.md`, `docs/getting-started.md`; CHANGELOG entry added. ## Gates (per RELEASE.md) - [x] `check_release_metadata.py` — no drift - [x] `ruff check ... --select E9,F63,F7,F82` — clean - [x] `verify_plugin_tests.py` — thresholds met - [x] `pytest` — 1865 passed - [ ] `pyright easycord tests` — 12 errors / 135 warnings, all **pre-existing on main** and unrelated to this release (CI does not gate on pyright); noted, not introduced here. After merge: tag `v5.61.0` and publish a GitHub Release with the built wheel + sdist. PyPI `twine upload` left as a manual step (requires credentials). ## Summary by Sourcery Release EasyCord v5.61.0 with new CLI tooling, plugin ergonomics helpers, Discord error handling middleware, and minor fixes, plus corresponding version and metadata updates. New Features: - Add offline `easycord try` CLI to run registered slash commands without Discord connectivity. - Introduce plugin ergonomics helpers including config mixin, decorator stacks, and task/timer lifecycle managers. - Add `discord_errors()` middleware to handle common Discord API errors with user-facing messages. Bug Fixes: - Correct `slash_mod_command` to require the real `moderate_members` permission instead of nonexistent `timeout_members`. - Fix `TaskManager.track` race that could leave replacement tasks untracked and uncancellable. - Remove unused imports in decorator stacks flagged by static analysis. Enhancements: - Route `server_stats` configuration writes through `ServerConfigStore.mutate` for consolidated locking and consistency. Build: - Bump project version to 5.61.0 in pyproject, package metadata, and module `__version__`, and add corresponding changelog entry. - Update release metadata entries for wheel and source distribution artifacts. Documentation: - Update README and getting-started docs to reference the v5.61.0 release, download URLs, and badges. Co-authored-by: tee <Thomas_BIRRELL@proton.me>
Summary
Adds
easycord try <target> <command>— a CLI subcommand that invokes a registered slash command through the existing offline harness (easycord.testing.invoke) and prints the captured responses. No token, no network.This closes a real gap in the developer toolkit: today you can list interactions (
inspect) and preview command sync (sync-plan), but there was no way to actually run a command and see its output from the terminal without booting a live bot.easycord try bot:bot ping easycord try bot:bot greet --set name=World --set times=3 easycord try bot:bot config --dm --no-admin # invoke in a DM, as a non-admin easycord try bot:bot ping --jsonDesign
Thin CLI surface over primitives already in the framework — no new core logic:
_load_bot()(easycord/cli.py) formodule:objectimport + validation, exactly likeinspect/sync-plan/doctor.testing.invoke()for offline execution and response capture.format_try_result()ineasycord/formatters.py, following the existing pure data→text formatter pattern; the same dict is emitted by--json.Behavior:
--set key=value(repeatable) → command kwargs, coercedint→float→bool→str.--user/--guild/--dm/--no-admincontrol the invocation context (defaults: admin, guild100, matchinginvoke).(ephemeral)marker and embed title/description when present.Files
easycord/cli.pycmd_try+_coerce_arg/_parse_set_args/_try_resulthelpers;trysubparsereasycord/formatters.pyformat_try_result()tests/test_cli_try.pydocs/developer-toolkit.mdTest plan
pytest tests/test_cli_try.py -q→ 10 passed (text output,--setint coercion via arithmetic,--dm/--no-admincontext, embed rendering,--json, unknown-command exit 1 + available list, command-error exit 1,_coerce_argtype inference,--setvalidation).pytest -q→ 1837 passed, no regressions.ruff check easycord tests --select E9,F63,F7,F82→ clean.python -m easycord.cli try demobot:bot ...verified text, ephemeral, multi-response,--json, coercion, and exit codes.Note
The target module is imported, so
bot.run(...)should be guarded behindif __name__ == "__main__":(theeasycord newtemplates already do this) or the import will block. Documented in the new doc section.Summary by Sourcery
Add a new
easycord tryCLI subcommand to run registered slash commands offline via the existing testing harness and display their responses, including JSON output.New Features:
easycord tryCLI subcommand to execute slash commands offline without a Discord token or network connection.Enhancements:
--set key=valuecommand arguments.Documentation:
Tests:
easycord try, covering argument coercion, context flags, embed rendering, JSON output, and error/unknown-command behavior.