From 940bcaadd5ca1cfb5e8c77b38a0c945e3d002ac5 Mon Sep 17 00:00:00 2001 From: Vaithee Baskaran Date: Mon, 20 Apr 2026 12:00:46 -0700 Subject: [PATCH 1/4] ci: add GitHub Actions workflow for conformance tests Adds a CI workflow that: - Checks out conformance, samples, and SDK repos - Starts the Flower Shop reference server with test data - Runs the full conformance test suite against it - Triggers on PRs, pushes to main, and nightly schedule Results are informational (continue-on-error) so the workflow does not block merges. This surfaces test failures early without gating PRs on a suite that depends on cross-repo compatibility. Addresses #38 --- .github/workflows/conformance-tests.yml | 113 ++++++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 .github/workflows/conformance-tests.yml diff --git a/.github/workflows/conformance-tests.yml b/.github/workflows/conformance-tests.yml new file mode 100644 index 0000000..3733d5e --- /dev/null +++ b/.github/workflows/conformance-tests.yml @@ -0,0 +1,113 @@ +# Copyright 2026 UCP Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: Conformance Tests + +on: + pull_request: + branches: [main] + push: + branches: [main] + schedule: + # Nightly at 2:17 AM UTC + - cron: "17 2 * * *" + workflow_dispatch: + +permissions: + contents: read + +env: + MERCHANT_SERVER_PORT: 8182 + SIMULATION_SECRET: super-secret-sim-key + DATABASE_PATH: /tmp/ucp_test + +jobs: + test: + name: Run Conformance Tests + runs-on: ubuntu-latest + steps: + - name: Check out conformance repo + uses: actions/checkout@v4 + with: + path: conformance + + - name: Check out samples repo + uses: actions/checkout@v4 + with: + repository: Universal-Commerce-Protocol/samples + path: samples + + - name: Check out SDK repo + uses: actions/checkout@v4 + with: + repository: Universal-Commerce-Protocol/sdk + path: sdk + + - name: Install uv + uses: astral-sh/setup-uv@v6 + + - name: Set up Python + run: uv python install 3.12 + + - name: Sync dependencies + run: | + uv sync --directory sdk/python/ + uv sync --directory samples/rest/python/server/ + uv sync --directory conformance/ + + - name: Initialize test database + run: | + rm -rf ${DATABASE_PATH} + mkdir -p ${DATABASE_PATH} + uv run --directory samples/rest/python/server import_csv.py \ + --products_db_path=${DATABASE_PATH}/products.db \ + --transactions_db_path=${DATABASE_PATH}/transactions.db \ + --data_dir=../../../../conformance/test_data/flower_shop + + - name: Start Flower Shop server + run: | + uv run --directory samples/rest/python/server server.py \ + --products_db_path=${DATABASE_PATH}/products.db \ + --transactions_db_path=${DATABASE_PATH}/transactions.db \ + --port=${MERCHANT_SERVER_PORT} \ + --simulation_secret=${SIMULATION_SECRET} & + # Wait for the server to be ready + for i in $(seq 1 30); do + if curl -sf http://localhost:${MERCHANT_SERVER_PORT}/.well-known/ucp > /dev/null 2>&1; then + echo "Server is ready" + break + fi + if [ "$i" -eq 30 ]; then + echo "Server failed to start within 30 seconds" + exit 1 + fi + sleep 1 + done + + - name: Run conformance tests + continue-on-error: true + working-directory: conformance + run: | + EXIT_CODE=0 + for test_file in *_test.py; do + echo "::group::${test_file}" + if ! uv run "${test_file}" \ + --server_url=http://localhost:${MERCHANT_SERVER_PORT} \ + --simulation_secret=${SIMULATION_SECRET} \ + --conformance_input=test_data/flower_shop/conformance_input.json; then + EXIT_CODE=1 + fi + echo "::endgroup::" + done + exit ${EXIT_CODE} From 65042def49f93444bc1bfee2177bffb6ad1fa460 Mon Sep 17 00:00:00 2001 From: Vaithee Baskaran Date: Fri, 24 Apr 2026 15:39:51 -0700 Subject: [PATCH 2/4] Address review: use python-sdk repo and gate continue-on-error to PR runs only --- .github/workflows/conformance-tests.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/conformance-tests.yml b/.github/workflows/conformance-tests.yml index 3733d5e..fe9dfe9 100644 --- a/.github/workflows/conformance-tests.yml +++ b/.github/workflows/conformance-tests.yml @@ -51,8 +51,8 @@ jobs: - name: Check out SDK repo uses: actions/checkout@v4 with: - repository: Universal-Commerce-Protocol/sdk - path: sdk + repository: Universal-Commerce-Protocol/python-sdk + path: python-sdk - name: Install uv uses: astral-sh/setup-uv@v6 @@ -62,7 +62,7 @@ jobs: - name: Sync dependencies run: | - uv sync --directory sdk/python/ + uv sync --directory python-sdk/ uv sync --directory samples/rest/python/server/ uv sync --directory conformance/ @@ -96,7 +96,7 @@ jobs: done - name: Run conformance tests - continue-on-error: true + continue-on-error: ${{ github.event_name == 'pull_request' }} working-directory: conformance run: | EXIT_CODE=0 From 36acfb7b5bbcd3092fce62f5b3480bd57c6768a7 Mon Sep 17 00:00:00 2001 From: Vaithee Baskaran Date: Fri, 24 Apr 2026 16:00:36 -0700 Subject: [PATCH 3/4] Disable uv cache prune to avoid post-job lock timeout --- .github/workflows/conformance-tests.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/conformance-tests.yml b/.github/workflows/conformance-tests.yml index fe9dfe9..af25628 100644 --- a/.github/workflows/conformance-tests.yml +++ b/.github/workflows/conformance-tests.yml @@ -56,6 +56,8 @@ jobs: - name: Install uv uses: astral-sh/setup-uv@v6 + with: + prune-cache: false - name: Set up Python run: uv python install 3.12 From 52e026ae91db3db70972a6eae7efb981efd09150 Mon Sep 17 00:00:00 2001 From: Vaithee Baskaran Date: Tue, 28 Apr 2026 08:15:19 -0700 Subject: [PATCH 4/4] fix: validate payment handler structure from profile instead of hardcoded list (#33) * fix: validate payment handler structure from profile instead of hardcoded list Replace hardcoded expected handler IDs (google_pay, mock_payment_handler, shop_pay) with structural validation that: - Discovers handlers from the business profile dynamically - Validates required fields (id, version) are present - Validates handler group names follow reverse-DNS convention - Works against any UCP merchant, not just the Flower Shop This makes the protocol conformance tests server-agnostic. Addresses #13 * Catch ValidationError specifically when validating handler group names --- protocol_test.py | 62 +++++++++++++++++++++++++++--------------------- 1 file changed, 35 insertions(+), 27 deletions(-) diff --git a/protocol_test.py b/protocol_test.py index 2b4d589..4ac7d47 100644 --- a/protocol_test.py +++ b/protocol_test.py @@ -17,7 +17,8 @@ from absl.testing import absltest import integration_test_utils import httpx -from ucp_sdk.models.schemas.ucp import BusinessSchema +from pydantic import ValidationError +from ucp_sdk.models.schemas.ucp import BusinessSchema, ReverseDomainName from ucp_sdk.models.schemas.shopping import checkout as checkout from ucp_sdk.models.schemas.shopping.payment import ( Payment, @@ -180,32 +181,39 @@ def test_discovery(self): f"Missing expected capabilities in discovery: {missing_caps}", ) - # Verify Payment Handlers - handlers = { - h.get("id") - for handlers in data.get("payment_handlers", {}).values() - for h in (handlers if isinstance(handlers, list) else [handlers]) - } - expected_handlers = {"google_pay", "mock_payment_handler", "shop_pay"} - missing_handlers = expected_handlers - handlers - self.assertFalse( - missing_handlers, - f"Missing expected payment handlers: {missing_handlers}", - ) - - # Specific check for Shop Pay config - shop_pay = next( - ( - h - for handlers in data.get("payment_handlers", {}).values() - for h in (handlers if isinstance(handlers, list) else [handlers]) - if h.get("id") == "shop_pay" - ), - None, - ) - self.assertIsNotNone(shop_pay, "Shop Pay handler not found") - self.assertEqual(shop_pay.get("name"), "com.shopify.shop_pay") - self.assertIn("shop_id", shop_pay.get("config")) + # Verify Payment Handlers - structural validation (server-agnostic) + if data.get("payment_handlers"): + handler_count = 0 + for handler_name, handler_list in data.get( + "payment_handlers", {} + ).items(): + # Validate handler group name using the SDK's ReverseDomainName + # model, which enforces the pattern defined in the UCP spec. + try: + ReverseDomainName(root=str(handler_name)) + except ValidationError as e: + self.fail( + f"Payment handler group name '{handler_name}' " + f"does not follow reverse-DNS convention: {e}" + ) + for h in ( + handler_list if isinstance(handler_list, list) else [handler_list] + ): + handler_count += 1 + # Validate required fields are present and non-empty + self.assertTrue( + h.get("id"), + "Payment handler missing 'id'", + ) + self.assertTrue( + h.get("version"), + f"Payment handler '{h.get('id')}' missing 'version'", + ) + self.assertGreater( + handler_count, + 0, + "payment_handlers is present but contains no handlers", + ) # Verify shopping capability shopping_services = data.get("services", {}).get("dev.ucp.shopping")