Changelog¶
This changelog is generated with towncrier.
1.1.0 (2026-06-26)¶
Features¶
- Simulate bot, system and webhook message sources.
env.create_usergainedbot,system,global_name,discriminatorandpublic_flagskeywords, so a simulated account posts withmessage.author.botset the way an application does.GuildHandle.create_webhook(channel)returns a newWebhookHandlewhosesend()posts a message withmessage.webhook_idset (the distinct webhook source).env.create_guildalso gainedowner,description,verification_level,notifications,content_filter,preferred_localeandafk_timeoutkeywords for seeding guild settings up front. (#18)
1.0.1 (2026-06-18)¶
Documentation¶
- Expanded the bundled
examples/into a realistic mixed-feature bot (prefix command, cooldown, permission-gated slash, modal, button confirm flow and a persistent select menu) with a test per feature, and added context-menu, deferred-followup and embed-assertion recipes to the cookbook.
1.0.0 (2026-06-17)¶
Features¶
- Added a performance benchmark suite and a CI guard for the offline-speed value proposition. A generous absolute budget catches catastrophic regressions (a real network call, an accidental quadratic) and a same-run scaling ratio proves message-send latency stays constant in channel size, without the flakiness of baseline percentage gating. See the new Performance page.
- Closed the honesty layer's remaining silent-drop blind spots and added property-based fuzzing to prove it stays closed. Message create, webhook execute, interaction responses and bulk message delete now route their bodies through
ctx.fieldslike every edit, so an unmodelled key (e.g.sticker_ids) fails loudly withUnsupportedFieldinstead of vanishing. As part of this,Webhook.send(username=...)is now modelled (the message posts under that per-message display name) rather than silently ignored;avatar_urlis accepted (simcord models no avatars); and creating a forum thread via webhook (thread_name/applied_tags) is rejected with a reason, since it is unmodelled offline. A Hypothesis sweep proves that across every route whose body flows throughctx.fields/list_fields, an unrecognised request field always raises and a recognised one never does, and a drift guard fails loudly if a new write route is neither honesty-vetted nor explicitly exempted with a reason.
0.11.0 (2026-06-16)¶
Features¶
- Added the final common-bot route sweep as the surface settles heading into 1.0:
Guild.delete(owner-only; the discord.py wrapper is deprecated but the route is kept for parity),Member.fetch_voice(read a member's voice state),TextChannel.follow(follow an announcement channel into a destination webhook), andGuild.vanity_invite(withguild.set_vanity_url(code)to populate it in the world builder). Routes whose result would have to be a constant empty value — integrations, welcome screen, widget, onboarding — are deliberately left as loudRouteNotImplementedgaps rather than faked; the parity matrix documents this as the frozen gap surface heading into 1.0.
Bug fixes¶
- Closed the last silent-fake gaps in the request layer: the bulk-ban, prune, channel-permission-overwrite, and voice-state handlers read the raw body directly and so silently dropped unrecognised fields. They now route through the same
RequestContext.fieldshonesty check as every other handler, raisingUnsupportedFieldon an unmodelled key. Role-filtered prune (include_roles) is now rejected loudly rather than pruning a different cohort than asked, andcompute_prune_count=falsecorrectly omits the count.
Miscellaneous¶
- Marked SimCord's public API as stable ahead of 1.0: the package now declares
Development Status :: 5 - Production/Stable, the README and docs landing page describe the semantic-versioning commitment that takes effect from the upcoming 1.0 (see Stability & versioning, which now also documents the supported discord.py range and the deprecation policy), and the test-coverage ratchet is raised to 95%.
0.10.0 (2026-06-15)¶
Features¶
- Create handlers now reject unrecognised request fields with
UnsupportedFieldinstead of silently dropping them, extending field-level honest parity from edits to creates (channels, roles, threads, webhooks, invites, emojis, stickers, scheduled events, stage instances and auto-moderation rules). The same honesty now also covers the JSON-array bodies of the bulk reorder endpoints, so an unknown per-item key fails loudly there too. Channel creation honours an explicitpositionrather than always appending. - Implemented several common-bot REST routes:
Guild.fetch_role, role and channel reordering (Guild.edit_role_positions,Channel.move), editing the bot's own nickname (guild.me.edit),Guild.leave,Client.fetch_guilds, and bot username edits (ClientUser.edit). Role reordering enforces hierarchy — moving a role to or above the bot's own top role raisesForbidden, as on real Discord.
Bug fixes¶
- Fixed sticker creation via the REST route: multipart scalar form fields (
name/description/tags) are now reconstructed into the request body. Also fixedcreate_roleto honour discord.py 2.7's gradientcolorspayload (previously the colour was silently dropped on create).
Documentation¶
- Added a "Stability & versioning" reference page documenting which parts of the API are public and semver-covered versus deliberately internal.
Miscellaneous¶
- The parity matrix now separates deliberately out-of-scope routes (actions a bot account cannot perform, e.g. creating group DMs) from not-yet-implemented gaps, with the new section generated and drift-guarded like the others.
0.9.0 (2026-06-15)¶
Features¶
- Make parity honesty machine-checked. Edit handlers now reject unrecognised request fields loudly with the new public
simcord.UnsupportedFieldinstead of silently dropping them (and wire through the previously-dropped voicebitrate/user_limit/rtc_region, channelparent_id, and guild rules/public-updates channel pointers); serializer payloads are conformance-tested against discord.py's real model constructors; and the parity matrix's "not yet implemented" list is now derived fromdiscord.http.HTTPClientand enforced in sync, so a discord.py upgrade that adds a route fails the build until triaged. Behaviour change: an edit that sends a field SimCord does not model now raises loudly rather than appearing to succeed, so a test that previously passed while exercising an unmodelled field (e.g.Guild.edit(icon=...), forum channel settings, member flags) will now fail withUnsupportedField— surfacing a real parity gap. Catchsimcord.UnsupportedField(or open a parity-gap issue) if your bot relies on one.
0.8.1 (2026-06-14)¶
Miscellaneous¶
- Split the monolithic
Backendclass into a sharedBackendBasekernel plus coupling-aligned subsystem mixins undersimcord.backend.ops. Pure structural refactor — theBackendpublic surface and all behaviour are unchanged.
0.8.0 (2026-06-14)¶
Features¶
- Support application-owned emojis (
Client.create_application_emoji,fetch_application_emojis,fetch_application_emoji, edit/delete) and stage instances (StageChannel.create_instance/fetch_instance,StageInstance.edit/delete) withSTAGE_INSTANCE_*gateway events. - Add the high-frequency moderation and announcement calls:
Guild.bulk_ban,Guild.prune_members/Guild.estimate_pruned_members(roleless members modelled as inactive), andMessage.publishfor announcement (news) channels, with a newguild.create_news_channel(...)builder. - Complete the thread surface:
Thread.join/leave,add_user/remove_user,fetch_members/fetch_member,Guild.active_threads,TextChannel.archived_threads, andThread.edit(archived=..., locked=..., auto_archive_duration=..., invitable=...)— which previously returned200but silently dropped the change.
Bug fixes¶
- Harden the new parity surface: a member who is kicked/banned/pruned now also leaves every thread (so
member_countand thread-member listings stay correct),bulk_banreturns itsbanned/failedsplit instead of a spuriousForbiddenwhen nobody could be banned (and rejects an emptyuser_ids),estimate_pruned_membersenforceskick_members, deleting a stage channel closes its live stage instance, opening a second stage instance on a channel is rejected, re-publishing an already-crossposted message raises (40033), the thread-member endpoints fail with50024on non-thread channels, and an application-emoji edit that omitsnameno longer errors.
Miscellaneous¶
- Drop the "multiple bots in one Env" non-feature from the docs (it is not a planned direction), and ratchet the coverage floor to 84%.
0.7.0 (2026-06-13)¶
Features¶
- Added
Guild.edit()(PATCH /guilds/{id}) — name, description, verification level, default notifications, explicit-content filter, AFK channel/timeout, system channel and preferred locale — and runtime guild creationClient.create_guild()(POST /guilds); guild edits record aGUILD_UPDATEaudit entry. - Added application command permission fetching:
AppCommand.fetch_permissions(guild)reads per-guild overrides seeded byGuildHandle.set_command_permissions(...), and returnsNotFoundwhen a command is unchanged from the guild default. - Added reaction clearing:
Message.clear_reactions()(DELETE /channels/{id}/messages/{id}/reactions, emitsMESSAGE_REACTION_REMOVE_ALL) andMessage.clear_reaction(emoji)(DELETE .../reactions/{emoji}, emitsMESSAGE_REACTION_REMOVE_EMOJI). - Added runtime channel management: bots can now
Guild.create_text_channel()/create_voice_channel()/ etc. (POST /guilds/{id}/channels) andGuild.fetch_channels()(GET /guilds/{id}/channels), reusing the same backend path as test-setup builders and recording aCHANNEL_CREATEaudit entry. - Added stage voice-state editing (
PATCH /guilds/{id}/voice-states/@meand/{user_id}):Member.request_to_speak()andMember.edit(suppress=...)now work, emittingVOICE_STATE_UPDATE. - Added the list-guild-members endpoint (
GET /guilds/{id}/members) soGuild.fetch_members()pages through the full member list. - Auto-moderation now evaluates mention-spam rules (
trigger_type5): a message whose user/role mention count exceedsmention_total_limitfiresAUTO_MODERATION_ACTION_EXECUTIONand is blocked, alongside the existing keyword triggers. - Completed webhook management: fetch/edit/delete a webhook by id or token (
GET/PATCH/DELETE /webhooks/{id}and the/{token}variants) and list a guild's webhooks (GET /guilds/{id}/webhooks), emittingWEBHOOKS_UPDATE. - Forum channels are now usable end to end:
ForumChannel.create_thread(name=..., content=...)creates a post (a public thread with its starter message) andapplied_tags, and forum tags can be configured viaForumChannel.edit(available_tags=...). - Implemented bulk message deletion (
TextChannel.delete_messages/purge):POST /channels/{id}/messages/bulk-deleteremoves 2–100 messages at once, emits a singleMESSAGE_DELETE_BULK, and records aMESSAGE_BULK_DELETEaudit-log entry. - Scheduled events now auto-transition on the virtual clock:
advance_time()moves an event from scheduled to active at its start time and to completed at its end time (emittingGUILD_SCHEDULED_EVENT_UPDATE), in addition to the existing manual status edits.
0.6.0 (2026-06-13)¶
Features¶
- Audit logs are now recorded for the privileged actions the backend performs — bans, kicks, member nick/timeout/role updates, role and channel edits/deletes, member voice moves, and scheduled-event CRUD. Read them through
guild.audit_logs()(withuser/action/before/afterfiltering), theon_audit_log_entry_createevent, orenv.guild.audit_log(). Reasons passed via discord.py'sreason=are captured (and now also stored on the ban record itself). (#15) - Polls are supported end to end: a bot sends
discord.Poll, users cast and retract votes withMemberActor.vote/remove_vote(firingMESSAGE_POLL_VOTE_ADD/REMOVE), poll answer voters are fetchable, and polls finalize either viaMessage.end_poll()or whenenv.advance_timepasses their deadline. (#16) - Guild scheduled events are supported: create/list/fetch/edit/delete plus subscriber listing, with
GUILD_SCHEDULED_EVENT_*events.MemberActor.subscribe_event/unsubscribe_eventmark interest, andGuildHandle.create_scheduled_eventsets one up directly. Stage/voice/external entity types are validated. (#17) - Voice state is modeled (state only — never audio). Users join/leave/move with
MemberActor.join_voice,leave_voiceandset_voice; server mute/deaf and channel moves overMember.edit/move_toare reflected and audit-logged; all fireVOICE_STATE_UPDATEsomember.voiceandon_voice_state_updatework. (#18) - Invites, custom emojis and stickers are supported: create/list/fetch/delete invites (
INVITE_CREATE/INVITE_DELETE) and guild expression CRUD (GUILD_EMOJIS_UPDATE/GUILD_STICKERS_UPDATE), with builder helpersGuildHandle.create_emoji/create_stickerfor setup. (#19) - Auto-moderation rules can be created, edited and deleted, and keyword rules are evaluated on message send: a matching
block_messageaction drops the message and firesAUTO_MODERATION_ACTION_EXECUTION, honoring exempt roles and channels. (#20) - New channel kinds can be created from builders: voice, stage, category and forum channels (
GuildHandle.create_voice_channel,create_stage_channel,create_category,create_forum_channel), andcreate_text_channelaccepts acategory=. (#21)
0.5.0 (2026-06-13)¶
Features¶
- Entity select menus are now fully supported.
MemberActor.selectaccepts the handles a user could pick (UserHandle/MemberActor,RoleHandle,ChannelHandle) for user, role, channel and mentionable selects, building the resolved data so the bot's callback receives realdiscord.Member/Role/channel objects. Wrong handle kinds and out-of-range value counts fail with aSetupError. (#12) - Added
Env.restart_bot()to simulate a bot restart while the virtual world persists. It detaches the current bot, attaches a freshly built one, and replays the existing guilds so the new client's cache repopulates — letting tests prove that persistent views (bot.add_viewinsetup_hook) re-attach to messages they never saw created. (#13)
Bug fixes¶
- Passing the wrong handle kind to a typed slash-command option (e.g. a
RoleHandleto aUSERoption) now fails with a clearSetupErrorat the call site, instead of resolving into the wrong bucket and failing deep inside discord.py. (#14)
0.4.0 (2026-06-13)¶
Features¶
- Added assertion helpers —
assert_sent,assert_responded,assert_message,assert_errorandassert_no_errors— whose failure messages print what the bot actually did (the channel's recent history, the interaction result, the captured errors). Env.create_guildnow accepts an optionalid=so a guild can be pinned to a known id — e.g. to match a bot that syncs its commands to a hardcoded guild id, keepingstrict_syncon.
0.3.0 (2026-06-13)¶
Features¶
- Added a
@pytest.mark.simcord(...)marker whose keyword arguments are forwarded tosimcord.run()(e.g.strict_sync=False,check_errors=False), so the bundledsimcord_envfixture can be configured per-test without writing a custom fixture.
Bug fixes¶
- Fixed
member.context_menu(...)failing to resolve context-menu commands whose names contain spaces (e.g."Report Member"). The name is no longer split into a subcommand path — only slash commands nest.
Documentation¶
- Documented the
@pytest.mark.simcord(...)marker in the fixtures guide as the way to overridesimcord.runoptions per test.
Miscellaneous¶
- Replaced remaining bare wire-protocol integer literals (channel types, message types, permission-overwrite target types, application-command types, modal component types) with
IntEnummembers, so the backend reads as the protocol does instead of scattering magic numbers. - Typed the backend's permission-overwrite
typefield as theOverwriteTypeenum (and coerce it at the HTTP boundary), so overwrites carry a uniform enum value everywhere instead of a mix of enums and bare ints.
0.2.0 (2026-06-12)¶
Features¶
- Gateway events outside the bot's declared intents are dropped, matching real Discord. Dropped events are recorded in the transcript so a mysteriously-quiet test can explain itself.
- Message content is censored without the
message_contentintent:content,embeds,attachments,componentsandpollare blanked on guild messages, with Discord's documented exemptions (DMs, bot-authored messages, messages mentioning the bot). Recorded asCENSOREDin the transcript. GUILD_CREATEnow inlines only the bot's own member. The rest arrive via authenticGUILD_MEMBERS_CHUNKresponses, soGuild.chunk()andGuild.query_members()work as they would in production.simcord.run(bot, approved_intents=...)can simulate unapproved privileged intents raisingdiscord.PrivilegedIntentsRequired, mirroring the Discord developer portal behaviour at connect time.- New Intents guide covering event delivery, message content censoring, member chunking, and privileged intents.
0.1.0 (2026-06-12)¶
Features¶
- Added
env.advance_time(seconds): fast-forward the virtual clock so view timeouts fire, cooldowns reset, andasyncio.sleepchains complete — instantly, with no real waiting. The event-loop clock and message timestamps advance together. - Added
env.raise_errors(), which re-raises everything the bot raised during the test (command handlers, app-command callbacks, event listeners) as anExceptionGroup— a one-call way to assert the bot ran cleanly. Does nothing when no errors were captured. - Failing tests now automatically include a transcript of everything that crossed the two seams — gateway events injected and REST calls the bot made, in order — attached by the pytest plugin. Also available programmatically as
env.transcript(). - Uninspected bot errors now fail the test:
simcord.runre-raises captured errors as anExceptionGroupat teardown unless the test readenv.errorsor calledenv.raise_errors(). Opt out withsimcord.run(bot, check_errors=False).
Bug fixes¶
- A deferred component interaction (callback type 6) followed by
edit_original_responsenow edits the clicked message in place, matching real Discord, instead of creating a new message.@originalfor a type-6 defer also resolves to the component's source message. - An interaction is now marked acknowledged only after its callback is handled successfully, so a callback that 400s (e.g. an oversized embed) no longer consumes the interaction — a retried response gets through instead of a spurious
40060. - DMing a bot now opens the channel successfully and fails on send with
403 50007(caught byexcept discord.Forbidden), matching Discord. Ephemeral messages no longer leak into the bot-facing channel history and pins endpoints, and webhook-authored messages now carrywebhook_id. - Serialized timestamps now come from the same virtual clock as snowflakes, so a message's
created_at(derived from its id) and itstimestampagree instead of differing by months, and timestamps are deterministic across runs. - Tightened server-side permission parity: the bot can no longer edit another user's message (
50005), assign/edit roles at or above its own top role or grant permissions it lacks (50013), or delete the@everyonerole.mention_everyoneis only set when the author actually has the permission. - Unimplemented routes now raise
RouteNotImplementeddirectly rather than as adiscord.HTTPException, so a bot's broadexcept discord.HTTPExceptioncan no longer silently swallow the "not implemented" signal.history(around=...)is now supported. Env.settle()now waits outasyncio.sleep-style pauses in handlers (cooldowns, backoff) instead of returning early, and only abandons tasks genuinely parked on a future. Assertions after an actor verb no longer race against a handler that paused before replying.
Miscellaneous¶
- CI hardening: pinned the PyPI publish action to a commit SHA, added a test gate before release publishing, and set default
contents: readpermissions on the CI workflows. - Centralised the Discord wire-protocol magic numbers (interaction, callback, option and component types) into
IntEnums insimcord.enums, replacing scattered bare integer constants in the actors, payload builders and interaction route. - Dropped Python 3.10 support; the minimum is now Python 3.11.
- Every state mutation now lives on
Backendpaired with its own gateway emit (channel/overwrite edits, member field/role edits, role edits), and route handlers parse, permission-check via the newctx.require_channel_permissions/ctx.require_guild_permissionshelpers, then call a single backend method. This removes the copy-pasted permission preamble and the "mutate then remember to announce" pattern, so a write can't be announced inconsistently or forgotten as route coverage grows. - Setup and not-implemented errors now attach supporting detail (available options, the parity-matrix pointer) as exception notes, keeping the primary message tight while still surfacing the context in tracebacks.
- The interaction response lifecycle is now a typed
Interactiondataclass with aResponseKindenum, replacing the untyped dict that was mutated across the route, actor, and result layers.InteractionResultno longer exposes the raw.recorddict; use its typed properties (acknowledged,deferred,ephemeral,modal,response,followups) instead. - The parity matrix's route inventory is now generated from the route table (
python -m simcord.parity) and guarded by a sync test, so it is exact by construction. Also:Backendis no longer in__all__(still importable, documented as internal/unstable), and CI now enforces a coverage floor.