Skip to content

Add truth or dare fun command - #51

Merged
youngcoder45 merged 4 commits into
TheCodeVerseHub:mainfrom
goldstac:feat/truth-or-dare-command
Oct 10, 2026
Merged

youngcoder45 merged 4 commits into
TheCodeVerseHub:mainfrom
goldstac:feat/truth-or-dare-command

Conversation

@goldstac

@goldstac goldstac commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Adds the Truth or Dare (TOD) command from #15, which asks for fun commands like TOD/RPS. RPS already existed in src/cogs/fun.py, so this fills in TOD.

What

  • New hybrid command /tod + &tod truth|dare in src/cogs/fun.py: picks a random prompt from built-in lists (15 truths / 15 dares), following the existing 8ball hard-coded-list pattern.
  • Slash dropdown choices (Truth / Dare); bare &tod now replies with usage instead of failing silently.
  • Listed in the fun help embed (src/cogs/help.py) and the README command table.

Testing

  • ruff check . clean; cog registers with Discord (guild-scoped sync on a test server: tod shows choice with choices [('Truth','truth'), ('Dare','dare')]).
  • Invoked all three paths with a mocked context: truth, dare, invalid input, and bare &tod: correct embeds for each, prompts randomized (14 distinct truths across 40 draws).
  • Verified live in a test server: /tod dropdown, &tod dare, &tod truth, and bare &tod usage reply.
  • pytest: 174 passed; the 2 failures also fail on a clean checkout (pre-existing, unrelated).

CI notes

Two pre-existing failures (also red on main) blocked the required checks and are fixed here, test-only:

  • test_award_message_xp_cooldown was missing the get_guild_settings patch its sibling tests use, so it queried a real database.
  • test_avatar_fetch_caching died on aiohttp 3.14's new required ClientResponse(stream_writer=...) argument (aioresponses <= 0.7.9 does not pass it; upstream fix unreleased, fix: support aiohttp 3.14 required stream_writer argument pnuckowski/aioresponses#288). A conftest shim supplies a stand-in on aiohttp >= 3.14 and is a no-op elsewhere.

Full suite: 176 passed locally on aiohttp 3.14.4 / aioresponses 0.7.9 (same as CI).

Implements the fun-command part of TheCodeVerseHub#15: /tod (and &tod truth|dare)
picks a random prompt from built-in truth/dare lists. Bare &tod now
replies with usage instead of failing silently. Documented in the
help embed and README command table.
@goldstac
goldstac requested review from a team as code owners October 6, 2026 15:06

@youngcoder45 youngcoder45 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good

@youngcoder45

Copy link
Copy Markdown
Contributor

The CI didnt passed this PR kindly Check it @goldstac

@goldstac

goldstac commented Oct 8, 2026

Copy link
Copy Markdown
Contributor Author

The CI didnt passed this PR kindly Check it @goldstac

ok sir 🫡 give me a few business days

test_award_message_xp_cooldown was the only test in its class without
the get_guild_settings patch, so _load_guild_config fell through to a
real Postgres query and failed in CI with 'relation guild_settings does
not exist'. Pre-existing: CI has been red on main for this test.
@goldstac

goldstac commented Oct 8, 2026

Copy link
Copy Markdown
Contributor Author

@youngcoder45 does this fix it?

@youngcoder45

Copy link
Copy Markdown
Contributor

@youngcoder45 does this fix it?

Idk why but it still doesnt fix @goldstac if u can fix its alr u can do, but if u cant just wait till tommorow I will look into it and tryna fix it.

@youngcoder45

Copy link
Copy Markdown
Contributor

Run pytest tests/
============================= test session starts ==============================
platform linux -- Python 3.14.8, pytest-9.1.1, pluggy-1.6.0 -- /opt/hostedtoolcache/Python/3.14.8/x64/bin/python
cachedir: .pytest_cache
rootdir: /home/runner/work/Miku/Miku
configfile: pytest.ini (WARNING: ignoring pytest config in pyproject.toml!)
plugins: asyncio-1.4.0, timeout-2.4.0, cov-7.1.0, anyio-4.15.1
asyncio: mode=Mode.AUTO, debug=False, asyncio_default_fixture_loop_scope=None, asyncio_default_test_loop_scope=function
timeout: 30.0s
timeout method: signal
timeout func_only: False
collecting ... collected 176 items

tests/test_achievements.py::test_pending_only_returns_achievements_whose_threshold_is_met PASSED [ 0%]
tests/test_achievements.py::test_pending_skips_already_unlocked PASSED [ 1%]
tests/test_achievements.py::test_every_achievement_check_reads_a_stat_that_exists PASSED [ 1%]
tests/test_achievements.py::test_member_below_every_threshold_writes_nothing PASSED [ 2%]
tests/test_achievements.py::test_unlock_inserts_ids_and_credits_the_reward_to_the_member PASSED [ 2%]
tests/test_achievements.py::test_unlocked_achievement_is_not_rewarded_twice PASSED [ 3%]
tests/test_achievements.py::test_message_listener_announces_the_unlock PASSED [ 3%]
tests/test_achievements.py::test_bot_messages_are_ignored PASSED [ 4%]
tests/test_achievements.py::test_throttle_collapses_a_burst_of_messages PASSED [ 5%]
tests/test_achievements.py::test_listener_failure_never_breaks_the_message_flow PASSED [ 5%]
tests/test_csrf.py::test_signed_token_round_trips PASSED [ 6%]
tests/test_csrf.py::TestWritesRequireAToken::test_session_without_a_token_is_rejected PASSED [ 6%]
tests/test_csrf.py::TestWritesRequireAToken::test_cookie_without_the_header_is_rejected PASSED [ 7%]
tests/test_csrf.py::TestWritesRequireAToken::test_header_must_match_the_cookie PASSED [ 7%]
tests/test_csrf.py::TestWritesRequireAToken::test_unsigned_cookie_is_rejected_even_when_repeated PASSED [ 8%]
tests/test_csrf.py::TestWritesRequireAToken::test_delete_is_guarded_too PASSED [ 9%]
tests/test_csrf.py::TestWritesRequireAToken::test_matching_token_passes_the_csrf_layer PASSED [ 9%]
tests/test_csrf.py::TestTokenIssuanceAndExemptions::test_page_load_hands_out_a_token PASSED [ 10%]
tests/test_csrf.py::TestTokenIssuanceAndExemptions::test_anonymous_page_load_gets_no_token PASSED [ 10%]
tests/test_csrf.py::TestTokenIssuanceAndExemptions::test_reads_are_never_blocked PASSED [ 11%]
tests/test_csrf.py::TestTokenIssuanceAndExemptions::test_auth_flow_is_exempt PASSED [ 11%]
tests/test_csrf.py::TestTokenIssuanceAndExemptions::test_anonymous_writes_still_report_401 PASSED [ 12%]
tests/test_dashboard.py::test_health_endpoint PASSED [ 13%]
tests/test_dashboard.py::TestFormulaHelpers::test_calc_level PASSED [ 13%]
tests/test_dashboard.py::TestFormulaHelpers::test_calc_xp_for_level PASSED [ 14%]
tests/test_dashboard.py::TestFormulaHelpers::test_level_xp_consistency PASSED [ 14%]
tests/test_dashboard.py::test_bot_stats_endpoint PASSED [ 15%]
tests/test_dashboard.py::test_login_redirect PASSED [ 15%]
tests/test_database.py::test_get_user_data PASSED [ 16%]
tests/test_database.py::test_get_user_rank PASSED [ 17%]
tests/test_database.py::test_get_leaderboard PASSED [ 17%]
tests/test_database.py::test_get_total_users PASSED [ 18%]
tests/test_database.py::test_update_user_xp PASSED [ 18%]
tests/test_database.py::test_get_guild_settings PASSED [ 19%]
tests/test_database.py::test_insert_audit_log_serializes_details PASSED [ 19%]
tests/test_database.py::test_insert_audit_log_defaults_to_empty_json_object PASSED [ 20%]
tests/test_database.py::test_get_audit_log_decodes_details PASSED [ 21%]
tests/test_database.py::test_get_role_rewards PASSED [ 21%]
tests/test_db_cache.py::TestUserWritePath::test_message_xp_reaches_the_cache PASSED [ 22%]
tests/test_db_cache.py::TestUserWritePath::test_update_user_xp_creates_and_updates_entries PASSED [ 22%]
tests/test_db_cache.py::TestUserWritePath::test_user_locks_are_shared_per_user PASSED [ 23%]
tests/test_db_cache.py::TestNewUserSentinel::test_marker_is_cleared_when_xp_is_written PASSED [ 23%]
tests/test_db_cache.py::TestNewUserSentinel::test_message_xp_accumulates_for_a_brand_new_user PASSED [ 24%]
tests/test_db_cache.py::TestNewUserSentinel::test_existing_row_is_never_marked_as_missing PASSED [ 25%]
tests/test_db_cache.py::TestFlush::test_flush_persists_dirty_users_in_one_batch PASSED [ 25%]
tests/test_db_cache.py::TestFlush::test_clean_entries_are_not_written_again PASSED [ 26%]
tests/test_db_cache.py::TestFlush::test_second_flush_updates_the_existing_row PASSED [ 26%]
tests/test_db_cache.py::TestFlush::test_failed_flush_keeps_entries_dirty_for_retry PASSED [ 27%]
tests/test_db_cache.py::TestConcurrentWriters::test_external_change_wins_over_stale_cached_xp PASSED [ 27%]
tests/test_db_cache.py::TestConcurrentWriters::test_our_write_lands_when_the_row_is_unchanged PASSED [ 28%]
tests/test_db_cache.py::TestConcurrentWriters::test_deleted_row_is_not_resurrected PASSED [ 28%]
tests/test_db_url.py::TestToAsyncpgDsn::test_no_driver_suffix_remains[***localhost:5432/miku] PASSED [ 29%]
tests/test_db_url.py::TestToAsyncpgDsn::test_no_driver_suffix_remains[***postgres:5432/miku] PASSED [ 30%]
tests/test_db_url.py::TestToAsyncpgDsn::test_no_driver_suffix_remains[***localhost:5432/miku] PASSED [ 30%]
tests/test_db_url.py::TestToAsyncpgDsn::test_no_driver_suffix_remains[***localhost:5432/miku] PASSED [ 31%]
tests/test_db_url.py::TestToAsyncpgDsn::test_credentials_and_query_are_preserved PASSED [ 31%]
tests/test_db_url.py::TestToAsyncpgDsn::test_malformed_input_is_returned_untouched[] PASSED [ 32%]
tests/test_db_url.py::TestToAsyncpgDsn::test_malformed_input_is_returned_untouched[not-a-url] PASSED [ 32%]
tests/test_db_url.py::TestToAsyncpgDsn::test_malformed_input_is_returned_untouched[postgresql://] PASSED [ 33%]
tests/test_db_url.py::TestToAsyncSqlalchemyUrl::test_asyncpg_driver_is_added[***localhost:5432/miku] PASSED [ 34%]
tests/test_db_url.py::TestToAsyncSqlalchemyUrl::test_asyncpg_driver_is_added[***localhost:5432/miku] PASSED [ 34%]
tests/test_db_url.py::TestToAsyncSqlalchemyUrl::test_sync_driver_is_replaced PASSED [ 35%]
tests/test_db_url.py::TestToAsyncSqlalchemyUrl::test_already_correct_url_is_stable PASSED [ 35%]
tests/test_db_url.py::TestConsumersUseTheHelper::test_bot_pool_strips_the_sqlalchemy_driver PASSED [ 36%]
tests/test_db_url.py::TestConsumersUseTheHelper::test_dashboard_pool_strips_the_sqlalchemy_driver PASSED [ 36%]
tests/test_entrypoint.py::test_main_puts_src_first_on_sys_path PASSED [ 37%]
tests/test_entrypoint.py::test_import_bot_resolves_to_src_bot_module PASSED [ 38%]
tests/test_entrypoint.py::test_bot_exposes_async_main PASSED [ 38%]
tests/test_formula.py::TestQuadraticFormula::test_xp_for_level PASSED [ 39%]
tests/test_formula.py::TestQuadraticFormula::test_level_1_calculation PASSED [ 39%]
tests/test_formula.py::TestQuadraticFormula::test_level_2_threshold PASSED [ 40%]
tests/test_formula.py::TestQuadraticFormula::test_level_calculation_consistency PASSED [ 40%]
tests/test_formula.py::TestQuadraticFormula::test_xp_to_next_level PASSED [ 41%]
tests/test_formula.py::TestQuadraticFormula::test_negative_xp PASSED [ 42%]
tests/test_formula.py::TestQuadraticFormula::test_zero_xp PASSED [ 42%]
tests/test_formula.py::TestLinearFormula::test_defaults PASSED [ 43%]
tests/test_formula.py::TestLinearFormula::test_xp_for_level PASSED [ 43%]
tests/test_formula.py::TestLinearFormula::test_level_2_threshold PASSED [ 44%]
tests/test_formula.py::TestLinearFormula::test_level_calculation_consistency PASSED [ 44%]
tests/test_formula.py::TestLinearFormula::test_custom_configuration PASSED [ 45%]
tests/test_formula.py::TestFormulaRegistry::test_register_and_get PASSED [ 46%]
tests/test_formula.py::TestFormulaRegistry::test_get_unknown_formula PASSED [ 46%]
tests/test_formula.py::TestFormulaRegistry::test_load_defaults PASSED [ 47%]
tests/test_formula.py::TestFormulaRegistry::test_singleton_instances PASSED [ 47%]
tests/test_formula.py::TestFormulaRegistry::test_register_invalid_class PASSED [ 48%]
tests/test_formula.py::TestFormulaRegistry::test_factory_method PASSED [ 48%]
tests/test_github_client.py::test_client_initialization PASSED [ 49%]
tests/test_github_client.py::test_client_with_token PASSED [ 50%]
tests/test_github_client.py::test_get_repo_success PASSED [ 50%]
tests/test_github_client.py::test_get_user_success PASSED [ 51%]
tests/test_github_client.py::test_search_repos PASSED [ 51%]
tests/test_github_client.py::test_error_handling PASSED [ 52%]
tests/test_health.py::TestHealthEndpoints::test_liveness_stays_public PASSED [ 52%]
tests/test_health.py::TestHealthEndpoints::test_db_check_reports_success PASSED [ 53%]
tests/test_health.py::TestHealthEndpoints::test_db_check_fails_with_503 PASSED [ 53%]
tests/test_health.py::TestHealthEndpoints::test_db_check_does_not_leak_the_error PASSED [ 54%]
tests/test_health.py::TestHealthEndpoints::test_readiness_returns_503_when_the_database_is_down PASSED [ 55%]
tests/test_health.py::TestHealthEndpoints::test_readiness_is_ready_when_the_database_answers PASSED [ 55%]
tests/test_health.py::TestBotStatsRequiresAuth::test_anonymous_request_is_rejected PASSED [ 56%]
tests/test_health.py::TestBotStatsRequiresAuth::test_expired_session_is_rejected PASSED [ 56%]
tests/test_health.py::TestBotStatsRequiresAuth::test_authenticated_request_still_gets_the_stats PASSED [ 57%]
tests/test_level_service.py::TestLevelService::test_calculate_level PASSED [ 57%]
tests/test_level_service.py::TestLevelService::test_calculate_xp_for_level PASSED [ 58%]
tests/test_level_service.py::TestLevelService::test_calculate_xp_to_next_level PASSED [ 59%]
tests/test_level_service.py::TestLevelService::test_cooldown PASSED [ 59%]
tests/test_level_service.py::TestLevelService::test_cooldown_expires PASSED [ 60%]
tests/test_level_service.py::TestLevelService::test_award_message_xp_bot PASSED [ 60%]
tests/test_level_service.py::TestLevelService::test_award_message_xp_no_guild PASSED [ 61%]
tests/test_level_service.py::TestLevelService::test_award_message_xp_success PASSED [ 61%]
tests/test_level_service.py::TestLevelService::test_award_message_xp_cooldown PASSED [ 62%]
tests/test_level_service.py::TestLevelService::test_award_message_xp_disabled PASSED [ 63%]
tests/test_level_service.py::TestLevelService::test_set_level PASSED [ 63%]
tests/test_level_service.py::TestLevelService::test_set_level_negative_raises PASSED [ 64%]
tests/test_level_service.py::TestLevelService::test_add_xp PASSED [ 64%]
tests/test_level_service.py::TestLevelService::test_remove_xp PASSED [ 65%]
tests/test_level_service.py::TestAdminMutationsWithCache::test_add_xp_keeps_xp_that_is_only_in_the_cache PASSED [ 65%]
tests/test_level_service.py::TestAdminMutationsWithCache::test_set_level_keeps_messages_and_cached_values PASSED [ 66%]
tests/test_level_service.py::TestAdminMutationsWithCache::test_reset_member_evicts_the_cache_before_deleting PASSED [ 67%]
tests/test_level_service.py::TestAdminMutationsWithCache::test_reset_guild_clears_the_cache_before_deleting PASSED [ 67%]
tests/test_level_service.py::TestAdminMutationsWithCache::test_without_a_cache_the_database_is_still_written PASSED [ 68%]
tests/test_level_service.py::TestXpSource::test_xp_sources_defined PASSED [ 68%]
tests/test_level_service.py::TestRestrictionType::test_restriction_types_defined PASSED [ 69%]
tests/test_quests.py::test_incomplete_quest_never_touches_the_database PASSED [ 69%]
tests/test_quests.py::test_claim_awards_when_the_row_transitions_to_claimed PASSED [ 70%]
tests/test_quests.py::test_replayed_claim_does_not_award_xp_again PASSED [ 71%]
tests/test_quests.py::test_claim_sql_guards_on_unclaimed_rows PASSED [ 71%]
tests/test_quests.py::test_claim_preserves_last_message_time PASSED [ 72%]
tests/test_rank_card.py::test_rank_card_initialization PASSED [ 72%]
tests/test_rank_card.py::test_rank_card_generation PASSED [ 73%]
tests/test_rank_card.py::test_rank_card_caching PASSED [ 73%]
tests/test_rank_card.py::test_avatar_fetch_caching FAILED [ 74%]

=================================== FAILURES ===================================
__________________________ test_avatar_fetch_caching ___________________________

@pytest.mark.asyncio
async def test_avatar_fetch_caching():
    """Test that avatars are downloaded once and then served from cache."""
    import io

    from aioresponses import aioresponses
    from PIL import Image

    from utils.rank_card import RankCardGenerator

    url = "https://cdn.example.com/avatar.png"
    generator = RankCardGenerator(avatar_cache_size=10, avatar_cache_ttl=60)

    # A real (tiny) PNG: the generator decodes the response with Pillow, so a
    # hand-rolled byte string that only *looks* like a PNG returns None.
    buffer = io.BytesIO()
    Image.new("RGBA", (8, 8), (255, 0, 0, 255)).save(buffer, format="PNG")
    png_bytes = buffer.getvalue()

    with aioresponses() as mocked:
        mocked.get(url, status=200, body=png_bytes)

        avatar = await generator._fetch_avatar(url)
      assert avatar is not None

E assert None is not None

tests/test_rank_card.py:100: AssertionError
------------------------------ Captured log call -------------------------------
ERROR miku.rank_card:rank_card.py:94 Unexpected avatar decode error
Traceback (most recent call last):
File "/home/runner/work/Miku/Miku/src/utils/rank_card.py", line 79, in _fetch_avatar
async with self._http.get(avatar_url) as resp:
~~~~~~~~~~~~~~^^^^^^^^^^^^
File "/opt/hostedtoolcache/Python/3.14.8/x64/lib/python3.14/site-packages/aiohttp/client.py", line 1756, in aenter
self._resp: _RetType_co = await self._coro
^^^^^^^^^^^^^^^^
File "", line 3, in _request
File "/opt/hostedtoolcache/Python/3.14.8/x64/lib/python3.14/unittest/mock.py", line 2344, in _execute_mock_call
result = await effect(*args, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/opt/hostedtoolcache/Python/3.14.8/x64/lib/python3.14/site-packages/aioresponses/core.py", line 559, in _request_mock
response = await self.match(method, url, **kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/opt/hostedtoolcache/Python/3.14.8/x64/lib/python3.14/site-packages/aioresponses/core.py", line 482, in match
response_or_exc = await matcher.build_response(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
url, allow_redirects=allow_redirects, **kwargs
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
)
^
File "/opt/hostedtoolcache/Python/3.14.8/x64/lib/python3.14/site-packages/aioresponses/core.py", line 228, in build_response
resp = self._build_response(
url=url,
...<7 lines>...
response_class=result.response_class,
reason=result.reason)
File "/opt/hostedtoolcache/Python/3.14.8/x64/lib/python3.14/site-packages/aioresponses/core.py", line 197, in _build_response
resp = response_class(method, url, **kwargs)
TypeError: ClientResponse.init() missing 1 required keyword-only argument: 'stream_writer'
=============================== warnings summary ===============================
../../../../../opt/hostedtoolcache/Python/3.14.8/x64/lib/python3.14/site-packages/fastapi/testclient.py:1
/opt/hostedtoolcache/Python/3.14.8/x64/lib/python3.14/site-packages/fastapi/testclient.py:1: StarletteDeprecationWarning: Using httpx with starlette.testclient is deprecated; install httpx2 instead.
from starlette.testclient import TestClient as TestClient # noqa

-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_rank_card.py::test_avatar_fetch_caching - assert None is not None
!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!
=================== 1 failed, 130 passed, 1 warning in 2.77s ===================
Unclosed client session
client_session: <aiohttp.client.ClientSession object at 0x7f03d2c05be0>

aiohttp 3.14 made stream_writer a required argument of ClientResponse.__init__;
aioresponses <= 0.7.9 does not pass it (upstream fix unreleased:
pnuckowski/aioresponses#288), so every mocked HTTP test died with TypeError.
Tests now inject a stand-in writer on aiohttp >= 3.14; no-op otherwise.
Pre-existing failure on main, unmasked by the cooldown-test fix.
@goldstac

Copy link
Copy Markdown
Contributor Author

@youngcoder45 ?

@youngcoder45
youngcoder45 merged commit 6a79cc0 into TheCodeVerseHub:main Oct 10, 2026
3 of 4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants