Skip to content

fix(cli): keep --json output machine-parseable - #1

Open
dacrypt wants to merge 3 commits into
mainfrom
fix/json-output-integrity
Open

fix(cli): keep --json output machine-parseable#1
dacrypt wants to merge 3 commits into
mainfrom
fix/json-output-integrity

Conversation

@dacrypt

@dacrypt dacrypt commented Aug 14, 2026

Copy link
Copy Markdown
Owner

Problem

--json output was emitted via console.print(json.dumps(...)) on the human-facing Rich console. Rich then does two things that corrupt the payload:

  1. Wraps to console width — 80 columns whenever stdout is not a TTY, i.e. every | jq invocation. The wrap inserts a literal newline inside string values.
  2. Parses [...] in values as markup — silently dropping it, or raising MarkupError and killing the command outright on a value like [/close].

Repro (before)

tesla --json providers status | jq .
# jq: parse error: Invalid string: control characters from U+0000 through U+001F must be escaped

COLUMNS=200 tesla --json providers status | jq .   # works — the bug is width-dependent

Any value containing an unmatched closing tag crashed the process:

rich.errors.MarkupError: closing tag '[/close]' at position 44 doesn't match any open tag

Fix

Add a dedicated console for machine-readable output and route every JSON emission site through it:

json_console = Console(soft_wrap=True, markup=False, highlight=False)

def write_json(payload: str) -> None:
    """Write a pre-serialized JSON document to stdout, verbatim."""
    json_console.print(payload)
  • 93 console.print(json.dumps(...)) / console.print(x.model_dump_json(...)) sites migrated across 21 files.
  • The 64 console.print_json() sites were already safe (Rich's own JSON renderer neither wraps nor parses markup) and are left untouched.

Verification

Before → after, piping each --json command into a strict JSON parser:

command before after
providers status BROKEN VALID
data sources BROKEN VALID
config show VALID VALID
alerts / events list / config validate / providers capabilities / automations list VALID VALID
tesla --json providers status | jq -r '.[].name'
# ble vehicle-api teslaMate abrp home-assistant apprise mqtt
  • 2024 unit tests pass (2015 baseline + 9 new regression tests)
  • ruff check clean

Tests added

tests/test_output_json_integrity.py — 9 tests covering both failure modes against a deliberately narrowed (40-col) console: long-value wrapping for render_dict / render_table / render_model / render_success, and markup-like values ([bold], [/close], [red]x[/red], [dim]x) round-tripping intact.

🤖 Generated with Claude Code

dacrypt and others added 3 commits April 24, 2026 21:23
Per-session power curve + curve stats from TeslaMate Postgres samples.
Plan: Option A-revised (Phase 1 only) — ralplan consensus approved.

- core/models/charge.py: ChargeSample, ChargeCurve, ChargeCurveStats; add process_id to ChargingSession
- core/backends/teslaMate.py: get_charge_curve (deterministic ROW_NUMBER stride), get_curve_stats (full-table aggregates), get_charging_process_end_date
- api/routes/teslaMate.py: GET /charging/{id}/curve|stats with 503/404 + conditional Cache-Control immutable
- ui/pages/ChargeSessions.tsx: list + sheet modal, dual-axis Recharts LineChart, stat cards
- ui/App.tsx: IonTab "Carga" + lazy route /charge/sessions
- infra/teslamate_stack.py: pin teslamate/grafana@sha256:e02d1f0...
- tests: golden fixture (1200 samples) + 14 unit tests + DDL schema sentinel

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…se 2+3

Un-defers Phase 2 (OOTB doctor) and Phase 3 (curiosidades) from the
ralplan plan. Triggers met: user requested completion of all phases.

Phase 2 — TeslaMate OOTB hardening:
- core/diagnostics/teslamate_doctor.py (new): 8 read-only checks (docker
  daemon, stack containers, db reachable, DSN host correctness, car
  registered, recent charge data, Grafana reachable, schema columns).
  run_doctor() never raises; each check wrapped to fail soft.
- cli/commands/teslaMate.py: `tesla teslaMate doctor [--json]` exit code
  = number of failed checks.
- infra/teslamate_stack.py: install() auto-syncs Tesla tokens from keyring
  at end (silent on failure); returns dsn_host_reachable + tokens_synced.
- api/routes/teslaMate.py: GET /doctor returns DoctorReport.
- ui/pages/Settings.tsx: "Diagnóstico" panel with run button + per-check
  rows (✓/✗) and yellow hints for failures.

Phase 3 — Charge session curiosidades:
- core/models/charge.py: ChargeEnrichment with RankInfo (90-day rank
  20→80%), PreconditioningInfo (early-power signature heuristic),
  SharedStallInfo (mid-session power-halving detection), AbrpCost (local
  fallback estimate via geofence cost_per_unit or cfg.cost_per_kwh).
- core/backends/teslaMate.py: get_charge_curve_enrichment + 4 helpers,
  each wrapped in _safe() to degrade gracefully on partial data.
- core/providers/impl/abrp.py: estimate_charge_cost operation; widened
  is_available() to work without user_token when cost_per_kwh > 0.
- api/routes/teslaMate.py: GET /charging/{id}/enrichment with 503/cache
  semantics matching curve/stats endpoints.
- ui/pages/ChargeSessions.tsx: "Curiosidades de esta sesión" section
  with up to 4 conditional cards (rank / preconditioning / shared stall
  / ABRP cost). Each card omitted when its data is null.

Tests: +23 new (2013 total). Ruff clean. UI build clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
JSON payloads were emitted with `console.print(json.dumps(...))` on the
human-facing Rich console. That console does two things to the payload:

1. Wraps it to the console width — 80 columns whenever stdout is not a TTY,
   i.e. every `tesla --json ... | jq` invocation. The wrap inserts a literal
   newline inside string values, producing an unparseable document.
2. Parses square brackets in values as Rich markup, silently dropping them —
   or raising MarkupError and killing the command on a value like "[/close]".

Repro before this change:

    tesla --json providers status | jq .
    # jq: parse error: Invalid string: control characters ... must be escaped
    COLUMNS=200 tesla --json providers status | jq .   # works — width-dependent

Add a dedicated `json_console` (soft_wrap, markup=False, highlight=False) plus
a `write_json()` helper, and route all 93 JSON emission sites through it. The
`console.print_json()` call sites were already safe — Rich's own JSON renderer
neither wraps nor parses markup — and are left alone.

Verified: every `--json` command now parses clean when piped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant