Skip to content

Commit f8dc64d

Browse files
committed
test(plugin): add alignment regression coverage for Tiny Actor Grid (#1273)
Add 23 regression tests verifying display-width consistency across: - Unicode faces (◕‿◕, ⬡‿⬡, ⊙‿⊙) - ASCII fallback mode (o_o, ^_^) - CJK labels (Korean 보안/성능, Chinese 安全/性能) - Emoji faces (🤖, 👻, 🔥) - ANSI color codes (zero-width verification) - Narrow terminal widths (40/80 columns) - Mixed content grid rows Closes #1273
1 parent 573c04c commit f8dc64d

1 file changed

Lines changed: 348 additions & 0 deletions

File tree

Lines changed: 348 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,348 @@
1+
#!/usr/bin/env python3
2+
"""Alignment regression tests for Tiny Actor Grid (#1273).
3+
4+
Ensures display-width consistency across Unicode faces, ASCII fallback,
5+
CJK labels, emoji, ANSI color codes, and narrow terminal widths.
6+
7+
Run with:
8+
python3 -m pytest tests/test_tiny_actor_alignment.py -v
9+
"""
10+
11+
import os
12+
import sys
13+
from pathlib import Path
14+
15+
# Ensure hooks/lib is importable
16+
_hooks_lib = str(Path(__file__).resolve().parent.parent / "hooks" / "lib")
17+
if _hooks_lib not in sys.path:
18+
sys.path.insert(0, _hooks_lib)
19+
20+
from buddy_renderer import ( # noqa: E402
21+
display_width,
22+
pad_to_display_width,
23+
render_box_line,
24+
render_face_banner,
25+
strip_ansi,
26+
truncate_to_display_width,
27+
)
28+
29+
# ---------------------------------------------------------------------------
30+
# Mock card data (inline — no external fixtures needed)
31+
# ---------------------------------------------------------------------------
32+
33+
UNICODE_FACES = [
34+
{"face": "\u25d5\u203f\u25d5", "label": "Security"},
35+
{"face": "\u2b21\u203f\u2b21", "label": "Architecture"},
36+
{"face": "\u2299\u203f\u2299", "label": "Performance"},
37+
{"face": "\u25c9\u203f\u25c9", "label": "Testing"},
38+
{"face": "\u00b0\u25c7\u00b0", "label": "Code Quality"},
39+
]
40+
41+
ASCII_FACES = [
42+
{"face": "o_o", "label": "Security"},
43+
{"face": "o.o", "label": "Architecture"},
44+
{"face": "^_^", "label": "Performance"},
45+
{"face": ">_<", "label": "Testing"},
46+
{"face": "=_=", "label": "Code Quality"},
47+
]
48+
49+
CJK_LABELS = [
50+
{"face": "\u25d5\u203f\u25d5", "label": "\ubcf4\uc548"}, # Korean: 보안
51+
{"face": "\u2b21\u203f\u2b21", "label": "\uc131\ub2a5"}, # Korean: 성능
52+
{"face": "\u2299\u203f\u2299", "label": "\uc811\uadfc\uc131"}, # Korean: 접근성
53+
{"face": "\u25c9\u203f\u25c9", "label": "\u5b89\u5168"}, # Chinese: 安全
54+
{"face": "\u00b0\u25c7\u00b0", "label": "\u6027\u80fd"}, # Chinese: 性能
55+
]
56+
57+
EMOJI_FACES = [
58+
{"face": "\U0001f916", "label": "Bot"},
59+
{"face": "\U0001f47b", "label": "Ghost"},
60+
{"face": "\U0001f525\U0001f916", "label": "Fire Bot"},
61+
{"face": "\u2728", "label": "Sparkle"},
62+
]
63+
64+
ANSI_COLORED_FACES = [
65+
{"face": "\033[31m\u25d5\u203f\u25d5\033[0m", "label": "Red"},
66+
{"face": "\033[32m\u2b21\u203f\u2b21\033[0m", "label": "Green"},
67+
{"face": "\033[1;34m\u2299\u203f\u2299\033[0m", "label": "Bold Blue"},
68+
{"face": "\033[38;5;208m^_^\033[0m", "label": "Orange"},
69+
]
70+
71+
72+
def _make_card_line(face: str, label: str, target_width: int) -> str:
73+
"""Simulate a grid cell: '| <face> <label> |' padded to target_width."""
74+
inner = f" {face} {label} "
75+
return pad_to_display_width(inner, target_width)
76+
77+
78+
def _make_box_rows(face: str, label: str, inner_width: int) -> list:
79+
"""Build a simple boxed card: top border, face row, label row, bottom."""
80+
top = "\u2500" * (inner_width + 2)
81+
face_cell = pad_to_display_width(f" {face} ", inner_width + 2)
82+
label_cell = pad_to_display_width(f" {label} ", inner_width + 2)
83+
return [
84+
f"\u250c{top}\u2510",
85+
f"\u2502{face_cell}\u2502",
86+
f"\u2502{label_cell}\u2502",
87+
f"\u2514{top}\u2518",
88+
]
89+
90+
91+
# ===================================================================
92+
# 1. Unicode mode — standard agent faces
93+
# ===================================================================
94+
95+
class TestUnicodeFaceAlignment:
96+
"""Cards with Unicode faces (◕‿◕ etc.) maintain consistent width."""
97+
98+
def test_all_face_banners_have_matching_border_widths(self):
99+
for card in UNICODE_FACES:
100+
lines = render_face_banner(card["face"])
101+
top, middle, bottom = lines[0], lines[1], lines[2]
102+
assert display_width(top) == display_width(bottom), (
103+
f"Top/bottom mismatch for {card['label']}: "
104+
f"top={display_width(top)}, bottom={display_width(bottom)}"
105+
)
106+
107+
def test_padded_cells_have_equal_display_width(self):
108+
target = 20
109+
widths = []
110+
for card in UNICODE_FACES:
111+
cell = _make_card_line(card["face"], card["label"], target)
112+
w = display_width(cell)
113+
widths.append((card["label"], w))
114+
first_width = widths[0][1]
115+
for label, w in widths:
116+
assert w == first_width, (
117+
f"Width mismatch: {label}={w}, expected={first_width}"
118+
)
119+
120+
def test_render_box_line_uniform_width(self):
121+
width = 24
122+
for card in UNICODE_FACES:
123+
line = render_box_line(f" {card['face']} {card['label']} ", width)
124+
assert display_width(line) == width + 2, (
125+
f"box_line width for {card['label']}: "
126+
f"got {display_width(line)}, expected {width + 2}"
127+
)
128+
129+
130+
# ===================================================================
131+
# 2. ASCII fallback mode
132+
# ===================================================================
133+
134+
class TestAsciiFallbackAlignment:
135+
"""ASCII-only faces (o_o, ^_^) keep consistent width with no Unicode."""
136+
137+
def test_no_unicode_in_ascii_faces(self):
138+
for card in ASCII_FACES:
139+
for ch in card["face"]:
140+
assert ord(ch) < 128, (
141+
f"Non-ASCII char U+{ord(ch):04X} in face '{card['face']}'"
142+
)
143+
144+
def test_face_banner_border_match(self):
145+
for card in ASCII_FACES:
146+
lines = render_face_banner(card["face"])
147+
assert display_width(lines[0]) == display_width(lines[2])
148+
149+
def test_padded_cells_equal_width(self):
150+
target = 18
151+
widths = set()
152+
for card in ASCII_FACES:
153+
cell = _make_card_line(card["face"], card["label"], target)
154+
widths.add(display_width(cell))
155+
assert len(widths) == 1, f"Non-uniform widths: {widths}"
156+
157+
158+
# ===================================================================
159+
# 3. CJK text in labels
160+
# ===================================================================
161+
162+
class TestCjkLabelAlignment:
163+
"""Labels with CJK double-width characters stay aligned."""
164+
165+
def test_cjk_chars_are_double_width(self):
166+
assert display_width("\ubcf4\uc548") == 4 # 보안: 2 chars x 2 width
167+
assert display_width("\u5b89\u5168") == 4 # 安全: 2 chars x 2 width
168+
assert display_width("\uc811\uadfc\uc131") == 6 # 접근성: 3 chars x 2 width
169+
170+
def test_padded_cells_uniform_despite_cjk(self):
171+
target = 22
172+
widths = set()
173+
for card in CJK_LABELS:
174+
cell = _make_card_line(card["face"], card["label"], target)
175+
widths.add(display_width(cell))
176+
assert len(widths) == 1, f"CJK width drift: {widths}"
177+
178+
def test_box_rows_consistent(self):
179+
inner = 18
180+
for card in CJK_LABELS:
181+
rows = _make_box_rows(card["face"], card["label"], inner)
182+
expected = display_width(rows[0])
183+
for i, row in enumerate(rows):
184+
assert display_width(row) == expected, (
185+
f"Row {i} width mismatch for '{card['label']}': "
186+
f"got {display_width(row)}, expected {expected}"
187+
)
188+
189+
def test_mixed_cjk_ascii_labels(self):
190+
"""CJK and ASCII labels padded to same width produce equal display width."""
191+
target = 24
192+
mixed = [
193+
{"face": "\u25d5\u203f\u25d5", "label": "\ubcf4\uc548 Check"},
194+
{"face": "\u2b21\u203f\u2b21", "label": "Security"},
195+
{"face": "\u2299\u203f\u2299", "label": "\u6027\u80fd Test"},
196+
]
197+
widths = set()
198+
for card in mixed:
199+
cell = _make_card_line(card["face"], card["label"], target)
200+
widths.add(display_width(cell))
201+
assert len(widths) == 1, f"Mixed CJK/ASCII width drift: {widths}"
202+
203+
204+
# ===================================================================
205+
# 4. Emoji in face or status
206+
# ===================================================================
207+
208+
class TestEmojiAlignment:
209+
"""Emoji characters should not break alignment."""
210+
211+
def test_emoji_face_display_width(self):
212+
# Common emoji should be treated as double-width
213+
assert display_width("\U0001f916") == 2 # robot
214+
assert display_width("\U0001f47b") == 2 # ghost
215+
assert display_width("\U0001f525") == 2 # fire
216+
217+
def test_emoji_face_banners_aligned(self):
218+
for card in EMOJI_FACES:
219+
lines = render_face_banner(card["face"])
220+
assert display_width(lines[0]) == display_width(lines[2]), (
221+
f"Emoji banner mismatch for {card['label']}"
222+
)
223+
224+
def test_emoji_padded_cells_uniform(self):
225+
target = 20
226+
widths = set()
227+
for card in EMOJI_FACES:
228+
cell = _make_card_line(card["face"], card["label"], target)
229+
widths.add(display_width(cell))
230+
assert len(widths) == 1, f"Emoji cell width drift: {widths}"
231+
232+
233+
# ===================================================================
234+
# 5. ANSI color codes
235+
# ===================================================================
236+
237+
class TestAnsiColorAlignment:
238+
"""ANSI escape codes should have zero display width."""
239+
240+
def test_ansi_codes_zero_width(self):
241+
plain = "\u25d5\u203f\u25d5"
242+
colored = "\033[31m\u25d5\u203f\u25d5\033[0m"
243+
assert display_width(plain) == display_width(colored)
244+
245+
def test_strip_ansi_removes_all_codes(self):
246+
colored = "\033[1;38;5;208mhello\033[0m"
247+
assert strip_ansi(colored) == "hello"
248+
249+
def test_ansi_face_banners_match_plain(self):
250+
plain_face = "\u25d5\u203f\u25d5"
251+
ansi_face = "\033[31m\u25d5\u203f\u25d5\033[0m"
252+
plain_lines = render_face_banner(plain_face)
253+
ansi_lines = render_face_banner(ansi_face)
254+
for i in range(len(plain_lines)):
255+
assert display_width(plain_lines[i]) == display_width(ansi_lines[i]), (
256+
f"ANSI banner line {i} width differs from plain"
257+
)
258+
259+
def test_ansi_colored_cells_uniform(self):
260+
target = 22
261+
widths = set()
262+
for card in ANSI_COLORED_FACES:
263+
cell = _make_card_line(card["face"], card["label"], target)
264+
widths.add(display_width(cell))
265+
assert len(widths) == 1, f"ANSI cell width drift: {widths}"
266+
267+
268+
# ===================================================================
269+
# 6. Narrow terminal widths
270+
# ===================================================================
271+
272+
class TestNarrowTerminalAlignment:
273+
"""Grid should not break at narrow terminal widths."""
274+
275+
def test_box_line_at_40_columns(self):
276+
width = 38 # inner width for 40-col terminal (minus box borders)
277+
for card in UNICODE_FACES:
278+
line = render_box_line(
279+
f" {card['face']} {card['label']} ", width
280+
)
281+
assert display_width(line) == width + 2
282+
283+
def test_box_line_at_80_columns(self):
284+
width = 78 # inner width for 80-col terminal
285+
for card in UNICODE_FACES:
286+
line = render_box_line(
287+
f" {card['face']} {card['label']} ", width
288+
)
289+
assert display_width(line) == width + 2
290+
291+
def test_truncation_respects_narrow_width(self):
292+
long_text = "\u25d5\u203f\u25d5 Security Specialist \ubcf4\uc548"
293+
for w in (10, 15, 20, 30):
294+
result = truncate_to_display_width(long_text, w)
295+
assert display_width(result) <= w, (
296+
f"Truncated to {w} but got width {display_width(result)}"
297+
)
298+
299+
def test_all_card_types_at_narrow_width(self):
300+
"""All card types stay uniform at 40 columns."""
301+
all_cards = UNICODE_FACES + ASCII_FACES + CJK_LABELS
302+
target = 36
303+
widths = set()
304+
for card in all_cards:
305+
cell = _make_card_line(card["face"], card["label"], target)
306+
widths.add(display_width(cell))
307+
assert len(widths) == 1, f"Narrow-width drift across card types: {widths}"
308+
309+
310+
# ===================================================================
311+
# 7. Integration: mixed content in a single grid row
312+
# ===================================================================
313+
314+
class TestMixedGridRow:
315+
"""Simulate a row with different content types — all cells same width."""
316+
317+
def test_mixed_row_uniform(self):
318+
"""Unicode, CJK, emoji, ANSI cards in one row have equal width."""
319+
target = 26
320+
cards = [
321+
{"face": "\u25d5\u203f\u25d5", "label": "Plain"},
322+
{"face": "\u25d5\u203f\u25d5", "label": "\ubcf4\uc548"},
323+
{"face": "\U0001f916", "label": "Bot"},
324+
{"face": "\033[31m\u25d5\u203f\u25d5\033[0m", "label": "Red"},
325+
]
326+
widths = set()
327+
for card in cards:
328+
cell = _make_card_line(card["face"], card["label"], target)
329+
widths.add(display_width(cell))
330+
assert len(widths) == 1, f"Mixed row width drift: {widths}"
331+
332+
def test_face_banner_widths_stable_across_types(self):
333+
"""render_face_banner top/bottom always match for any face type."""
334+
faces = [
335+
"\u25d5\u203f\u25d5", # Unicode
336+
"o_o", # ASCII
337+
"\U0001f916", # Emoji
338+
"\033[32m\u25d5\u203f\u25d5\033[0m", # ANSI
339+
"\u2b21\u203f\u2b21", # Unicode alt
340+
]
341+
for face in faces:
342+
lines = render_face_banner(face)
343+
top_w = display_width(lines[0])
344+
bottom_w = display_width(lines[2])
345+
assert top_w == bottom_w, (
346+
f"Face banner mismatch for '{strip_ansi(face)}': "
347+
f"top={top_w}, bottom={bottom_w}"
348+
)

0 commit comments

Comments
 (0)