import json from pathlib import Path from typer.testing import CliRunner from tht.cli import app runner = CliRunner() def _config(path: Path, *, absolute_sessions: Path | None = None) -> Path: sessions = absolute_sessions or Path("sessions") path.write_text( "dwh:\n" " type: postgres_direct\n" " connection:\n" " database: patient_db\n" " schema: private_schema\n" " user: pii_user\n" " password: super-secret\n" "roots:\n" " artifacts: artifacts\n" " indexes: indexes\n" f" sessions: {sessions}\n" ) return path def test_doctor_json_reports_portable_path_statuses_without_secrets(monkeypatch, tmp_path): cfg = _config(tmp_path / "demo.yaml") monkeypatch.setenv("THT_DATA_ROOT", str(tmp_path / "data")) result = runner.invoke(app, ["doctor", "--json", "--config", str(cfg)]) assert result.exit_code == 0 payload = json.loads(result.stdout) assert payload == { "ok": True, "components": { "config": {"status": "ok"}, "data_root": {"status": "ok"}, "workspace_paths": {"status": "ok", "legacy_absolute": []}, }, } assert "super-secret" not in result.stdout assert "patient_db" not in result.stdout assert "pii_user" not in result.stdout assert result.stderr == "" def test_doctor_json_flags_absolute_legacy_paths(monkeypatch, tmp_path): cfg = _config(tmp_path / "demo.yaml", absolute_sessions=tmp_path / "old-sessions") monkeypatch.setenv("THT_DATA_ROOT", str(tmp_path / "data")) result = runner.invoke(app, ["doctor", "--json", "--config", str(cfg)]) assert result.exit_code == 0 payload = json.loads(result.stdout) assert payload["components"]["workspace_paths"] == { "status": "warning", "legacy_absolute": ["sessions"], } assert str(tmp_path) not in result.stdout def test_doctor_json_returns_structured_config_error(monkeypatch, tmp_path): cfg = _config(tmp_path / "demo.yaml") monkeypatch.setenv("THT_DATA_ROOT", str(tmp_path / "data")) cfg.write_text(cfg.read_text().replace("sessions: sessions", "sessions: ../../private")) result = runner.invoke(app, ["doctor", "--json", "--config", str(cfg)]) assert result.exit_code == 1 payload = json.loads(result.stdout) assert payload["ok"] is False assert payload["components"]["workspace_paths"]["status"] == "error" assert "outside workspace root" in payload["components"]["workspace_paths"]["message"] assert result.stderr == "" def test_doctor_json_does_not_echo_invalid_config_values(monkeypatch, tmp_path): cfg = _config(tmp_path / "demo.yaml") monkeypatch.setenv("THT_DATA_ROOT", str(tmp_path / "data")) cfg.write_text(cfg.read_text().replace("database: patient_db", "")) result = runner.invoke(app, ["doctor", "--json", "--config", str(cfg)]) assert result.exit_code == 1 payload = json.loads(result.stdout) assert payload["components"]["config"] == { "status": "error", "message": "configuration is invalid or unreadable", } assert "super-secret" not in result.stdout assert "pii_user" not in result.stdout def test_doctor_json_normalizes_malformed_yaml(monkeypatch, tmp_path): cfg = tmp_path / "demo.yaml" cfg.write_text("password: super-secret\nroots: [unterminated") monkeypatch.setenv("THT_DATA_ROOT", str(tmp_path / "data")) result = runner.invoke(app, ["doctor", "--json", "--config", str(cfg)]) assert result.exit_code == 1 assert json.loads(result.stdout)["components"]["config"] == { "status": "error", "message": "configuration is invalid or unreadable", } assert result.stderr == "" assert "super-secret" not in result.stdout assert "Traceback" not in result.stdout def test_doctor_json_normalizes_unreadable_config(monkeypatch, tmp_path): cfg = tmp_path / "demo.yaml" cfg.mkdir() monkeypatch.setenv("THT_DATA_ROOT", str(tmp_path / "data")) result = runner.invoke(app, ["doctor", "--json", "--config", str(cfg)]) assert result.exit_code == 1 assert json.loads(result.stdout)["components"]["config"] == { "status": "error", "message": "configuration is invalid or unreadable", } assert result.stderr == "" def test_doctor_human_output_is_actionable_and_redacted(monkeypatch, tmp_path): cfg = _config(tmp_path / "demo.yaml", absolute_sessions=tmp_path / "patient-private") monkeypatch.delenv("THT_DATA_ROOT", raising=False) result = runner.invoke(app, ["doctor", "--config", str(cfg)]) assert result.exit_code == 0 assert "data_root: warning - set THT_DATA_ROOT to enable portable storage" in result.stdout assert "workspace_paths: warning - absolute legacy roots: sessions" in result.stdout assert str(tmp_path) not in result.stdout assert "patient_db" not in result.stdout assert "super-secret" not in result.stdout def test_doctor_human_config_error_is_actionable_and_redacted(monkeypatch, tmp_path): cfg = tmp_path / "patient-private.yaml" cfg.write_text("password: super-secret\nroots: [unterminated") monkeypatch.setenv("THT_DATA_ROOT", str(tmp_path / "data")) result = runner.invoke(app, ["doctor", "--config", str(cfg)]) assert result.exit_code == 1 assert "config: error - configuration is invalid or unreadable" in result.stdout assert str(tmp_path) not in result.stdout assert "super-secret" not in result.stdout assert result.stderr == ""