mirror of
https://github.com/langflow-ai/langflow.git
synced 2026-07-25 10:32:22 +08:00
fix(i18n): address PR review comments — tests, nightly build decoupling, GP test workflow
- Add pytest tests for gp_client.py (HMAC auth, HTTP errors, timeout)
- Add pytest tests for upload_strings.py and download_translations.py
- Add conftest.py to set sys.path so GP scripts can be imported from repo root
- Add scripts/gp/__init__.py to make gp a proper package for test imports
- Add gp-test.yml workflow to run GP script tests on PRs touching scripts/gp/**
- Decouple download-gp-bundle from nightly build chain:
- Add continue-on-error: true so GP failure never blocks the build
- Remove download-gp-bundle from needs/if of frontend-tests-linux,
frontend-tests-windows, and release-nightly-build
- Add warning annotation step that fires on GP download failure
This commit is contained in:
27
.github/workflows/gp-test.yml
vendored
Normal file
27
.github/workflows/gp-test.yml
vendored
Normal file
@ -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
|
||||
8
.github/workflows/nightly_build.yml
vendored
8
.github/workflows/nightly_build.yml
vendored
@ -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
|
||||
|
||||
0
scripts/gp/__init__.py
Normal file
0
scripts/gp/__init__.py
Normal file
6
scripts/gp/tests/conftest.py
Normal file
6
scripts/gp/tests/conftest.py
Normal file
@ -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))
|
||||
@ -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))
|
||||
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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)
|
||||
|
||||
2
src/frontend/package-lock.json
generated
2
src/frontend/package-lock.json
generated
@ -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",
|
||||
|
||||
Reference in New Issue
Block a user