mirror of
https://github.com/alexhopeoconnor/firmware.git
synced 2026-10-19 09:51:30 +10:00
* Start of MCP server and test suite * Add MCP server for interacting with meshtastic devices and testing framework / TUI * Update mcp-server/README.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * fix mcp-server review feedback from thread Agent-Logs-Url: https://github.com/meshtastic/firmware/sessions/91dc128a-ed50-4d07-8bb2-3dc6623a05f7 Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com> * Enhance StreamAPI and PhoneAPI for improved log record handling and concurrency control * Semgrep fixes * Trunk and semgrep fixes * optimize pio streaming tee file writes Agent-Logs-Url: https://github.com/meshtastic/firmware/sessions/04e26c6b-6a2b-45be-bbeb-79ae4d0be633 Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com> * chore: remove redundant log handle assignment Agent-Logs-Url: https://github.com/meshtastic/firmware/sessions/04e26c6b-6a2b-45be-bbeb-79ae4d0be633 Co-authored-by: thebentern <9000580+thebentern@users.noreply.github.com> * Consolidate type imports and remove placeholder test files * Add tests for config persistence and more exchange messages * Refactor position test to validate on-demand request/reply behavior * Remove position request/reply test and update README for telemetry behavior * Fix transmit history file to get removed on factory reset --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
128 lines
3.6 KiB
Python
128 lines
3.6 KiB
Python
"""Cross-run history for ``meshtastic-mcp-test-tui``.
|
|
|
|
Persists one JSON object per pytest run to
|
|
``mcp-server/tests/.history/runs.jsonl``. The TUI reads the last N
|
|
entries on launch to render a duration sparkline in the header — a
|
|
quick read on whether the suite is slowing down over time.
|
|
|
|
Schema (keep small; the file can grow for months):
|
|
|
|
{"run": 42, "ts": 1729100000.0, "duration_s": 387.2,
|
|
"passed": 52, "failed": 0, "skipped": 23, "exit_code": 0,
|
|
"seed": "mcp-user-host"}
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import pathlib
|
|
import time
|
|
from dataclasses import asdict, dataclass
|
|
from typing import Iterable
|
|
|
|
# Sparkline glyphs, low → high. 8 levels is the Unicode convention.
|
|
_SPARK_BLOCKS = "▁▂▃▄▅▆▇█"
|
|
|
|
|
|
@dataclass
|
|
class RunRecord:
|
|
run: int
|
|
ts: float
|
|
duration_s: float
|
|
passed: int
|
|
failed: int
|
|
skipped: int
|
|
exit_code: int
|
|
seed: str
|
|
|
|
|
|
class HistoryStore:
|
|
"""Append-only JSONL store with bounded read.
|
|
|
|
Writes are fsynced after each append (the file is tiny; fsync cost
|
|
is negligible and protects against truncation on a crash).
|
|
"""
|
|
|
|
def __init__(self, path: pathlib.Path, *, keep_last: int = 50) -> None:
|
|
self._path = path
|
|
self._keep_last = keep_last
|
|
|
|
def append(self, record: RunRecord) -> None:
|
|
try:
|
|
self._path.parent.mkdir(parents=True, exist_ok=True)
|
|
with self._path.open("a", encoding="utf-8") as fh:
|
|
fh.write(json.dumps(asdict(record)) + "\n")
|
|
fh.flush()
|
|
except Exception:
|
|
# Non-fatal: history is cosmetic.
|
|
pass
|
|
|
|
def read_recent(self) -> list[RunRecord]:
|
|
"""Return the last ``keep_last`` records in chronological order."""
|
|
if not self._path.is_file():
|
|
return []
|
|
try:
|
|
lines = self._path.read_text(encoding="utf-8").splitlines()
|
|
except OSError:
|
|
return []
|
|
out: list[RunRecord] = []
|
|
# Parse tail-first so we don't waste work on a huge history.
|
|
for line in lines[-self._keep_last :]:
|
|
line = line.strip()
|
|
if not line:
|
|
continue
|
|
try:
|
|
raw = json.loads(line)
|
|
except json.JSONDecodeError:
|
|
continue
|
|
try:
|
|
out.append(RunRecord(**raw))
|
|
except TypeError:
|
|
# Schema drift; skip the record rather than crash.
|
|
continue
|
|
return out
|
|
|
|
def record_run(
|
|
self,
|
|
*,
|
|
run: int,
|
|
duration_s: float,
|
|
passed: int,
|
|
failed: int,
|
|
skipped: int,
|
|
exit_code: int,
|
|
seed: str,
|
|
) -> RunRecord:
|
|
rec = RunRecord(
|
|
run=run,
|
|
ts=time.time(),
|
|
duration_s=float(duration_s),
|
|
passed=int(passed),
|
|
failed=int(failed),
|
|
skipped=int(skipped),
|
|
exit_code=int(exit_code),
|
|
seed=seed,
|
|
)
|
|
self.append(rec)
|
|
return rec
|
|
|
|
|
|
def sparkline(values: Iterable[float], *, width: int = 20) -> str:
|
|
"""Render a Unicode block-character sparkline from the last ``width`` values.
|
|
|
|
Returns an empty string for empty input so the header handles
|
|
"no history yet" gracefully.
|
|
"""
|
|
buf = [v for v in values if v >= 0][-width:]
|
|
if not buf:
|
|
return ""
|
|
lo, hi = min(buf), max(buf)
|
|
if hi - lo < 1e-9:
|
|
return _SPARK_BLOCKS[len(_SPARK_BLOCKS) // 2] * len(buf)
|
|
n = len(_SPARK_BLOCKS) - 1
|
|
out = []
|
|
for v in buf:
|
|
idx = int(round((v - lo) / (hi - lo) * n))
|
|
out.append(_SPARK_BLOCKS[max(0, min(n, idx))])
|
|
return "".join(out)
|