From 2bad0c19a7800c9164fb2d8804d76b2b4367e464 Mon Sep 17 00:00:00 2001 From: ohad6k Date: Thu, 23 Jul 2026 16:04:59 +0300 Subject: [PATCH] test: cover the card display layer (46 tests) The card is the artifact users screenshot and post, and it renders model-written text into a fixed-width terminal frame and into HTML. Before this it had one happy-path subprocess test and one HTML label assertion; a stdlib line-coverage pass put emulo.py at 55.8% with print_card, _print_card_plain, load_card, show_card, _law_bar, fmt_tokens and months_between at 0% in-process. Adds tests/test_card_render.py: - fmt_tokens / months_between unit boundaries and unparseable dates - _law_bar width invariance, proportional fill, clamping, zero denominator, and the non-ratio counts a drifting reducer emits - HTML escaping of mined text in every slot, three-law cap, dropped stat cells, grade fallback, art path resolution - load_card stats.json-over-card.json merge precedence and the missing-card exit code and hint - the frame-closure invariant: every framed line is one width across eight terminal sizes, narrow and wide layouts, long wrapped laws - the UnicodeEncodeError -> ASCII fallback card - show_card writing card.html without opening a browser - strip_frontmatter / cursor_rule / install_destination edge cases Two tests are xfail, documenting a real bug left unfixed here: months_between catches (ValueError, IndexError) but not TypeError, so a card.json with `"first_date": null` tracebacks out of both print_card and render_card_html. emulo.py line coverage 55.8% -> 64.0%. Suite: 396 -> 442 tests, green. Co-Authored-By: Claude Opus 4.8 --- tests/test_card_render.py | 437 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 437 insertions(+) create mode 100644 tests/test_card_render.py diff --git a/tests/test_card_render.py b/tests/test_card_render.py new file mode 100644 index 0000000..2843583 --- /dev/null +++ b/tests/test_card_render.py @@ -0,0 +1,437 @@ +"""Unit tests for the card display layer of emulo.py. + +The card is the one artifact users screenshot and post, and it renders text a +language model wrote into a fixed-width terminal frame and into HTML. Only the +happy path was covered (one subprocess run in test_emulo.py, one HTML label +assertion in test_profile_store.py); everything below is the malformed-input, +boundary and layout behaviour that a regression would otherwise reach the +screenshot before anyone noticed. +""" + +import contextlib +import importlib.util +import io +import json +import os +import shutil +import sys +import tempfile +import unittest +from pathlib import Path +from unittest import mock + + +ROOT = Path(__file__).resolve().parents[1] +SPEC = importlib.util.spec_from_file_location("emulo_card", ROOT / "emulo.py") +emulo = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(emulo) + + +def sample_card(**overrides): + card = { + "archetype": "Proof-First Builder", + "laws": [ + {"text": "done means it runs live in production for a real user", "count": "18/20"}, + {"text": "fix the one thing", "count": "15/20"}, + {"text": "no filler in the report", "count": "9/20"}, + ], + "truth": "asks the agent to explain his own system back to him", + "stats": { + "sessions": 1656, + "messages": 7678, + "tokens": 2950000, + "first_date": "2025-11-02", + "last_date": "2026-07-08", + }, + } + card.update(overrides) + return card + + +def render_terminal_card(card, columns=80, stream=None): + """Render the static card and return its stdout as a string.""" + buf = stream if stream is not None else io.StringIO() + size = os.terminal_size((columns, 48)) + with mock.patch.object(shutil, "get_terminal_size", return_value=size): + with contextlib.redirect_stdout(buf): + emulo.print_card(card, still=True) + return buf.getvalue() + + +def frame_lines(rendered): + return [line for line in rendered.split("\n") if line.strip()] + + +class TokenAndDateFormattingTest(unittest.TestCase): + def test_fmt_tokens_switches_units_at_the_exact_boundaries(self): + self.assertEqual("0", emulo.fmt_tokens(0)) + self.assertEqual("999", emulo.fmt_tokens(999)) + self.assertEqual("1K", emulo.fmt_tokens(1000)) + self.assertEqual("999K", emulo.fmt_tokens(999_999)) + self.assertEqual("1.0M", emulo.fmt_tokens(1_000_000)) + self.assertEqual("3.0M", emulo.fmt_tokens(2_950_000)) + + def test_fmt_tokens_truncates_thousands_instead_of_rounding_up(self): + # 1999 tokens must never read as "2K" on a card people screenshot + self.assertEqual("1K", emulo.fmt_tokens(1999)) + + def test_months_between_counts_an_inclusive_span(self): + self.assertEqual(9, emulo.months_between("2025-11-02", "2026-07-08")) + self.assertEqual(1, emulo.months_between("2026-07-01", "2026-07-31")) + self.assertEqual(12, emulo.months_between("2025-01-05", "2025-12-31")) + + def test_months_between_clamps_a_reversed_range_to_one_month(self): + self.assertEqual(1, emulo.months_between("2026-07-08", "2025-11-02")) + + def test_months_between_returns_zero_for_unparseable_dates(self): + for first, last in ( + ("", ""), + ("not-a-date", "2026-07-08"), + ("2025-11-02", "nope"), + ("2025", "2026"), + ("2026-7-8", "2026-09-01"), + ): + with self.subTest(first=first, last=last): + self.assertEqual(0, emulo.months_between(first, last)) + + @unittest.expectedFailure + def test_months_between_survives_null_dates(self): + # BUG: months_between catches (ValueError, IndexError) but a card.json + # with `"first_date": null` yields None, and None[:4] raises TypeError. + # That escapes both print_card and render_card_html, so `emulo --card` + # tracebacks on a reducer that emitted nulls instead of empty strings. + # Fix is one word: add TypeError to the except tuple. Not fixing here. + self.assertEqual(0, emulo.months_between(None, None)) + + +class LawBarTest(unittest.TestCase): + def test_bar_is_always_exactly_the_requested_width(self): + for count in ("18/20", "0/20", "20/20", "1/3"): + with self.subTest(count=count): + self.assertEqual(26, len(emulo._law_bar(count))) + self.assertEqual(40, len(emulo._law_bar(count, width=40))) + + def test_bar_fills_in_proportion_to_the_ratio(self): + self.assertEqual("█" * 23 + "░" * 3, emulo._law_bar("18/20")) + self.assertEqual("░" * 26, emulo._law_bar("0/20")) + self.assertEqual("█" * 26, emulo._law_bar("20/20")) + + def test_bar_clamps_ratios_outside_zero_to_one(self): + self.assertEqual("█" * 10, emulo._law_bar("30/20", width=10)) + self.assertEqual("░" * 10, emulo._law_bar("-5/20", width=10)) + + def test_zero_denominator_does_not_divide_by_zero(self): + self.assertEqual("█" * 10, emulo._law_bar("18/0", width=10)) + + def test_non_ratio_counts_get_no_bar(self): + # these are the shapes a reducer actually emits when it drifts: + # a prose count, a bare number, a null, an over-split string + for count in ("2 sessions", "abc", "5", "", None, "1/2/3", "x/y"): + with self.subTest(count=count): + self.assertIsNone(emulo._law_bar(count)) + + +class CardHtmlTest(unittest.TestCase): + def test_mined_text_is_html_escaped_in_every_slot(self): + html = emulo.render_card_html(sample_card( + archetype="", + laws=[{"text": "ships & proves", "count": "9/20"}], + truth="he said 5 > 3 & meant it", + )) + self.assertNotIn("