"""Regression tests for Myco's core game loop and UI builders.""" import ast import os import unittest from types import SimpleNamespace from pathlib import Path from unittest.mock import patch from game.catalog import load_mushrooms from game.engine import ( DEFAULT_HF_MODEL_ID, collect_current, companion_reply, discover_mushroom, eat_current, follow_whisper, hf_companion_status, hf_model_id, pick_current, study_current, ) from game.progression import current_area, next_area, next_mystery, revealed_mysteries from ui.gradio_app import build_app, move_player from ui.renderers import ( dex_markdown, forest_scene, game_intro_markdown, game_status_markdown, home_forest_markdown, mushroom_card, progress_markdown, world_map_markdown, ) def _fake_hf_response(calls, kwargs, content): calls.append(kwargs) message = SimpleNamespace(content=content) choice = SimpleNamespace(message=message) return SimpleNamespace(choices=(choice,)) class MycoCoreTests(unittest.TestCase): def test_catalog_has_expected_size_and_rarities(self): mushrooms = load_mushrooms() rarities = {mushroom.rarity for mushroom in mushrooms} self.assertGreaterEqual(len(mushrooms), 20) self.assertIn("Common", rarities) self.assertIn("Rare", rarities) self.assertIn("Legendary", rarities) def test_discovery_chat_and_scene_render(self): mushroom, current, history = discover_mushroom([]) scene = forest_scene(current, []) self.assertEqual(current["name"], mushroom.name) self.assertEqual(history[0]["role"], "assistant") self.assertIn("forest-scene", scene) self.assertIn(mushroom.name, scene) self.assertIn("clue", current) self.assertIn("secret", current) def test_play_tab_has_game_hud(self): hud = game_intro_markdown() self.assertIn("game-hud-panel", hud) self.assertIn("Wake the Whispering Ring", hud) self.assertIn("Search", hud) def test_home_keeps_original_tree_and_mushroom_postcard(self): home = home_forest_markdown() self.assertIn('class="home-forest-postcard"', home) self.assertNotIn('class="forest-map"', home) self.assertNotIn('class="scene-stage"', home) self.assertIn('class="home-milky-way" title="Milky Way">🌌', home) self.assertIn('class="home-myco" title="Myco">🍄', home) self.assertIn('class="home-brown-mushroom" title="Brown mushroom">🍄‍🟫', home) self.assertIn("🌲", home) self.assertIn("🌳", home) self.assertIn("✨", home) self.assertIn("✦", home) self.assertIn("✧", home) self.assertIn("The Milky Way is awake", home) self.assertIn("home-whisper", home) def test_hf_default_model_is_gpt_oss_20b(self): self.assertEqual(DEFAULT_HF_MODEL_ID, "openai/gpt-oss-20b") with patch.dict(os.environ, {"MYCO_HF_MODEL_ID": ""}, clear=False): self.assertEqual(hf_model_id(), DEFAULT_HF_MODEL_ID) with patch.dict(os.environ, {"MYCO_HF_MODEL_ID": "custom/model"}, clear=False): self.assertEqual(hf_model_id(), "custom/model") def test_hf_companion_status_explains_token_state(self): no_token_env = { "HF_TOKEN": "", "HF_API_TOKEN": "", "HF_BUILD_SMALL_HACKATHON_TOKEN": "", } with patch.dict(os.environ, no_token_env, clear=False): self.assertIn("fallback voice is active", hf_companion_status()) with patch.dict(os.environ, {"HF_TOKEN": "fake-token"}, clear=False): self.assertIn("connected to Hugging Face model", hf_companion_status()) def test_discovery_uses_hf_api_when_token_is_configured(self): calls = [] def fake_client(token): return SimpleNamespace( token=token, chat_completion=lambda **kwargs: _fake_hf_response( calls, kwargs, "gpt-oss Myco spotted a glowing cap.", ), ) api_env = { "HF_TOKEN": "fake-token", "HF_API_TOKEN": "", "HF_BUILD_SMALL_HACKATHON_TOKEN": "", } with patch.dict(os.environ, api_env, clear=False): with patch("game.engine.InferenceClient", fake_client): _mushroom, _current, history = discover_mushroom([]) self.assertEqual(history[-1]["content"], "gpt-oss Myco spotted a glowing cap.") self.assertEqual(calls[0]["model"], DEFAULT_HF_MODEL_ID) def test_companion_reply_adds_user_and_assistant_messages(self): _mushroom, current, history = discover_mushroom([]) no_token_env = { "HF_TOKEN": "", "HF_API_TOKEN": "", "HF_BUILD_SMALL_HACKATHON_TOKEN": "", } with patch.dict(os.environ, no_token_env, clear=False): prompt, updated_history = companion_reply("What is this?", history, current) self.assertEqual(prompt, "") self.assertEqual(updated_history[-2]["role"], "user") self.assertEqual(updated_history[-1]["role"], "assistant") self.assertIn(current["name"], updated_history[-1]["content"]) def test_companion_api_uses_hf_token_when_configured(self): _mushroom, current, history = discover_mushroom([]) class FakeMessage: content = "A model-powered Myco reply." class FakeChoice: message = FakeMessage() class FakeResponse: choices = (FakeChoice(),) class FakeClient: def __init__(self, token): self.token = token def chat_completion(self, **kwargs): self.kwargs = kwargs return FakeResponse() api_env = { "HF_TOKEN": "fake-token", "HF_API_TOKEN": "", "HF_BUILD_SMALL_HACKATHON_TOKEN": "", } with patch.dict(os.environ, api_env, clear=False): with patch("game.engine.InferenceClient", FakeClient): prompt, updated_history = companion_reply("Should I eat this?", history, current) self.assertEqual(prompt, "") self.assertEqual(updated_history[-1]["content"], "A model-powered Myco reply.") def test_companion_prompt_uses_journey_context_not_generic_chat(self): calls = [] def contextual_client(token): return SimpleNamespace( token=token, chat_completion=lambda **kwargs: _fake_hf_response( calls, kwargs, "Myco remembers the path with you.", ), ) _mushroom, current, history = discover_mushroom([]) collection = [ {"name": "Moonveil", "rarity": "Rare"}, {"name": "Glowcap", "rarity": "Common"}, ] api_env = { "HF_TOKEN": "fake-token", "HF_API_TOKEN": "", "HF_BUILD_SMALL_HACKATHON_TOKEN": "", } with patch.dict(os.environ, api_env, clear=False): with patch("game.engine.InferenceClient", contextual_client): companion_reply("What do you notice?", history, current, collection, (2, 1)) prompt_text = calls[-1]["messages"][-1]["content"] system_text = calls[-1]["messages"][0]["content"] self.assertIn("Location: eastern glow log", prompt_text) self.assertIn("Weather:", prompt_text) self.assertIn("Rare finds: Moonveil", prompt_text) self.assertIn("Notable event:", prompt_text) self.assertIn("Recent conversation:", prompt_text) self.assertIn("Myco is not a chatbot", system_text) self.assertIn("Current location: eastern glow log", system_text) self.assertIn("Rare mushrooms found: Moonveil", system_text) self.assertIn("Avoid phrases", system_text) self.assertIn("As an AI", system_text) self.assertIn("Do not act like a generic assistant", system_text) def test_companion_api_falls_back_without_token(self): _mushroom, current, history = discover_mushroom([]) no_token_env = { "HF_TOKEN": "", "HF_API_TOKEN": "", "HF_BUILD_SMALL_HACKATHON_TOKEN": "", } with patch.dict(os.environ, no_token_env, clear=False): prompt, updated_history = companion_reply("Should I eat this?", history, current) self.assertEqual(prompt, "") self.assertIn(current["name"], updated_history[-1]["content"]) self.assertIn("unknown", updated_history[-1]["content"].lower()) def test_companion_api_falls_back_on_malformed_response(self): _mushroom, current, history = discover_mushroom([]) def broken_client(token): return SimpleNamespace( token=token, chat_completion=lambda **kwargs: SimpleNamespace(choices=()), ) api_env = { "HF_TOKEN": " fake-token ", "HF_API_TOKEN": "", "HF_BUILD_SMALL_HACKATHON_TOKEN": "", } with patch.dict(os.environ, api_env, clear=False): with patch("game.engine.InferenceClient", broken_client): prompt, updated_history = companion_reply("What is this?", history, current) self.assertEqual(prompt, "") self.assertIn(current["name"], updated_history[-1]["content"]) def test_collect_current_prevents_duplicates(self): _mushroom, current, history = discover_mushroom([]) collection, history = collect_current(current, [], history) duplicate_collection, duplicate_history = collect_current(current, collection, history) self.assertEqual(len(collection), 1) self.assertEqual(duplicate_collection, collection) self.assertIn("already", duplicate_history[-1]["content"]) def test_discoveries_include_memorable_story_moments(self): _mushroom, current, history = discover_mushroom([]) self.assertIn("event_title", current) self.assertIn("encounter", current) self.assertIn("Impossible Mushroom", current["objective"]) self.assertIn("mystery_title", current) self.assertIn("mystery_reveal", current) self.assertIn(current["event_title"], history[-1]["content"]) def test_single_mystery_reaches_magical_final_reveal(self): catalog = (load_mushrooms()[0],) collection = [{"name": f"clue-{index}"} for index in range(5)] _mushroom, current, _history = discover_mushroom(collection, catalog) followed, history = follow_whisper(current, collection, []) assert followed is not None self.assertEqual(followed["mystery_title"], "The Impossible Bloom") self.assertIn("blooms inside the MycoDex", followed["mystery_reveal"]) self.assertIn("first spore", followed["memory_fragment"]) self.assertIn("recovering Myco's lost forest", history[-1]["content"]) def test_follow_whisper_reveals_memory_or_game_over(self): rare = { "name": "Silvercap", "rarity": "Rare", "habitat": "moon roots", "lore": "It rings like a tiny bell.", "edible": "Unknown", "magic": "Unknown", "danger": "Unknown", "event_title": "Moonlit Sporefall", "event_emoji": "🌌", "event_choice": "Follow the brightest spore.", "event_consequence": "A memory wakes.", "encounter": "A lost traveler waits.", "clue": "A clue", "secret": "A secret", } followed, history = follow_whisper(rare, [], []) assert followed is not None self.assertEqual(followed["whisper_followed"], "Yes") self.assertIn("Memory fragment", followed["memory_fragment"]) self.assertIn("recovering Myco's lost forest", history[-1]["content"]) poison = {**rare, "name": "Ghost Gill"} game_over, history = follow_whisper(poison, [], []) assert game_over is not None self.assertEqual(game_over["game_over"], "Yes") self.assertIn("GAME OVER", history[-1]["content"]) def test_pick_current_can_game_over_on_poison(self): poisonous = { "name": "Ghost Gill", "rarity": "Legendary", "habitat": "fog pockets", "lore": "Its gills vanish unless you look from the corner of your eye.", "edible": "Unknown", "magic": "Unknown", "danger": "Unknown", "clue": "A bad clue", "secret": "A bad secret", } collection, picked, history = pick_current(poisonous, [], []) self.assertEqual(collection, []) assert picked is not None self.assertEqual(picked["game_over"], "Yes") self.assertEqual(picked["health"], "0") self.assertEqual(picked["score_delta"], "-25") self.assertIn("GAME OVER", history[-1]["content"]) def test_pick_current_collects_safe_mushroom(self): _mushroom, current, history = discover_mushroom([]) current["name"] = "Glowcap" current["rarity"] = "Common" current["danger"] = "Unknown" collection, picked, history = pick_current(current, [], history) self.assertEqual(len(collection), 1) assert picked is not None self.assertEqual(picked["picked"], "Yes") self.assertEqual(picked["score_delta"], "10") self.assertEqual(picked["score_total"], "10") self.assertEqual(picked["health"], "3") self.assertIn("+10 spores", picked["reward_text"]) self.assertIn("Score: 10", history[-1]["content"]) def test_study_and_eat_are_safe(self): _mushroom, current, history = discover_mushroom([]) studied, study_history = study_current(current, history) self.assertIsNotNone(studied) assert studied is not None eat_history = eat_current(studied, study_history) self.assertEqual(studied["studied"], "Yes") self.assertNotEqual(studied["magic"], "Unknown") self.assertIn("Clue:", study_history[-1]["content"]) self.assertIn("blocks", eat_history[-1]["content"]) def test_movement_updates_forest_scene_position(self): _mushroom, current, _history = discover_mushroom([]) position, scene, status = move_player((1, 1), "north", current, []) self.assertEqual(position, (1, 0)) self.assertIn("pos-1-0", scene) self.assertIn("Forest movement grid", scene) self.assertIn("Map position", status) def test_progress_and_dex_markdown_handle_empty_and_collected_state(self): _mushroom, current, _history = discover_mushroom([]) self.assertIn("MycoDex is empty", dex_markdown([])) self.assertIn(current["name"], dex_markdown([current])) self.assertIn("Forest Progress", progress_markdown([current])) self.assertIn("Forest Atlas", world_map_markdown([current])) self.assertIn("First Spore", world_map_markdown([current])) self.assertEqual(world_map_markdown([current]).count('class="map-card '), 4) self.assertIn("Value:", mushroom_card(load_mushrooms()[0], state=current)) self.assertIn("Effect:", mushroom_card(load_mushrooms()[0], state=current)) self.assertIn("Quest Log", game_status_markdown(current, [current])) self.assertIn("Mystery thread", game_status_markdown(current, [current])) self.assertIn("Impossible bloom", game_status_markdown(current, [current])) self.assertIn("Score:", game_status_markdown(current, [current])) self.assertIn("Health:", game_status_markdown(current, [current])) self.assertIn("Event:", game_status_markdown(current, [current])) self.assertIn("Mystery", game_status_markdown(current, [current])) self.assertIn(current["name"], game_status_markdown(current, [current])) self.assertEqual(current_area(0).name, "Mossy Trail") self.assertIsNotNone(next_area(0)) upcoming_mystery = next_mystery(0) self.assertIsNotNone(upcoming_mystery) assert upcoming_mystery is not None self.assertEqual(upcoming_mystery.name, "First Spore") self.assertEqual(revealed_mysteries(3)[-1].name, "Whispering Ring") def test_python_files_have_no_duplicate_functions_or_future_imports(self): future_import = "from __future__ import " + "annotations" for path in Path(".").glob("**/*.py"): if "__pycache__" in path.parts or ".git" in path.parts: continue source = path.read_text() tree = ast.parse(source) function_lines = {} for node in ast.walk(tree): if isinstance(node, ast.FunctionDef): function_lines.setdefault(node.name, []).append(node.lineno) duplicates = {name: lines for name, lines in function_lines.items() if len(lines) > 1} self.assertEqual(duplicates, {}, f"duplicate functions in {path}") self.assertNotIn(future_import, source, f"future import in {path}") def test_gradio_app_has_single_clean_composition(self): source = Path("ui/gradio_app.py").read_text() self.assertEqual(source.count('"""Gradio app composition for Myco."""'), 1) self.assertEqual(source.count("import gradio as gr"), 1) self.assertEqual(source.count("def build_app"), 1) self.assertNotIn("outputs=[", source) self.assertNotIn("textbox_component", source) self.assertIn('with gr.Tabs(selected="home") as tabs:', source) self.assertIn('with gr.Tab("🎮 Play", id="play"):', source) self.assertIn('start_play_button.click(open_play_tab, outputs=(tabs,), show_progress="hidden")', source) self.assertIn('with gr.Group(elem_id="game-shell"):', source) self.assertIn('search_button = gr.Button("🎮 Search Clearing", variant="primary")', source) self.assertIn('with gr.Group(elem_id="near-monitor-controls"):', source) self.assertIn('pick_button = gr.Button("🍄 Pick", variant="primary")', source) self.assertIn('whisper_button = gr.Button("🌌 Follow Whisper")', source) self.assertIn('show_progress="hidden"', source) def test_gradio_app_rejects_old_corrupted_callback_fragments(self): source = Path("ui/gradio_app.py").read_text() forbidden_fragments = ( "def ask_myco(", "def add_to_dex(", "def study_mushroom(", "def begin_forest_search(", "def explore(", "explore_button", "outputs=[", "def build_app() ->", ) for fragment in forbidden_fragments: self.assertNotIn(fragment, source) def test_engine_has_single_myco_reply_implementation(self): source = Path("game/engine.py").read_text() self.assertEqual(source.count("def myco_reply"), 1) self.assertNotIn("def companion_reply", source) self.assertIn("companion_reply = myco_reply", source) def test_gradio_app_builds(self): demo = build_app() self.assertEqual(type(demo).__name__, "Blocks") if __name__ == "__main__": unittest.main()