Files
MeshTerm/tests/test_macterminal.py
T
Jean-Pierre MartineauandClaude Opus 5 41c2cf416b The profile says a path, so three launches leave one profile
Verified on the Mac, and three of the four things it taught were the
opposite of what the code assumed.

RunCommandAsShell is false. Under *true* Terminal takes the whole command
string as the name of a program and the window reads "Command not found:
MESHTERM_REOPENED=1 / Could not create a new process and open a
pseudo-tty". The key's name says otherwise; the measurement wins.

The environment rides on /usr/bin/env rather than on a VAR=value prefix,
for the same reason. env is a real executable, so the line is valid as a
bare argv and as a shell command both, and the window cannot land on that
message however Terminal decides to read the string.

Terminal names an imported settings set after the FILE, not the name key
inside it -- and re-importing the same bytes reuses that profile, while
different bytes under the same filename add "MeshTerm 1", then
"MeshTerm 2". Opening one file four times, three identical and one a word
apart, left exactly two profiles. So the command cannot live in the
profile: it carries argv and the reader's MESHTERM_* variables, and a
profile per command line would fill their settings list with junk only
they can remove. The variable part moved behind a fixed path instead --
the profile says ~/.meshterm/launch.sh and never changes, and the script
underneath says whatever this launch needs.

Three launches with three different command lines now leave one profile
named MeshTerm, the window drawing CascadiaMonoPL-Regular at 13pt on
opaque black at 80x30, with the app running in it. The reader's default
profile is still theirs, blur and all.

shellExitAction 1 while here: tidy the window away on a clean exit, keep
it on a crash, because a crash nobody sees is a bug report nobody writes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-19 16:42:51 -04:00

215 lines
8.8 KiB
Python

# SPDX-License-Identifier: Apache-2.0
"""The macOS Terminal profile: what it says, and what it refuses to touch.
The archive shapes here were not invented. They were read back off a profile installed on
a real Mac and confirmed rendering — so the encoder is pinned to bytes that are known to
work, rather than to a reading of Apple's format.
"""
from __future__ import annotations
import plistlib
import sys
from pathlib import Path
import pytest
from meshterm.core import macterminal
from meshterm.core.relaunch import REOPENED_ENV
from meshterm.ui.theme import _VT_SLOTS
def test_color_archive_is_the_shape_appkit_writes() -> None:
"""A colour decodes to a calibrated-RGB ``NSColor``, six places and NUL-terminated."""
decoded = plistlib.loads(macterminal.ns_color("#aa0000"))
assert decoded["$archiver"] == "NSKeyedArchiver"
assert decoded["$version"] == 100000
colour = decoded["$objects"][1]
assert colour["NSColorSpace"] == 1
assert colour["NSRGB"] == b"0.666667 0.000000 0.000000\x00"
assert decoded["$objects"][2]["$classname"] == "NSColor"
def test_font_archive_names_the_bundled_face() -> None:
"""The font archive carries the PostScript name, not the family name."""
decoded = plistlib.loads(macterminal.ns_font())
assert macterminal.FONT_FACE in decoded["$objects"]
assert decoded["$objects"][1]["NSSize"] == macterminal.FONT_SIZE
assert decoded["$objects"][3]["$classname"] == "NSFont"
def test_every_palette_slot_comes_from_the_theme() -> None:
"""The window cannot disagree with the screen it is holding.
Sixteen keys, in the dim-then-bright order Terminal expects, each one the theme's own
hex. A slot changed in :data:`~meshterm.ui.theme._VT_SLOTS` lands here on the next
write, which is the reason the profile is generated rather than shipped.
"""
profile = macterminal.build_profile()
assert len(macterminal._ANSI_KEYS) == len(_VT_SLOTS) == 16
for key, (_slot, _label, rgb) in zip(macterminal._ANSI_KEYS, _VT_SLOTS, strict=True):
assert profile[key] == macterminal.ns_color(rgb), key
def test_the_window_is_opaque_and_bold_is_not_brightness() -> None:
"""The two settings the profile exists to guarantee, beside the font."""
profile = macterminal.build_profile()
assert profile["BackgroundColor"] == macterminal.ns_color("#000000")
assert "BackgroundBlur" not in profile
assert profile["UseBrightBold"] is False
# Tidy itself away on a clean exit, stay put on a crash the reader needs to read.
assert profile["shellExitAction"] == 1
def test_the_profile_is_the_same_bytes_whatever_the_command_line() -> None:
"""The finding that shaped the module, pinned.
Terminal names an imported settings set after the *file*, reuses it when the bytes
match, and adds "MeshTerm 1" when they do not. So the profile must not carry anything
that varies between launches — the launcher path is fixed, and the varying command
lives in the file it points at.
"""
import plistlib
plain = plistlib.dumps(macterminal.build_profile("/Users/x/.meshterm/launch.sh"))
again = plistlib.dumps(macterminal.build_profile("/Users/x/.meshterm/launch.sh"))
assert plain == again
# And what actually goes in is a path, never an argv or an environment.
profile = macterminal.build_profile("/Users/x/.meshterm/launch.sh")
assert profile["CommandString"] == "/Users/x/.meshterm/launch.sh"
assert "--mock" not in str(profile["CommandString"])
assert "MESHTERM_" not in str(profile["CommandString"])
def test_the_launcher_carries_the_command_and_is_executable(
tmp_path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""What varies lives here instead, regenerated on every launch."""
monkeypatch.setenv("MESHTERM_HOME", str(tmp_path / "home"))
monkeypatch.setattr(sys, "argv", ["meshterm", "--mock"])
path = macterminal.write_launcher()
body = path.read_text(encoding="utf-8")
assert path.name == "launch.sh"
assert body.startswith("#!/bin/sh")
assert "exec /usr/bin/env " in body
assert body.rstrip().endswith("--mock")
if sys.platform != "win32": # NTFS has no execute bit for chmod to set
assert path.stat().st_mode & 0o111
def test_a_bare_profile_runs_nothing() -> None:
"""Without a command it is an entry in the list, not a launcher."""
profile = macterminal.build_profile()
assert "CommandString" not in profile
assert "RunCommandAsShell" not in profile
def test_the_profile_round_trips_as_a_terminal_file() -> None:
"""What is written is what Terminal reads: a plist, with the nested archives intact."""
written = plistlib.dumps(macterminal.build_profile("true"), fmt=plistlib.FMT_XML)
reloaded = plistlib.loads(written)
assert reloaded["name"] == macterminal.PROFILE_NAME
assert reloaded["type"] == "Window Settings"
assert reloaded["CommandString"] == "true"
assert plistlib.loads(reloaded["ANSIRedColor"])["$objects"][1]["NSColorSpace"] == 1
def test_launch_command_carries_meshterm_variables_and_the_marker(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Terminal starts the command from a fresh shell, so what it needs travels with it."""
monkeypatch.setenv("MESHTERM_HOME", "/Users/someone/meshterm test")
monkeypatch.delenv(REOPENED_ENV, raising=False)
monkeypatch.setattr(sys, "argv", ["meshterm", "--mock"])
command = macterminal.launch_command()
assert f"{REOPENED_ENV}=1" in command
# Quoted, because the reader's path may contain a space and the shell would split it.
assert "MESHTERM_HOME='/Users/someone/meshterm test'" in command
assert command.endswith("--mock")
# A real executable, not a `VAR=value` shell prefix: measured on macOS 26, Terminal
# reads the string as a bare program name under one setting and as a shell line under
# the other, and a prefix puts "Command not found: MESHTERM_REOPENED=1" on the screen.
assert command.startswith("/usr/bin/env ")
def test_an_unrelated_variable_is_not_carried(monkeypatch: pytest.MonkeyPatch) -> None:
"""The hop is not a chance to launder the whole environment into a new shell."""
monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", "nope")
assert "AWS_SECRET_ACCESS_KEY" not in macterminal.launch_command()
def test_a_reopened_session_never_reopens_again(monkeypatch: pytest.MonkeyPatch) -> None:
"""The guard that keeps a relaunch from chaining, since the profile's command sets it."""
monkeypatch.setattr(sys, "platform", "darwin")
monkeypatch.setenv("TERM_PROGRAM", "Apple_Terminal")
monkeypatch.setenv(REOPENED_ENV, "1")
assert macterminal.available() is False
assert macterminal.reopen() is False
@pytest.mark.parametrize("program", ["iTerm.app", "ghostty", "vscode", "WezTerm"])
def test_another_terminal_is_left_alone(program: str, monkeypatch: pytest.MonkeyPatch) -> None:
"""Nothing is offered to a terminal that already has truecolor and a chosen font."""
monkeypatch.setattr(sys, "platform", "darwin")
monkeypatch.delenv(REOPENED_ENV, raising=False)
monkeypatch.setenv("TERM_PROGRAM", program)
assert macterminal.in_apple_terminal() is False
assert macterminal.available() is False
def test_nothing_happens_off_macos(monkeypatch: pytest.MonkeyPatch) -> None:
"""Every entry point is inert on the platforms this module is not about."""
monkeypatch.setattr(sys, "platform", "win32")
monkeypatch.setenv("TERM_PROGRAM", "Apple_Terminal")
assert macterminal.in_apple_terminal() is False
assert macterminal.available() is False
assert macterminal.reopen() is False
assert macterminal.install_font() is False
def test_the_profile_is_written_under_meshterm_home(
tmp_path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""It lives in MeshTerm's own directory, and follows ``MESHTERM_HOME`` when set."""
monkeypatch.setenv("MESHTERM_HOME", str(tmp_path / "home"))
path = macterminal.write_profile()
assert path == tmp_path / "home" / "MeshTerm.terminal"
assert plistlib.loads(path.read_bytes())["name"] == "MeshTerm"
def test_the_readers_preferences_are_never_named(tmp_path, monkeypatch) -> None:
"""Terminal discards external edits to its prefs on quit, so nothing here writes them.
A guard against the obvious future shortcut rather than against today's code: the
whole design rests on handing Terminal a file and letting it do the importing.
"""
source = (macterminal.__file__).replace(".pyc", ".py")
with open(source, encoding="utf-8") as handle:
body = handle.read().split('"""', 2)[2]
assert "com.apple.Terminal.plist" not in body
assert "Library/Preferences" not in body
assert "defaults" not in body.replace("default_config_dir", "")
def test_the_font_goes_to_the_users_own_library() -> None:
"""No administrator rights, and undoable from Font Book."""
assert macterminal.USER_FONT_DIR == Path.home() / "Library" / "Fonts"