simcord 2.0.1__tar.gz → 2.0.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (200) hide show
  1. {simcord-2.0.1 → simcord-2.0.2}/CHANGELOG.md +9 -0
  2. {simcord-2.0.1 → simcord-2.0.2}/PKG-INFO +1 -1
  3. {simcord-2.0.1 → simcord-2.0.2}/pyproject.toml +1 -1
  4. {simcord-2.0.1 → simcord-2.0.2}/src/simcord/backend/ops/guilds.py +17 -4
  5. {simcord-2.0.1 → simcord-2.0.2}/src/simcord/backend/serializers.py +12 -1
  6. {simcord-2.0.1 → simcord-2.0.2}/src/simcord/http/routes/guilds.py +2 -2
  7. {simcord-2.0.1 → simcord-2.0.2}/tests/integration/test_edge_cases.py +6 -1
  8. {simcord-2.0.1 → simcord-2.0.2}/tests/integration/test_roles_and_ordering.py +75 -0
  9. {simcord-2.0.1 → simcord-2.0.2}/uv.lock +1 -1
  10. {simcord-2.0.1 → simcord-2.0.2}/.github/ISSUE_TEMPLATE/bug.md +0 -0
  11. {simcord-2.0.1 → simcord-2.0.2}/.github/ISSUE_TEMPLATE/feature.md +0 -0
  12. {simcord-2.0.1 → simcord-2.0.2}/.github/ISSUE_TEMPLATE/parity-gap.md +0 -0
  13. {simcord-2.0.1 → simcord-2.0.2}/.github/dependabot.yml +0 -0
  14. {simcord-2.0.1 → simcord-2.0.2}/.github/workflows/ci.yml +0 -0
  15. {simcord-2.0.1 → simcord-2.0.2}/.github/workflows/dpy-master.yml +0 -0
  16. {simcord-2.0.1 → simcord-2.0.2}/.github/workflows/release.yml +0 -0
  17. {simcord-2.0.1 → simcord-2.0.2}/.gitignore +0 -0
  18. {simcord-2.0.1 → simcord-2.0.2}/.pre-commit-config.yaml +0 -0
  19. {simcord-2.0.1 → simcord-2.0.2}/.python-version +0 -0
  20. {simcord-2.0.1 → simcord-2.0.2}/.readthedocs.yaml +0 -0
  21. {simcord-2.0.1 → simcord-2.0.2}/CODE_OF_CONDUCT.md +0 -0
  22. {simcord-2.0.1 → simcord-2.0.2}/CONTRIBUTING.md +0 -0
  23. {simcord-2.0.1 → simcord-2.0.2}/LICENSE +0 -0
  24. {simcord-2.0.1 → simcord-2.0.2}/README.md +0 -0
  25. {simcord-2.0.1 → simcord-2.0.2}/SECURITY.md +0 -0
  26. {simcord-2.0.1 → simcord-2.0.2}/benchmarks/_helpers.py +0 -0
  27. {simcord-2.0.1 → simcord-2.0.2}/benchmarks/test_dispatch_benchmarks.py +0 -0
  28. {simcord-2.0.1 → simcord-2.0.2}/benchmarks/test_perf_guards.py +0 -0
  29. {simcord-2.0.1 → simcord-2.0.2}/changes/README.md +0 -0
  30. {simcord-2.0.1 → simcord-2.0.2}/docs/api.md +0 -0
  31. {simcord-2.0.1 → simcord-2.0.2}/docs/architecture.md +0 -0
  32. {simcord-2.0.1 → simcord-2.0.2}/docs/assets/simcord-terminal-demo.gif +0 -0
  33. {simcord-2.0.1 → simcord-2.0.2}/docs/assets/simcord-transparent.webp +0 -0
  34. {simcord-2.0.1 → simcord-2.0.2}/docs/assets/social-card.png +0 -0
  35. {simcord-2.0.1 → simcord-2.0.2}/docs/changelog.md +0 -0
  36. {simcord-2.0.1 → simcord-2.0.2}/docs/concepts.md +0 -0
  37. {simcord-2.0.1 → simcord-2.0.2}/docs/cookbook.md +0 -0
  38. {simcord-2.0.1 → simcord-2.0.2}/docs/faq.md +0 -0
  39. {simcord-2.0.1 → simcord-2.0.2}/docs/guides/ai-coding-agents.md +0 -0
  40. {simcord-2.0.1 → simcord-2.0.2}/docs/guides/components.md +0 -0
  41. {simcord-2.0.1 → simcord-2.0.2}/docs/guides/diagnostics.md +0 -0
  42. {simcord-2.0.1 → simcord-2.0.2}/docs/guides/fixtures.md +0 -0
  43. {simcord-2.0.1 → simcord-2.0.2}/docs/guides/intents.md +0 -0
  44. {simcord-2.0.1 → simcord-2.0.2}/docs/guides/interactions.md +0 -0
  45. {simcord-2.0.1 → simcord-2.0.2}/docs/guides/messages.md +0 -0
  46. {simcord-2.0.1 → simcord-2.0.2}/docs/guides/mocks-vs-simulation.md +0 -0
  47. {simcord-2.0.1 → simcord-2.0.2}/docs/guides/moderation-and-guild.md +0 -0
  48. {simcord-2.0.1 → simcord-2.0.2}/docs/guides/permissions.md +0 -0
  49. {simcord-2.0.1 → simcord-2.0.2}/docs/guides/test-without-token.md +0 -0
  50. {simcord-2.0.1 → simcord-2.0.2}/docs/guides/testing-discord-py-bots.md +0 -0
  51. {simcord-2.0.1 → simcord-2.0.2}/docs/guides/testing-slash-commands.md +0 -0
  52. {simcord-2.0.1 → simcord-2.0.2}/docs/guides/threads-reactions-dms.md +0 -0
  53. {simcord-2.0.1 → simcord-2.0.2}/docs/guides/time-control.md +0 -0
  54. {simcord-2.0.1 → simcord-2.0.2}/docs/index.md +0 -0
  55. {simcord-2.0.1 → simcord-2.0.2}/docs/installation.md +0 -0
  56. {simcord-2.0.1 → simcord-2.0.2}/docs/llms.txt +0 -0
  57. {simcord-2.0.1 → simcord-2.0.2}/docs/parity-matrix.md +0 -0
  58. {simcord-2.0.1 → simcord-2.0.2}/docs/performance.md +0 -0
  59. {simcord-2.0.1 → simcord-2.0.2}/docs/quickstart.md +0 -0
  60. {simcord-2.0.1 → simcord-2.0.2}/docs/robots.txt +0 -0
  61. {simcord-2.0.1 → simcord-2.0.2}/docs/stability.md +0 -0
  62. {simcord-2.0.1 → simcord-2.0.2}/examples/README.md +0 -0
  63. {simcord-2.0.1 → simcord-2.0.2}/examples/__init__.py +0 -0
  64. {simcord-2.0.1 → simcord-2.0.2}/examples/bot.py +0 -0
  65. {simcord-2.0.1 → simcord-2.0.2}/examples/conftest.py +0 -0
  66. {simcord-2.0.1 → simcord-2.0.2}/examples/test_bot.py +0 -0
  67. {simcord-2.0.1 → simcord-2.0.2}/mkdocs.yml +0 -0
  68. {simcord-2.0.1 → simcord-2.0.2}/overrides/main.html +0 -0
  69. {simcord-2.0.1 → simcord-2.0.2}/scripts/check_discoverability.py +0 -0
  70. {simcord-2.0.1 → simcord-2.0.2}/src/simcord/__init__.py +0 -0
  71. {simcord-2.0.1 → simcord-2.0.2}/src/simcord/_dpy_internals.py +0 -0
  72. {simcord-2.0.1 → simcord-2.0.2}/src/simcord/actors.py +0 -0
  73. {simcord-2.0.1 → simcord-2.0.2}/src/simcord/asserts.py +0 -0
  74. {simcord-2.0.1 → simcord-2.0.2}/src/simcord/backend/__init__.py +0 -0
  75. {simcord-2.0.1 → simcord-2.0.2}/src/simcord/backend/cdn.py +0 -0
  76. {simcord-2.0.1 → simcord-2.0.2}/src/simcord/backend/errors.py +0 -0
  77. {simcord-2.0.1 → simcord-2.0.2}/src/simcord/backend/models/__init__.py +0 -0
  78. {simcord-2.0.1 → simcord-2.0.2}/src/simcord/backend/models/auditlog.py +0 -0
  79. {simcord-2.0.1 → simcord-2.0.2}/src/simcord/backend/models/automod.py +0 -0
  80. {simcord-2.0.1 → simcord-2.0.2}/src/simcord/backend/models/channel.py +0 -0
  81. {simcord-2.0.1 → simcord-2.0.2}/src/simcord/backend/models/expression.py +0 -0
  82. {simcord-2.0.1 → simcord-2.0.2}/src/simcord/backend/models/guild.py +0 -0
  83. {simcord-2.0.1 → simcord-2.0.2}/src/simcord/backend/models/interaction.py +0 -0
  84. {simcord-2.0.1 → simcord-2.0.2}/src/simcord/backend/models/invite.py +0 -0
  85. {simcord-2.0.1 → simcord-2.0.2}/src/simcord/backend/models/member.py +0 -0
  86. {simcord-2.0.1 → simcord-2.0.2}/src/simcord/backend/models/message.py +0 -0
  87. {simcord-2.0.1 → simcord-2.0.2}/src/simcord/backend/models/role.py +0 -0
  88. {simcord-2.0.1 → simcord-2.0.2}/src/simcord/backend/models/scheduled_event.py +0 -0
  89. {simcord-2.0.1 → simcord-2.0.2}/src/simcord/backend/models/stage_instance.py +0 -0
  90. {simcord-2.0.1 → simcord-2.0.2}/src/simcord/backend/models/user.py +0 -0
  91. {simcord-2.0.1 → simcord-2.0.2}/src/simcord/backend/models/voice.py +0 -0
  92. {simcord-2.0.1 → simcord-2.0.2}/src/simcord/backend/models/webhook.py +0 -0
  93. {simcord-2.0.1 → simcord-2.0.2}/src/simcord/backend/ops/__init__.py +0 -0
  94. {simcord-2.0.1 → simcord-2.0.2}/src/simcord/backend/ops/audit.py +0 -0
  95. {simcord-2.0.1 → simcord-2.0.2}/src/simcord/backend/ops/base.py +0 -0
  96. {simcord-2.0.1 → simcord-2.0.2}/src/simcord/backend/ops/channels.py +0 -0
  97. {simcord-2.0.1 → simcord-2.0.2}/src/simcord/backend/ops/commands.py +0 -0
  98. {simcord-2.0.1 → simcord-2.0.2}/src/simcord/backend/ops/expressions.py +0 -0
  99. {simcord-2.0.1 → simcord-2.0.2}/src/simcord/backend/ops/invites.py +0 -0
  100. {simcord-2.0.1 → simcord-2.0.2}/src/simcord/backend/ops/messages.py +0 -0
  101. {simcord-2.0.1 → simcord-2.0.2}/src/simcord/backend/ops/permissions.py +0 -0
  102. {simcord-2.0.1 → simcord-2.0.2}/src/simcord/backend/ops/polls.py +0 -0
  103. {simcord-2.0.1 → simcord-2.0.2}/src/simcord/backend/ops/reactions.py +0 -0
  104. {simcord-2.0.1 → simcord-2.0.2}/src/simcord/backend/ops/scheduled_events.py +0 -0
  105. {simcord-2.0.1 → simcord-2.0.2}/src/simcord/backend/ops/voice.py +0 -0
  106. {simcord-2.0.1 → simcord-2.0.2}/src/simcord/backend/ops/webhooks.py +0 -0
  107. {simcord-2.0.1 → simcord-2.0.2}/src/simcord/backend/permissions.py +0 -0
  108. {simcord-2.0.1 → simcord-2.0.2}/src/simcord/backend/state.py +0 -0
  109. {simcord-2.0.1 → simcord-2.0.2}/src/simcord/builders.py +0 -0
  110. {simcord-2.0.1 → simcord-2.0.2}/src/simcord/components.py +0 -0
  111. {simcord-2.0.1 → simcord-2.0.2}/src/simcord/enums.py +0 -0
  112. {simcord-2.0.1 → simcord-2.0.2}/src/simcord/env.py +0 -0
  113. {simcord-2.0.1 → simcord-2.0.2}/src/simcord/gateway.py +0 -0
  114. {simcord-2.0.1 → simcord-2.0.2}/src/simcord/http/__init__.py +0 -0
  115. {simcord-2.0.1 → simcord-2.0.2}/src/simcord/http/_helpers.py +0 -0
  116. {simcord-2.0.1 → simcord-2.0.2}/src/simcord/http/client.py +0 -0
  117. {simcord-2.0.1 → simcord-2.0.2}/src/simcord/http/router.py +0 -0
  118. {simcord-2.0.1 → simcord-2.0.2}/src/simcord/http/routes/__init__.py +0 -0
  119. {simcord-2.0.1 → simcord-2.0.2}/src/simcord/http/routes/application.py +0 -0
  120. {simcord-2.0.1 → simcord-2.0.2}/src/simcord/http/routes/auditlogs.py +0 -0
  121. {simcord-2.0.1 → simcord-2.0.2}/src/simcord/http/routes/automod.py +0 -0
  122. {simcord-2.0.1 → simcord-2.0.2}/src/simcord/http/routes/channels.py +0 -0
  123. {simcord-2.0.1 → simcord-2.0.2}/src/simcord/http/routes/commands.py +0 -0
  124. {simcord-2.0.1 → simcord-2.0.2}/src/simcord/http/routes/expressions.py +0 -0
  125. {simcord-2.0.1 → simcord-2.0.2}/src/simcord/http/routes/interactions.py +0 -0
  126. {simcord-2.0.1 → simcord-2.0.2}/src/simcord/http/routes/invites.py +0 -0
  127. {simcord-2.0.1 → simcord-2.0.2}/src/simcord/http/routes/messages.py +0 -0
  128. {simcord-2.0.1 → simcord-2.0.2}/src/simcord/http/routes/reactions.py +0 -0
  129. {simcord-2.0.1 → simcord-2.0.2}/src/simcord/http/routes/scheduled_events.py +0 -0
  130. {simcord-2.0.1 → simcord-2.0.2}/src/simcord/http/routes/stage_instances.py +0 -0
  131. {simcord-2.0.1 → simcord-2.0.2}/src/simcord/intents.py +0 -0
  132. {simcord-2.0.1 → simcord-2.0.2}/src/simcord/interactions.py +0 -0
  133. {simcord-2.0.1 → simcord-2.0.2}/src/simcord/parity.py +0 -0
  134. {simcord-2.0.1 → simcord-2.0.2}/src/simcord/py.typed +0 -0
  135. {simcord-2.0.1 → simcord-2.0.2}/src/simcord/pytest_plugin.py +0 -0
  136. {simcord-2.0.1 → simcord-2.0.2}/src/simcord/results.py +0 -0
  137. {simcord-2.0.1 → simcord-2.0.2}/tests/conformance/_world.py +0 -0
  138. {simcord-2.0.1 → simcord-2.0.2}/tests/conformance/test_field_honesty.py +0 -0
  139. {simcord-2.0.1 → simcord-2.0.2}/tests/conformance/test_field_honesty_fuzz.py +0 -0
  140. {simcord-2.0.1 → simcord-2.0.2}/tests/conformance/test_route_coverage.py +0 -0
  141. {simcord-2.0.1 → simcord-2.0.2}/tests/conformance/test_serializers.py +0 -0
  142. {simcord-2.0.1 → simcord-2.0.2}/tests/conftest.py +0 -0
  143. {simcord-2.0.1 → simcord-2.0.2}/tests/fixtures/__init__.py +0 -0
  144. {simcord-2.0.1 → simcord-2.0.2}/tests/fixtures/sample_bot/__init__.py +0 -0
  145. {simcord-2.0.1 → simcord-2.0.2}/tests/fixtures/sample_bot/events.py +0 -0
  146. {simcord-2.0.1 → simcord-2.0.2}/tests/fixtures/sample_bot/general.py +0 -0
  147. {simcord-2.0.1 → simcord-2.0.2}/tests/fixtures/sample_bot/interactions.py +0 -0
  148. {simcord-2.0.1 → simcord-2.0.2}/tests/fixtures/sample_bot/moderation.py +0 -0
  149. {simcord-2.0.1 → simcord-2.0.2}/tests/integration/test_app_commands.py +0 -0
  150. {simcord-2.0.1 → simcord-2.0.2}/tests/integration/test_application_emojis.py +0 -0
  151. {simcord-2.0.1 → simcord-2.0.2}/tests/integration/test_asserts.py +0 -0
  152. {simcord-2.0.1 → simcord-2.0.2}/tests/integration/test_audit_logs.py +0 -0
  153. {simcord-2.0.1 → simcord-2.0.2}/tests/integration/test_automod.py +0 -0
  154. {simcord-2.0.1 → simcord-2.0.2}/tests/integration/test_bot_users_and_webhooks.py +0 -0
  155. {simcord-2.0.1 → simcord-2.0.2}/tests/integration/test_channels.py +0 -0
  156. {simcord-2.0.1 → simcord-2.0.2}/tests/integration/test_command_permissions.py +0 -0
  157. {simcord-2.0.1 → simcord-2.0.2}/tests/integration/test_common_bot_sweep.py +0 -0
  158. {simcord-2.0.1 → simcord-2.0.2}/tests/integration/test_components_v2_interactions.py +0 -0
  159. {simcord-2.0.1 → simcord-2.0.2}/tests/integration/test_components_v2_messages.py +0 -0
  160. {simcord-2.0.1 → simcord-2.0.2}/tests/integration/test_coverage_sweep.py +0 -0
  161. {simcord-2.0.1 → simcord-2.0.2}/tests/integration/test_diagnostics.py +0 -0
  162. {simcord-2.0.1 → simcord-2.0.2}/tests/integration/test_error_capture_and_edits.py +0 -0
  163. {simcord-2.0.1 → simcord-2.0.2}/tests/integration/test_expressions.py +0 -0
  164. {simcord-2.0.1 → simcord-2.0.2}/tests/integration/test_forum.py +0 -0
  165. {simcord-2.0.1 → simcord-2.0.2}/tests/integration/test_gateway_and_automod_exempt.py +0 -0
  166. {simcord-2.0.1 → simcord-2.0.2}/tests/integration/test_guilds.py +0 -0
  167. {simcord-2.0.1 → simcord-2.0.2}/tests/integration/test_intents.py +0 -0
  168. {simcord-2.0.1 → simcord-2.0.2}/tests/integration/test_interaction_followups.py +0 -0
  169. {simcord-2.0.1 → simcord-2.0.2}/tests/integration/test_interactions.py +0 -0
  170. {simcord-2.0.1 → simcord-2.0.2}/tests/integration/test_invites.py +0 -0
  171. {simcord-2.0.1 → simcord-2.0.2}/tests/integration/test_lifecycle.py +0 -0
  172. {simcord-2.0.1 → simcord-2.0.2}/tests/integration/test_members.py +0 -0
  173. {simcord-2.0.1 → simcord-2.0.2}/tests/integration/test_message_routes.py +0 -0
  174. {simcord-2.0.1 → simcord-2.0.2}/tests/integration/test_messages.py +0 -0
  175. {simcord-2.0.1 → simcord-2.0.2}/tests/integration/test_misc_branches.py +0 -0
  176. {simcord-2.0.1 → simcord-2.0.2}/tests/integration/test_moderation.py +0 -0
  177. {simcord-2.0.1 → simcord-2.0.2}/tests/integration/test_polls.py +0 -0
  178. {simcord-2.0.1 → simcord-2.0.2}/tests/integration/test_reactions.py +0 -0
  179. {simcord-2.0.1 → simcord-2.0.2}/tests/integration/test_scheduled_events.py +0 -0
  180. {simcord-2.0.1 → simcord-2.0.2}/tests/integration/test_settlement_contract.py +0 -0
  181. {simcord-2.0.1 → simcord-2.0.2}/tests/integration/test_setup_hook_suspension.py +0 -0
  182. {simcord-2.0.1 → simcord-2.0.2}/tests/integration/test_sharding.py +0 -0
  183. {simcord-2.0.1 → simcord-2.0.2}/tests/integration/test_stage_instances.py +0 -0
  184. {simcord-2.0.1 → simcord-2.0.2}/tests/integration/test_threads.py +0 -0
  185. {simcord-2.0.1 → simcord-2.0.2}/tests/integration/test_time.py +0 -0
  186. {simcord-2.0.1 → simcord-2.0.2}/tests/integration/test_transport_edges.py +0 -0
  187. {simcord-2.0.1 → simcord-2.0.2}/tests/integration/test_voice_state.py +0 -0
  188. {simcord-2.0.1 → simcord-2.0.2}/tests/integration/test_world_helpers.py +0 -0
  189. {simcord-2.0.1 → simcord-2.0.2}/tests/unit/test_assert_helpers.py +0 -0
  190. {simcord-2.0.1 → simcord-2.0.2}/tests/unit/test_backend_lookup_errors.py +0 -0
  191. {simcord-2.0.1 → simcord-2.0.2}/tests/unit/test_components_v2_validation.py +0 -0
  192. {simcord-2.0.1 → simcord-2.0.2}/tests/unit/test_errors.py +0 -0
  193. {simcord-2.0.1 → simcord-2.0.2}/tests/unit/test_intent_rules.py +0 -0
  194. {simcord-2.0.1 → simcord-2.0.2}/tests/unit/test_parity.py +0 -0
  195. {simcord-2.0.1 → simcord-2.0.2}/tests/unit/test_parity_tooling.py +0 -0
  196. {simcord-2.0.1 → simcord-2.0.2}/tests/unit/test_permission_validators.py +0 -0
  197. {simcord-2.0.1 → simcord-2.0.2}/tests/unit/test_permissions.py +0 -0
  198. {simcord-2.0.1 → simcord-2.0.2}/tests/unit/test_pytest_plugin.py +0 -0
  199. {simcord-2.0.1 → simcord-2.0.2}/tests/unit/test_router.py +0 -0
  200. {simcord-2.0.1 → simcord-2.0.2}/tests/unit/test_version.py +0 -0
@@ -4,6 +4,15 @@ This changelog is generated with [towncrier](https://towncrier.readthedocs.io/).
4
4
 
5
5
  <!-- towncrier release notes start -->
6
6
 
7
+ ## 2.0.2 (2026-09-18)
8
+
9
+ ### Bug fixes
10
+
11
+ - Fixed ``create_role`` leaving stale ``Role.position`` values in the bot's cache: inserting a role bumps every existing role's position, and the backend now announces each shift with ``GUILD_ROLE_UPDATE`` after ``GUILD_ROLE_CREATE``, as real Discord does. ``Member.top_role``, ``Role`` comparisons, and hierarchy checks now see the true ordering after role creation.
12
+ - Fixed ``role.colour`` always reading ``0`` in the bot's cache: role payloads now carry the ``colors`` object (``primary_color``/``secondary_color``/``tertiary_color``) that discord.py reads, alongside the deprecated flat ``color`` field.
13
+ - Fixed guild role listings to match real Discord's ordering: ``GET``/``PATCH /guilds/{id}/roles`` responses and the ``roles`` array inside guild payloads (``GUILD_CREATE``/``GUILD_UPDATE`` events and guild REST responses) are now sorted by role id ascending instead of insertion order (observable when a guild is created with an explicit id, which can make ``@everyone``'s snowflake the largest).
14
+
15
+
7
16
  ## 2.0.1 (2026-09-10)
8
17
 
9
18
  ### Bug fixes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: simcord
3
- Version: 2.0.1
3
+ Version: 2.0.2
4
4
  Summary: Offline discord.py bot testing with pytest. No network, token, or test server.
5
5
  Project-URL: Homepage, https://github.com/SilentHacks/simcord
6
6
  Project-URL: Documentation, https://simcord.readthedocs.io/
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "simcord"
7
- version = "2.0.1"
7
+ version = "2.0.2"
8
8
  description = "Offline discord.py bot testing with pytest. No network, token, or test server."
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -180,9 +180,23 @@ class GuildMixin(BackendBase):
180
180
 
181
181
  # ----------------------------------------------------------------- roles
182
182
 
183
+ def announce_role_updates(self, guild_id: int, roles: Iterable[Role]) -> None:
184
+ """Announce each role's current state with GUILD_ROLE_UPDATE.
185
+
186
+ Real Discord emits an update for every role whose position changed as a
187
+ side effect of another operation; without them a client's cache keeps
188
+ stale positions, so position-driven mutations must announce their shifts.
189
+ """
190
+ for role in list(roles):
191
+ self.emit(
192
+ "GUILD_ROLE_UPDATE",
193
+ {"guild_id": str(guild_id), "role": serializers.role_payload(role)},
194
+ )
195
+
183
196
  def create_role(self, guild_id: int, name: str, *, permissions: int = 0, **fields: Any) -> Role:
184
197
  guild = self.get_guild(guild_id)
185
198
  position = fields.pop("position", None)
199
+ shifted: list[Role] = []
186
200
  if position is None:
187
201
  # New roles insert just above @everyone, pushing existing roles up
188
202
  # (so the bot's integration role stays on top), as on real Discord.
@@ -190,6 +204,7 @@ class GuildMixin(BackendBase):
190
204
  for existing in guild.roles.values():
191
205
  if existing.position >= 1:
192
206
  existing.position += 1
207
+ shifted.append(existing)
193
208
  role = Role(
194
209
  id=self.snowflake(),
195
210
  name=name,
@@ -202,6 +217,7 @@ class GuildMixin(BackendBase):
202
217
  "GUILD_ROLE_CREATE",
203
218
  {"guild_id": str(guild_id), "role": serializers.role_payload(role)},
204
219
  )
220
+ self.announce_role_updates(guild_id, shifted)
205
221
  return role
206
222
 
207
223
  def edit_role(self, guild_id: int, role_id: int, changes: Mapping[str, Any]) -> Role:
@@ -229,10 +245,7 @@ class GuildMixin(BackendBase):
229
245
  if role.position != new_position:
230
246
  role.position = new_position
231
247
  changed.append(role)
232
- for role in changed:
233
- self.emit(
234
- "GUILD_ROLE_UPDATE", {"guild_id": str(guild_id), "role": serializers.role_payload(role)}
235
- )
248
+ self.announce_role_updates(guild_id, changed)
236
249
  return guild
237
250
 
238
251
  def delete_role(self, guild_id: int, role_id: int) -> None:
@@ -88,6 +88,11 @@ def role_payload(role: Role) -> role_types.Role:
88
88
  "permissions": str(role.permissions),
89
89
  "position": role.position,
90
90
  "color": role.color,
91
+ "colors": {
92
+ "primary_color": role.color,
93
+ "secondary_color": None,
94
+ "tertiary_color": None,
95
+ },
91
96
  "hoist": role.hoist,
92
97
  "managed": role.managed,
93
98
  "mentionable": role.mentionable,
@@ -98,6 +103,12 @@ def role_payload(role: Role) -> role_types.Role:
98
103
  )
99
104
 
100
105
 
106
+ def roles_payload(guild: Guild) -> list[role_types.Role]:
107
+ # Discord serves a guild's role list sorted by role id ascending (the
108
+ # @everyone role shares the guild id, so it can sort anywhere).
109
+ return [role_payload(r) for r in sorted(guild.roles.values(), key=lambda r: r.id)]
110
+
111
+
101
112
  def member_payload(
102
113
  backend: BackendBase, guild: Guild, member: Member, *, with_user: bool = True
103
114
  ) -> member_types.MemberWithUser:
@@ -355,7 +366,7 @@ def guild_create_payload(backend: BackendBase, guild: Guild) -> guild_types.Guil
355
366
  "features": [],
356
367
  "emojis": [guild_emoji_payload(backend, e) for e in guild.emojis.values()],
357
368
  "stickers": [sticker_payload(backend, s) for s in guild.stickers.values()],
358
- "roles": [role_payload(r) for r in guild.roles.values()],
369
+ "roles": roles_payload(guild),
359
370
  "member_count": len(guild.members),
360
371
  "large": False,
361
372
  "unavailable": False,
@@ -640,7 +640,7 @@ def delete_role(ctx: RequestContext) -> Any:
640
640
  @route("GET", "/guilds/{guild_id}/roles")
641
641
  def get_roles(ctx: RequestContext) -> Any:
642
642
  guild = ctx.backend.get_guild(ctx.int_arg("guild_id"))
643
- return [dict(serializers.role_payload(r)) for r in guild.roles.values()]
643
+ return [dict(r) for r in serializers.roles_payload(guild)]
644
644
 
645
645
 
646
646
  @route("GET", "/guilds/{guild_id}/roles/{role_id}")
@@ -668,4 +668,4 @@ def move_role_positions(ctx: RequestContext) -> Any:
668
668
  backend.require_role_assignable(guild_id, bot_id, int(item["id"]))
669
669
  backend.require_position_assignable(guild_id, bot_id, int(item["position"]))
670
670
  guild = backend.reorder_roles(guild_id, positions)
671
- return [dict(serializers.role_payload(r)) for r in guild.roles.values()]
671
+ return [dict(r) for r in serializers.roles_payload(guild)]
@@ -69,7 +69,12 @@ async def test_role_reorder_changes_positions(env):
69
69
  await env.settle()
70
70
 
71
71
  assert env.backend.get_role(env.guild.id, low.id).position == high_pos
72
- assert "GUILD_ROLE_UPDATE" in env.transcript()
72
+ # The reorder itself announced the moved role (create_role updates alone
73
+ # would leave this assertion passing otherwise).
74
+ assert any(
75
+ name == "GUILD_ROLE_UPDATE" and payload["role"]["id"] == str(low.id)
76
+ for _, name, payload in env.backend.transcript
77
+ )
73
78
 
74
79
 
75
80
  async def test_second_stage_instance_rejected(env):
@@ -54,6 +54,81 @@ async def test_edit_role_positions(env):
54
54
  assert guild.get_role(low.id).position == high_pos
55
55
 
56
56
 
57
+ async def test_create_role_announces_position_shifts(env):
58
+ guild = env.bot.get_guild(env.guild.id)
59
+ await guild.create_role(name="admin")
60
+ await guild.create_role(name="mod")
61
+ await env.settle()
62
+
63
+ # Each create inserts at position 1 and bumps every existing role up; the
64
+ # backend announces each shift with GUILD_ROLE_UPDATE (after the create, as
65
+ # real Discord does), so every cached position matches the backend's.
66
+ backend_guild = env.backend.get_guild(env.guild.id)
67
+ for backend_role in backend_guild.roles.values():
68
+ cached = guild.get_role(backend_role.id)
69
+ assert cached is not None
70
+ assert cached.position == backend_role.position
71
+ assert backend_guild.roles[guild.id].position == 0
72
+
73
+ # Each create lands first, then the shift updates it caused.
74
+ role_events = [
75
+ (name, payload["role"]["name"])
76
+ for _, name, payload in env.backend.transcript
77
+ if name in ("GUILD_ROLE_CREATE", "GUILD_ROLE_UPDATE")
78
+ ]
79
+ assert role_events[:2] == [("GUILD_ROLE_CREATE", "admin"), ("GUILD_ROLE_UPDATE", "TestBot")]
80
+ assert role_events[2] == ("GUILD_ROLE_CREATE", "mod")
81
+ assert all(name == "GUILD_ROLE_UPDATE" for name, _ in role_events[3:])
82
+
83
+ # The bot's managed role rides on top of the hierarchy, so hierarchy checks
84
+ # like "bot must outrank target" see a true ordering.
85
+ assert guild.me.top_role.position == max(r.position for r in backend_guild.roles.values())
86
+
87
+
88
+ async def test_delete_role_leaves_position_gap(env):
89
+ guild = env.bot.get_guild(env.guild.id)
90
+ low = await guild.create_role(name="low")
91
+ mid = await guild.create_role(name="mid")
92
+ await env.settle()
93
+
94
+ await mid.delete()
95
+ await env.settle()
96
+
97
+ # Discord does not renumber positions on delete: the hole at 1 stays a hole.
98
+ assert env.backend.get_role(guild.id, low.id).position == 2
99
+ assert guild.get_role(low.id).position == 2
100
+
101
+
102
+ async def test_role_colour_reaches_the_cache(env):
103
+ guild = env.bot.get_guild(env.guild.id)
104
+ role = await guild.create_role(name="Tinted", colour=discord.Colour(0x112233))
105
+ await env.settle()
106
+
107
+ # discord.py reads the role "colors" object (primary/secondary/tertiary);
108
+ # the deprecated flat "color" field alone leaves every cached colour at 0.
109
+ assert role.colour == discord.Colour(0x112233)
110
+ cached = guild.get_role(role.id)
111
+ assert cached.colour == discord.Colour(0x112233)
112
+ assert cached.secondary_colour is None
113
+
114
+
115
+ async def test_guild_roles_list_sorted_by_id(env):
116
+ odd = env.create_guild("Odd", id=8_000_000_000_000_000_000)
117
+ await env.settle()
118
+ guild = env.bot.get_guild(odd.id)
119
+ await guild.create_role(name="one")
120
+ await guild.create_role(name="two")
121
+ await env.settle()
122
+
123
+ # Real Discord returns a guild's roles sorted by id ascending — with an
124
+ # explicit guild id the @everyone role's snowflake is the largest, so
125
+ # insertion order alone would wrongly put it first.
126
+ fetched = await guild.fetch_roles()
127
+ ids = [r.id for r in fetched]
128
+ assert ids == sorted(ids)
129
+ assert ids[-1] == odd.id
130
+
131
+
57
132
  async def test_reorder_cannot_lift_role_above_bot(env):
58
133
  guild = env.bot.get_guild(env.guild.id)
59
134
  role = await guild.create_role(name="Climber")
@@ -1402,7 +1402,7 @@ wheels = [
1402
1402
 
1403
1403
  [[package]]
1404
1404
  name = "simcord"
1405
- version = "2.0.1"
1405
+ version = "2.0.2"
1406
1406
  source = { editable = "." }
1407
1407
  dependencies = [
1408
1408
  { name = "discord-py" },
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes