Fix three small CLI rough edges: records --width, the simulator's label, two help pages

records --width now refuses anything but 1, 2, or 4 with a usage error, the
way --category already refused an unknown discipline. A width that doesn't
exist used to come back as exit 5, indistinguishable from an empty database,
and --width 0 was falsy and so listed every width. (#15)

A device with no port is labelled by its name alone. The simulator fell
through to the serial branch and came out as "… (nothing transmits) ()". (#14)

platform and specimen pass their own one-line help, so Typer stops printing
their docstrings — notes for whoever reads cli.py, Sphinx roles and source
paths included — and their summaries fit in meshterm --help. A new test runs
--help on every registered command and refuses those markers anywhere. (#16)

Built from the issues alone; the pull requests filed against them were
closed unreviewed and their code was never read.

Closes #14, closes #15, closes #16.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jean-Pierre MartineauandClaude Opus 5.5 committed 2026-09-27 10:59:17 -04:00
1 parent 1949d5b1a3
commit 0cd40bbd6c
6 files changed
+103 -5

No files matched your search

+12
View File
@@ -9,6 +9,18 @@ one caveat SemVer makes for a leading zero: while the major version is still `0`
## [Unreleased]
### Fixed
- **`meshterm records --width` refuses a width that doesn't exist.** Hash widths are 1, 2,
or 4. Any other number used to look like an empty result, so a typo and an empty
database gave the same answer, and `--width 0` was ignored and listed every width. Now
it's a usage error that names the three choices, like `--category` already was.
- **The simulator's row in `meshterm --mock devices` no longer ends in an empty `()`.**
The brackets hold a serial port, and the simulator doesn't have one.
- **`meshterm platform --help` and `meshterm specimen --help` read like the others.** They
used to print notes meant for someone reading the source code, markup and all, and cut
their summaries short in `meshterm --help`. Now each has one plain line.
## [0.10.0] — 2026-09-25
**Regions, a uConsole with no bridge, and Bluetooth that works on Linux.** MeshTerm now
+8 -2
View File
@@ -603,7 +603,10 @@ def _report_no_windows_console(console: Console) -> None:
console.print("try [accent]meshterm --help[/accent].")
@app.command(name="platform")
@app.command(
name="platform",
help="Show which platform MeshTerm runs as, and why",
)
def platform_command() -> None:
"""Print the resolved platform and every input the resolution looked at.
@@ -650,7 +653,10 @@ def platform_command() -> None:
)
@app.command(name="specimen")
@app.command(
name="specimen",
help="Print every mark, icon, and colour MeshTerm draws",
)
def specimen_command() -> None:
"""Print the visual-language specimen for the active platform.
+2
View File
@@ -273,6 +273,8 @@ class DiscoveredDevice:
if self.is_spi:
return f"{self.name or 'SPI radio'} ({self.port})"
name = self.product or self.description or self.vendor_label or "Serial device"
if not self.port: # the simulator has nothing to point at, so no "()"
return name
suffix = f"({self.port})"
if name.endswith(suffix): # avoid "… (COM11) (COM11)"
name = name[: -len(suffix)].rstrip()
+10 -1
View File
@@ -26,6 +26,9 @@ from ..services.records import CATEGORIES, CATEGORY_BY_ID, Category
from ..ui.script import NONE
from .base import Tool, ToolResult, register
#: The hash widths a record can be set at: MeshCore's path hashes are 1, 2 or 4 bytes.
WIDTHS = (1, 2, 4)
if TYPE_CHECKING: # pragma: no cover - typing only
from ..persistence.repository import DiscoveredPath
from ..ui.fields import NodeRef
@@ -183,7 +186,13 @@ class TrophyCaseTool(Tool):
f"--category must be one of: {choices} (got {category!r})"
)
tool_params["category"] = category
if width:
if width is not None:
# The same closed set as --category, for the same reason: `--width 3`
# found nothing and exited 5, indistinguishable from an empty database,
# and `--width 0` was falsy and so quietly listed every width.
if width not in WIDTHS:
choices = ", ".join(map(str, WIDTHS))
raise typer.BadParameter(f"--width must be one of: {choices} (got {width})")
tool_params["width"] = width
run_tool_command(self, tool_params)
+55 -1
View File
@@ -566,8 +566,19 @@ def test_a_destructive_command_without_yes_is_a_silent_usage_error(run, args) ->
[
(("contacts", "--sort", "bogus"), "--sort"),
(("records", "--category", "long_hual"), "--category"),
(("records", "--width", "3"), "--width"),
(("records", "--width", "0"), "--width"),
(("records", "--width", "-1"), "--width"),
(("records", "--json", "--width", "3"), "--width"),
],
ids=[
"contacts-sort",
"records-category",
"records-width-3",
"records-width-0",
"records-width-negative",
"records-width-3-json",
],
ids=["contacts-sort", "records-category"],
)
def test_a_value_outside_a_closed_set_is_refused_rather_than_ignored(run, args, flag) -> None: # noqa: ANN001
"""A typo in a closed-set option used to be answered with a different question.
@@ -754,6 +765,49 @@ def test_a_closing_message_goes_to_stderr_too(run) -> None: # noqa: ANN001
assert "records stored" in result.stderr
@pytest.mark.parametrize("width", ["1", "2", "4"])
def test_every_real_hash_width_is_accepted(run, width) -> None: # noqa: ANN001
"""Refusing the widths that don't exist must not refuse the three that do."""
result = run("records", "--width", width)
assert result.exit_code == exitcodes.NO_RESULT, result.stderr
assert "records stored" in result.stderr
def test_the_simulator_is_listed_without_empty_brackets(run) -> None: # noqa: ANN001
"""``--mock devices`` named the simulator ``… (nothing transmits) ()``: a port it lacks."""
result = run("devices")
assert result.exit_code == exitcodes.OK, result.stderr
assert "the built-in simulator (nothing transmits)" in result.stdout
assert "()" not in result.stdout
#: What a docstring written for the source leaks into ``--help`` when Typer is left to
#: use it: a Sphinx role, reST's double backticks, or a path into the package.
_SOURCE_NOTES = re.compile(r":(func|mod|class|meth|attr|data):`|``|meshterm/[\w/]+\.py")
@pytest.mark.parametrize("leaf", _leaf_commands(), ids=lambda leaf: "-".join(leaf))
def test_every_command_help_is_written_for_the_terminal(run, leaf) -> None: # noqa: ANN001
"""A help page is for someone at a prompt asking what a command does.
``platform`` and ``specimen`` printed their whole docstrings — notes for whoever reads
``cli.py``, Sphinx roles and source paths included — and the same markup cut their
one-line summaries short in ``meshterm --help``.
"""
result = run(*leaf, "--help")
assert result.exit_code == exitcodes.OK, result.output
assert not _SOURCE_NOTES.search(result.output), result.output
def test_the_command_list_has_no_source_notes(run) -> None: # noqa: ANN001
"""The one-line summaries in ``meshterm --help`` come from the same help text."""
result = run("--help")
assert not _SOURCE_NOTES.search(result.output), result.output
for line in result.output.splitlines():
if line.split()[:1] in (["platform"], ["specimen"]):
assert not line.rstrip().endswith("..."), f"summary cut short: {line!r}"
# -- the simulator's own history ------------------------------------------------------
+16 -1
View File
@@ -14,7 +14,7 @@ import pytest
from meshterm.core.config import DeviceProfile
from meshterm.core.device_store import DeviceStore
from meshterm.core.discovery import DiscoveredDevice, discover_devices
from meshterm.core.discovery import TRANSPORT_MOCK, DiscoveredDevice, discover_devices
from meshterm.core.selection import DeviceSelectionError, resolve_device
@@ -112,6 +112,21 @@ def test_label_does_not_repeat_the_port() -> None:
assert DiscoveredDevice("COM5", product="Wio SX1262").label == "Wio SX1262 (COM5)"
def test_a_device_with_no_port_gets_no_empty_brackets() -> None:
"""The simulator has nothing to point at, so its label ends at its name, not in "()".
The serial branch is also where every transport without one of its own lands, and it
appended ``({port})`` whether or not there was a port.
"""
sim = DiscoveredDevice(
transport=TRANSPORT_MOCK,
description="the built-in simulator (nothing transmits)",
name="simulator",
)
assert sim.label == "the built-in simulator (nothing transmits)"
assert DiscoveredDevice(product="Wio SX1262").label == "Wio SX1262"
def test_stable_id_precedence() -> None:
"""stable_id prefers serial number, then vid:pid, then the port name."""
assert DiscoveredDevice("COM5", serial_number="SN1", vid=1, pid=2).stable_id == "sn:SN1"