Skip to content

Commit 6b52094

Browse files
committed
Redesign mascot as single-line, fix ASCII alignment; bump to 0.1.1
The 5-line hand-drawn ASCII ghost body didn't align cleanly (text columns drifted since each face line had a different length) and looked crude compared to just using the real ghost emoji, which renders fine on any modern terminal. Replaced it with a single-line icon (star-flanked emoji, colored per state) plus a plain ASCII face fallback (o o)/(O O)/(x x) for terminals that can't render it -- both paths now use a fixed-width icon so the summary/detail text lines up. Version bump reflects the mascot feature and the judge-cache/env-var fixes shipped this session.
1 parent cb1a51f commit 6b52094

5 files changed

Lines changed: 59 additions & 71 deletions

File tree

CHANGELOG.md

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,27 @@ All notable changes to ghostrun are documented here. The format is based on
66

77
## [Unreleased]
88

9+
## [0.1.1] - 2026-07-28
10+
911
### Added
10-
- **Terminal mascot** — a small ASCII ghost printed once at the end of a test
11-
session, reacting to what the interceptor actually did: calm when every
12-
call replayed from cache, alert (yellow) when the network was actually
13-
touched, and a distinct miss face (red) when `--ghostrun-replay` hit an
14-
uncached request. Color and emoji only render on a real UTF-8-capable TTY;
15-
silent on CI/piped output, and always silenceable via `GHOSTRUN_NO_MASCOT=1`.
12+
- **Terminal mascot** — a one-line marker printed once at the end of a test
13+
session, reacting to what the interceptor actually did: calm (`☆ 👻 ☆`,
14+
cyan) when every call replayed from cache, alert (yellow) when the network
15+
was actually touched, and a distinct miss face (red) when
16+
`--ghostrun-replay` hit an uncached request. Falls back to a plain ASCII
17+
face (`(o o)` / `(O O)` / `(x x)`) on terminals/codepages that can't render
18+
the emoji. Silent on CI/piped output and `NO_COLOR`, and always
19+
silenceable via `GHOSTRUN_NO_MASCOT=1`.
20+
21+
### Fixed
22+
- Judge verdict caching ignored a per-test `cache_dir` override passed to
23+
`@ghostrun.record()`, so verdicts silently leaked into the global cache dir
24+
instead of living next to the test as documented — the root cause of every
25+
CI matrix job failing on `--ghostrun-replay`. `recording()` now scopes
26+
`cache_dir`/`mode` onto the active config for the run.
27+
- `GHOSTRUN_*` environment variables were misnamed `ghostrun_*` (lowercase
28+
prefix) across the codebase, docs, and CI following the gentest→ghostrun
29+
rename.
1630
- **`ghostrun init`** — scaffolds a working first test in one command. Detects
1731
whether `openai` or `anthropic` is importable in the project and generates
1832
a matching starter test (or a generic httpx-based one if neither is found),

ghostrun/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ def test_reply():
2424
from .interceptor import CacheMiss, UnsupportedHttpx
2525
from .record import record, recording
2626

27-
__version__ = "0.1.0"
27+
__version__ = "0.1.1"
2828

2929

3030
def configure(**kwargs) -> Config:

ghostrun/mascot.py

Lines changed: 36 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
"""The ghostrun terminal mascot: a one-line-per-session ASCII ghost that reacts
2-
to what the interceptor actually did (replayed from cache, hit the network, or
1+
"""The ghostrun terminal mascot: a one-line-per-session marker that reacts to
2+
what the interceptor actually did (replayed from cache, hit the network, or
33
missed in strict replay mode).
44
55
Deliberately shown at most once, at the end of a test session -- never per-line
@@ -14,39 +14,24 @@
1414
import sys
1515

1616
RESET = "\x1b[0m"
17-
DIM = "\x1b[2m"
1817
BOLD = "\x1b[1m"
18+
DIM = "\x1b[2m"
1919

2020
_COLOR = {
2121
"replayed": "\x1b[36m", # cyan -- calm, nothing cost anything
2222
"recorded": "\x1b[33m", # yellow -- the network was actually touched
2323
"miss": "\x1b[31m", # red -- something's wrong
2424
}
2525

26-
_EMOJI = {"replayed": "👻", "recorded": "🌐", "miss": "⚠️"}
27-
28-
_FACES = {
29-
"replayed": [
30-
" .-''''-.",
31-
" / o o \\",
32-
" | .. |",
33-
" \\ '--' /",
34-
" `------`",
35-
],
36-
"recorded": [
37-
" .-''''-.",
38-
" / O O \\",
39-
" | o |",
40-
" \\ '--' /",
41-
" `------`",
42-
],
43-
"miss": [
44-
" .-''''-.",
45-
" / x x \\",
46-
" | /\\ |",
47-
" \\ '--' /",
48-
" `------`",
49-
],
26+
# The real 👻 emoji is the mascot on any terminal that can render it -- it
27+
# already looks better than anything drawn from punctuation. ASCII faces are
28+
# only the fallback for terminals/codepages that can't show it.
29+
_ASCII_FACE = {"replayed": "(o o)", "recorded": "(O O)", "miss": "(x x)"}
30+
31+
_DETAIL = {
32+
"replayed": "no network touched",
33+
"recorded": "the network was touched -- new cache written",
34+
"miss": "re-run with --ghostrun-record to fix",
5035
}
5136

5237

@@ -68,29 +53,17 @@ def _supports_color(stream) -> bool:
6853
return bool(getattr(stream, "isatty", lambda: False)())
6954

7055

71-
def _supports_emoji(stream) -> bool:
56+
def _supports_unicode(stream) -> bool:
7257
encoding = getattr(stream, "encoding", None) or ""
7358
return "UTF" in encoding.upper()
7459

7560

76-
def _summary_line(stats: dict, state: str, emoji: bool, unicode_safe: bool) -> str:
77-
parts = [
78-
f"{stats.get('replayed', 0)} replayed",
79-
f"{stats.get('recorded', 0)} recorded",
80-
]
81-
if stats.get("misses", 0):
82-
parts.append(f"{stats['misses']} missed")
83-
mark = f"{_EMOJI[state]} " if emoji else ""
84-
sep = "·" if unicode_safe else "-"
85-
return f"{mark}ghostrun {sep} " + ", ".join(parts)
86-
87-
8861
def render(stats: dict, stream=None) -> str:
89-
"""Render the mascot block for this session's stats, or "" to show nothing.
62+
"""Render the mascot line(s) for this session's stats, or "" for nothing.
9063
9164
Returns an empty string when nothing ghostrun-related happened (no
9265
replays, records, or misses) so a suite that never calls @ghostrun.record
93-
doesn't get an unexplained ghost printed at it.
66+
doesn't get an unexplained mascot printed at it.
9467
"""
9568
stream = stream if stream is not None else sys.stdout
9669
state = _state(stats)
@@ -100,27 +73,29 @@ def render(stats: dict, stream=None) -> str:
10073
return ""
10174

10275
color = _supports_color(stream)
103-
emoji = color and _supports_emoji(stream)
104-
105-
face = _FACES[state]
106-
summary = _summary_line(stats, state, emoji, unicode_safe=emoji)
107-
detail = {
108-
"replayed": "no network touched",
109-
"recorded": "the network was touched -- new cache written",
110-
"miss": "re-run with --ghostrun-record to fix",
111-
}[state]
76+
unicode_safe = color and _supports_unicode(stream)
11277

11378
c = _COLOR[state] if color else ""
11479
r = RESET if color else ""
80+
bold = BOLD if color else ""
11581
dim = DIM if color else ""
82+
sep = "·" if unicode_safe else "-"
11683

117-
lines = []
118-
for i, art_line in enumerate(face):
119-
colored_art = f"{c}{art_line}{r}"
120-
if i == 0:
121-
lines.append(f"{colored_art} {BOLD if color else ''}{summary}{r}")
122-
elif i == 1:
123-
lines.append(f"{colored_art} {dim}{detail}{r}")
124-
else:
125-
lines.append(colored_art)
126-
return "\n" + "\n".join(lines) + "\n"
84+
if unicode_safe:
85+
icon = f"{c}{r} \U0001f47b {c}{r}"
86+
icon_width = 5 # visual columns: star, space, ghost(~2), space, star
87+
else:
88+
icon = f"{c}{_ASCII_FACE[state]}{r}"
89+
icon_width = len(_ASCII_FACE[state])
90+
91+
parts = [f"{stats.get('replayed', 0)} replayed", f"{stats.get('recorded', 0)} recorded"]
92+
if stats.get("misses", 0):
93+
parts.append(f"{stats['misses']} missed")
94+
summary = f"{bold}ghostrun {sep} " + ", ".join(parts) + r
95+
detail = f"{dim}{_DETAIL[state]}{r}"
96+
97+
pad = " " * icon_width
98+
return (
99+
f"\n{icon} {summary}"
100+
f"\n{pad} {detail}\n"
101+
)

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "ghostrun"
7-
version = "0.1.0"
7+
version = "0.1.1"
88
description = "pytest for LLMs: deterministic HTTP record/replay, local LLM-as-judge semantic assertions, and prompt regression diffing for testing GenAI applications."
99
readme = "README.md"
1010
requires-python = ">=3.9"

tests/test_mascot.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,7 @@ def test_opt_out_env_var(monkeypatch):
4545
def test_state_selection(stats, expected_state, monkeypatch):
4646
monkeypatch.delenv("GHOSTRUN_NO_MASCOT", raising=False)
4747
block = mascot.render(stats, stream=FakeStream(isatty=False))
48-
face_line = mascot._FACES[expected_state][1] # eyes line is state-distinctive
49-
assert face_line in block
48+
assert mascot._ASCII_FACE[expected_state] in block
5049

5150

5251
def test_color_and_emoji_only_on_tty_utf8(monkeypatch):

0 commit comments

Comments
 (0)