From 1996cacd6a234191fa654731eaf9eb94ef067ba0 Mon Sep 17 00:00:00 2001 From: Yi-111-a <153097222+Yi-111-a@users.noreply.github.com> Date: Sat, 12 Sep 2026 18:39:06 +0800 Subject: [PATCH 1/2] feat: report the OpenSSL runtime in specify version HTTPS failures on Windows are hard to triage because several unrelated toolchains ship their own libssl-3-x64.dll, and only the one the interpreter actually loaded matters. `specify version` reported Python, Platform, Architecture and OS Version, but nothing about OpenSSL, so answering "which OpenSSL is in use?" required a separate snippet. Add an `OpenSSL` row sourced from ssl.OPENSSL_VERSION. The row is skipped when that attribute is unavailable, so the table degrades rather than erroring. Related to #4433 - this does not fix the abort, it only makes the runtime visible to whoever triages it. --- src/specify_cli/__init__.py | 7 +++++++ tests/test_cli_version.py | 18 ++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/src/specify_cli/__init__.py b/src/specify_cli/__init__.py index 93f10a1950..44a029b93b 100644 --- a/src/specify_cli/__init__.py +++ b/src/specify_cli/__init__.py @@ -455,6 +455,7 @@ def version( ): """Display version and system information.""" import platform + import ssl cli_version = get_speckit_version() @@ -489,6 +490,12 @@ def version( info_table.add_row("Platform", platform.system()) info_table.add_row("Architecture", platform.machine()) info_table.add_row("OS Version", platform.version()) + # The OpenSSL runtime the interpreter actually loaded. HTTPS failure + # reports (#4433) hinge on which OpenSSL is in play, and on Windows it is + # not obvious from the outside, so surface it here. + openssl_version = getattr(ssl, "OPENSSL_VERSION", "") + if openssl_version: + info_table.add_row("OpenSSL", openssl_version) panel = Panel( info_table, diff --git a/tests/test_cli_version.py b/tests/test_cli_version.py index 041ff62e55..4c42577c4e 100644 --- a/tests/test_cli_version.py +++ b/tests/test_cli_version.py @@ -1,6 +1,7 @@ """Tests for CLI version reporting.""" import json +import ssl from unittest.mock import patch from typer.testing import CliRunner @@ -77,3 +78,20 @@ def test_version_json_requires_features(self): assert result.exit_code != 0 assert "--json requires --features" in result.output + + def test_version_reports_openssl_runtime(self): + """specify version reports the OpenSSL runtime the interpreter loaded. + + Regression test for the triage gap in #4433: HTTPS failures on Windows + are commonly blamed on a PATH-preceded OpenSSL DLL, but ``specify + version`` reported no OpenSSL information at all, so a report had no way + to show which runtime was actually in use. + """ + with patch("specify_cli.get_speckit_version", return_value="1.2.3"): + result = runner.invoke(app, ["version"]) + + assert result.exit_code == 0 + + expected = ssl.OPENSSL_VERSION + assert expected, "test host reports no ssl.OPENSSL_VERSION to assert against" + assert expected in result.output From bfeea287430a20d4a13b567bfed51e68708ae2d8 Mon Sep 17 00:00:00 2001 From: Yi-111-a <153097222+Yi-111-a@users.noreply.github.com> Date: Wed, 16 Sep 2026 04:52:44 +0000 Subject: [PATCH 2/2] fix: tolerate interpreters built without the ssl extension MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The eager `import ssl` ran before the --features/--json early returns, so an interpreter without _ssl failed `specify version` outright — including the feature surface that never needed ssl. Import it lazily at the point of use and treat ImportError as an empty OpenSSL value so the row is skipped as documented. Adds regression tests covering both paths. --- src/specify_cli/__init__.py | 11 ++++++++--- tests/test_cli_version.py | 24 ++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/src/specify_cli/__init__.py b/src/specify_cli/__init__.py index 44a029b93b..fcc366a045 100644 --- a/src/specify_cli/__init__.py +++ b/src/specify_cli/__init__.py @@ -455,7 +455,6 @@ def version( ): """Display version and system information.""" import platform - import ssl cli_version = get_speckit_version() @@ -492,8 +491,14 @@ def version( info_table.add_row("OS Version", platform.version()) # The OpenSSL runtime the interpreter actually loaded. HTTPS failure # reports (#4433) hinge on which OpenSSL is in play, and on Windows it is - # not obvious from the outside, so surface it here. - openssl_version = getattr(ssl, "OPENSSL_VERSION", "") + # not obvious from the outside, so surface it here. An interpreter built + # without the ssl extension skips the row rather than failing the command. + try: + import ssl + + openssl_version = getattr(ssl, "OPENSSL_VERSION", "") + except ImportError: + openssl_version = "" if openssl_version: info_table.add_row("OpenSSL", openssl_version) diff --git a/tests/test_cli_version.py b/tests/test_cli_version.py index 4c42577c4e..ae64fa53bc 100644 --- a/tests/test_cli_version.py +++ b/tests/test_cli_version.py @@ -2,6 +2,7 @@ import json import ssl +import sys from unittest.mock import patch from typer.testing import CliRunner @@ -95,3 +96,26 @@ def test_version_reports_openssl_runtime(self): expected = ssl.OPENSSL_VERSION assert expected, "test host reports no ssl.OPENSSL_VERSION to assert against" assert expected in result.output + + def test_version_skips_openssl_row_when_ssl_unavailable(self, monkeypatch): + """An interpreter built without the ssl extension skips the OpenSSL row. + + ``sys.modules["ssl"] = None`` makes ``import ssl`` raise ImportError, + simulating a build without ``_ssl``. The command must still succeed — + only the OpenSSL row is omitted. + """ + monkeypatch.setitem(sys.modules, "ssl", None) + with patch("specify_cli.get_speckit_version", return_value="1.2.3"): + result = runner.invoke(app, ["version"]) + + assert result.exit_code == 0 + assert "OpenSSL" not in result.output + + def test_version_features_never_touches_ssl(self, monkeypatch): + """--features/--json return early and must not require ssl at all.""" + monkeypatch.setitem(sys.modules, "ssl", None) + with patch("specify_cli.get_speckit_version", return_value="1.2.3"): + result = runner.invoke(app, ["version", "--features", "--json"]) + + assert result.exit_code == 0 + assert json.loads(result.output)["version"] == "1.2.3"