Each test docstring states the behaviour that the test proves, in the
simple present. No assertion message, test id, or other string literal
changed. Four statements now agree with the code: the Trace highlight
stays on the last row, a menu count, and two others.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The meshcore library does not serialize commands, and every command
waiting for "OK or ERROR" takes the first ERROR to arrive. At connect the
channel prewarm runs beside the clock sync, which this companion refuses
as malformed; a slot read handed that refusal took it for "no such slot",
ended the probe there, and cached a list missing every channel after it
for the rest of the session.
Only the firmware's NOT_FOUND (or a code-less refusal from old firmware)
now ends the list. Any other refusal is asked again once, and a second
one, or no answer at all, is a failed read the probe reports as
unfinished and does not cache.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Apache-2.0 does not require per-file headers, but a file lifted out of the tree carried no marking at all. One comment line, first (or right after a shebang), on every .py under meshterm/, tests/, packaging/ and scripts/, plus the extensionless SPI bridge; tests/test_spdx.py keeps a new file from landing without one.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Rename the package directory, imports, brand strings, env-var prefixes,
console-script entry point, and the config/data dir (~/.config/meshterm).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Channel messages carry only a slot index on the wire, which the firmware
assigns from the channel's current position. The old idx->identity cache
was trusted on a hit, so a slot reorder or re-key (in-app, via the CLI/
config tool, or on another client) left the cache stale and misfiled
later messages under the wrong channel.
channel_id_for now reads the occupying slot fresh from the device on
every call, so identity always reflects who is actually in the slot right
now; _channel_ids is demoted to a priming/backfill warm map and a
read-failure fallback. Inbound recording moves onto a single serialized
worker draining an asyncio.Queue, so channel messages still persist in
arrival order despite each needing an async device read, and a slow read
can no longer stall the event pump.
Second root cause, same symptom: a Public message could still land in
another channel (e.g. #bots) even after the fresh-read fix. The meshcore
library's get_channel waits for "the next CHANNEL_INFO event" with no
correlation to the slot it asked for, and the dispatcher fans that event
to every in-flight waiter. Two concurrent get_channel calls therefore both
resolve on whichever response arrives first, and one caller silently
receives the other's channel — which fired whenever the chat worker's read
raced any other channel read (conversation-list builder, channels screen,
refresh_channels). Fixed in the device wrapper: a per-device asyncio.Lock
serializes channel reads so at most one is outstanding and the response is
unambiguously ours, plus get_channel now validates the response's
channel_idx matches the requested slot and raises rather than handing back
foreign channel data (the chat layer then falls back to the last-known
identity instead of misrouting).
Adds tests for filing by the current slot occupant after an out-of-band
reorder, for arrival-order preservation across a burst, and for the
serialized/index-validated channel read (no cross-talk, mismatch rejected).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>