Files
MeshTerm/tests/conftest.py
T
Jean-Pierre MartineauandClaude Opus 5 59f10f904c The escape table was a list where it needed to be a category
Turning markup off on the scripted console stopped a node name being read
as a style. It also stopped every tag MeshTerm writes *itself* from being
consumed — so `config export-key > key.hex` wrote a file with
`[muted]` and `[/muted]` around the private key that was supposed to be
its whole content. `PlainUi.note` is documented to take markup and now
resolves it to text, which fixes the leak wherever it is written rather
than only where it was found; the key itself is bare on both faces.

Escaping covered the newline, the tab and the return, and let ESC
through — a live colour run written into the caller's file, which is the
one thing "not a single escape sequence" exists to prevent. BEL was
worse: dropped in silence, so the printed name was not the advertised
one, and U+2028 ended the record like a newline. It is now every C0 and
C1 control plus the two Unicode separators, named escapes where one
reads and numeric everywhere else. Legitimate non-ASCII is untouched.

The covered-command list is now walked out of the Typer app instead of
kept by hand. It was six commands of forty-nine, and both violations
above sat in the forty-three — the `---` in `about` drawn as a rule the
width of a 16384-cell console, and the tags above. Verified by putting
each bug back and watching the sweep name it.

Three assertions could not fail. `len(set(MEANINGS)) == len(MEANINGS)`
is true of every dict, so "no two outcomes share a status" was unenforced
— renumbering NO_DEVICE to 4 left all three green while status 3 vanished
from --help. `" " * script.GUTTER in record` reads the constant it
checks, narrowing to `" " in record` exactly when the gutter narrows.
Both are literals now.

The four rule tests read `result.output`, which is stdout and stderr
mixed, while the rules they enforce are about stdout — so a heading that
only ever appeared on stderr would have satisfied them.

And $MESHTERM_HOME is isolated for every test, not just the CLI ones:
--db moves the history and nothing else, so any test touching a store,
a preference or the logger was reading the developer's own directory.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-08 02:52:30 -04:00

130 lines
5.4 KiB
Python

"""Shared fixtures and helpers: plain-text screen reading, and pinned path rendering.
:func:`plain` is THE way a test reads a rendered screen — every screen test needs the
same move (strip the ANSI colour escapes, join the rendered lines) before it can assert
on content, and each file re-deriving it is how the copies drift.
The powerline verdict is environmental — the suite may run inside VS Code or Windows
Terminal (both chip-capable) or a bare CI shell (not) — and screen assertions must
not change with the developer's glass. ``MESHTERM_POWERLINE=0`` is the supported pin;
the cached verdict is cleared around each test so no ordering leaks it. Powerline-
specific tests pass explicit modes or monkeypatch the widget's own switch.
``NO_COLOR`` is environmental the same way: Rich honours it by stripping colour while
keeping attributes, which silently voids every colour assertion in the suite (a shell
run by an agent harness sets it). The autouse fixture below deletes it, so the tests
always see the colours the app really emits.
"""
from __future__ import annotations
import os
import re
from collections.abc import Callable, Iterable, Iterator
import pytest
from meshterm.platforms import REGULAR, set_platform
from meshterm.ui.termfont import powerline_support
#: ANSI SGR escapes — the colour runs a rendered line carries between its characters.
_ANSI = re.compile(r"\x1b\[[0-9;]*m")
def plain(rendered: str | Iterable[str]) -> str:
"""The plain-text form of rendered screen output, for content assertions.
Takes what a render handed back — a list of ANSI lines (joined with newlines) or a
single already-joined string — and strips the colour escapes, so assertions read
the screen the way a viewer does.
"""
if not isinstance(rendered, str):
rendered = "\n".join(rendered)
return _ANSI.sub("", rendered)
@pytest.fixture(autouse=True)
def _plain_path_rendering(monkeypatch: pytest.MonkeyPatch) -> Iterator[None]:
monkeypatch.setenv("MESHTERM_POWERLINE", "0")
# An inherited NO_COLOR makes Rich strip colour (attributes kept) and every colour
# assertion silently reads bare text — see the module docstring.
monkeypatch.delenv("NO_COLOR", raising=False)
powerline_support.cache_clear()
yield
powerline_support.cache_clear()
@pytest.fixture
def powerline(monkeypatch: pytest.MonkeyPatch) -> Callable[[bool], None]:
"""Pin the powerline verdict for a test that is *about* chips.
The suite runs with ``MESHTERM_POWERLINE=0`` so screen assertions don't change with
the developer's glass (see the module docstring); a test of the chip language itself
needs the other answer, and asks for it through the supported override rather than by
reaching into the widget. ``monkeypatch`` restores the env and the autouse fixture
above clears the memoized verdict either side, so nothing leaks.
"""
def _set(on: bool) -> None:
monkeypatch.setenv("MESHTERM_POWERLINE", "1" if on else "0")
powerline_support.cache_clear()
return _set
@pytest.fixture(autouse=True)
def _reset_transmit_gate() -> Iterator[None]:
"""Every test starts having transmitted nothing.
The transmit clock is process-wide (see :mod:`meshterm.core.transmit_gate`), which is
right for a session and wrong for a suite: a test that sends anything would otherwise
leave a cooldown standing for whatever ran next, and the next test's flow would stop
on a countdown nobody scripted.
"""
from meshterm.core.transmit_gate import current as transmit_clock
transmit_clock().reset()
yield
transmit_clock().reset()
@pytest.fixture(autouse=True)
def _isolate_config_dir(tmp_path_factory: pytest.TempPathFactory) -> Iterator[None]:
"""No test may reach the developer's own ``~/.meshterm``.
``$MESHTERM_HOME`` moves the whole directory — the history, the contact and channel
caches, the outbox, the stored admin passwords, the device profiles, the preferences
and the log. ``--db`` moves only the first of those, which is why isolating a CLI test
with ``--db`` alone was not isolation at all: a synthetic contact written by a probe
sat in the real cache and made ``test_cli_contract`` fail on one machine and pass on
every other, and every run appended to the developer's real ``meshterm.log`` and read
their real ``log_level``.
Autouse and unconditional, because the exposure is not the CLI's: anything that
touches ``Settings.load()``, a preference, a store or the logger has it. A test that
wants a *populated* directory sets the variable itself; ``monkeypatch.setenv`` inside
a test still wins over this.
"""
home = tmp_path_factory.mktemp("meshterm-home")
previous = os.environ.get("MESHTERM_HOME")
os.environ["MESHTERM_HOME"] = str(home)
yield
if previous is None:
os.environ.pop("MESHTERM_HOME", None)
else:
os.environ["MESHTERM_HOME"] = previous
@pytest.fixture(autouse=True)
def _reset_platform() -> Iterator[None]:
"""Every test starts and ends on :data:`~meshterm.platforms.REGULAR`.
A test that calls ``set_platform(PICOCALC)`` (directly, or via the gallery harness)
can't leak that choice into whatever runs next — regular is the suite's baseline, the
same way it is the default for a process that never passes ``--platform``/
``MESHTERM_PLATFORM`` (see :mod:`meshterm.platforms`).
"""
set_platform(REGULAR)
yield
set_platform(REGULAR)