simcord 2.0.2__tar.gz → 2.2.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.2.0/.github/workflows/ci.yml +13 -0
- {simcord-2.0.2 → simcord-2.2.0}/.github/workflows/release.yml +4 -46
- simcord-2.2.0/.github/workflows/verify.yml +99 -0
- {simcord-2.0.2 → simcord-2.2.0}/.gitignore +5 -0
- {simcord-2.0.2 → simcord-2.2.0}/CHANGELOG.md +32 -0
- {simcord-2.0.2 → simcord-2.2.0}/CONTRIBUTING.md +12 -0
- {simcord-2.0.2 → simcord-2.2.0}/PKG-INFO +27 -5
- {simcord-2.0.2 → simcord-2.2.0}/README.md +15 -4
- {simcord-2.0.2 → simcord-2.2.0}/docs/api.md +35 -0
- {simcord-2.0.2 → simcord-2.2.0}/docs/architecture.md +15 -0
- {simcord-2.0.2 → simcord-2.2.0}/docs/concepts.md +12 -7
- {simcord-2.0.2 → simcord-2.2.0}/docs/cookbook.md +26 -1
- {simcord-2.0.2 → simcord-2.2.0}/docs/faq.md +8 -0
- {simcord-2.0.2 → simcord-2.2.0}/docs/guides/ai-coding-agents.md +14 -0
- {simcord-2.0.2 → simcord-2.2.0}/docs/guides/components.md +7 -0
- {simcord-2.0.2 → simcord-2.2.0}/docs/guides/diagnostics.md +17 -8
- simcord-2.2.0/docs/guides/preview.md +398 -0
- {simcord-2.0.2 → simcord-2.2.0}/docs/guides/time-control.md +4 -1
- {simcord-2.0.2 → simcord-2.2.0}/docs/index.md +6 -0
- {simcord-2.0.2 → simcord-2.2.0}/docs/installation.md +27 -5
- {simcord-2.0.2 → simcord-2.2.0}/docs/llms.txt +1 -0
- {simcord-2.0.2 → simcord-2.2.0}/docs/parity-matrix.md +9 -4
- {simcord-2.0.2 → simcord-2.2.0}/docs/quickstart.md +4 -4
- {simcord-2.0.2 → simcord-2.2.0}/docs/stability.md +17 -2
- simcord-2.2.0/examples/README.md +53 -0
- {simcord-2.0.2 → simcord-2.2.0}/examples/bot.py +13 -0
- simcord-2.2.0/examples/preview_example.py +78 -0
- {simcord-2.0.2 → simcord-2.2.0}/examples/test_bot.py +13 -0
- {simcord-2.0.2 → simcord-2.2.0}/mkdocs.yml +1 -0
- {simcord-2.0.2 → simcord-2.2.0}/pyproject.toml +9 -2
- simcord-2.2.0/scripts/capture_visual_reference.py +457 -0
- {simcord-2.0.2 → simcord-2.2.0}/scripts/check_discoverability.py +2 -0
- simcord-2.2.0/scripts/check_package.py +47 -0
- simcord-2.2.0/scripts/compare_visual_reference.py +588 -0
- simcord-2.2.0/scripts/discord_reference_bot.py +242 -0
- {simcord-2.0.2 → simcord-2.2.0}/src/simcord/__init__.py +21 -1
- {simcord-2.0.2 → simcord-2.2.0}/src/simcord/_dpy_internals.py +155 -10
- {simcord-2.0.2 → simcord-2.2.0}/src/simcord/actors.py +17 -13
- simcord-2.2.0/src/simcord/backend/access.py +71 -0
- {simcord-2.0.2 → simcord-2.2.0}/src/simcord/backend/models/interaction.py +1 -0
- {simcord-2.0.2 → simcord-2.2.0}/src/simcord/backend/models/user.py +2 -0
- {simcord-2.0.2 → simcord-2.2.0}/src/simcord/backend/ops/base.py +3 -2
- {simcord-2.0.2 → simcord-2.2.0}/src/simcord/backend/serializers.py +1 -1
- {simcord-2.0.2 → simcord-2.2.0}/src/simcord/builders.py +5 -0
- {simcord-2.0.2 → simcord-2.2.0}/src/simcord/env.py +391 -48
- {simcord-2.0.2 → simcord-2.2.0}/src/simcord/http/router.py +6 -2
- simcord-2.2.0/src/simcord/preview/__init__.py +398 -0
- simcord-2.2.0/src/simcord/preview/_actions.py +673 -0
- simcord-2.2.0/src/simcord/preview/_assets.py +209 -0
- simcord-2.2.0/src/simcord/preview/_capture.py +644 -0
- simcord-2.2.0/src/simcord/preview/_markdown.py +145 -0
- simcord-2.2.0/src/simcord/preview/_media.py +167 -0
- simcord-2.2.0/src/simcord/preview/_pages.py +343 -0
- simcord-2.2.0/src/simcord/preview/_server.py +287 -0
- simcord-2.2.0/src/simcord/preview/_snapshot.py +750 -0
- simcord-2.2.0/src/simcord/preview/protocol.schema.json +59 -0
- simcord-2.2.0/src/simcord/preview/static/app.js +885 -0
- simcord-2.2.0/src/simcord/preview/static/components.js +859 -0
- simcord-2.2.0/src/simcord/preview/static/fonts/OFL-1.1-Noto-Arabic.txt +93 -0
- simcord-2.2.0/src/simcord/preview/static/fonts/OFL-1.1-Noto-CJK.txt +92 -0
- simcord-2.2.0/src/simcord/preview/static/fonts/OFL-1.1-Noto-Devanagari.txt +93 -0
- simcord-2.2.0/src/simcord/preview/static/fonts/OFL-1.1-Noto-Emoji.txt +92 -0
- simcord-2.2.0/src/simcord/preview/static/fonts/OFL-1.1-Noto-Hebrew.txt +93 -0
- simcord-2.2.0/src/simcord/preview/static/fonts/OFL-1.1-Noto.txt +93 -0
- simcord-2.2.0/src/simcord/preview/static/fonts/manifest.json +202 -0
- simcord-2.2.0/src/simcord/preview/static/fonts/noto-color-emoji-v2.051.ttf +0 -0
- simcord-2.2.0/src/simcord/preview/static/fonts/noto-sans-arabic-v2.012.ttf +0 -0
- simcord-2.2.0/src/simcord/preview/static/fonts/noto-sans-devanagari-v2.007.ttf +0 -0
- simcord-2.2.0/src/simcord/preview/static/fonts/noto-sans-hebrew-v3.001.ttf +0 -0
- simcord-2.2.0/src/simcord/preview/static/fonts/noto-sans-latin-italic-v2.015.ttf +0 -0
- simcord-2.2.0/src/simcord/preview/static/fonts/noto-sans-latin-v2.015.ttf +0 -0
- simcord-2.2.0/src/simcord/preview/static/fonts/noto-sans-mono-v2.014.ttf +0 -0
- simcord-2.2.0/src/simcord/preview/static/fonts/noto-sans-sc-v2.004.ttf +0 -0
- simcord-2.2.0/src/simcord/preview/static/index.html +27 -0
- simcord-2.2.0/src/simcord/preview/static/preview.css +993 -0
- simcord-2.2.0/src/simcord/types.py +17 -0
- simcord-2.2.0/tests/fixtures/preview/__init__.py +1 -0
- simcord-2.2.0/tests/fixtures/preview/catalog.py +483 -0
- simcord-2.2.0/tests/fixtures/preview/coverage.json +9596 -0
- simcord-2.2.0/tests/fixtures/preview/measurements.json +346 -0
- simcord-2.2.0/tests/fixtures/preview/profiles.json +87 -0
- simcord-2.2.0/tests/fixtures/preview/states.json +137 -0
- simcord-2.2.0/tests/integration/preview_helpers.py +122 -0
- {simcord-2.0.2 → simcord-2.2.0}/tests/integration/test_app_commands.py +7 -0
- {simcord-2.0.2 → simcord-2.2.0}/tests/integration/test_bot_users_and_webhooks.py +3 -0
- {simcord-2.0.2 → simcord-2.2.0}/tests/integration/test_diagnostics.py +12 -3
- {simcord-2.0.2 → simcord-2.2.0}/tests/integration/test_lifecycle.py +37 -0
- {simcord-2.0.2 → simcord-2.2.0}/tests/integration/test_misc_branches.py +1 -1
- {simcord-2.0.2 → simcord-2.2.0}/tests/integration/test_moderation.py +23 -0
- simcord-2.2.0/tests/integration/test_preview.py +620 -0
- simcord-2.2.0/tests/integration/test_preview_additional.py +673 -0
- simcord-2.2.0/tests/integration/test_preview_capture.py +350 -0
- simcord-2.2.0/tests/integration/test_preview_capture_dx.py +296 -0
- simcord-2.2.0/tests/integration/test_preview_coverage.py +327 -0
- simcord-2.2.0/tests/integration/test_preview_dx.py +177 -0
- simcord-2.2.0/tests/integration/test_preview_edges.py +445 -0
- simcord-2.2.0/tests/integration/test_preview_fixes.py +638 -0
- simcord-2.2.0/tests/integration/test_preview_fonts.py +143 -0
- simcord-2.2.0/tests/integration/test_preview_security.py +208 -0
- {simcord-2.0.2 → simcord-2.2.0}/tests/integration/test_settlement_contract.py +273 -0
- simcord-2.2.0/tests/integration/test_time.py +189 -0
- simcord-2.2.0/tests/unit/test_dpy_internals.py +543 -0
- simcord-2.2.0/tests/unit/test_lazy_import.py +33 -0
- simcord-2.2.0/tests/unit/test_router.py +97 -0
- {simcord-2.0.2 → simcord-2.2.0}/uv.lock +402 -2
- simcord-2.0.2/.github/workflows/ci.yml +0 -59
- simcord-2.0.2/examples/README.md +0 -29
- simcord-2.0.2/tests/integration/test_time.py +0 -38
- simcord-2.0.2/tests/unit/test_router.py +0 -39
- {simcord-2.0.2 → simcord-2.2.0}/.github/ISSUE_TEMPLATE/bug.md +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/.github/ISSUE_TEMPLATE/feature.md +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/.github/ISSUE_TEMPLATE/parity-gap.md +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/.github/dependabot.yml +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/.github/workflows/dpy-master.yml +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/.pre-commit-config.yaml +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/.python-version +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/.readthedocs.yaml +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/CODE_OF_CONDUCT.md +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/LICENSE +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/SECURITY.md +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/benchmarks/_helpers.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/benchmarks/test_dispatch_benchmarks.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/benchmarks/test_perf_guards.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/changes/README.md +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/docs/assets/simcord-terminal-demo.gif +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/docs/assets/simcord-transparent.webp +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/docs/assets/social-card.png +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/docs/changelog.md +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/docs/guides/fixtures.md +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/docs/guides/intents.md +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/docs/guides/interactions.md +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/docs/guides/messages.md +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/docs/guides/mocks-vs-simulation.md +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/docs/guides/moderation-and-guild.md +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/docs/guides/permissions.md +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/docs/guides/test-without-token.md +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/docs/guides/testing-discord-py-bots.md +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/docs/guides/testing-slash-commands.md +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/docs/guides/threads-reactions-dms.md +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/docs/performance.md +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/docs/robots.txt +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/examples/__init__.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/examples/conftest.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/overrides/main.html +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/src/simcord/asserts.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/src/simcord/backend/__init__.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/src/simcord/backend/cdn.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/src/simcord/backend/errors.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/src/simcord/backend/models/__init__.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/src/simcord/backend/models/auditlog.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/src/simcord/backend/models/automod.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/src/simcord/backend/models/channel.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/src/simcord/backend/models/expression.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/src/simcord/backend/models/guild.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/src/simcord/backend/models/invite.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/src/simcord/backend/models/member.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/src/simcord/backend/models/message.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/src/simcord/backend/models/role.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/src/simcord/backend/models/scheduled_event.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/src/simcord/backend/models/stage_instance.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/src/simcord/backend/models/voice.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/src/simcord/backend/models/webhook.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/src/simcord/backend/ops/__init__.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/src/simcord/backend/ops/audit.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/src/simcord/backend/ops/channels.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/src/simcord/backend/ops/commands.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/src/simcord/backend/ops/expressions.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/src/simcord/backend/ops/guilds.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/src/simcord/backend/ops/invites.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/src/simcord/backend/ops/messages.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/src/simcord/backend/ops/permissions.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/src/simcord/backend/ops/polls.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/src/simcord/backend/ops/reactions.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/src/simcord/backend/ops/scheduled_events.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/src/simcord/backend/ops/voice.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/src/simcord/backend/ops/webhooks.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/src/simcord/backend/permissions.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/src/simcord/backend/state.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/src/simcord/components.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/src/simcord/enums.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/src/simcord/gateway.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/src/simcord/http/__init__.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/src/simcord/http/_helpers.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/src/simcord/http/client.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/src/simcord/http/routes/__init__.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/src/simcord/http/routes/application.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/src/simcord/http/routes/auditlogs.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/src/simcord/http/routes/automod.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/src/simcord/http/routes/channels.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/src/simcord/http/routes/commands.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/src/simcord/http/routes/expressions.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/src/simcord/http/routes/guilds.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/src/simcord/http/routes/interactions.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/src/simcord/http/routes/invites.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/src/simcord/http/routes/messages.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/src/simcord/http/routes/reactions.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/src/simcord/http/routes/scheduled_events.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/src/simcord/http/routes/stage_instances.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/src/simcord/intents.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/src/simcord/interactions.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/src/simcord/parity.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/src/simcord/py.typed +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/src/simcord/pytest_plugin.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/src/simcord/results.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/tests/conformance/_world.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/tests/conformance/test_field_honesty.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/tests/conformance/test_field_honesty_fuzz.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/tests/conformance/test_route_coverage.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/tests/conformance/test_serializers.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/tests/conftest.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/tests/fixtures/__init__.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/tests/fixtures/sample_bot/__init__.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/tests/fixtures/sample_bot/events.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/tests/fixtures/sample_bot/general.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/tests/fixtures/sample_bot/interactions.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/tests/fixtures/sample_bot/moderation.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/tests/integration/test_application_emojis.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/tests/integration/test_asserts.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/tests/integration/test_audit_logs.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/tests/integration/test_automod.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/tests/integration/test_channels.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/tests/integration/test_command_permissions.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/tests/integration/test_common_bot_sweep.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/tests/integration/test_components_v2_interactions.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/tests/integration/test_components_v2_messages.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/tests/integration/test_coverage_sweep.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/tests/integration/test_edge_cases.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/tests/integration/test_error_capture_and_edits.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/tests/integration/test_expressions.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/tests/integration/test_forum.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/tests/integration/test_gateway_and_automod_exempt.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/tests/integration/test_guilds.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/tests/integration/test_intents.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/tests/integration/test_interaction_followups.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/tests/integration/test_interactions.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/tests/integration/test_invites.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/tests/integration/test_members.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/tests/integration/test_message_routes.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/tests/integration/test_messages.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/tests/integration/test_polls.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/tests/integration/test_reactions.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/tests/integration/test_roles_and_ordering.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/tests/integration/test_scheduled_events.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/tests/integration/test_setup_hook_suspension.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/tests/integration/test_sharding.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/tests/integration/test_stage_instances.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/tests/integration/test_threads.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/tests/integration/test_transport_edges.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/tests/integration/test_voice_state.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/tests/integration/test_world_helpers.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/tests/unit/test_assert_helpers.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/tests/unit/test_backend_lookup_errors.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/tests/unit/test_components_v2_validation.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/tests/unit/test_errors.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/tests/unit/test_intent_rules.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/tests/unit/test_parity.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/tests/unit/test_parity_tooling.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/tests/unit/test_permission_validators.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/tests/unit/test_permissions.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/tests/unit/test_pytest_plugin.py +0 -0
- {simcord-2.0.2 → simcord-2.2.0}/tests/unit/test_version.py +0 -0
|
@@ -32,54 +32,12 @@ jobs:
|
|
|
32
32
|
print(f"Releasing simcord {project_version}")
|
|
33
33
|
PY
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
needs: version
|
|
37
|
-
|
|
38
|
-
steps:
|
|
39
|
-
- uses: actions/checkout@v7
|
|
40
|
-
- uses: astral-sh/setup-uv@v7
|
|
41
|
-
- run: uv sync --extra dev
|
|
42
|
-
- run: uv run ruff check src tests examples benchmarks scripts
|
|
43
|
-
- run: uv run ruff format --check src tests examples benchmarks scripts
|
|
44
|
-
- run: uv run pyright src
|
|
45
|
-
|
|
46
|
-
test:
|
|
47
|
-
needs: version
|
|
48
|
-
runs-on: ubuntu-latest
|
|
49
|
-
strategy:
|
|
50
|
-
fail-fast: false
|
|
51
|
-
matrix:
|
|
52
|
-
python-version: ["3.11", "3.12", "3.13", "3.14"]
|
|
53
|
-
steps:
|
|
54
|
-
- uses: actions/checkout@v7
|
|
55
|
-
- uses: astral-sh/setup-uv@v7
|
|
56
|
-
with:
|
|
57
|
-
python-version: ${{ matrix.python-version }}
|
|
58
|
-
- run: uv sync --extra dev
|
|
59
|
-
- run: uv run coverage run -m pytest tests examples -q
|
|
60
|
-
- run: uv run coverage report
|
|
61
|
-
|
|
62
|
-
docs:
|
|
63
|
-
needs: version
|
|
64
|
-
runs-on: ubuntu-latest
|
|
65
|
-
steps:
|
|
66
|
-
- uses: actions/checkout@v7
|
|
67
|
-
- uses: astral-sh/setup-uv@v7
|
|
68
|
-
- run: uv sync --extra docs
|
|
69
|
-
- run: uv run mkdocs build --strict
|
|
70
|
-
- run: uv run python scripts/check_discoverability.py site
|
|
71
|
-
|
|
72
|
-
benchmark:
|
|
73
|
-
needs: version
|
|
74
|
-
runs-on: ubuntu-latest
|
|
75
|
-
steps:
|
|
76
|
-
- uses: actions/checkout@v7
|
|
77
|
-
- uses: astral-sh/setup-uv@v7
|
|
78
|
-
- run: uv sync --extra bench
|
|
79
|
-
- run: uv run pytest benchmarks
|
|
35
|
+
verify:
|
|
36
|
+
needs: [version]
|
|
37
|
+
uses: ./.github/workflows/verify.yml
|
|
80
38
|
|
|
81
39
|
build:
|
|
82
|
-
needs: [version,
|
|
40
|
+
needs: [version, verify]
|
|
83
41
|
runs-on: ubuntu-latest
|
|
84
42
|
steps:
|
|
85
43
|
- uses: actions/checkout@v7
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
name: Verify
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_call:
|
|
5
|
+
|
|
6
|
+
permissions:
|
|
7
|
+
contents: read
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
lint:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v7
|
|
14
|
+
- uses: astral-sh/setup-uv@v7
|
|
15
|
+
# screenshot extra pulls in playwright/PIL/markdown-it so pyright resolves
|
|
16
|
+
# the lazy optional imports in simcord.preview with real type info.
|
|
17
|
+
- run: uv sync --extra dev --extra screenshot
|
|
18
|
+
- run: uv run ruff check src tests examples benchmarks scripts
|
|
19
|
+
- run: uv run ruff format --check src tests examples benchmarks scripts
|
|
20
|
+
- run: uv run pyright src
|
|
21
|
+
|
|
22
|
+
test:
|
|
23
|
+
runs-on: ubuntu-latest
|
|
24
|
+
strategy:
|
|
25
|
+
fail-fast: false
|
|
26
|
+
matrix:
|
|
27
|
+
python-version: ["3.11", "3.12", "3.13", "3.14"]
|
|
28
|
+
steps:
|
|
29
|
+
- uses: actions/checkout@v7
|
|
30
|
+
- uses: astral-sh/setup-uv@v7
|
|
31
|
+
with:
|
|
32
|
+
python-version: ${{ matrix.python-version }}
|
|
33
|
+
# preview extra adds markdown-it + Pillow so non-browser preview tests run;
|
|
34
|
+
# playwright stays absent so capture tests skip via importorskip.
|
|
35
|
+
- run: uv sync --extra dev --extra preview
|
|
36
|
+
- run: uv run coverage run -m pytest tests examples -q
|
|
37
|
+
- run: uv run coverage report
|
|
38
|
+
|
|
39
|
+
docs:
|
|
40
|
+
runs-on: ubuntu-latest
|
|
41
|
+
steps:
|
|
42
|
+
- uses: actions/checkout@v7
|
|
43
|
+
- uses: astral-sh/setup-uv@v7
|
|
44
|
+
- run: uv sync --extra docs
|
|
45
|
+
- run: uv run mkdocs build --strict
|
|
46
|
+
- run: uv run python scripts/check_discoverability.py site
|
|
47
|
+
|
|
48
|
+
preview-package:
|
|
49
|
+
name: Preview package + pinned Linux Chromium
|
|
50
|
+
runs-on: ubuntu-22.04
|
|
51
|
+
timeout-minutes: 20
|
|
52
|
+
steps:
|
|
53
|
+
- uses: actions/checkout@v7
|
|
54
|
+
- uses: astral-sh/setup-uv@v7
|
|
55
|
+
with:
|
|
56
|
+
python-version: "3.12"
|
|
57
|
+
- run: uv sync --extra screenshot --extra pytest
|
|
58
|
+
- run: uv run python -m playwright install --with-deps chromium
|
|
59
|
+
- run: uv run pytest tests/integration/test_preview*.py -q
|
|
60
|
+
- run: uv build
|
|
61
|
+
- run: uv run python scripts/check_package.py dist
|
|
62
|
+
- name: Install the wheel outside the checkout
|
|
63
|
+
shell: bash
|
|
64
|
+
run: |
|
|
65
|
+
set -euo pipefail
|
|
66
|
+
tmp="$(mktemp -d)"
|
|
67
|
+
mkdir -p package-smoke-artifacts
|
|
68
|
+
uv venv --python 3.12 "$tmp/venv"
|
|
69
|
+
wheel="$(printf '%s\n' "$GITHUB_WORKSPACE"/dist/*.whl)"
|
|
70
|
+
uv pip install --python "$tmp/venv/bin/python" \
|
|
71
|
+
"simcord[screenshot] @ file://$wheel"
|
|
72
|
+
"$tmp/venv/bin/python" -m playwright install --with-deps chromium
|
|
73
|
+
cp examples/bot.py examples/preview_example.py "$tmp/"
|
|
74
|
+
(
|
|
75
|
+
cd "$tmp"
|
|
76
|
+
"$tmp/venv/bin/python" preview_example.py > "$GITHUB_WORKSPACE/package-smoke-artifacts/preview-report.json"
|
|
77
|
+
)
|
|
78
|
+
cp "$tmp/preview-example.png" package-smoke-artifacts/
|
|
79
|
+
- uses: actions/upload-artifact@v7
|
|
80
|
+
if: failure()
|
|
81
|
+
with:
|
|
82
|
+
name: preview-package-failure
|
|
83
|
+
path: package-smoke-artifacts/
|
|
84
|
+
if-no-files-found: ignore
|
|
85
|
+
|
|
86
|
+
benchmark:
|
|
87
|
+
runs-on: ubuntu-latest
|
|
88
|
+
steps:
|
|
89
|
+
- uses: actions/checkout@v7
|
|
90
|
+
- uses: astral-sh/setup-uv@v7
|
|
91
|
+
# The perf guards (absolute budget + same-run scaling ratio) fail the build
|
|
92
|
+
# on a catastrophic regression; the JSON is uploaded as a non-gating trend.
|
|
93
|
+
- run: uv sync --extra bench
|
|
94
|
+
- run: uv run pytest benchmarks --benchmark-json=benchmark.json
|
|
95
|
+
- uses: actions/upload-artifact@v7
|
|
96
|
+
if: always()
|
|
97
|
+
with:
|
|
98
|
+
name: benchmark-results
|
|
99
|
+
path: benchmark.json
|
|
@@ -4,6 +4,38 @@ This changelog is generated with [towncrier](https://towncrier.readthedocs.io/).
|
|
|
4
4
|
|
|
5
5
|
<!-- towncrier release notes start -->
|
|
6
6
|
|
|
7
|
+
## 2.2.0 (2026-09-23)
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
- Add an optional local component preview with real callback interactions, authorized viewer contexts, offline media assets, and deterministic Playwright screenshot reports. The base package remains lazy and networkless; install `simcord[preview]` for the bridge or `simcord[screenshot]` plus Chromium for capture.
|
|
12
|
+
- Breaking in SimCord 2.2: remove `Env.http_log`; use `Env.http_requests` and its `HttpLogEntry` records instead.
|
|
13
|
+
- Extend the local component preview: `env.preview(port=...)` pins the loopback port for stable capability URLs and pre-created SSH forwards, `await preview.snapshot()` returns the detached JSON projection as the structured/agent read surface, `screenshot(path=None)` captures in memory with PNG bytes on `PreviewCapture.png`, `window.simcordPreview` exposes `viewerId`/`targetId`, localhost origins are accepted, and internal bridge methods are now private by default.
|
|
14
|
+
- Hardened the local preview with deterministic presentation diagnostics, generation-scoped media readiness, modal focus isolation, safe page release, and strict action-envelope validation.
|
|
15
|
+
- Preview snapshots now publish protocol 2: `messageIndex` is picker-only summary data, `messages` maps authorized IDs to full projections, and `targetId` replaces `selected`. Component and modal controls use scoped stable control keys, mutating browser actions carry an explicit target and published revision, and `presentation_time` is deterministic and timezone-aware. Snapshot consumers must migrate; protocol 1 keys are not supported.
|
|
16
|
+
- Settlement now recognizes intentional timer-bounded waits — store-registered View/Modal expiry timers, `wait_for` timeouts on listener waits, `discord.ext.tasks` loop intervals, and wake-up timers scheduled inside `env.external_wait` — and makes them virtual-only: they fire only via `env.advance_time()` or their awaited input, never the wall clock, so they no longer burn real settle time or expire mid-operation. Migration: tests that relied on a View expiring on the wall clock without `advance_time` must now call `await env.advance_time(<timeout>)`.
|
|
17
|
+
|
|
18
|
+
### Documentation
|
|
19
|
+
|
|
20
|
+
- Improve the preview demo onboarding: `examples/preview_example.py` gains a `--keep-open` flag
|
|
21
|
+
that keeps the session live for clicking (it calls `preview.wait_closed()`), the URL and
|
|
22
|
+
human-facing notes move to stderr so stdout carries only the JSON capture report, the
|
|
23
|
+
quickstart's next-steps list links to the preview guide, and CONTRIBUTING documents which
|
|
24
|
+
optional extras unlock the preview tests, example, and type-checking.
|
|
25
|
+
|
|
26
|
+
### Miscellaneous
|
|
27
|
+
|
|
28
|
+
- Removed redundant code in the preview snapshot projection.
|
|
29
|
+
- The local component preview renders the Discord dark theme only: the `theme=` option and the browser theme toggle were removed. `profile.theme` still reports `"dark"` in snapshots and capture reports.
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
## 2.1.0 (2026-09-19)
|
|
33
|
+
|
|
34
|
+
### Features
|
|
35
|
+
|
|
36
|
+
- 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.
|
|
37
|
+
|
|
38
|
+
|
|
7
39
|
## 2.0.2 (2026-09-18)
|
|
8
40
|
|
|
9
41
|
### Bug fixes
|
|
@@ -19,6 +19,18 @@ uv run pyright src
|
|
|
19
19
|
The pre-commit hooks mirror the CI `lint` job (ruff on commit, pyright on push)
|
|
20
20
|
using the project's own pinned tools, so a clean commit means a green CI lint.
|
|
21
21
|
|
|
22
|
+
Optional extras layer on top of `dev` depending on what you work on:
|
|
23
|
+
|
|
24
|
+
| Extra | Unlocks |
|
|
25
|
+
| --- | --- |
|
|
26
|
+
| `preview` | `markdown-it-py` + `Pillow`, so the non-browser preview tests run; browser capture tests still skip via `importorskip`. |
|
|
27
|
+
| `screenshot` | Playwright as well — the full preview suite, `examples/preview_example.py`, and real types for pyright's check of the lazy optional imports. |
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
uv sync --extra dev --extra screenshot # matches the CI lint job
|
|
31
|
+
uv run playwright install --with-deps chromium # needed once for browser capture tests + the example
|
|
32
|
+
```
|
|
33
|
+
|
|
22
34
|
## Project layout
|
|
23
35
|
|
|
24
36
|
- `src/simcord/backend/`: the virtual Discord with dataclass models, wire-format
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: simcord
|
|
3
|
-
Version: 2.0
|
|
3
|
+
Version: 2.2.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/
|
|
@@ -30,6 +30,7 @@ Requires-Dist: pytest>=8; extra == 'bench'
|
|
|
30
30
|
Provides-Extra: dev
|
|
31
31
|
Requires-Dist: coverage[toml]>=7; extra == 'dev'
|
|
32
32
|
Requires-Dist: hypothesis>=6; extra == 'dev'
|
|
33
|
+
Requires-Dist: jsonschema>=4; extra == 'dev'
|
|
33
34
|
Requires-Dist: pre-commit>=3; extra == 'dev'
|
|
34
35
|
Requires-Dist: pyright>=1.1; extra == 'dev'
|
|
35
36
|
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
|
|
@@ -40,9 +41,19 @@ Requires-Dist: towncrier>=24; extra == 'dev'
|
|
|
40
41
|
Provides-Extra: docs
|
|
41
42
|
Requires-Dist: mkdocs-material>=9; extra == 'docs'
|
|
42
43
|
Requires-Dist: mkdocstrings[python]>=0.26; extra == 'docs'
|
|
44
|
+
Provides-Extra: preview
|
|
45
|
+
Requires-Dist: aiohttp>=3.9; extra == 'preview'
|
|
46
|
+
Requires-Dist: markdown-it-py>=3; extra == 'preview'
|
|
47
|
+
Requires-Dist: pillow>=10; extra == 'preview'
|
|
43
48
|
Provides-Extra: pytest
|
|
49
|
+
Requires-Dist: jsonschema>=4; extra == 'pytest'
|
|
44
50
|
Requires-Dist: pytest-asyncio>=0.24; extra == 'pytest'
|
|
45
51
|
Requires-Dist: pytest>=8; extra == 'pytest'
|
|
52
|
+
Provides-Extra: screenshot
|
|
53
|
+
Requires-Dist: aiohttp>=3.9; extra == 'screenshot'
|
|
54
|
+
Requires-Dist: markdown-it-py>=3; extra == 'screenshot'
|
|
55
|
+
Requires-Dist: pillow>=10; extra == 'screenshot'
|
|
56
|
+
Requires-Dist: playwright>=1.45; extra == 'screenshot'
|
|
46
57
|
Description-Content-Type: text/markdown
|
|
47
58
|
|
|
48
59
|
<div align="center">
|
|
@@ -116,9 +127,14 @@ uv add --dev "simcord[pytest]"
|
|
|
116
127
|
```
|
|
117
128
|
|
|
118
129
|
Requires **Python >=3.11** (tested on **3.11–3.14**) and **discord.py >=2.7.1,<3**.
|
|
119
|
-
The locked CI matrix tests discord.py 2.7.1; a separate weekly workflow
|
|
120
|
-
|
|
121
|
-
|
|
130
|
+
The locked CI matrix tests discord.py 2.7.1; a separate weekly workflow checks upstream
|
|
131
|
+
`master`, rather than continuously testing every released 2.x version. The base simulation
|
|
132
|
+
adds no runtime dependency beyond discord.py.
|
|
133
|
+
|
|
134
|
+
For an authenticated local browser page showing real component callbacks, install
|
|
135
|
+
`simcord[preview]`; for deterministic PNGs also run `playwright install chromium` after installing
|
|
136
|
+
`simcord[screenshot]`. See the [component preview and screenshots guide](https://simcord.readthedocs.io/en/latest/guides/preview/)
|
|
137
|
+
for lifecycle, access, limits, SSH forwarding, diagnostics, and fidelity boundaries.
|
|
122
138
|
|
|
123
139
|
## Quickstart
|
|
124
140
|
|
|
@@ -228,6 +244,7 @@ See [Core concepts](https://simcord.readthedocs.io/en/latest/concepts/) for the
|
|
|
228
244
|
| **Polls** | `vote`, `remove_vote` | Poll answers and results. |
|
|
229
245
|
| **Voice & events** | `join_voice`, `leave_voice`, `set_voice`, `subscribe_event` | Voice state, scheduled-event subscriptions. |
|
|
230
246
|
| **DMs** | `send_dm` | Direct-message channels and flows. |
|
|
247
|
+
| **Browser preview** | `env.preview`, `preview.refresh`, `preview.screenshot` | Loopback component pages, real callbacks, authorized viewers, explicit refresh, and immutable capture diagnostics. |
|
|
231
248
|
|
|
232
249
|
Responses come back as a rich [`InteractionResult`](https://simcord.readthedocs.io/en/latest/api/)
|
|
233
250
|
exposing `acknowledged`, `deferred`, `ephemeral`, `response`, `followups` and `modal`. Threads,
|
|
@@ -263,7 +280,11 @@ When something goes wrong, the `env` tells you what happened:
|
|
|
263
280
|
|
|
264
281
|
- `env.transcript()`: the ordered log of gateway events and REST calls, auto-attached to
|
|
265
282
|
failing pytest tests.
|
|
266
|
-
- `env.
|
|
283
|
+
- `env.http_requests`: structured records for every attempted REST request (`method`, `path`,
|
|
284
|
+
`params`, `json`, `reason`). Values are discord.py transport arguments, not wire-normalized
|
|
285
|
+
strings or headers; uploaded files are not captured. JSON bodies retain their supplied
|
|
286
|
+
JSON-compatible shape, including top-level arrays. `params` and `json` are detached deep
|
|
287
|
+
snapshots, so later source-container mutations do not change a record.
|
|
267
288
|
- `env.errors`: exceptions the bot swallowed.
|
|
268
289
|
- `env.inject_error("POST", "/channels/*/messages", status=500)`: make matching REST calls
|
|
269
290
|
fail, to test your bot's error handling.
|
|
@@ -313,6 +334,7 @@ More in the [architecture docs](https://simcord.readthedocs.io/en/latest/archite
|
|
|
313
334
|
| 🍳 [Recipes](https://simcord.readthedocs.io/en/latest/cookbook/) | Copy-paste patterns for common cases. |
|
|
314
335
|
| 📋 [Parity matrix](https://simcord.readthedocs.io/en/latest/parity-matrix/) | Exactly what's implemented. |
|
|
315
336
|
| 🔖 [API reference](https://simcord.readthedocs.io/en/latest/api/) | Every public object and verb. |
|
|
337
|
+
| 🖥️ [Component preview](https://simcord.readthedocs.io/en/latest/guides/preview/) | Real browser callbacks, page-local viewers, offline assets, and deterministic screenshot reports. |
|
|
316
338
|
|
|
317
339
|
## Contributing
|
|
318
340
|
|
|
@@ -69,9 +69,14 @@ uv add --dev "simcord[pytest]"
|
|
|
69
69
|
```
|
|
70
70
|
|
|
71
71
|
Requires **Python >=3.11** (tested on **3.11–3.14**) and **discord.py >=2.7.1,<3**.
|
|
72
|
-
The locked CI matrix tests discord.py 2.7.1; a separate weekly workflow
|
|
73
|
-
|
|
74
|
-
|
|
72
|
+
The locked CI matrix tests discord.py 2.7.1; a separate weekly workflow checks upstream
|
|
73
|
+
`master`, rather than continuously testing every released 2.x version. The base simulation
|
|
74
|
+
adds no runtime dependency beyond discord.py.
|
|
75
|
+
|
|
76
|
+
For an authenticated local browser page showing real component callbacks, install
|
|
77
|
+
`simcord[preview]`; for deterministic PNGs also run `playwright install chromium` after installing
|
|
78
|
+
`simcord[screenshot]`. See the [component preview and screenshots guide](https://simcord.readthedocs.io/en/latest/guides/preview/)
|
|
79
|
+
for lifecycle, access, limits, SSH forwarding, diagnostics, and fidelity boundaries.
|
|
75
80
|
|
|
76
81
|
## Quickstart
|
|
77
82
|
|
|
@@ -181,6 +186,7 @@ See [Core concepts](https://simcord.readthedocs.io/en/latest/concepts/) for the
|
|
|
181
186
|
| **Polls** | `vote`, `remove_vote` | Poll answers and results. |
|
|
182
187
|
| **Voice & events** | `join_voice`, `leave_voice`, `set_voice`, `subscribe_event` | Voice state, scheduled-event subscriptions. |
|
|
183
188
|
| **DMs** | `send_dm` | Direct-message channels and flows. |
|
|
189
|
+
| **Browser preview** | `env.preview`, `preview.refresh`, `preview.screenshot` | Loopback component pages, real callbacks, authorized viewers, explicit refresh, and immutable capture diagnostics. |
|
|
184
190
|
|
|
185
191
|
Responses come back as a rich [`InteractionResult`](https://simcord.readthedocs.io/en/latest/api/)
|
|
186
192
|
exposing `acknowledged`, `deferred`, `ephemeral`, `response`, `followups` and `modal`. Threads,
|
|
@@ -216,7 +222,11 @@ When something goes wrong, the `env` tells you what happened:
|
|
|
216
222
|
|
|
217
223
|
- `env.transcript()`: the ordered log of gateway events and REST calls, auto-attached to
|
|
218
224
|
failing pytest tests.
|
|
219
|
-
- `env.
|
|
225
|
+
- `env.http_requests`: structured records for every attempted REST request (`method`, `path`,
|
|
226
|
+
`params`, `json`, `reason`). Values are discord.py transport arguments, not wire-normalized
|
|
227
|
+
strings or headers; uploaded files are not captured. JSON bodies retain their supplied
|
|
228
|
+
JSON-compatible shape, including top-level arrays. `params` and `json` are detached deep
|
|
229
|
+
snapshots, so later source-container mutations do not change a record.
|
|
220
230
|
- `env.errors`: exceptions the bot swallowed.
|
|
221
231
|
- `env.inject_error("POST", "/channels/*/messages", status=500)`: make matching REST calls
|
|
222
232
|
fail, to test your bot's error handling.
|
|
@@ -266,6 +276,7 @@ More in the [architecture docs](https://simcord.readthedocs.io/en/latest/archite
|
|
|
266
276
|
| 🍳 [Recipes](https://simcord.readthedocs.io/en/latest/cookbook/) | Copy-paste patterns for common cases. |
|
|
267
277
|
| 📋 [Parity matrix](https://simcord.readthedocs.io/en/latest/parity-matrix/) | Exactly what's implemented. |
|
|
268
278
|
| 🔖 [API reference](https://simcord.readthedocs.io/en/latest/api/) | Every public object and verb. |
|
|
279
|
+
| 🖥️ [Component preview](https://simcord.readthedocs.io/en/latest/guides/preview/) | Real browser callbacks, page-local viewers, offline assets, and deterministic screenshot reports. |
|
|
269
280
|
|
|
270
281
|
## Contributing
|
|
271
282
|
|
|
@@ -26,6 +26,17 @@ 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. The `json` field preserves the supplied
|
|
37
|
+
JSON-compatible shape, including top-level arrays. `params` and `json` are detached deep
|
|
38
|
+
snapshots, so later mutations to source containers do not alter a record.
|
|
39
|
+
|
|
29
40
|
## Builders
|
|
30
41
|
|
|
31
42
|
Synchronous, omnipotent handles for arranging the virtual Discord. Returned by `env`/`guild`
|
|
@@ -46,6 +57,30 @@ The simulated human that drives your bot. Created by `guild.add_member(...)`. Se
|
|
|
46
57
|
|
|
47
58
|
::: simcord.MemberActor
|
|
48
59
|
|
|
60
|
+
## Local preview
|
|
61
|
+
|
|
62
|
+
The optional [`Env.preview`](guides/preview.md#start-and-stop-a-session) context manager serves
|
|
63
|
+
viewer-authorized messages and real component callbacks from a loopback browser. It is not a
|
|
64
|
+
Discord connection. Install `simcord[preview]` for the bridge, or
|
|
65
|
+
`simcord[screenshot]` and `playwright install --with-deps chromium` for managed PNG capture.
|
|
66
|
+
`port=` pins the loopback origin so a pre-created forward can use the same origin on both ends;
|
|
67
|
+
it never makes the capability-bearing URL stable or safe to log. Keep `preview.url` secret.
|
|
68
|
+
|
|
69
|
+
::: simcord.preview.Preview
|
|
70
|
+
|
|
71
|
+
::: simcord.preview.Preview.screenshot
|
|
72
|
+
options:
|
|
73
|
+
heading_level: 4
|
|
74
|
+
|
|
75
|
+
::: simcord.preview.PreviewCapture
|
|
76
|
+
|
|
77
|
+
`Preview.snapshot()` returns the detached JSON projection the bundled page renders — the structured
|
|
78
|
+
read surface for non-visual checks and text-only tooling. `PreviewCapture` is an immutable report.
|
|
79
|
+
Its `ready`, `complete`, and `calibrated` fields are independent; inspect `diagnostics`, `action`,
|
|
80
|
+
`profile`, and `geometry` rather than inferring success from a PNG path. When `screenshot()` is
|
|
81
|
+
called with `path=None`, `PreviewCapture.path` is `None` and `PreviewCapture.png` carries the PNG
|
|
82
|
+
bytes in memory. Internal `/api/*` payloads and DOM/CSS names are not extension APIs.
|
|
83
|
+
|
|
49
84
|
## Results
|
|
50
85
|
|
|
51
86
|
Returned by the interaction verbs (`slash`, `context_menu`, `click`, `select`,
|
|
@@ -63,6 +63,21 @@ test ──► builders/actors ──► virtual backend (single source of truth
|
|
|
63
63
|
discord.py's master branch to catch drift early.
|
|
64
64
|
- **Deterministic snowflakes** with valid embedded timestamps, from a fixed virtual epoch.
|
|
65
65
|
|
|
66
|
+
## Optional browser presentation
|
|
67
|
+
|
|
68
|
+
`Env.preview(...)` adds a local presentation layer above the same backend; it is not another
|
|
69
|
+
Discord transport. An `aiohttp` server binds to `127.0.0.1` only, serves packaged ES modules/CSS,
|
|
70
|
+
and exposes viewer-filtered snapshots and closed action envelopes. A random capability in the URL
|
|
71
|
+
fragment plus page context headers authorize reads and real actor callbacks. Python owns the
|
|
72
|
+
session and default presentation; each browser page and managed screenshot has an independent
|
|
73
|
+
viewer/focus/generation, so one tab cannot retarget another.
|
|
74
|
+
|
|
75
|
+
Preview polling is explicit and revision-based rather than websocket-driven. `refresh()` settles
|
|
76
|
+
the environment and republishes current projections; it does not make background mutations live.
|
|
77
|
+
Optional Pillow media work is bounded and outside Env's loop. Optional Playwright capture pins a
|
|
78
|
+
settled projection and reports geometry, readiness, completeness, calibration, and diagnostics.
|
|
79
|
+
Base imports do not allocate any preview server, worker, or browser.
|
|
80
|
+
|
|
66
81
|
## What it will never do
|
|
67
82
|
|
|
68
83
|
Connect to Discord. There is no "integration mode"; automating a real client violates
|
|
@@ -23,8 +23,7 @@ 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
|
-
A fresh `Env` is created for each test, so tests never leak state into each other.
|
|
26
|
+
holds the virtual clock, and exposes the diagnostics (`env.http_requests`, `env.transcript()`).
|
|
28
27
|
|
|
29
28
|
!!! info "One environment per event loop"
|
|
30
29
|
`Env` monkeypatches `loop.create_task` and `time.monotonic` while it's live so it can
|
|
@@ -95,10 +94,16 @@ assert channel.last_message.content == "Pong!" # the reply is already here
|
|
|
95
94
|
```
|
|
96
95
|
|
|
97
96
|
Bot-owned work stays bot-owned across parking, timeout, cancellation, later actions, startup,
|
|
98
|
-
and restart. Recognized waits — `Client.wait_for
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
97
|
+
and restart. Recognized waits — `Client.wait_for` (including its `timeout=`), View/Modal
|
|
98
|
+
completion, store-registered View/Modal expiry timers, `discord.ext.tasks` loop intervals,
|
|
99
|
+
composed `gather`/`shield`/`wait`/`TaskGroup`, explicit `env.external_wait(...)`, and verified
|
|
100
|
+
far-future sleeps — may remain. A timer whose only job is resuming a recognized wait — a
|
|
101
|
+
`wait_for` deadline on a listener, a View/Modal expiry, a loop interval, a wake-up timer
|
|
102
|
+
scheduled inside `external_wait` — is virtual: it fires only via `env.advance_time()` or the
|
|
103
|
+
awaited input, never the wall clock. Verified far-future sleeps keep their real fallback, and
|
|
104
|
+
a View sent with no dispatchable items never registers (its expiry keeps real-time behavior).
|
|
105
|
+
Unknown waits remain active and time out. Use a reasoned, scoped declaration for external
|
|
106
|
+
input:
|
|
102
107
|
|
|
103
108
|
```python
|
|
104
109
|
await env.external_wait(stop.wait(), reason="wait for the next message")
|
|
@@ -132,7 +137,7 @@ channel.history(viewer=mod) # ephemeral-aware: what `mod` would see
|
|
|
132
137
|
channel.pinned_messages() # list[discord.Message]
|
|
133
138
|
guild.get_ban(target) # ban record | None
|
|
134
139
|
env.errors # exceptions the bot swallowed
|
|
135
|
-
env.
|
|
140
|
+
env.http_requests # structured REST request records
|
|
136
141
|
```
|
|
137
142
|
|
|
138
143
|
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
|
|
|
@@ -265,6 +269,27 @@ async def test_reaction_role(simcord_env):
|
|
|
265
269
|
assert any(r.name == "Gamer" for r in alice.member.roles)
|
|
266
270
|
```
|
|
267
271
|
|
|
272
|
+
## Preview a component message and capture a screenshot
|
|
273
|
+
|
|
274
|
+
```python
|
|
275
|
+
async def test_panel_preview(simcord_env):
|
|
276
|
+
channel = simcord_env.create_guild().create_text_channel("roles")
|
|
277
|
+
alice = simcord_env.guild.add_member(simcord_env.create_user("alice"))
|
|
278
|
+
await alice.slash(channel, "panel")
|
|
279
|
+
|
|
280
|
+
async with simcord_env.preview(channel, viewers=[alice]) as preview:
|
|
281
|
+
capture = await preview.screenshot("panel.png")
|
|
282
|
+
assert capture.complete and capture.ready
|
|
283
|
+
snapshot = await preview.snapshot()
|
|
284
|
+
target = snapshot["messages"].get(snapshot["targetId"])
|
|
285
|
+
assert target is not None
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
Reach for a preview when assertions alone aren't enough: the screenshot verifies what a user would
|
|
289
|
+
actually see, and `snapshot()` returns the structured projection for agent or text-only checks. It
|
|
290
|
+
requires the `simcord[preview]` extra (plus `simcord[screenshot]` and Chromium for `screenshot()`);
|
|
291
|
+
see the [preview guide](guides/preview.md) for lifecycle, viewer access, and resource limits.
|
|
292
|
+
|
|
268
293
|
## See also
|
|
269
294
|
|
|
270
295
|
- [Messages](guides/messages.md) · [Slash commands](guides/interactions.md) ·
|
|
@@ -27,6 +27,14 @@ No. The library makes no outbound network requests and collects no usage or runt
|
|
|
27
27
|
telemetry. Package hosts and source platforms may publish their own aggregate statistics,
|
|
28
28
|
but SimCord does not transmit data from your tests.
|
|
29
29
|
|
|
30
|
+
## Does the component preview connect to Discord?
|
|
31
|
+
|
|
32
|
+
No. The optional preview uses a loopback-only `aiohttp` server and an authenticated capability;
|
|
33
|
+
it does not need a bot token or a Discord connection. Install `simcord[preview]` for the browser
|
|
34
|
+
bridge, and `simcord[screenshot]` plus `playwright install chromium` for managed screenshots.
|
|
35
|
+
The [preview guide](guides/preview.md) documents forwarding, access checks, limits, and
|
|
36
|
+
incomplete/uncalibrated captures.
|
|
37
|
+
|
|
30
38
|
## Is this a mock library?
|
|
31
39
|
|
|
32
40
|
It's more than a mock. It is a **simulator**. A mock returns canned responses; SimCord
|
|
@@ -111,6 +111,20 @@ all required repository gates before reporting completion.
|
|
|
111
111
|
- Do not test source text, call counts, or private plumbing when behavior is observable.
|
|
112
112
|
- Do not connect CI to a Discord test guild.
|
|
113
113
|
|
|
114
|
+
## Use the local component preview
|
|
115
|
+
|
|
116
|
+
When a change affects a panel, modal, or other component presentation, install
|
|
117
|
+
`simcord[preview]` and use [Component preview and screenshots](preview.md). A browser click
|
|
118
|
+
dispatches the real callback; do not replace it with a DOM mock. Use `await preview.refresh()` after
|
|
119
|
+
Python-side mutations, wait for `window.simcordPreview.ready`, and inspect `lastAction`,
|
|
120
|
+
`complete`, `calibration`, and `diagnostics` separately. Prefer `await preview.snapshot()` for
|
|
121
|
+
assertions and text-only agents: it settles, republishes, and returns the protocol-2 projection dict
|
|
122
|
+
(`messages[targetId]`, `messageIndex`, `timeline`, `diagnostics`, `lastAction`, …) without a browser.
|
|
123
|
+
Browser tooling can read `viewerId`/`targetId` on `window.simcordPreview` alongside `ready`.
|
|
124
|
+
Pass `port=` to `env.preview` for a repeatable capability URL across reruns and pre-created SSH
|
|
125
|
+
forwards. Managed captures require `simcord[screenshot]` plus `playwright install --with-deps chromium`;
|
|
126
|
+
never put the capability-bearing `preview.url` in agent logs or artifacts.
|
|
127
|
+
|
|
114
128
|
## Machine-readable documentation
|
|
115
129
|
|
|
116
130
|
SimCord publishes a curated [`llms.txt`](../llms.txt) index for agent navigation. It is an optional convenience, not a replacement for HTML documentation, `robots.txt`, sitemaps, or ordinary search indexing.
|
|
@@ -165,6 +165,13 @@ Pass a freshly built client — re-running the same instance would re-execute `s
|
|
|
165
165
|
- [Slash commands](interactions.md) — invoking commands and the interaction lifecycle.
|
|
166
166
|
- [Time control](time-control.md) — firing view timeouts and cooldowns instantly.
|
|
167
167
|
- [Recipes](../cookbook.md) — a reusable paginator test, among others.
|
|
168
|
+
|
|
169
|
+
## Inspect the result in a browser
|
|
170
|
+
|
|
171
|
+
For an authenticated local page that renders the same real callback flow, see
|
|
172
|
+
[Component preview and screenshots](preview.md). The preview is optional, keeps Python and browser
|
|
173
|
+
presentations independent, and labels stale, incomplete, and uncalibrated output explicitly.
|
|
174
|
+
|
|
168
175
|
## Settlement and external input
|
|
169
176
|
|
|
170
177
|
View/Modal completion waits are recognized by settlement. If a component flow waits on a
|
|
@@ -107,19 +107,28 @@ 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 `json` field preserves every supplied JSON-compatible shape, including top-level arrays.
|
|
129
|
+
`params` and `json` are detached deep snapshots, so later mutations to source containers do
|
|
130
|
+
not alter a record. The request record is appended before fault injection and route handling,
|
|
131
|
+
so failed and unimplemented requests remain observable.
|
|
123
132
|
|
|
124
133
|
## Injecting API failures
|
|
125
134
|
|