From f317fdd343f7da1b9221381b9d04a59bda0f8c41 Mon Sep 17 00:00:00 2001 From: Dzmitry Talkach Date: Wed, 1 Jul 2026 12:42:10 +0200 Subject: [PATCH 1/4] feat(platform): add gcloud WIF credential helper command (PYSDK-146) Adds `aignostics user token` CLI command implementing the gcloud pluggable external credential source contract. Outputs an Aignostics id_token as JSON so gcloud can use it for Workload Identity Federation when accessing GCS. Co-Authored-By: Claude Sonnet 4.6 --- src/aignostics/platform/__init__.py | 3 +- src/aignostics/platform/_cli.py | 48 +++++++++++++++- tests/aignostics/platform/cli_test.py | 79 ++++++++++++++++++++++++++- 3 files changed, 126 insertions(+), 4 deletions(-) diff --git a/src/aignostics/platform/__init__.py b/src/aignostics/platform/__init__.py index fea6f14f4..d917b1f7c 100644 --- a/src/aignostics/platform/__init__.py +++ b/src/aignostics/platform/__init__.py @@ -35,7 +35,7 @@ from aignx.codegen.models import UserReadResponse as User from aignx.codegen.models import VersionReadResponse as ApplicationVersion -from ._cli import cli_sdk, cli_user +from ._cli import cli_auth, cli_sdk, cli_user from ._client import Client from ._constants import ( API_ROOT_DEV, @@ -184,6 +184,7 @@ "User", "UserInfo", "calculate_file_crc32c", + "cli_auth", "cli_sdk", "cli_user", "download_file", diff --git a/src/aignostics/platform/_cli.py b/src/aignostics/platform/_cli.py index 649783601..2c31e8d62 100644 --- a/src/aignostics/platform/_cli.py +++ b/src/aignostics/platform/_cli.py @@ -9,8 +9,10 @@ from aignostics.utils import console +from ._authentication import get_token from ._sdk_metadata import get_item_sdk_metadata_json_schema, get_run_sdk_metadata_json_schema from ._service import Service +from ._settings import settings cli_user = typer.Typer(name="user", help="User operations such as login, logout and whoami.") @@ -85,7 +87,6 @@ def whoami( logger.exception(message) console.print(message, style="error") sys.exit(1) - sys.exit(1) cli_sdk = typer.Typer(name="sdk", help="Platform operations such as dumping the SDK metadata schema.") @@ -135,3 +136,48 @@ def item_sdk_metadata_schema( logger.exception(message) console.print(message, style="error") sys.exit(1) + + +cli_auth = typer.Typer(name="auth", help="Authentication token operations for external integrations.") + + +@cli_user.command("token") +def auth_token() -> None: + """Print an Aignostics access token for use as a gcloud external credential helper. + + Outputs a JSON document to stdout in the format expected by gcloud's pluggable + authentication executable credential source (Workload Identity Federation). + + Configure as an executable credential source by adding the following to your ADC + credentials JSON file, then run ``gcloud auth application-default login`` to activate + it. The GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES environment variable must be set + to 1 for gcloud to call this helper. + + On success writes: {"version": 1, "success": true, "token_type": "...", "id_token": "...", "expiration_time": ...} + + On failure writes: {"version": 1, "success": false, "code": "1", "message": "..."} + """ + try: + token = get_token(use_cache=True) + stored = settings().token_file.read_text(encoding="utf-8") + expiry = int(stored.rsplit(":", 1)[-1]) + print( + json.dumps({ + "version": 1, + "success": True, + "token_type": "urn:ietf:params:oauth:token-type:id_token", + "id_token": token, + "expiration_time": expiry, + }) + ) + except Exception as e: + logger.debug("Failed to obtain Aignostics token for WIF credential helper: {}", e) + print( + json.dumps({ + "version": 1, + "success": False, + "code": "1", + "message": str(e), + }) + ) + sys.exit(1) diff --git a/tests/aignostics/platform/cli_test.py b/tests/aignostics/platform/cli_test.py index 0395f5987..2874d3887 100644 --- a/tests/aignostics/platform/cli_test.py +++ b/tests/aignostics/platform/cli_test.py @@ -1,6 +1,7 @@ """Tests to verify the CLI functionality of the platform module.""" -from unittest.mock import patch +import json +from unittest.mock import MagicMock, patch import pytest from typer.testing import CliRunner @@ -609,7 +610,6 @@ def test_sdk_item_metadata_schema_no_pretty(runner: CliRunner) -> None: assert "schema_version" in output assert "platform_bucket" in output # In non-pretty mode, output should still be valid JSON - import json # Try to parse the output as JSON (should not raise an error) try: @@ -621,3 +621,78 @@ def test_sdk_item_metadata_schema_no_pretty(runner: CliRunner) -> None: pytest.fail("No JSON found in output") except json.JSONDecodeError: pytest.fail("Output is not valid JSON") + + +class TestAuthTokenCLI: + """Test cases for the ``aignostics auth token`` command (gcloud WIF credential helper).""" + + _MOCK_TOKEN = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MzQ1Njc4OTB9.sig" # noqa: S105 + _MOCK_EXPIRY = 1734567890 + + @pytest.mark.integration + @staticmethod + def test_auth_token_success_exit_code(record_property, runner: CliRunner) -> None: + """Exit code is 0 when a cached token is available.""" + record_property("tested-item-id", "SPEC-PLATFORM-CLI") + stored = f"{TestAuthTokenCLI._MOCK_TOKEN}:{TestAuthTokenCLI._MOCK_EXPIRY}" + mock_settings = MagicMock() + mock_settings.return_value.token_file.read_text.return_value = stored + + with ( + patch("aignostics.platform._cli.get_token", return_value=TestAuthTokenCLI._MOCK_TOKEN), + patch("aignostics.platform._cli.settings", mock_settings), + ): + result = runner.invoke(cli, ["auth", "token"]) + + assert result.exit_code == 0 + + @pytest.mark.integration + @staticmethod + def test_auth_token_success_json_structure(record_property, runner: CliRunner) -> None: + """Valid gcloud external-credential-helper JSON is written to stdout on success.""" + record_property("tested-item-id", "SPEC-PLATFORM-CLI") + stored = f"{TestAuthTokenCLI._MOCK_TOKEN}:{TestAuthTokenCLI._MOCK_EXPIRY}" + mock_settings = MagicMock() + mock_settings.return_value.token_file.read_text.return_value = stored + + with ( + patch("aignostics.platform._cli.get_token", return_value=TestAuthTokenCLI._MOCK_TOKEN), + patch("aignostics.platform._cli.settings", mock_settings), + ): + result = runner.invoke(cli, ["auth", "token"]) + + json_start = result.output.find("{") + response = json.loads(result.output[json_start:]) + + assert response["version"] == 1 + assert response["success"] is True + assert response["token_type"] == "urn:ietf:params:oauth:token-type:id_token" # noqa: S105 + assert response["id_token"] == TestAuthTokenCLI._MOCK_TOKEN + assert response["expiration_time"] == TestAuthTokenCLI._MOCK_EXPIRY + + @pytest.mark.integration + @staticmethod + def test_auth_token_failure_exit_code(record_property, runner: CliRunner) -> None: + """Exit code is 1 when token retrieval fails.""" + record_property("tested-item-id", "SPEC-PLATFORM-CLI") + with patch("aignostics.platform._cli.get_token", side_effect=RuntimeError("no credentials")): + result = runner.invoke(cli, ["auth", "token"]) + + assert result.exit_code == 1 + + @pytest.mark.integration + @staticmethod + def test_auth_token_failure_json_structure(record_property, runner: CliRunner) -> None: + """Gcloud-compatible error JSON is written to stdout when token retrieval fails.""" + record_property("tested-item-id", "SPEC-PLATFORM-CLI") + with patch("aignostics.platform._cli.get_token", side_effect=RuntimeError("no credentials")): + result = runner.invoke(cli, ["auth", "token"]) + + json_start = result.output.find("{") + response = json.loads(result.output[json_start:]) + + assert response["version"] == 1 + assert response["success"] is False + assert "code" in response + assert "message" in response + assert "no credentials" in response["message"] From 914422cd956cb907cdc01018770627c149361e64 Mon Sep 17 00:00:00 2001 From: Dzmitry Talkach Date: Wed, 1 Jul 2026 13:06:44 +0200 Subject: [PATCH 2/4] Update the dependencies --- pyproject.toml | 3 +++ uv.lock | 24 +++++++++++++++--------- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index dcbd087b0..11295728b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -101,6 +101,7 @@ dependencies = [ "httpx>=0.28.1,<1", "idc-index-data==24.0.3", "ijson>=3.4.0.post0,<4", + "joserfc>=1.6.7", "jsf>=0.11.2,<1", "jsonschema[format-nongpl]>=4.25.1,<5", "loguru>=0.7.3,<1", @@ -114,6 +115,8 @@ dependencies = [ "pyarrow>=23.0.1,<24; python_version >= '3.14'", "pyjwt[crypto]>=2.13.0,<3", # CVE-2026-32597 requires >=2.12.0 (Renovate #475) "python-dateutil>=2.9.0.post0,<3", + "python-engineio>=4.13.3", + "python-socketio>=5.16.3", # "pywebview[qt6]>=5.4,<6; sys_platform == 'linux'", "requests>=2.33.0,<3", # CVE-2026-25645 requires >= 2.33.0 "requests-oauthlib>=2.0.0,<3", diff --git a/uv.lock b/uv.lock index 2ac82b69b..385b61a04 100644 --- a/uv.lock +++ b/uv.lock @@ -54,6 +54,7 @@ dependencies = [ { name = "humanize" }, { name = "idc-index-data" }, { name = "ijson" }, + { name = "joserfc" }, { name = "jsf" }, { name = "jsonschema", extra = ["format-nongpl"] }, { name = "loguru" }, @@ -78,7 +79,9 @@ dependencies = [ { name = "pygments" }, { name = "pyjwt", extra = ["crypto"] }, { name = "python-dateutil" }, + { name = "python-engineio" }, { name = "python-multipart" }, + { name = "python-socketio" }, { name = "pywin32", marker = "sys_platform == 'win32'" }, { name = "pyyaml" }, { name = "requests" }, @@ -198,6 +201,7 @@ requires-dist = [ { name = "idc-index-data", specifier = "==24.0.3" }, { name = "ijson", specifier = ">=3.4.0.post0,<4" }, { name = "ipython", marker = "extra == 'marimo'", specifier = ">=9.8.0,<10" }, + { name = "joserfc", specifier = ">=1.6.7" }, { name = "jsf", specifier = ">=0.11.2,<1" }, { name = "jsonschema", extras = ["format-nongpl"], specifier = ">=4.25.1,<5" }, { name = "jupyter", marker = "extra == 'jupyter'", specifier = ">=1.1.1,<2" }, @@ -231,7 +235,9 @@ requires-dist = [ { name = "pyinstaller", marker = "extra == 'pyinstaller'", specifier = ">=6.14.0,<7" }, { name = "pyjwt", extras = ["crypto"], specifier = ">=2.13.0,<3" }, { name = "python-dateutil", specifier = ">=2.9.0.post0,<3" }, + { name = "python-engineio", specifier = ">=4.13.3" }, { name = "python-multipart", specifier = ">=0.0.26" }, + { name = "python-socketio", specifier = ">=5.16.3" }, { name = "pywin32", marker = "sys_platform == 'win32'", specifier = ">=312,<313" }, { name = "pyyaml", specifier = ">=6.0.3,<7" }, { name = "requests", specifier = ">=2.33.0,<3" }, @@ -3004,14 +3010,14 @@ wheels = [ [[package]] name = "joserfc" -version = "1.6.5" +version = "1.7.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3b/dc/5f768c2e391e9afabe5d18e3221346deb5fb6338565f1ccc9e7c6d7befdd/joserfc-1.6.5.tar.gz", hash = "sha256:1482a7db78fb4602e44ed89e51b599d052e091288c7c532c5b694e20149dec48", size = 231881, upload-time = "2026-05-06T04:58:13.408Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/26/abe1ad855eb334b5ebc9c6495d4798e12bee70e5e8e815d54570710b8312/joserfc-1.7.2.tar.gz", hash = "sha256:537ffb8888b2df039cb5b6d017d7cff6f09d521ce65d89cc9b8ab752b1cff947", size = 233183, upload-time = "2026-06-29T09:03:10.868Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/54/3b/ad1cb22e75c963b1f07c8a2329bf47227ce7e4361df5eb2fb101b2ce33ef/joserfc-1.6.5-py3-none-any.whl", hash = "sha256:e9878a0f8243fe7b95e11fdda81374ca9f7a689e302751579d3dfdeec559675e", size = 70464, upload-time = "2026-05-06T04:58:11.668Z" }, + { url = "https://files.pythonhosted.org/packages/13/80/d1b30336582cced4dce0dae776508a6011723e32f907bc7a702c0b25890a/joserfc-1.7.2-py3-none-any.whl", hash = "sha256:ddd818c0ca9b4f17bbc2d72cb3966e6ded7502be089316c62c3cc64ae86132b5", size = 70426, upload-time = "2026-06-29T09:03:09.393Z" }, ] [[package]] @@ -6230,14 +6236,14 @@ wheels = [ [[package]] name = "python-engineio" -version = "4.13.1" +version = "4.13.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "simple-websocket" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/34/12/bdef9dbeedbe2cdeba2a2056ad27b1fb081557d34b69a97f574843462cae/python_engineio-4.13.1.tar.gz", hash = "sha256:0a853fcef52f5b345425d8c2b921ac85023a04dfcf75d7b74696c61e940fd066", size = 92348, upload-time = "2026-02-06T23:38:06.12Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fb/a0/f75491f942184d9960b15e763270f765fe9f239745ca5f9e16289011aed4/python_engineio-4.13.3.tar.gz", hash = "sha256:572b7783e341fed21edbc7cea297ccd378dad79265fdde96aa4664420a7c06c9", size = 79734, upload-time = "2026-06-20T22:53:52.197Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/aa/54/0cce26da03a981f949bb8449c9778537f75f5917c172e1d2992ff25cb57d/python_engineio-4.13.1-py3-none-any.whl", hash = "sha256:f32ad10589859c11053ad7d9bb3c9695cdf862113bfb0d20bc4d890198287399", size = 59847, upload-time = "2026-02-06T23:38:04.861Z" }, + { url = "https://files.pythonhosted.org/packages/5b/96/82f6328e410515fab21d5602ba35b9377a47b5a141a0c1f9efa00ce21eb4/python_engineio-4.13.3-py3-none-any.whl", hash = "sha256:1f60ecaf1358190f0e26c48c578a60428dc02a8f1295bc3dbf53d1b31116821f", size = 59993, upload-time = "2026-06-20T22:53:50.775Z" }, ] [[package]] @@ -6260,15 +6266,15 @@ wheels = [ [[package]] name = "python-socketio" -version = "5.16.1" +version = "5.16.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "bidict" }, { name = "python-engineio" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/59/81/cf8284f45e32efa18d3848ed82cdd4dcc1b657b082458fbe01ad3e1f2f8d/python_socketio-5.16.1.tar.gz", hash = "sha256:f863f98eacce81ceea2e742f6388e10ca3cdd0764be21d30d5196470edf5ea89", size = 128508, upload-time = "2026-02-06T23:42:07Z" } +sdist = { url = "https://files.pythonhosted.org/packages/32/2d/ffce71017c106b75099fea569df6518c63fee5d6202ce0cfe7b01e6f22c3/python_socketio-5.16.3.tar.gz", hash = "sha256:89b136f677ae65607a84cecda9b4d6c5377b40a97582c504c25df89af16d520e", size = 128095, upload-time = "2026-06-15T22:07:04.003Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/07/c7/deb8c5e604404dbf10a3808a858946ca3547692ff6316b698945bb72177e/python_socketio-5.16.1-py3-none-any.whl", hash = "sha256:a3eb1702e92aa2f2b5d3ba00261b61f062cce51f1cfb6900bf3ab4d1934d2d35", size = 82054, upload-time = "2026-02-06T23:42:05.772Z" }, + { url = "https://files.pythonhosted.org/packages/0a/38/8c5e72d53ff8eb27497c4f268a7f6d9121e727a50b65248288ad79a93053/python_socketio-5.16.3-py3-none-any.whl", hash = "sha256:e7ad14202a5e6448824c7c2f86161d04e13dec05992257df5c709e6a2798c041", size = 82087, upload-time = "2026-06-15T22:07:02.498Z" }, ] [package.optional-dependencies] From b444f77aa82ae35a3ff82b2875b53c42b2f09cd1 Mon Sep 17 00:00:00 2001 From: Dzmitry Talkach Date: Wed, 1 Jul 2026 14:30:15 +0200 Subject: [PATCH 3/4] test(platform): strengthen TestAuthTokenCLI by testing through get_token Replace get_token mock with a real token file on disk so that get_token's cache-hit path (file existence check, expiry parsing, 5-minute window validation) executes for real. For success tests: write token:future_expiry to tmp_path and mock settings at the authentication layer; only _inform_sentry_about_user is suppressed to avoid JWT network calls. For failure tests: point settings at a non-existent file so the cache miss occurs naturally, then mock _authenticate to raise so get_token's full failure path is exercised without real OAuth flows. Co-Authored-By: Claude Sonnet 4.6 --- tests/aignostics/platform/cli_test.py | 67 ++++++++++++++++++--------- 1 file changed, 45 insertions(+), 22 deletions(-) diff --git a/tests/aignostics/platform/cli_test.py b/tests/aignostics/platform/cli_test.py index 2874d3887..1e3e72f01 100644 --- a/tests/aignostics/platform/cli_test.py +++ b/tests/aignostics/platform/cli_test.py @@ -1,6 +1,7 @@ """Tests to verify the CLI functionality of the platform module.""" import json +from datetime import UTC, datetime, timedelta from unittest.mock import MagicMock, patch import pytest @@ -627,39 +628,49 @@ class TestAuthTokenCLI: """Test cases for the ``aignostics auth token`` command (gcloud WIF credential helper).""" _MOCK_TOKEN = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MzQ1Njc4OTB9.sig" # noqa: S105 - _MOCK_EXPIRY = 1734567890 + + @staticmethod + def _settings_with_token_file(token_file): + """Return a mock settings object pointing at the given token file path.""" + mock = MagicMock() + mock.token_file = token_file + return mock @pytest.mark.integration @staticmethod - def test_auth_token_success_exit_code(record_property, runner: CliRunner) -> None: + def test_auth_token_success_exit_code(record_property, runner: CliRunner, tmp_path) -> None: """Exit code is 0 when a cached token is available.""" record_property("tested-item-id", "SPEC-PLATFORM-CLI") - stored = f"{TestAuthTokenCLI._MOCK_TOKEN}:{TestAuthTokenCLI._MOCK_EXPIRY}" - mock_settings = MagicMock() - mock_settings.return_value.token_file.read_text.return_value = stored + future_expiry = int((datetime.now(tz=UTC) + timedelta(hours=1)).timestamp()) + token_file = tmp_path / "token" + token_file.write_text(f"{TestAuthTokenCLI._MOCK_TOKEN}:{future_expiry}", encoding="utf-8") + mock_settings = TestAuthTokenCLI._settings_with_token_file(token_file) with ( - patch("aignostics.platform._cli.get_token", return_value=TestAuthTokenCLI._MOCK_TOKEN), - patch("aignostics.platform._cli.settings", mock_settings), + patch("aignostics.platform._authentication.settings", return_value=mock_settings), + patch("aignostics.platform._cli.settings", return_value=mock_settings), + patch("aignostics.platform._authentication._inform_sentry_about_user"), ): - result = runner.invoke(cli, ["auth", "token"]) + result = runner.invoke(cli, ["user", "token"]) assert result.exit_code == 0 @pytest.mark.integration @staticmethod - def test_auth_token_success_json_structure(record_property, runner: CliRunner) -> None: + def test_auth_token_success_json_structure(record_property, runner: CliRunner, tmp_path) -> None: """Valid gcloud external-credential-helper JSON is written to stdout on success.""" record_property("tested-item-id", "SPEC-PLATFORM-CLI") - stored = f"{TestAuthTokenCLI._MOCK_TOKEN}:{TestAuthTokenCLI._MOCK_EXPIRY}" - mock_settings = MagicMock() - mock_settings.return_value.token_file.read_text.return_value = stored + future_expiry = int((datetime.now(tz=UTC) + timedelta(hours=1)).timestamp()) + token_file = tmp_path / "token" + token_file.write_text(f"{TestAuthTokenCLI._MOCK_TOKEN}:{future_expiry}", encoding="utf-8") + mock_settings = TestAuthTokenCLI._settings_with_token_file(token_file) with ( - patch("aignostics.platform._cli.get_token", return_value=TestAuthTokenCLI._MOCK_TOKEN), - patch("aignostics.platform._cli.settings", mock_settings), + patch("aignostics.platform._authentication.settings", return_value=mock_settings), + patch("aignostics.platform._cli.settings", return_value=mock_settings), + patch("aignostics.platform._authentication._inform_sentry_about_user"), ): - result = runner.invoke(cli, ["auth", "token"]) + result = runner.invoke(cli, ["user", "token"]) json_start = result.output.find("{") response = json.loads(result.output[json_start:]) @@ -668,25 +679,37 @@ def test_auth_token_success_json_structure(record_property, runner: CliRunner) - assert response["success"] is True assert response["token_type"] == "urn:ietf:params:oauth:token-type:id_token" # noqa: S105 assert response["id_token"] == TestAuthTokenCLI._MOCK_TOKEN - assert response["expiration_time"] == TestAuthTokenCLI._MOCK_EXPIRY + assert response["expiration_time"] == future_expiry @pytest.mark.integration @staticmethod - def test_auth_token_failure_exit_code(record_property, runner: CliRunner) -> None: + def test_auth_token_failure_exit_code(record_property, runner: CliRunner, tmp_path) -> None: """Exit code is 1 when token retrieval fails.""" record_property("tested-item-id", "SPEC-PLATFORM-CLI") - with patch("aignostics.platform._cli.get_token", side_effect=RuntimeError("no credentials")): - result = runner.invoke(cli, ["auth", "token"]) + mock_settings = TestAuthTokenCLI._settings_with_token_file(tmp_path / "no_token") + + with ( + patch("aignostics.platform._authentication.settings", return_value=mock_settings), + patch("aignostics.platform._cli.settings", return_value=mock_settings), + patch("aignostics.platform._authentication._authenticate", side_effect=RuntimeError("no credentials")), + ): + result = runner.invoke(cli, ["user", "token"]) assert result.exit_code == 1 @pytest.mark.integration @staticmethod - def test_auth_token_failure_json_structure(record_property, runner: CliRunner) -> None: + def test_auth_token_failure_json_structure(record_property, runner: CliRunner, tmp_path) -> None: """Gcloud-compatible error JSON is written to stdout when token retrieval fails.""" record_property("tested-item-id", "SPEC-PLATFORM-CLI") - with patch("aignostics.platform._cli.get_token", side_effect=RuntimeError("no credentials")): - result = runner.invoke(cli, ["auth", "token"]) + mock_settings = TestAuthTokenCLI._settings_with_token_file(tmp_path / "no_token") + + with ( + patch("aignostics.platform._authentication.settings", return_value=mock_settings), + patch("aignostics.platform._cli.settings", return_value=mock_settings), + patch("aignostics.platform._authentication._authenticate", side_effect=RuntimeError("no credentials")), + ): + result = runner.invoke(cli, ["user", "token"]) json_start = result.output.find("{") response = json.loads(result.output[json_start:]) From 2e4c9f17a2797b8b866e729520daa2763477ec2d Mon Sep 17 00:00:00 2001 From: Dzmitry Talkach Date: Wed, 1 Jul 2026 14:44:02 +0200 Subject: [PATCH 4/4] fix(platform): fix lint errors in TestAuthTokenCLI tests Remove stray backslash line continuation and add missing return type annotation on _settings_with_token_file staticmethod. Co-Authored-By: Claude Sonnet 4.6 --- tests/aignostics/platform/cli_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/aignostics/platform/cli_test.py b/tests/aignostics/platform/cli_test.py index 1e3e72f01..f2e4d988a 100644 --- a/tests/aignostics/platform/cli_test.py +++ b/tests/aignostics/platform/cli_test.py @@ -630,7 +630,7 @@ class TestAuthTokenCLI: _MOCK_TOKEN = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MzQ1Njc4OTB9.sig" # noqa: S105 @staticmethod - def _settings_with_token_file(token_file): + def _settings_with_token_file(token_file) -> MagicMock: """Return a mock settings object pointing at the given token file path.""" mock = MagicMock() mock.token_file = token_file