AdmiralTaco's picture
UI: lead with the council, lab attribution, story spine, raw-output panel
1127d3b
Raw
History Blame Contribute Delete
6.29 kB
"""Headless tests for the Game wrapper, using the dummy policy (no GPU/Gradio).
Run: python -m pytest tests/ -q
"""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from ttw import narrate
from ttw.dummy import make_random_policy
from ttw.game import Game
def _game():
return Game(make_random_policy(seed=3), deck_seed=1)
def test_baseline_recorded_before_any_step():
g = _game()
assert not g.price_frame().empty # turn 0 prices recorded
assert g.gini_frame().iloc[0]["turn"] == 0
def test_step_advances_and_grows_history():
g = _game()
rows_before = len(g.price_frame())
g.step()
assert g.world.turn == 1
assert len(g.price_frame()) > rows_before
assert g.gini_frame().iloc[-1]["turn"] == 1
def test_price_frame_shape_is_long_per_good():
g = _game()
g.step()
pf = g.price_frame()
assert set(pf.columns) == {"turn", "good", "price"}
# one row per good per recorded turn (turn 0 baseline + turn 1)
assert (pf["turn"] == 1).sum() == 5
def test_town_frame_lists_all_creatures_sorted_by_wealth():
g = _game()
g.step()
tf = g.town_frame()
assert len(tf) == 5
nets = list(tf["net worth"])
assert nets == sorted(nets, reverse=True) # richest first
def test_tempt_fate_draws_legend_and_tickers():
g = _game()
title = g.tempt_fate()
assert isinstance(title, str) and title
assert any("inspired by" in line for line in g.ticker)
def test_shock_gold_rush_echoes_to_ticker():
g = _game()
before = sum(c.pebbles for c in g.world.alive())
g.shock("gold_rush", amount=10)
assert sum(c.pebbles for c in g.world.alive()) == before + 10 * 5
assert any("windfall" in line.lower() for line in g.ticker)
def test_traces_markdown_handles_dummy_policy_gracefully():
"""Dummy policy has no raw thoughts; traces view must not crash."""
g = _game()
g.step()
assert isinstance(g.traces_markdown(), str) # no exception, returns a string
def test_legend_markdown_before_and_after_tempt():
g = _game()
assert "No legend yet" in g.legend_markdown()
g.tempt_fate()
md = g.legend_markdown()
assert "Inspired by" in md and "✨" in md
assert g.current_legend is not None
def test_avatar_and_mood_emoji():
assert narrate.avatar("Fenn") == "🦊"
assert narrate.avatar("Nobody") == "🐾" # fallback
assert narrate.mood_emoji(10) == "😊"
assert narrate.mood_emoji(4) == "😣"
def test_town_frame_has_avatars_and_mood_faces():
g = _game()
g.step()
tf = g.town_frame()
assert any("🦊" in str(v) for v in tf["creature"]) # avatars present
assert all(any(face in str(m) for face in "πŸ˜ŠπŸ™‚πŸ˜ŸπŸ˜£") for m in tf["mood"])
# Phase 6 operator-console HTML accessors (arity is enforced in app.py).
_FIREWALL_BANNED = ("true", "false", "truth", "legend_key")
def test_phase6_accessors_return_str():
g = _game()
assert isinstance(g.heat_bar_html(), str)
assert isinstance(g.relationship_map_html(), str)
assert isinstance(g.cutscene_html(), str)
def test_heat_bar_html_well_formed():
from ttw.world import INVESTIGATION_THRESHOLD
g = _game()
html = g.heat_bar_html()
assert html # non-empty
assert str(INVESTIGATION_THRESHOLD) in html # threshold readout, not hardcoded
assert "color:#" in html # inline color set
assert not any(b in html for b in _FIREWALL_BANNED) # truth firewall intact
def test_relationship_map_html_covers_cast_and_patron():
g = _game()
# Drive a couple of moves so at least one sentiment cell is nonzero.
g.queue_player_move("bribe", target="Fenn", amount=40)
g.step()
g.queue_player_move("short", good="berries", amount=20)
g.step()
html = g.relationship_map_html()
assert "<table" in html
assert "Patron" in html
for name in g.world.creatures:
assert name in html
assert "#2e2519" in html # forced dark text on the light cells
def test_cutscene_html_never_blank():
g = _game()
before = g.cutscene_html()
assert before # placeholder before any events, never blank
assert "The wood is still." in before
g.step()
after = g.cutscene_html()
assert after
assert "#fff8e6" not in after # the dark surface is never lightened
assert "#161b22" in after # and is the intended dark playbill background
# Judge-facing polish: lab attribution, the story spine, and the raw-output panel.
def test_creature_lab_color_maps_engine_and_falls_back():
from ttw.council import creature_lab_color
assert creature_lab_color("Fenn") == "#76b900" # NVIDIA green (nemotron)
assert creature_lab_color("Oona") == "#10a37f" # OpenAI teal (gptoss)
assert creature_lab_color("Nobody") == "#cdbf9a" # neutral fallback
def test_chapter_markdown_is_html_stepper_with_one_active():
g = _game()
spine = g.chapter_markdown()
# All five chapter short-titles are present as pills.
for title in ("First Whispers", "The Heat Rises", "Reckoning", "The Long Game"):
assert title in spine
# Exactly one pill is the active one (the accent underline appears once).
assert spine.count("border-bottom:3px solid #2f6b3d") == 1
def test_traces_markdown_tags_the_model():
g = _game()
# Thoughts are normally filled from policy.state["raw"]; set them directly to
# test the model tag rendering without a live council.
g.thoughts = {"Oona": "hold the line", "Pip": "buy acorns"}
md = g.traces_markdown()
assert "[gpt-oss-20B]" in md # Oona's model
assert "[ttw-trader-0.5B]" in md # Pip's model
def test_raw_output_markdown_empty_state():
g = _game() # dummy policy exposes no .state
md = g.raw_output_markdown()
assert isinstance(md, str)
assert "No raw model output" in md
def test_raw_output_markdown_tags_and_fence_safe():
g = _game()
g.policy.state = {"raw": {"Oona": '{"thought":"x"} ``` # inject'}}
md = g.raw_output_markdown()
assert "gpt-oss-20B" in md # tagged by model
assert "```json" in md # rendered as a fenced block
# Exactly the open/close fence remain; a stray fence in model output that
# would break out and inject markup is neutralized.
assert md.count("```") == 2