Skip to content

Commit 47d4a3a

Browse files
author
CryptoJones
committed
feat: L20 — triage doctor env-diagnostics subcommand (#33)
Read-only doctor subcommand: version, python, locale + source, store path, log path. Plain + --json. 3 tests.
1 parent cab37a0 commit 47d4a3a

2 files changed

Lines changed: 115 additions & 2 deletions

File tree

src/triage/cli.py

Lines changed: 82 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@
66

77
import argparse
88
import json
9+
import os
910
import sys
10-
from typing import Sequence
11+
from pathlib import Path
12+
from typing import Any, Sequence
1113

12-
from . import __version__, cron, i18n, log_writer, scheduler, theme
14+
from . import __version__, cron, i18n, log_writer, scheduler, store, theme
1315
from .i18n import _
1416
from .model import Task
1517
from .sources import github_ci, github_pr, runpod
@@ -467,6 +469,73 @@ def cmd_theme(args: argparse.Namespace) -> int:
467469
return 0
468470

469471

472+
def _detect_locale_source() -> str:
473+
"""Best-effort detection of which signal won locale resolution.
474+
475+
Read-only; used only by `triage doctor`. Mirrors the precedence
476+
chain in `i18n.resolve_lang` but reports the source name instead
477+
of returning a code.
478+
"""
479+
if os.environ.get("TRIAGE_LANG"):
480+
return "TRIAGE_LANG"
481+
for var in ("LC_ALL", "LC_MESSAGES", "LANG"):
482+
v = os.environ.get(var)
483+
if v and v != "C" and v != "POSIX":
484+
return var
485+
try:
486+
import locale as _locale
487+
if _locale.getlocale()[0]:
488+
return "locale.getlocale()"
489+
except Exception:
490+
pass
491+
return "default"
492+
493+
494+
def cmd_doctor(args: argparse.Namespace) -> int:
495+
"""Print a read-only environment-diagnostics summary for bug reports."""
496+
store_root = store.default_root() if getattr(args, "home", None) is None else Path(args.home)
497+
log_disabled_env = os.environ.get("TRIAGE_NO_LOG") == "1"
498+
log_path = None if log_disabled_env else log_writer.get_path()
499+
drift = i18n.check_locales()
500+
locale_count = len(i18n.list_available()) - 1 # minus en baseline
501+
502+
info: dict[str, Any] = {
503+
"version": __version__,
504+
"python": f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}",
505+
"locale": {
506+
"resolved": i18n.current_lang(),
507+
"source": _detect_locale_source(),
508+
"available": locale_count + 1, # include en
509+
"drift": len(drift),
510+
},
511+
"store": {
512+
"path": str(store_root),
513+
"exists": store_root.exists(),
514+
},
515+
"log": {
516+
"path": str(log_path) if log_path else None,
517+
"enabled": log_path is not None,
518+
},
519+
}
520+
521+
if getattr(args, "json", False):
522+
print(json.dumps(info, indent=2, sort_keys=True))
523+
return 0
524+
525+
print(f"triage v{info['version']} (python {info['python']})")
526+
print(f" locale: {info['locale']['resolved']} "
527+
f"source={info['locale']['source']} "
528+
f"available={info['locale']['available']} "
529+
f"drift={info['locale']['drift']}")
530+
print(f" store: {info['store']['path']} "
531+
f"exists={info['store']['exists']}")
532+
if info['log']['enabled']:
533+
print(f" log: {info['log']['path']}")
534+
else:
535+
print(" log: disabled")
536+
return 0
537+
538+
470539
def cmd_lang(args: argparse.Namespace) -> int:
471540
"""List available output languages (or switch via --lang on any command)."""
472541
as_json = getattr(args, "json", False)
@@ -681,6 +750,17 @@ def build_parser() -> argparse.ArgumentParser:
681750
)
682751
lng.set_defaults(func=cmd_lang)
683752

753+
doc = sub.add_parser(
754+
"doctor",
755+
help="Print a read-only environment-diagnostics summary (for bug reports).",
756+
)
757+
doc.add_argument(
758+
"--json",
759+
action="store_true",
760+
help="Emit machine-parseable JSON instead of human-readable text.",
761+
)
762+
doc.set_defaults(func=cmd_doctor)
763+
684764
return p
685765

686766

tests/test_cli.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,3 +129,36 @@ def test_self_loop_warning_appears_on_stderr(home, capsys, monkeypatch):
129129
main(["list"])
130130
captured = capsys.readouterr()
131131
assert "self-block" in captured.err
132+
133+
134+
def test_doctor_text_output_contains_core_fields(home, capsys, monkeypatch):
135+
monkeypatch.setenv("TRIAGE_NO_LOG", "1")
136+
assert main(["doctor"]) == 0
137+
out = capsys.readouterr().out
138+
assert "triage v" in out
139+
assert "locale:" in out
140+
assert "store:" in out
141+
assert "log:" in out
142+
assert str(home) in out
143+
144+
145+
def test_doctor_json_output_has_expected_shape(home, capsys, monkeypatch):
146+
monkeypatch.setenv("TRIAGE_NO_LOG", "1")
147+
assert main(["doctor", "--json"]) == 0
148+
payload = json.loads(capsys.readouterr().out)
149+
assert "version" in payload
150+
assert "python" in payload
151+
assert payload["store"]["path"] == str(home)
152+
assert payload["log"]["enabled"] is False # TRIAGE_NO_LOG=1
153+
assert payload["locale"]["resolved"] in {"en", "es", "fr"} # whatever resolved
154+
assert payload["locale"]["drift"] == 0
155+
assert payload["locale"]["available"] >= 17
156+
157+
158+
def test_doctor_reports_explicit_env_locale_source(home, capsys, monkeypatch):
159+
monkeypatch.setenv("TRIAGE_NO_LOG", "1")
160+
monkeypatch.setenv("TRIAGE_LANG", "es")
161+
assert main(["doctor", "--json"]) == 0
162+
payload = json.loads(capsys.readouterr().out)
163+
assert payload["locale"]["resolved"] == "es"
164+
assert payload["locale"]["source"] == "TRIAGE_LANG"

0 commit comments

Comments
 (0)