diff --git a/.github/workflows/gp-test.yml b/.github/workflows/gp-test.yml new file mode 100644 index 0000000000..e5237ca87c --- /dev/null +++ b/.github/workflows/gp-test.yml @@ -0,0 +1,27 @@ +name: GP Scripts Tests + +on: + pull_request: + paths: + - "scripts/gp/**" + workflow_dispatch: + +jobs: + test: + runs-on: ubuntu-latest + name: Test Globalization Pipeline scripts + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install dependencies + run: pip install -r scripts/gp/requirements.txt pytest + + - name: Run GP script tests + run: python -m pytest scripts/gp/tests/ -v diff --git a/.github/workflows/nightly_build.yml b/.github/workflows/nightly_build.yml index a0ab91de61..ffeb75ed2b 100644 --- a/.github/workflows/nightly_build.yml +++ b/.github/workflows/nightly_build.yml @@ -246,8 +246,12 @@ jobs: git diff --staged --quiet || git commit -m "chore: update translations from Globalization Pipeline [skip ci]" git push + - name: Report GP download failure + if: failure() + run: echo "::warning::GP translation download failed — translations may be out of date. Check the 'Download translations from GP' step above for details." + frontend-tests-linux: - if: always() && github.repository == 'langflow-ai/langflow' && !inputs.skip_frontend_tests + if: github.repository == 'langflow-ai/langflow' && !inputs.skip_frontend_tests name: Run Frontend Tests - Linux needs: [resolve-release-branch, create-nightly-tag] uses: ./.github/workflows/typescript_test.yml @@ -263,7 +267,7 @@ jobs: TAVILY_API_KEY: ${{ secrets.TAVILY_API_KEY }} frontend-tests-windows: - if: always() && github.repository == 'langflow-ai/langflow' && !inputs.skip_frontend_tests + if: github.repository == 'langflow-ai/langflow' && !inputs.skip_frontend_tests name: Run Frontend Tests - Windows needs: [resolve-release-branch, create-nightly-tag] # Windows tests are non-blocking - the release-nightly-build job only checks diff --git a/scripts/gp/__init__.py b/scripts/gp/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/scripts/gp/tests/conftest.py b/scripts/gp/tests/conftest.py new file mode 100644 index 0000000000..c66e428288 --- /dev/null +++ b/scripts/gp/tests/conftest.py @@ -0,0 +1,6 @@ +"""Add scripts/gp to sys.path so bare imports (gp_client, upload_strings, etc.) resolve.""" + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).parent.parent)) diff --git a/scripts/gp/tests/test_download_translations.py b/scripts/gp/tests/test_download_translations.py index 50dc2731d1..546a12101a 100644 --- a/scripts/gp/tests/test_download_translations.py +++ b/scripts/gp/tests/test_download_translations.py @@ -5,15 +5,11 @@ from unittest.mock import patch import pytest +import download_translations as dl_mod + def _run_main(output_dir: str): - """Helper to invoke download_translations.main() with an --output argument.""" with patch("sys.argv", ["download_translations.py", "--output", output_dir]): - import importlib - - import scripts.gp.download_translations as dl_mod - - importlib.reload(dl_mod) dl_mod.main() @@ -28,8 +24,8 @@ SAMPLE_RESPONSE = { class TestDownloadTranslations: def test_writes_json_files_for_each_language(self, tmp_path): with ( - patch("scripts.gp.download_translations.get_strings", return_value=SAMPLE_RESPONSE), - patch("scripts.gp.download_translations.TARGET_LANGS", ["fr", "es"]), + patch.object(dl_mod, "get_strings", return_value=SAMPLE_RESPONSE), + patch.object(dl_mod, "TARGET_LANGS", ["fr", "es"]), ): _run_main(str(tmp_path)) @@ -46,8 +42,8 @@ class TestDownloadTranslations: return SAMPLE_RESPONSE with ( - patch("scripts.gp.download_translations.get_strings", side_effect=_get_strings), - patch("scripts.gp.download_translations.TARGET_LANGS", ["fr", "ja"]), + patch.object(dl_mod, "get_strings", side_effect=_get_strings), + patch.object(dl_mod, "TARGET_LANGS", ["fr", "ja"]), ): _run_main(str(tmp_path)) @@ -61,22 +57,20 @@ class TestDownloadTranslations: return SAMPLE_RESPONSE with ( - patch("scripts.gp.download_translations.get_strings", side_effect=_get_strings), - patch("scripts.gp.download_translations.TARGET_LANGS", ["fr", "de"]), + patch.object(dl_mod, "get_strings", side_effect=_get_strings), + patch.object(dl_mod, "TARGET_LANGS", ["fr", "de"]), pytest.raises(SystemExit) as exc_info, ): _run_main(str(tmp_path)) assert exc_info.value.code == 1 - # Successful language should still have been written assert (tmp_path / "fr.json").exists() def test_exits_cleanly_when_all_succeed(self, tmp_path): with ( - patch("scripts.gp.download_translations.get_strings", return_value=SAMPLE_RESPONSE), - patch("scripts.gp.download_translations.TARGET_LANGS", ["fr"]), + patch.object(dl_mod, "get_strings", return_value=SAMPLE_RESPONSE), + patch.object(dl_mod, "TARGET_LANGS", ["fr"]), ): - # Should not raise _run_main(str(tmp_path)) assert (tmp_path / "fr.json").exists() @@ -90,8 +84,8 @@ class TestDownloadTranslations: } with ( - patch("scripts.gp.download_translations.get_strings", return_value=flat_response), - patch("scripts.gp.download_translations.TARGET_LANGS", ["es"]), + patch.object(dl_mod, "get_strings", return_value=flat_response), + patch.object(dl_mod, "TARGET_LANGS", ["es"]), ): _run_main(str(tmp_path)) diff --git a/scripts/gp/tests/test_gp_client.py b/scripts/gp/tests/test_gp_client.py index ab6661350c..516c3b9907 100644 --- a/scripts/gp/tests/test_gp_client.py +++ b/scripts/gp/tests/test_gp_client.py @@ -3,10 +3,14 @@ import base64 import hashlib import hmac +import importlib from unittest.mock import MagicMock, patch import pytest +import gp_client + + # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- @@ -28,16 +32,8 @@ def _expected_signature(password: str, message: str) -> str: class TestGetHeaders: def test_get_request_authorization_format(self): - with patch.dict( - "os.environ", - {"GP_ADMIN_USER_ID": "user123", "GP_ADMIN_PASSWORD": "secret"}, - ): - import importlib - - from scripts.gp import gp_client - + with patch.dict("os.environ", {"GP_ADMIN_USER_ID": "user123", "GP_ADMIN_PASSWORD": "secret"}): importlib.reload(gp_client) - headers = gp_client.get_headers("https://example.com/api", "GET") assert headers["Authorization"].startswith("GP-HMAC user123:") @@ -46,49 +42,24 @@ class TestGetHeaders: assert "Content-Type" not in headers def test_put_request_includes_content_type(self): - with patch.dict( - "os.environ", - {"GP_ADMIN_USER_ID": "user123", "GP_ADMIN_PASSWORD": "secret"}, - ): - import importlib - - from scripts.gp import gp_client - + with patch.dict("os.environ", {"GP_ADMIN_USER_ID": "user123", "GP_ADMIN_PASSWORD": "secret"}): importlib.reload(gp_client) - headers = gp_client.get_headers("https://example.com/api", "PUT", {"key": "val"}) assert headers["Content-Type"] == "application/json" def test_patch_request_uses_merge_patch_content_type(self): - with patch.dict( - "os.environ", - {"GP_ADMIN_USER_ID": "user123", "GP_ADMIN_PASSWORD": "secret"}, - ): - import importlib - - from scripts.gp import gp_client - + with patch.dict("os.environ", {"GP_ADMIN_USER_ID": "user123", "GP_ADMIN_PASSWORD": "secret"}): importlib.reload(gp_client) - headers = gp_client.get_headers("https://example.com/api", "PATCH", {}) assert headers["Content-Type"] == "application/merge-patch+json" def test_hmac_signature_is_deterministic_for_same_inputs(self): - with patch.dict( - "os.environ", - {"GP_ADMIN_USER_ID": "user123", "GP_ADMIN_PASSWORD": "secret"}, - ): - import importlib - - from scripts.gp import gp_client - + with patch.dict("os.environ", {"GP_ADMIN_USER_ID": "user123", "GP_ADMIN_PASSWORD": "secret"}): importlib.reload(gp_client) - - with patch("scripts.gp.gp_client.datetime") as mock_dt: + with patch("gp_client.datetime") as mock_dt: mock_dt.now.return_value.strftime.return_value = "Thu, 26 Mar 2026 12:00:00 UTC" - h1 = gp_client.get_headers("https://example.com", "GET") h2 = gp_client.get_headers("https://example.com", "GET") @@ -106,18 +77,10 @@ class TestListBundles: mock_response.json.return_value = {"bundleIds": ["langflow-ui"]} with ( - patch.dict( - "os.environ", - {"GP_ADMIN_USER_ID": "u", "GP_ADMIN_PASSWORD": "p"}, - ), - patch("scripts.gp.gp_client.requests.get", return_value=mock_response) as mock_get, + patch.dict("os.environ", {"GP_ADMIN_USER_ID": "u", "GP_ADMIN_PASSWORD": "p"}), + patch("gp_client.requests.get", return_value=mock_response) as mock_get, ): - import importlib - - from scripts.gp import gp_client - importlib.reload(gp_client) - result = gp_client.list_bundles() mock_get.assert_called_once() @@ -131,18 +94,10 @@ class TestListBundles: mock_response.raise_for_status.side_effect = req.exceptions.HTTPError("401 Unauthorized") with ( - patch.dict( - "os.environ", - {"GP_ADMIN_USER_ID": "u", "GP_ADMIN_PASSWORD": "p"}, - ), - patch("scripts.gp.gp_client.requests.get", return_value=mock_response), + patch.dict("os.environ", {"GP_ADMIN_USER_ID": "u", "GP_ADMIN_PASSWORD": "p"}), + patch("gp_client.requests.get", return_value=mock_response), ): - import importlib - - from scripts.gp import gp_client - importlib.reload(gp_client) - with pytest.raises(req.exceptions.HTTPError): gp_client.list_bundles() @@ -150,21 +105,10 @@ class TestListBundles: import requests as req with ( - patch.dict( - "os.environ", - {"GP_ADMIN_USER_ID": "u", "GP_ADMIN_PASSWORD": "p"}, - ), - patch( - "scripts.gp.gp_client.requests.get", - side_effect=req.exceptions.Timeout, - ), + patch.dict("os.environ", {"GP_ADMIN_USER_ID": "u", "GP_ADMIN_PASSWORD": "p"}), + patch("gp_client.requests.get", side_effect=req.exceptions.Timeout), ): - import importlib - - from scripts.gp import gp_client - importlib.reload(gp_client) - with pytest.raises(req.exceptions.Timeout): gp_client.list_bundles() @@ -180,18 +124,10 @@ class TestUploadStrings: mock_response.json.return_value = {"status": "ok"} with ( - patch.dict( - "os.environ", - {"GP_ADMIN_USER_ID": "u", "GP_ADMIN_PASSWORD": "p"}, - ), - patch("scripts.gp.gp_client.requests.put", return_value=mock_response) as mock_put, + patch.dict("os.environ", {"GP_ADMIN_USER_ID": "u", "GP_ADMIN_PASSWORD": "p"}), + patch("gp_client.requests.put", return_value=mock_response) as mock_put, ): - import importlib - - from scripts.gp import gp_client - importlib.reload(gp_client) - result = gp_client.upload_strings({"hello": "Hello"}) mock_put.assert_called_once() @@ -204,18 +140,10 @@ class TestUploadStrings: mock_response.raise_for_status.side_effect = req.exceptions.HTTPError("500") with ( - patch.dict( - "os.environ", - {"GP_ADMIN_USER_ID": "u", "GP_ADMIN_PASSWORD": "p"}, - ), - patch("scripts.gp.gp_client.requests.put", return_value=mock_response), + patch.dict("os.environ", {"GP_ADMIN_USER_ID": "u", "GP_ADMIN_PASSWORD": "p"}), + patch("gp_client.requests.put", return_value=mock_response), ): - import importlib - - from scripts.gp import gp_client - importlib.reload(gp_client) - with pytest.raises(req.exceptions.HTTPError): gp_client.upload_strings({"hello": "Hello"}) @@ -231,18 +159,10 @@ class TestGetStrings: mock_response.json.return_value = {"resourceStrings": {"hello": {"value": "Bonjour"}}} with ( - patch.dict( - "os.environ", - {"GP_ADMIN_USER_ID": "u", "GP_ADMIN_PASSWORD": "p"}, - ), - patch("scripts.gp.gp_client.requests.get", return_value=mock_response), + patch.dict("os.environ", {"GP_ADMIN_USER_ID": "u", "GP_ADMIN_PASSWORD": "p"}), + patch("gp_client.requests.get", return_value=mock_response), ): - import importlib - - from scripts.gp import gp_client - importlib.reload(gp_client) - result = gp_client.get_strings("fr") assert result["resourceStrings"]["hello"]["value"] == "Bonjour" @@ -254,17 +174,9 @@ class TestGetStrings: mock_response.raise_for_status.side_effect = req.exceptions.HTTPError("403 Forbidden") with ( - patch.dict( - "os.environ", - {"GP_ADMIN_USER_ID": "u", "GP_ADMIN_PASSWORD": "p"}, - ), - patch("scripts.gp.gp_client.requests.get", return_value=mock_response), + patch.dict("os.environ", {"GP_ADMIN_USER_ID": "u", "GP_ADMIN_PASSWORD": "p"}), + patch("gp_client.requests.get", return_value=mock_response), ): - import importlib - - from scripts.gp import gp_client - importlib.reload(gp_client) - with pytest.raises(req.exceptions.HTTPError): gp_client.get_strings("fr") diff --git a/scripts/gp/tests/test_upload_strings.py b/scripts/gp/tests/test_upload_strings.py index 30c4e10f76..9664de145e 100644 --- a/scripts/gp/tests/test_upload_strings.py +++ b/scripts/gp/tests/test_upload_strings.py @@ -5,15 +5,11 @@ from unittest.mock import patch import pytest +import upload_strings as upload_mod + def _run_main(source_path: str): - """Helper to invoke upload_strings.main() with a --source argument.""" with patch("sys.argv", ["upload_strings.py", "--source", source_path]): - import importlib - - import scripts.gp.upload_strings as upload_mod - - importlib.reload(upload_mod) upload_mod.main() @@ -23,10 +19,10 @@ class TestUploadStrings: source.write_text(json.dumps({"hello": "Hello", "bye": "Bye"}), encoding="utf-8") with ( - patch("scripts.gp.upload_strings.list_bundles", return_value={"bundleIds": ["langflow-ui"]}), - patch("scripts.gp.upload_strings.create_bundle") as mock_create, - patch("scripts.gp.upload_strings.upload_strings") as mock_upload, - patch("scripts.gp.upload_strings.GP_BUNDLE", "langflow-ui"), + patch.object(upload_mod, "list_bundles", return_value={"bundleIds": ["langflow-ui"]}), + patch.object(upload_mod, "create_bundle") as mock_create, + patch.object(upload_mod, "upload_strings") as mock_upload, + patch.object(upload_mod, "GP_BUNDLE", "langflow-ui"), ): _run_main(str(source)) @@ -38,10 +34,10 @@ class TestUploadStrings: source.write_text(json.dumps({"hello": "Hello"}), encoding="utf-8") with ( - patch("scripts.gp.upload_strings.list_bundles", return_value={"bundleIds": []}), - patch("scripts.gp.upload_strings.create_bundle") as mock_create, - patch("scripts.gp.upload_strings.upload_strings") as mock_upload, - patch("scripts.gp.upload_strings.GP_BUNDLE", "langflow-ui"), + patch.object(upload_mod, "list_bundles", return_value={"bundleIds": []}), + patch.object(upload_mod, "create_bundle") as mock_create, + patch.object(upload_mod, "upload_strings") as mock_upload, + patch.object(upload_mod, "GP_BUNDLE", "langflow-ui"), ): _run_main(str(source)) @@ -53,10 +49,10 @@ class TestUploadStrings: source.write_text("{}", encoding="utf-8") with ( - patch("scripts.gp.upload_strings.list_bundles", return_value={"bundleIds": ["langflow-ui"]}), - patch("scripts.gp.upload_strings.create_bundle"), - patch("scripts.gp.upload_strings.upload_strings") as mock_upload, - patch("scripts.gp.upload_strings.GP_BUNDLE", "langflow-ui"), + patch.object(upload_mod, "list_bundles", return_value={"bundleIds": ["langflow-ui"]}), + patch.object(upload_mod, "create_bundle"), + patch.object(upload_mod, "upload_strings") as mock_upload, + patch.object(upload_mod, "GP_BUNDLE", "langflow-ui"), ): _run_main(str(source)) @@ -66,9 +62,9 @@ class TestUploadStrings: missing = str(tmp_path / "missing.json") with ( - patch("scripts.gp.upload_strings.list_bundles", return_value={"bundleIds": []}), - patch("scripts.gp.upload_strings.create_bundle"), - patch("scripts.gp.upload_strings.upload_strings"), + patch.object(upload_mod, "list_bundles", return_value={"bundleIds": []}), + patch.object(upload_mod, "create_bundle"), + patch.object(upload_mod, "upload_strings"), pytest.raises(FileNotFoundError), ): _run_main(missing) diff --git a/src/frontend/package-lock.json b/src/frontend/package-lock.json index 1f80ea893d..e362b90716 100644 --- a/src/frontend/package-lock.json +++ b/src/frontend/package-lock.json @@ -19080,7 +19080,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc",