simcord 2.0.2__tar.gz → 2.1.0__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.
- {simcord-2.0.2 → simcord-2.1.0}/CHANGELOG.md +7 -0
- {simcord-2.0.2 → simcord-2.1.0}/PKG-INFO +6 -2
- {simcord-2.0.2 → simcord-2.1.0}/README.md +5 -1
- {simcord-2.0.2 → simcord-2.1.0}/docs/api.md +10 -0
- {simcord-2.0.2 → simcord-2.1.0}/docs/concepts.md +3 -3
- {simcord-2.0.2 → simcord-2.1.0}/docs/cookbook.md +5 -1
- {simcord-2.0.2 → simcord-2.1.0}/docs/guides/diagnostics.md +19 -8
- {simcord-2.0.2 → simcord-2.1.0}/docs/parity-matrix.md +1 -1
- {simcord-2.0.2 → simcord-2.1.0}/docs/quickstart.md +3 -4
- {simcord-2.0.2 → simcord-2.1.0}/docs/stability.md +7 -0
- {simcord-2.0.2 → simcord-2.1.0}/pyproject.toml +1 -1
- {simcord-2.0.2 → simcord-2.1.0}/src/simcord/__init__.py +2 -0
- {simcord-2.0.2 → simcord-2.1.0}/src/simcord/backend/ops/base.py +2 -0
- {simcord-2.0.2 → simcord-2.1.0}/src/simcord/env.py +13 -1
- {simcord-2.0.2 → simcord-2.1.0}/src/simcord/http/router.py +3 -1
- simcord-2.1.0/src/simcord/types.py +17 -0
- {simcord-2.0.2 → simcord-2.1.0}/tests/integration/test_diagnostics.py +14 -2
- {simcord-2.0.2 → simcord-2.1.0}/tests/integration/test_misc_branches.py +1 -1
- {simcord-2.0.2 → simcord-2.1.0}/tests/integration/test_moderation.py +23 -0
- simcord-2.1.0/tests/unit/test_router.py +80 -0
- {simcord-2.0.2 → simcord-2.1.0}/uv.lock +1 -1
- simcord-2.0.2/tests/unit/test_router.py +0 -39
- {simcord-2.0.2 → simcord-2.1.0}/.github/ISSUE_TEMPLATE/bug.md +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/.github/ISSUE_TEMPLATE/feature.md +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/.github/ISSUE_TEMPLATE/parity-gap.md +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/.github/dependabot.yml +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/.github/workflows/ci.yml +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/.github/workflows/dpy-master.yml +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/.github/workflows/release.yml +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/.gitignore +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/.pre-commit-config.yaml +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/.python-version +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/.readthedocs.yaml +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/CODE_OF_CONDUCT.md +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/CONTRIBUTING.md +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/LICENSE +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/SECURITY.md +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/benchmarks/_helpers.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/benchmarks/test_dispatch_benchmarks.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/benchmarks/test_perf_guards.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/changes/README.md +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/docs/architecture.md +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/docs/assets/simcord-terminal-demo.gif +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/docs/assets/simcord-transparent.webp +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/docs/assets/social-card.png +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/docs/changelog.md +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/docs/faq.md +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/docs/guides/ai-coding-agents.md +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/docs/guides/components.md +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/docs/guides/fixtures.md +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/docs/guides/intents.md +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/docs/guides/interactions.md +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/docs/guides/messages.md +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/docs/guides/mocks-vs-simulation.md +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/docs/guides/moderation-and-guild.md +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/docs/guides/permissions.md +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/docs/guides/test-without-token.md +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/docs/guides/testing-discord-py-bots.md +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/docs/guides/testing-slash-commands.md +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/docs/guides/threads-reactions-dms.md +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/docs/guides/time-control.md +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/docs/index.md +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/docs/installation.md +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/docs/llms.txt +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/docs/performance.md +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/docs/robots.txt +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/examples/README.md +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/examples/__init__.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/examples/bot.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/examples/conftest.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/examples/test_bot.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/mkdocs.yml +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/overrides/main.html +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/scripts/check_discoverability.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/src/simcord/_dpy_internals.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/src/simcord/actors.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/src/simcord/asserts.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/src/simcord/backend/__init__.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/src/simcord/backend/cdn.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/src/simcord/backend/errors.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/src/simcord/backend/models/__init__.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/src/simcord/backend/models/auditlog.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/src/simcord/backend/models/automod.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/src/simcord/backend/models/channel.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/src/simcord/backend/models/expression.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/src/simcord/backend/models/guild.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/src/simcord/backend/models/interaction.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/src/simcord/backend/models/invite.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/src/simcord/backend/models/member.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/src/simcord/backend/models/message.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/src/simcord/backend/models/role.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/src/simcord/backend/models/scheduled_event.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/src/simcord/backend/models/stage_instance.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/src/simcord/backend/models/user.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/src/simcord/backend/models/voice.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/src/simcord/backend/models/webhook.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/src/simcord/backend/ops/__init__.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/src/simcord/backend/ops/audit.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/src/simcord/backend/ops/channels.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/src/simcord/backend/ops/commands.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/src/simcord/backend/ops/expressions.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/src/simcord/backend/ops/guilds.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/src/simcord/backend/ops/invites.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/src/simcord/backend/ops/messages.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/src/simcord/backend/ops/permissions.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/src/simcord/backend/ops/polls.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/src/simcord/backend/ops/reactions.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/src/simcord/backend/ops/scheduled_events.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/src/simcord/backend/ops/voice.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/src/simcord/backend/ops/webhooks.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/src/simcord/backend/permissions.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/src/simcord/backend/serializers.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/src/simcord/backend/state.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/src/simcord/builders.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/src/simcord/components.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/src/simcord/enums.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/src/simcord/gateway.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/src/simcord/http/__init__.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/src/simcord/http/_helpers.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/src/simcord/http/client.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/src/simcord/http/routes/__init__.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/src/simcord/http/routes/application.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/src/simcord/http/routes/auditlogs.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/src/simcord/http/routes/automod.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/src/simcord/http/routes/channels.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/src/simcord/http/routes/commands.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/src/simcord/http/routes/expressions.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/src/simcord/http/routes/guilds.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/src/simcord/http/routes/interactions.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/src/simcord/http/routes/invites.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/src/simcord/http/routes/messages.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/src/simcord/http/routes/reactions.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/src/simcord/http/routes/scheduled_events.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/src/simcord/http/routes/stage_instances.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/src/simcord/intents.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/src/simcord/interactions.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/src/simcord/parity.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/src/simcord/py.typed +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/src/simcord/pytest_plugin.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/src/simcord/results.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/tests/conformance/_world.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/tests/conformance/test_field_honesty.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/tests/conformance/test_field_honesty_fuzz.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/tests/conformance/test_route_coverage.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/tests/conformance/test_serializers.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/tests/conftest.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/tests/fixtures/__init__.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/tests/fixtures/sample_bot/__init__.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/tests/fixtures/sample_bot/events.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/tests/fixtures/sample_bot/general.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/tests/fixtures/sample_bot/interactions.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/tests/fixtures/sample_bot/moderation.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/tests/integration/test_app_commands.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/tests/integration/test_application_emojis.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/tests/integration/test_asserts.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/tests/integration/test_audit_logs.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/tests/integration/test_automod.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/tests/integration/test_bot_users_and_webhooks.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/tests/integration/test_channels.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/tests/integration/test_command_permissions.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/tests/integration/test_common_bot_sweep.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/tests/integration/test_components_v2_interactions.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/tests/integration/test_components_v2_messages.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/tests/integration/test_coverage_sweep.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/tests/integration/test_edge_cases.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/tests/integration/test_error_capture_and_edits.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/tests/integration/test_expressions.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/tests/integration/test_forum.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/tests/integration/test_gateway_and_automod_exempt.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/tests/integration/test_guilds.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/tests/integration/test_intents.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/tests/integration/test_interaction_followups.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/tests/integration/test_interactions.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/tests/integration/test_invites.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/tests/integration/test_lifecycle.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/tests/integration/test_members.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/tests/integration/test_message_routes.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/tests/integration/test_messages.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/tests/integration/test_polls.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/tests/integration/test_reactions.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/tests/integration/test_roles_and_ordering.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/tests/integration/test_scheduled_events.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/tests/integration/test_settlement_contract.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/tests/integration/test_setup_hook_suspension.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/tests/integration/test_sharding.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/tests/integration/test_stage_instances.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/tests/integration/test_threads.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/tests/integration/test_time.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/tests/integration/test_transport_edges.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/tests/integration/test_voice_state.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/tests/integration/test_world_helpers.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/tests/unit/test_assert_helpers.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/tests/unit/test_backend_lookup_errors.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/tests/unit/test_components_v2_validation.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/tests/unit/test_errors.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/tests/unit/test_intent_rules.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/tests/unit/test_parity.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/tests/unit/test_parity_tooling.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/tests/unit/test_permission_validators.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/tests/unit/test_permissions.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/tests/unit/test_pytest_plugin.py +0 -0
- {simcord-2.0.2 → simcord-2.1.0}/tests/unit/test_version.py +0 -0
|
@@ -4,6 +4,13 @@ This changelog is generated with [towncrier](https://towncrier.readthedocs.io/).
|
|
|
4
4
|
|
|
5
5
|
<!-- towncrier release notes start -->
|
|
6
6
|
|
|
7
|
+
## 2.1.0 (2026-09-19)
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
- Add `Env.http_requests` records with query parameters and audit reasons while preserving the deprecated live `Env.http_log` tuple list for 2.x compatibility.
|
|
12
|
+
|
|
13
|
+
|
|
7
14
|
## 2.0.2 (2026-09-18)
|
|
8
15
|
|
|
9
16
|
### Bug fixes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: simcord
|
|
3
|
-
Version: 2.0
|
|
3
|
+
Version: 2.1.0
|
|
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/
|
|
@@ -263,7 +263,11 @@ When something goes wrong, the `env` tells you what happened:
|
|
|
263
263
|
|
|
264
264
|
- `env.transcript()`: the ordered log of gateway events and REST calls, auto-attached to
|
|
265
265
|
failing pytest tests.
|
|
266
|
-
- `env.
|
|
266
|
+
- `env.http_requests`: structured records for every REST request (`method`, `path`, `params`,
|
|
267
|
+
`json`, `reason`). Values are discord.py transport arguments, not wire-normalized strings or
|
|
268
|
+
headers; uploaded files are not captured.
|
|
269
|
+
- `env.http_log`: deprecated live `(method, path, json_body)` tuples for 2.x compatibility;
|
|
270
|
+
access emits `DeprecationWarning`.
|
|
267
271
|
- `env.errors`: exceptions the bot swallowed.
|
|
268
272
|
- `env.inject_error("POST", "/channels/*/messages", status=500)`: make matching REST calls
|
|
269
273
|
fail, to test your bot's error handling.
|
|
@@ -216,7 +216,11 @@ When something goes wrong, the `env` tells you what happened:
|
|
|
216
216
|
|
|
217
217
|
- `env.transcript()`: the ordered log of gateway events and REST calls, auto-attached to
|
|
218
218
|
failing pytest tests.
|
|
219
|
-
- `env.
|
|
219
|
+
- `env.http_requests`: structured records for every REST request (`method`, `path`, `params`,
|
|
220
|
+
`json`, `reason`). Values are discord.py transport arguments, not wire-normalized strings or
|
|
221
|
+
headers; uploaded files are not captured.
|
|
222
|
+
- `env.http_log`: deprecated live `(method, path, json_body)` tuples for 2.x compatibility;
|
|
223
|
+
access emits `DeprecationWarning`.
|
|
220
224
|
- `env.errors`: exceptions the bot swallowed.
|
|
221
225
|
- `env.inject_error("POST", "/channels/*/messages", status=500)`: make matching REST calls
|
|
222
226
|
fail, to test your bot's error handling.
|
|
@@ -26,6 +26,16 @@ top-level `simcord` package.
|
|
|
26
26
|
Use `await env.external_wait(awaitable, reason="...")` for one explicitly scoped external
|
|
27
27
|
input wait. Unknown waits remain active and produce a diagnostic timeout.
|
|
28
28
|
|
|
29
|
+
|
|
30
|
+
## Request observability
|
|
31
|
+
|
|
32
|
+
::: simcord.HttpLogEntry
|
|
33
|
+
|
|
34
|
+
`Env.http_requests` exposes these records for transport-level assertions. The `params` and
|
|
35
|
+
`reason` fields contain discord.py transport arguments, not wire-normalized query strings or
|
|
36
|
+
encoded headers; uploaded files are not captured. `Env.http_log` is a deprecated compatibility
|
|
37
|
+
list of legacy tuples.
|
|
38
|
+
|
|
29
39
|
## Builders
|
|
30
40
|
|
|
31
41
|
Synchronous, omnipotent handles for arranging the virtual Discord. Returned by `env`/`guild`
|
|
@@ -23,8 +23,8 @@ async def test_something(simcord_env):
|
|
|
23
23
|
```
|
|
24
24
|
|
|
25
25
|
The `Env` owns the virtual backend, tracks the bot's background tasks, captures errors,
|
|
26
|
-
holds the virtual clock, and exposes the diagnostics (`env.
|
|
27
|
-
|
|
26
|
+
holds the virtual clock, and exposes the diagnostics (`env.http_requests`, `env.transcript()`).
|
|
27
|
+
`env.http_log` is retained as a deprecated compatibility view.
|
|
28
28
|
|
|
29
29
|
!!! info "One environment per event loop"
|
|
30
30
|
`Env` monkeypatches `loop.create_task` and `time.monotonic` while it's live so it can
|
|
@@ -132,7 +132,7 @@ channel.history(viewer=mod) # ephemeral-aware: what `mod` would see
|
|
|
132
132
|
channel.pinned_messages() # list[discord.Message]
|
|
133
133
|
guild.get_ban(target) # ban record | None
|
|
134
134
|
env.errors # exceptions the bot swallowed
|
|
135
|
-
env.
|
|
135
|
+
env.http_requests # structured REST request records
|
|
136
136
|
```
|
|
137
137
|
|
|
138
138
|
Interaction verbs return a richer [`InteractionResult`](api.md#simcord.InteractionResult) capturing
|
|
@@ -246,7 +246,11 @@ async def test_no_double_post(simcord_env):
|
|
|
246
246
|
|
|
247
247
|
await alice.send(channel, "!announce")
|
|
248
248
|
|
|
249
|
-
posts = [
|
|
249
|
+
posts = [
|
|
250
|
+
entry
|
|
251
|
+
for entry in simcord_env.http_requests
|
|
252
|
+
if entry.method == "POST" and "/messages" in entry.path
|
|
253
|
+
]
|
|
250
254
|
assert len(posts) == 1
|
|
251
255
|
```
|
|
252
256
|
|
|
@@ -107,19 +107,30 @@ The [pytest plugin attaches this to failing tests automatically](fixtures.md#the
|
|
|
107
107
|
so a failure shows you exactly what the bot did, in order — usually enough to diagnose
|
|
108
108
|
without a debugger.
|
|
109
109
|
|
|
110
|
-
##
|
|
110
|
+
## HTTP request records (`env.http_requests`)
|
|
111
111
|
|
|
112
|
-
For fine-grained assertions, `env.
|
|
113
|
-
`(
|
|
112
|
+
For fine-grained assertions, `env.http_requests` is the list of every REST call the bot made,
|
|
113
|
+
as [`simcord.HttpLogEntry`](../api.md#simcord.HttpLogEntry) records:
|
|
114
114
|
|
|
115
115
|
```python
|
|
116
|
-
posts = [
|
|
117
|
-
|
|
118
|
-
|
|
116
|
+
posts = [
|
|
117
|
+
entry
|
|
118
|
+
for entry in simcord_env.http_requests
|
|
119
|
+
if entry.method == "POST" and "/messages" in entry.path
|
|
120
|
+
]
|
|
121
|
+
assert len(posts) == 1
|
|
122
|
+
assert posts[0].json["content"] == "Pong!"
|
|
119
123
|
```
|
|
120
124
|
|
|
121
|
-
|
|
122
|
-
|
|
125
|
+
Each record exposes `method`, `path`, `params`, `json`, and `reason`. These values are the
|
|
126
|
+
arguments discord.py passed to its transport: query parameters are not wire-normalized
|
|
127
|
+
strings, and `reason` is not an encoded HTTP header value. Uploaded files are not captured.
|
|
128
|
+
The request record is appended before fault injection and route handling, so failed and
|
|
129
|
+
unimplemented requests remain observable.
|
|
130
|
+
|
|
131
|
+
`env.http_log` remains available as a deprecated live list of legacy
|
|
132
|
+
`(method, path, json_body)` tuples for 2.x compatibility. Access emits `DeprecationWarning`;
|
|
133
|
+
use `env.http_requests` for new assertions.
|
|
123
134
|
|
|
124
135
|
## Injecting API failures
|
|
125
136
|
|
|
@@ -46,7 +46,7 @@ serializer payloads are conformance-tested against discord.py's own model parser
|
|
|
46
46
|
| Guilds (create/edit/delete) | ✅ | `Client.create_guild`, `Guild.edit`, `Guild.delete` (owner-only); `GUILD_UPDATE` audit; `Guild.leave`, `Client.fetch_guilds`, `ClientUser.edit` (bot username); `Guild.vanity_invite` (settable via `guild.set_vanity_url`) |
|
|
47
47
|
| Channels (create/edit/delete, overwrites) | ✅ | Runtime create + list; text, voice, stage, category & forum kinds; reorder/move (`Channel.move`); announcement `TextChannel.follow` |
|
|
48
48
|
| Webhooks | ✅ | Create, execute (with per-message `username` override), fetch/edit/delete (by id or token), guild listing |
|
|
49
|
-
| Fault injection / HTTP log | ✅ | `env.inject_error`, `env.http_log` |
|
|
49
|
+
| Fault injection / HTTP log | ✅ | `env.inject_error`, structured `env.http_requests` records, deprecated `env.http_log` tuples |
|
|
50
50
|
| Audit logs | ✅ | Recorded for ban/kick/role/member/channel/event actions; `guild.audit_logs()`, filtering |
|
|
51
51
|
| Polls | ✅ | Message-level poll object; `actor.vote`, expiry (route + `advance_time`), vote events |
|
|
52
52
|
| Scheduled events | ✅ | CRUD + subscribe/unsubscribe; auto status transitions via `advance_time` |
|
|
@@ -124,10 +124,9 @@ see [Core concepts](concepts.md) for the deep dive.
|
|
|
124
124
|
`alice.click(...)`, `alice.select(...)`, `alice.react(...)`, `alice.submit_modal(...)`.
|
|
125
125
|
Do only what a real user physically could. Each one **waits for your bot to finish
|
|
126
126
|
reacting** before returning — so there's never an `asyncio.sleep` in your tests.
|
|
127
|
-
- **Queries** — `channel.history()`, `
|
|
128
|
-
[`env.
|
|
129
|
-
|
|
130
|
-
Python against real `discord.Message` objects.
|
|
127
|
+
- **Queries** — `channel.history()`, [`env.errors`](guides/diagnostics.md) (errors your bot
|
|
128
|
+
swallowed), and [`env.http_requests`](guides/diagnostics.md) (structured REST request
|
|
129
|
+
records). Assert with plain Python against real `discord.Message` objects.
|
|
131
130
|
|
|
132
131
|
## Testing failure paths
|
|
133
132
|
|
|
@@ -68,11 +68,18 @@ The public surface is exactly what `simcord` exports from its top-level package
|
|
|
68
68
|
- the world builders — `GuildHandle`, `ChannelHandle`, `UserHandle`,
|
|
69
69
|
`RoleHandle` — and the `MemberActor` that drives simulated users;
|
|
70
70
|
- the result objects `ResponseMessage` and `InteractionResult`;
|
|
71
|
+
- the request-observability type `HttpLogEntry` and `Env.http_requests`;
|
|
71
72
|
- the assertion helpers (`assert_responded`, `assert_sent`, `assert_message`,
|
|
72
73
|
`assert_error`, `assert_no_errors`);
|
|
73
74
|
- the error and parity-signal types `BackendError`, `SetupError`,
|
|
74
75
|
`RouteNotImplemented` and `UnsupportedField`.
|
|
75
76
|
|
|
77
|
+
`Env.http_requests` is the semver-covered request-observability API. Its
|
|
78
|
+
`HttpLogEntry` fields preserve discord.py transport arguments (`params`, `json`, and
|
|
79
|
+
`reason`); they are not wire-normalized query strings or encoded headers, and files are
|
|
80
|
+
not captured. `Env.http_log` remains the live 2.x tuple list and emits
|
|
81
|
+
`DeprecationWarning` on access.
|
|
82
|
+
|
|
76
83
|
The `pytest` plugin (the `simcord_env` fixture) is part of the public surface
|
|
77
84
|
too.
|
|
78
85
|
|
|
@@ -46,6 +46,7 @@ from .builders import ( # noqa: E402
|
|
|
46
46
|
from .env import Env, run # noqa: E402
|
|
47
47
|
from .http import RouteNotImplemented, UnsupportedField # noqa: E402
|
|
48
48
|
from .results import InteractionResult, ResponseMessage # noqa: E402
|
|
49
|
+
from .types import HttpLogEntry # noqa: E402
|
|
49
50
|
|
|
50
51
|
try:
|
|
51
52
|
__version__ = version("simcord")
|
|
@@ -57,6 +58,7 @@ __all__ = (
|
|
|
57
58
|
"ChannelHandle",
|
|
58
59
|
"Env",
|
|
59
60
|
"GuildHandle",
|
|
61
|
+
"HttpLogEntry",
|
|
60
62
|
"InteractionResult",
|
|
61
63
|
"MemberActor",
|
|
62
64
|
"ResponseMessage",
|
|
@@ -13,6 +13,7 @@ import datetime
|
|
|
13
13
|
from collections.abc import Callable, Mapping, Sequence
|
|
14
14
|
from typing import Any
|
|
15
15
|
|
|
16
|
+
from ...types import HttpLogEntry
|
|
16
17
|
from .. import errors, permissions
|
|
17
18
|
from ..cdn import CdnStore
|
|
18
19
|
from ..models import (
|
|
@@ -59,6 +60,7 @@ class BackendBase:
|
|
|
59
60
|
self.cdn = CdnStore()
|
|
60
61
|
self.subscribers: list[EventListener] = []
|
|
61
62
|
self.http_log: list[tuple[str, str, dict[str, Any] | None]] = []
|
|
63
|
+
self.http_requests: list[HttpLogEntry] = []
|
|
62
64
|
#: Interleaved record of everything that crossed either seam, in order:
|
|
63
65
|
#: ("HTTP", "METHOD /path", body) and ("GATEWAY", event, payload).
|
|
64
66
|
self.transcript: list[tuple[str, str, Any]] = []
|
|
@@ -6,6 +6,7 @@ import asyncio
|
|
|
6
6
|
import contextvars
|
|
7
7
|
import math
|
|
8
8
|
import time
|
|
9
|
+
import warnings
|
|
9
10
|
import weakref
|
|
10
11
|
from collections.abc import Iterator
|
|
11
12
|
from contextlib import contextmanager
|
|
@@ -22,6 +23,7 @@ from .backend.errors import SetupError
|
|
|
22
23
|
from .builders import GuildHandle, UserHandle
|
|
23
24
|
from .gateway import ShardRouter
|
|
24
25
|
from .http import FakeHTTPClient, FakeWebhookAdapter
|
|
26
|
+
from .types import HttpLogEntry
|
|
25
27
|
|
|
26
28
|
_BOT_SCOPE: contextvars.ContextVar[tuple[Any, int] | None] = contextvars.ContextVar(
|
|
27
29
|
"simcord_bot_scope", default=None
|
|
@@ -906,9 +908,19 @@ class Env:
|
|
|
906
908
|
|
|
907
909
|
# ----------------------------------------------------------- diagnostics
|
|
908
910
|
|
|
911
|
+
@property
|
|
912
|
+
def http_requests(self) -> list[HttpLogEntry]:
|
|
913
|
+
"""Every REST call the bot made as a structured request record."""
|
|
914
|
+
return self.backend.http_requests
|
|
915
|
+
|
|
909
916
|
@property
|
|
910
917
|
def http_log(self) -> list[tuple[str, str, dict[str, Any] | None]]:
|
|
911
|
-
"""
|
|
918
|
+
"""Deprecated live ``(method, path, json)`` REST call tuples."""
|
|
919
|
+
warnings.warn(
|
|
920
|
+
"Env.http_log is deprecated; use Env.http_requests instead",
|
|
921
|
+
DeprecationWarning,
|
|
922
|
+
stacklevel=2,
|
|
923
|
+
)
|
|
912
924
|
return self.backend.http_log
|
|
913
925
|
|
|
914
926
|
def transcript(self) -> str:
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
Unknown routes always fail loudly with :class:`RouteNotImplemented` — a
|
|
4
4
|
testing tool must never silently fake success. The router also services
|
|
5
5
|
test-injected faults (``env.inject_error``) and records every call to
|
|
6
|
-
``backend.
|
|
6
|
+
``backend.http_requests`` plus the deprecated ``backend.http_log`` tuple list.
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
9
|
from __future__ import annotations
|
|
@@ -17,6 +17,7 @@ from typing import Any
|
|
|
17
17
|
from ..backend import Backend
|
|
18
18
|
from ..backend.errors import BackendError
|
|
19
19
|
from ..backend.models import Channel
|
|
20
|
+
from ..types import HttpLogEntry
|
|
20
21
|
|
|
21
22
|
|
|
22
23
|
class RouteNotImplemented(BackendError):
|
|
@@ -192,6 +193,7 @@ def dispatch(
|
|
|
192
193
|
reason: str | None = None,
|
|
193
194
|
) -> Any:
|
|
194
195
|
backend.http_log.append((method, path, json if isinstance(json, dict) else None))
|
|
196
|
+
backend.http_requests.append(HttpLogEntry(method, path, dict(params or {}), json, reason))
|
|
195
197
|
backend.transcript.append(("HTTP", f"{method} {path}", json if isinstance(json, dict) else None))
|
|
196
198
|
_check_faults(backend, method, path)
|
|
197
199
|
segments = path.strip("/").split("/")
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Public observability value types."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True, slots=True)
|
|
10
|
+
class HttpLogEntry:
|
|
11
|
+
"""A transport-level REST request made by the bot."""
|
|
12
|
+
|
|
13
|
+
method: str
|
|
14
|
+
path: str
|
|
15
|
+
params: dict[str, Any]
|
|
16
|
+
json: Any | None
|
|
17
|
+
reason: str | None
|
|
@@ -77,8 +77,20 @@ async def test_unimplemented_route_attaches_parity_note(env, channel):
|
|
|
77
77
|
|
|
78
78
|
async def test_http_log_records_bot_calls(env, channel, alice):
|
|
79
79
|
await alice.send(channel, "!ping")
|
|
80
|
-
sends = [
|
|
81
|
-
|
|
80
|
+
sends = [
|
|
81
|
+
(entry.method, entry.path)
|
|
82
|
+
for entry in env.http_requests
|
|
83
|
+
if entry.method == "POST" and entry.path.endswith("/messages")
|
|
84
|
+
]
|
|
85
|
+
assert sends, env.http_requests
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
async def test_http_log_legacy_tuples_warn_and_remain_live(env):
|
|
89
|
+
with pytest.warns(DeprecationWarning, match="use Env.http_requests"):
|
|
90
|
+
legacy = env.http_log
|
|
91
|
+
assert legacy is env.backend.http_log
|
|
92
|
+
legacy.append(("TEST", "/compatibility", None))
|
|
93
|
+
assert env.backend.http_log[-1] == ("TEST", "/compatibility", None)
|
|
82
94
|
|
|
83
95
|
|
|
84
96
|
async def test_embed_limits_enforced(env, channel):
|
|
@@ -10,7 +10,7 @@ async def test_bot_typing_indicator(env, channel):
|
|
|
10
10
|
pass
|
|
11
11
|
await env.settle()
|
|
12
12
|
|
|
13
|
-
assert any(method == "POST" and path.endswith("/typing") for
|
|
13
|
+
assert any(entry.method == "POST" and entry.path.endswith("/typing") for entry in env.http_requests)
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
async def test_everyone_mention_requires_permission(env, channel):
|
|
@@ -16,6 +16,29 @@ async def test_bulk_ban(env):
|
|
|
16
16
|
assert alice.id not in env.backend.get_guild(env.guild.id).members
|
|
17
17
|
|
|
18
18
|
|
|
19
|
+
async def test_ban_request_records_query_params_and_reason(env):
|
|
20
|
+
target = env.guild.add_member(env.create_user("baddie"))
|
|
21
|
+
await env.settle()
|
|
22
|
+
|
|
23
|
+
guild = env.bot.get_guild(env.guild.id)
|
|
24
|
+
await guild.ban(
|
|
25
|
+
discord.Object(id=target.id),
|
|
26
|
+
delete_message_seconds=86400,
|
|
27
|
+
reason="cleanup needed",
|
|
28
|
+
)
|
|
29
|
+
await env.settle()
|
|
30
|
+
|
|
31
|
+
bans = [
|
|
32
|
+
entry
|
|
33
|
+
for entry in env.http_requests
|
|
34
|
+
if entry.method == "PUT" and entry.path.endswith(f"/bans/{target.id}")
|
|
35
|
+
]
|
|
36
|
+
assert len(bans) == 1
|
|
37
|
+
assert bans[0].params == {"delete_message_seconds": 86400}
|
|
38
|
+
assert bans[0].reason == "cleanup needed"
|
|
39
|
+
assert bans[0].json is None
|
|
40
|
+
|
|
41
|
+
|
|
19
42
|
async def test_bulk_ban_already_banned_reported_as_failed(env):
|
|
20
43
|
# When nobody can be banned (all already banned), Discord still returns the
|
|
21
44
|
# split result — not a 403 — with every id in `failed`.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from simcord import HttpLogEntry
|
|
4
|
+
from simcord.backend import Backend
|
|
5
|
+
from simcord.backend.errors import BackendError
|
|
6
|
+
from simcord.http import router
|
|
7
|
+
from simcord.http import routes as _routes # noqa: F401 — registers handlers
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_unknown_route_raises_with_route_name():
|
|
11
|
+
backend = Backend()
|
|
12
|
+
with pytest.raises(router.RouteNotImplemented, match="GET /made/up/route"):
|
|
13
|
+
router.dispatch(backend, "GET", "/made/up/route")
|
|
14
|
+
assert isinstance(backend.http_requests[-1], HttpLogEntry)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_literal_segments_beat_parameters():
|
|
18
|
+
backend = Backend()
|
|
19
|
+
guild = backend.create_guild("g")
|
|
20
|
+
channel = backend.create_channel(guild.id, "general")
|
|
21
|
+
message = backend.create_message(channel.id, backend.bot_user.id, "hi")
|
|
22
|
+
backend.set_pinned(channel.id, message.id, True)
|
|
23
|
+
# ".../messages/pins" must not be captured by ".../messages/{message_id}".
|
|
24
|
+
result = router.dispatch(backend, "GET", f"/channels/{channel.id}/messages/pins")
|
|
25
|
+
assert [int(i["message"]["id"]) for i in result["items"]] == [message.id]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_http_request_records_defaults_and_coexists_with_legacy_tuples():
|
|
29
|
+
backend = Backend()
|
|
30
|
+
guild = backend.create_guild("g")
|
|
31
|
+
path = f"/guilds/{guild.id}"
|
|
32
|
+
router.dispatch(backend, "GET", path)
|
|
33
|
+
entry = backend.http_requests[-1]
|
|
34
|
+
assert (entry.method, entry.path, entry.params, entry.json, entry.reason) == (
|
|
35
|
+
"GET",
|
|
36
|
+
path,
|
|
37
|
+
{},
|
|
38
|
+
None,
|
|
39
|
+
None,
|
|
40
|
+
)
|
|
41
|
+
assert backend.http_log[-1] == ("GET", path, None)
|
|
42
|
+
|
|
43
|
+
payload = {"name": "g"}
|
|
44
|
+
router.dispatch(backend, "GET", path, json=payload)
|
|
45
|
+
assert backend.http_requests[-1].json is payload
|
|
46
|
+
assert backend.http_log[-1] == ("GET", path, payload)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_http_request_records_snapshot_transport_values_before_fault():
|
|
50
|
+
backend = Backend()
|
|
51
|
+
guild = backend.create_guild("g")
|
|
52
|
+
path = f"/guilds/{guild.id}"
|
|
53
|
+
params = {"limit": 3}
|
|
54
|
+
payload = ["raw", 1]
|
|
55
|
+
backend.faults.append(
|
|
56
|
+
{
|
|
57
|
+
"method": "GET",
|
|
58
|
+
"path": path,
|
|
59
|
+
"status": 500,
|
|
60
|
+
"code": 0,
|
|
61
|
+
"message": "boom",
|
|
62
|
+
"times": 1,
|
|
63
|
+
}
|
|
64
|
+
)
|
|
65
|
+
with pytest.raises(BackendError):
|
|
66
|
+
router.dispatch(backend, "GET", path, params=params, json=payload, reason="cleanup needed")
|
|
67
|
+
entry = backend.http_requests[-1]
|
|
68
|
+
assert entry.params == params and entry.params is not params
|
|
69
|
+
assert entry.json is payload
|
|
70
|
+
assert entry.reason == "cleanup needed"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_snowflakes_are_monotonic_and_timestamped():
|
|
74
|
+
backend = Backend()
|
|
75
|
+
flakes = [backend.snowflake() for _ in range(100)]
|
|
76
|
+
assert flakes == sorted(flakes) and len(set(flakes)) == 100
|
|
77
|
+
import discord
|
|
78
|
+
|
|
79
|
+
# Embedded timestamps decode to the fixed virtual epoch (2026+).
|
|
80
|
+
assert discord.utils.snowflake_time(flakes[0]).year >= 2026
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import pytest
|
|
2
|
-
|
|
3
|
-
from simcord.backend import Backend
|
|
4
|
-
from simcord.http import router
|
|
5
|
-
from simcord.http import routes as _routes # noqa: F401 — registers handlers
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
def test_unknown_route_raises_with_route_name():
|
|
9
|
-
backend = Backend()
|
|
10
|
-
with pytest.raises(router.RouteNotImplemented, match="GET /made/up/route"):
|
|
11
|
-
router.dispatch(backend, "GET", "/made/up/route")
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def test_literal_segments_beat_parameters():
|
|
15
|
-
backend = Backend()
|
|
16
|
-
guild = backend.create_guild("g")
|
|
17
|
-
channel = backend.create_channel(guild.id, "general")
|
|
18
|
-
message = backend.create_message(channel.id, backend.bot_user.id, "hi")
|
|
19
|
-
backend.set_pinned(channel.id, message.id, True)
|
|
20
|
-
# ".../messages/pins" must not be captured by ".../messages/{message_id}".
|
|
21
|
-
result = router.dispatch(backend, "GET", f"/channels/{channel.id}/messages/pins")
|
|
22
|
-
assert [int(i["message"]["id"]) for i in result["items"]] == [message.id]
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def test_http_log_records_calls():
|
|
26
|
-
backend = Backend()
|
|
27
|
-
guild = backend.create_guild("g")
|
|
28
|
-
router.dispatch(backend, "GET", f"/guilds/{guild.id}")
|
|
29
|
-
assert backend.http_log[-1][:2] == ("GET", f"/guilds/{guild.id}")
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def test_snowflakes_are_monotonic_and_timestamped():
|
|
33
|
-
backend = Backend()
|
|
34
|
-
flakes = [backend.snowflake() for _ in range(100)]
|
|
35
|
-
assert flakes == sorted(flakes) and len(set(flakes)) == 100
|
|
36
|
-
import discord
|
|
37
|
-
|
|
38
|
-
# Embedded timestamps decode to the fixed virtual epoch (2026+).
|
|
39
|
-
assert discord.utils.snowflake_time(flakes[0]).year >= 2026
|
|
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
|
|
File without changes
|
|
File without changes
|