Spaces:
Sleeping
Sleeping
| """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">🌌</span>', home) | |
| self.assertIn('class="home-myco" title="Myco">🍄</span>', home) | |
| self.assertIn('class="home-brown-mushroom" title="Brown mushroom">🍄🟫</span>', 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() | |