blob: bf6ecbba04e112d03a27afb802f1cd26fb29a40b [file]
#!/usr/bin/env bash
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you 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.
# Runs the common.ai adapter tests for agent frameworks that cannot join the workspace lock.
#
# Strands Agents caps mcp, and Google ADK caps opentelemetry and websockets, below the versions
# uv.lock resolves; the OpenAI Agents SDK needs openai>=3 from 0.21.0, while the workspace holds
# openai below 3, so only its older releases could join the lock. Their adapter tests are skipped
# everywhere else in CI. This installs one
# framework into the CI image and runs the tests of the framework-neutral tools and their adapters,
# and of durable execution for Strands; the other frameworks' tests skip themselves. One framework
# per invocation, so a bad release of one cannot mask the others.
#
# By default every package already in the image is held at its installed version with uv's
# --override, so the framework is tested against the same dependencies as the rest of Airflow and
# its caps on them are overridden. With --framework-pins the framework's own requirements win
# instead, which is the environment a user who installs it gets.
#
# An override replaces a requirement rather than relaxing a cap, so the OpenAI Agents SDK's own
# openai>=3 would be replaced too, and its newest release would be tested against the image's
# openai 2.x, a pairing no user gets. While the image has openai 2.x, holding its packages
# therefore installs the newest release that accepts it, which is what Airflow's constraints
# resolve to.
#
# The newest framework release older than the repository's uv exclude-newer window is installed.
set -euo pipefail
TEST_PATHS=(
"providers/common/ai/tests/unit/common/ai/tools"
"providers/common/ai/tests/unit/common/ai/durable/test_strands.py"
"providers/common/ai/tests/unit/common/ai/durable/test_strands_storage.py"
)
framework="${1:-}"
# What to install when the image's packages are held; with --framework-pins the newest release.
held_requirement="${framework}"
case "${framework}" in
strands-agents) import_check="import strands" ;;
google-adk) import_check="import google.adk" ;;
openai-agents)
import_check="import agents"
if python -c 'import sys; from importlib.metadata import version; sys.exit(int(version("openai").split(".")[0]) >= 3)'; then
held_requirement="openai-agents<0.21"
fi
;;
*)
echo "Usage: $0 <strands-agents|google-adk|openai-agents> [--framework-pins]" >&2
exit 1
;;
esac
framework_pins="false"
if [[ ${2:-} == "--framework-pins" ]]; then
framework_pins="true"
elif [[ -n ${2:-} ]]; then
echo "Unknown argument: ${2}. The only option after the framework is --framework-pins." >&2
exit 1
fi
cd "${AIRFLOW_SOURCES:-/opt/airflow}"
if [[ ${framework_pins} == "true" ]]; then
echo "Installing ${framework} with its own dependency pins"
# The workspace's [tool.uv] overrides and constraints, such as its openai<3, shape uv.lock, not
# what a user who installs the framework gets, so leave them out and keep only its
# exclude-newer window.
exclude_newer=$(python -c 'import tomllib; print(tomllib.load(open("pyproject.toml", "rb"))["tool"]["uv"]["exclude-newer"])')
uv pip install --no-config --exclude-newer "${exclude_newer}" "${framework}"
else
overrides=$(mktemp)
before=$(mktemp)
after=$(mktemp)
trap 'rm -f "${overrides}" "${before}" "${after}"' EXIT
uv pip freeze | sort > "${before}"
# Only name==version lines: editable and local installs cannot be expressed as an override,
# and the frameworks do not depend on any of them. Overriding the rest means nothing the
# image ships should change; the check below is there in case something still does.
grep -E '^[A-Za-z0-9_.-]+==' "${before}" > "${overrides}"
echo "Installing ${held_requirement}, holding the image's $(wc -l < "${overrides}") installed packages"
uv pip install --override "${overrides}" "${held_requirement}"
uv pip freeze | sort > "${after}"
changed=$(comm -23 "${before}" "${after}")
if [[ -n ${changed} ]]; then
echo "Installing ${framework} changed packages the image already had:" >&2
echo "${changed}" >&2
exit 1
fi
fi
# Log the versions that decide whether the adapter works; the framework itself must be among them.
uv pip freeze | grep -iE '^(strands-agents|google-adk|openai-agents|openai|mcp|opentelemetry-(api|sdk)|websockets|google-genai)==' \
|| { echo "${framework} is not installed after uv pip install" >&2; exit 1; }
# The adapter tests skip themselves when their framework is missing, so a broken install would
# otherwise pass as green.
python -c "${import_check}"
# --skip-db-tests: the job runs with backend "none", which has no database to set up.
pytest "${TEST_PATHS[@]}" --skip-db-tests -p no:cacheprovider --color=yes -ra