Skip to content

Commit e537e34

Browse files
authored
Merge pull request #83 from red-hat-data-services/feat/RHAIENG-4643-langgraph-react-integration-test
feat: add integration test for LangGraph React Agent deployment (RHAIENG-4643)
2 parents e7c4fc6 + 4ac9160 commit e537e34

10 files changed

Lines changed: 371 additions & 43 deletions

File tree

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
name: "Setup Cluster Tools"
2+
description: "Install Python 3.12, uv, oc, helm, and login to OpenShift. Requires actions/checkout to run first."
3+
4+
inputs:
5+
oc-token:
6+
description: "OpenShift service account token"
7+
required: true
8+
cluster-api-url:
9+
description: "OpenShift cluster API URL"
10+
required: true
11+
12+
runs:
13+
using: "composite"
14+
steps:
15+
- name: Install Python 3.12
16+
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
17+
with:
18+
python-version: "3.12"
19+
20+
- name: Install uv
21+
uses: astral-sh/setup-uv@6b9c6063abd6010835644d4c2e1bef4cf5cd0fca # v6.0.1
22+
23+
- name: Install OpenShift CLI and Helm
24+
uses: redhat-actions/openshift-tools-installer@144527c7d98999f2652264c048c7a9bd103f8a82 # v1.13.1
25+
with:
26+
oc: "4"
27+
helm: "3"
28+
29+
- name: Login to OpenShift
30+
shell: bash
31+
env:
32+
OC_TOKEN: ${{ inputs.oc-token }}
33+
CLUSTER_API_URL: ${{ inputs.cluster-api-url }}
34+
run: |
35+
oc login \
36+
--token="$OC_TOKEN" \
37+
--server="$CLUSTER_API_URL" \
38+
--namespace=ci-testing
Lines changed: 48 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
# QG4: Agent Deployment Health Checks
22
#
3-
# Foundation workflow: validates that the GitHub Actions runner can connect
4-
# to the OpenShift cluster. Agent deployment and health checks will be
5-
# wired up in follow-up tickets.
3+
# Nightly workflow: logs into the OpenShift demo cluster, deploys each
4+
# agent via build-openshift + Helm, validates GET /health, and tears down.
65

7-
name: "QG4: Agent Deployment Health Checks"
6+
name: "QG4: Agent Deployment Integration Tests"
87

98
on:
109
schedule:
@@ -17,49 +16,18 @@ permissions:
1716
jobs:
1817
verify-cluster-connection:
1918
name: "Verify Cluster Connection"
19+
if: github.repository == 'red-hat-data-services/agentic-starter-kits'
2020
runs-on: ubuntu-latest
2121
timeout-minutes: 10
2222
steps:
2323
- name: Checkout code
2424
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
2525

26-
- name: Install Python 3.12
27-
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
26+
- name: Setup cluster tools
27+
uses: ./.github/actions/setup-cluster
2828
with:
29-
python-version: "3.12"
30-
31-
- name: Install uv
32-
uses: astral-sh/setup-uv@6b9c6063abd6010835644d4c2e1bef4cf5cd0fca # v6.0.1
33-
34-
- name: Install OpenShift CLI and Helm
35-
uses: redhat-actions/openshift-tools-installer@144527c7d98999f2652264c048c7a9bd103f8a82 # v1.13.1
36-
with:
37-
oc: "4"
38-
helm: "3"
39-
40-
- name: Verify installed tools
41-
run: |
42-
echo "--- oc ---"
43-
oc version --client
44-
echo ""
45-
echo "--- helm ---"
46-
helm version --short
47-
echo ""
48-
echo "--- python ---"
49-
python3 --version
50-
echo ""
51-
echo "--- uv ---"
52-
uv --version
53-
54-
- name: Login to OpenShift
55-
env:
56-
OC_TOKEN: ${{ secrets.OC_TOKEN }}
57-
CLUSTER_API_URL: ${{ secrets.CLUSTER_API_URL }}
58-
run: |
59-
oc login \
60-
--token="$OC_TOKEN" \
61-
--server="$CLUSTER_API_URL" \
62-
--namespace=ci-testing
29+
oc-token: ${{ secrets.OC_TOKEN }}
30+
cluster-api-url: ${{ secrets.CLUSTER_API_URL }}
6331

6432
- name: Verify cluster connection
6533
run: |
@@ -78,3 +46,43 @@ jobs:
7846
- name: Logout
7947
if: always()
8048
run: oc logout || true
49+
50+
test-agent:
51+
name: "${{ matrix.agent.name }}"
52+
needs: verify-cluster-connection
53+
if: github.repository == 'red-hat-data-services/agentic-starter-kits'
54+
runs-on: ubuntu-latest
55+
timeout-minutes: 20
56+
strategy:
57+
fail-fast: false
58+
matrix:
59+
agent:
60+
- { name: langgraph-react-agent, dir: agents/langgraph/react_agent }
61+
env:
62+
API_KEY: ${{ vars.API_KEY }}
63+
BASE_URL: ${{ vars.BASE_URL }}
64+
MODEL_ID: ${{ vars.MODEL_ID }}
65+
steps:
66+
- name: Checkout code
67+
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
68+
69+
- name: Setup cluster tools
70+
uses: ./.github/actions/setup-cluster
71+
with:
72+
oc-token: ${{ secrets.OC_TOKEN }}
73+
cluster-api-url: ${{ secrets.CLUSTER_API_URL }}
74+
75+
- name: Run integration test
76+
working-directory: ${{ matrix.agent.dir }}
77+
run: make test-integration
78+
79+
- name: Upload test results
80+
if: always()
81+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
82+
with:
83+
name: ${{ matrix.agent.name }}-results
84+
path: ${{ matrix.agent.dir }}/results.xml
85+
86+
- name: Logout
87+
if: always()
88+
run: oc logout || true

agents/langgraph/react_agent/Makefile

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ VALUES_FILE := values.yaml
55
CONTAINER_CLI := $(shell command -v podman 2>/dev/null || command -v docker 2>/dev/null)
66
MODEL ?= llama3.1:8b
77

8-
.PHONY: init re-init env ollama llama-server run-app run-app-fresh run-cli build push build-openshift deploy undeploy test dry-run help
8+
.PHONY: init re-init env ollama llama-server run-app run-app-fresh run-cli build push build-openshift deploy undeploy test test-integration dry-run help
99

1010
help: ## Show this help
1111
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " %-12s %s\n", $$1, $$2}'
@@ -169,4 +169,9 @@ undeploy: ## Remove deployment from cluster
169169
helm uninstall $(AGENT_NAME)
170170

171171
test: ## Run tests
172-
uv run --extra dev python -m pytest tests/
172+
uv run --extra dev python -m pytest tests/ --ignore=tests/integration
173+
174+
test-integration: ## Run integration deployment test
175+
PYTHONPATH=$$(git rev-parse --show-toplevel)/tests \
176+
uv run --extra dev python -m pytest tests/integration/test_deployment.py \
177+
-v --tb=long --junitxml=results.xml

agents/langgraph/react_agent/pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ dependencies = [
2525
[project.optional-dependencies]
2626
dev = [
2727
"pytest>=9.0.2",
28+
"httpx>=0.27",
2829
]
2930
eval = [
3031
"unitxt>=1.24.0",
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Re-export shared integration fixtures so pytest discovers them.
2+
from integration.conftest import cluster_auth, repo_root # noqa: F401
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
from __future__ import annotations
2+
3+
import logging
4+
import os
5+
6+
import pytest
7+
from integration.utils import (
8+
MakeTargetError,
9+
RouteNotFoundError,
10+
get_route,
11+
health_check,
12+
run_make,
13+
)
14+
15+
logger = logging.getLogger(__name__)
16+
17+
AGENT_NAME = "langgraph-react-agent"
18+
INTERNAL_REGISTRY = "image-registry.openshift-image-registry.svc:5000"
19+
20+
21+
@pytest.fixture(scope="module")
22+
def agent_dir(repo_root):
23+
return repo_root / "agents" / "langgraph" / "react_agent"
24+
25+
26+
def _write_env_file(agent_dir, container_image):
27+
"""Write a .env file so Makefile targets can source it."""
28+
missing = [v for v in ("BASE_URL", "MODEL_ID") if v not in os.environ]
29+
if missing:
30+
pytest.fail(
31+
f"Missing required env vars: {', '.join(missing)}. "
32+
"Set them in the CI workflow or export locally."
33+
)
34+
env_path = agent_dir / ".env"
35+
env_path.write_text(
36+
f"API_KEY={os.environ.get('API_KEY', 'not-needed')}\n"
37+
f"BASE_URL={os.environ['BASE_URL']}\n"
38+
f"MODEL_ID={os.environ['MODEL_ID']}\n"
39+
f"CONTAINER_IMAGE={container_image}\n"
40+
)
41+
return env_path
42+
43+
44+
@pytest.fixture(scope="module")
45+
def deployed_agent(cluster_auth, agent_dir):
46+
namespace = cluster_auth["namespace"]
47+
container_image = f"{INTERNAL_REGISTRY}/{namespace}/{AGENT_NAME}:latest"
48+
env_path = _write_env_file(agent_dir, container_image)
49+
50+
deployed = False
51+
try:
52+
logger.info("Building image on cluster via build-openshift...")
53+
run_make("build-openshift", cwd=agent_dir, timeout=600)
54+
55+
logger.info("Deploying to cluster...")
56+
run_make("deploy", cwd=agent_dir, timeout=300)
57+
deployed = True
58+
59+
route_url = get_route(AGENT_NAME, namespace=namespace)
60+
logger.info("Agent deployed at %s", route_url)
61+
62+
yield route_url
63+
64+
except (MakeTargetError, RouteNotFoundError) as exc:
65+
pytest.fail(f"Deployment failed: {exc}")
66+
67+
finally:
68+
if deployed:
69+
logger.info("Tearing down deployment...")
70+
try:
71+
run_make("undeploy", cwd=agent_dir, timeout=120)
72+
except MakeTargetError:
73+
logger.warning(
74+
"Cleanup failed — manual undeploy may be needed", exc_info=True
75+
)
76+
env_path.unlink(missing_ok=True)
77+
78+
79+
@pytest.mark.integration
80+
def test_health_endpoint(deployed_agent):
81+
route_url = deployed_agent
82+
result = health_check(f"{route_url}/health", retries=12, backoff=5.0)
83+
84+
assert result["status"] == "healthy"
85+
assert result["agent_initialized"] is True

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,8 @@ markers = [
5555
"model_baseline: Model-level tests (prompt injection resistance) - not agent-specific",
5656
"slow: Tests that run multiple iterations (pass@k, repeated queries)",
5757
"unit: Fast unit tests for adapter/config packages (no network, no agent required)",
58-
"integration: Integration tests for adapter orchestration (mocked HTTP, no live agent)",
58+
# --- Integration test suite (agents/*/tests/integration/) ---
59+
"integration: Integration tests (adapter orchestration, deployment health checks)",
5960
]
6061

6162
[tool.setuptools.packages.find]

tests/integration/__init__.py

Whitespace-only changes.

tests/integration/conftest.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
from __future__ import annotations
2+
3+
import subprocess
4+
from pathlib import Path
5+
6+
import pytest
7+
8+
9+
@pytest.fixture(scope="session")
10+
def cluster_auth():
11+
try:
12+
user = subprocess.run(
13+
["oc", "whoami"],
14+
capture_output=True,
15+
text=True,
16+
timeout=15,
17+
check=True,
18+
).stdout.strip()
19+
20+
namespace = subprocess.run(
21+
["oc", "project", "-q"],
22+
capture_output=True,
23+
text=True,
24+
timeout=15,
25+
check=True,
26+
).stdout.strip()
27+
except (
28+
subprocess.CalledProcessError,
29+
FileNotFoundError,
30+
subprocess.TimeoutExpired,
31+
):
32+
pytest.skip("Not logged into an OpenShift cluster")
33+
34+
if namespace != "ci-testing":
35+
pytest.fail(
36+
f"Integration tests must run in ci-testing namespace, got: {namespace}"
37+
)
38+
39+
return {"user": user, "namespace": namespace}
40+
41+
42+
@pytest.fixture(scope="session")
43+
def repo_root() -> Path:
44+
root = Path(__file__).resolve().parent.parent.parent
45+
assert (root / "agents").is_dir(), f"Repo root not found at {root}"
46+
return root

0 commit comments

Comments
 (0)