-
Notifications
You must be signed in to change notification settings - Fork 12
feat: add integration deployment test for LangGraph HITL agent #90
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| # Re-export shared integration fixtures so pytest discovers them. | ||
| from integration.conftest import cluster_auth, repo_root # noqa: F401 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,90 @@ | ||
| from __future__ import annotations | ||
|
|
||
| import logging | ||
| import os | ||
|
|
||
| import pytest | ||
| from integration.utils import ( | ||
| MakeTargetError, | ||
| RouteNotFoundError, | ||
| get_route, | ||
| health_check, | ||
| load_agent_name, | ||
| run_make, | ||
| ) | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
| INTERNAL_REGISTRY = "image-registry.openshift-image-registry.svc:5000" | ||
|
|
||
|
|
||
| @pytest.fixture(scope="module") | ||
| def agent_dir(repo_root): | ||
| return repo_root / "agents" / "langgraph" / "human_in_the_loop" | ||
|
|
||
|
|
||
| @pytest.fixture(scope="module") | ||
| def agent_name(agent_dir): | ||
| return load_agent_name(agent_dir) | ||
|
|
||
|
|
||
| def _write_env_file(agent_dir, container_image): | ||
| """Write a .env file so Makefile targets can source it.""" | ||
| missing = [v for v in ("BASE_URL", "MODEL_ID") if v not in os.environ] | ||
| if missing: | ||
| pytest.fail( | ||
| f"Missing required env vars: {', '.join(missing)}. " | ||
| "Set them in the CI workflow or export locally." | ||
| ) | ||
| env_path = agent_dir / ".env" | ||
| env_path.write_text( | ||
| f"API_KEY={os.environ.get('API_KEY', 'not-needed')}\n" | ||
| f"BASE_URL={os.environ['BASE_URL']}\n" | ||
| f"MODEL_ID={os.environ['MODEL_ID']}\n" | ||
| f"CONTAINER_IMAGE={container_image}\n" | ||
| ) | ||
| return env_path | ||
|
|
||
|
|
||
| @pytest.fixture(scope="module") | ||
| def deployed_agent(cluster_auth, agent_dir, agent_name): | ||
| namespace = cluster_auth["namespace"] | ||
| container_image = f"{INTERNAL_REGISTRY}/{namespace}/{agent_name}:latest" | ||
| env_path = _write_env_file(agent_dir, container_image) | ||
|
|
||
| deployed = False | ||
| try: | ||
| logger.info("Building image on cluster via build-openshift...") | ||
| run_make("build-openshift", cwd=agent_dir, timeout=600) | ||
|
|
||
| logger.info("Deploying to cluster...") | ||
| run_make("deploy", cwd=agent_dir, timeout=300) | ||
| deployed = True | ||
|
|
||
| route_url = get_route(agent_name, namespace=namespace) | ||
| logger.info("Agent deployed at %s", route_url) | ||
|
|
||
| yield route_url | ||
|
|
||
| except (MakeTargetError, RouteNotFoundError) as exc: | ||
| pytest.fail(f"Deployment failed: {exc}") | ||
|
|
||
| finally: | ||
| if deployed: | ||
| logger.info("Tearing down deployment...") | ||
| try: | ||
| run_make("undeploy", cwd=agent_dir, timeout=120) | ||
| except MakeTargetError: | ||
| logger.warning( | ||
| "Cleanup failed — manual undeploy may be needed", exc_info=True | ||
| ) | ||
| env_path.unlink(missing_ok=True) | ||
|
|
||
|
|
||
| @pytest.mark.integration | ||
| def test_health_endpoint(deployed_agent): | ||
| route_url = deployed_agent | ||
| result = health_check(f"{route_url}/health", retries=12, backoff=5.0) | ||
|
|
||
| assert result["status"] == "healthy" | ||
| assert result["agent_initialized"] is True | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -8,9 +8,18 @@ | |||||||||||||||||||||||||
| from pathlib import Path | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| import httpx | ||||||||||||||||||||||||||
| import yaml | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| logger = logging.getLogger(__name__) | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| def load_agent_name(agent_dir: str | Path) -> str: | ||||||||||||||||||||||||||
| data = yaml.safe_load((Path(agent_dir) / "agent.yaml").read_text()) | ||||||||||||||||||||||||||
| if not isinstance(data, dict) or "name" not in data: | ||||||||||||||||||||||||||
| raise ValueError(f"No 'name' field in {agent_dir}/agent.yaml") | ||||||||||||||||||||||||||
| return str(data["name"]).strip() | ||||||||||||||||||||||||||
|
Comment on lines
+17
to
+20
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Validate Line 20 currently coerces any YAML value to string ( Suggested fix def load_agent_name(agent_dir: str | Path) -> str:
data = yaml.safe_load((Path(agent_dir) / "agent.yaml").read_text())
if not isinstance(data, dict) or "name" not in data:
raise ValueError(f"No 'name' field in {agent_dir}/agent.yaml")
- return str(data["name"]).strip()
+ name = data["name"]
+ if not isinstance(name, str) or not name.strip():
+ raise ValueError(f"Invalid 'name' value in {agent_dir}/agent.yaml: expected non-empty string")
+ return name.strip()As per coding guidelines, "Focus on major issues impacting performance, readability, maintainability and security. Avoid nitpicks and avoid verbosity." 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| _REDACT_PATTERNS = [ | ||||||||||||||||||||||||||
| re.compile(r"(API_KEY=)\S+"), | ||||||||||||||||||||||||||
| re.compile(r'(apiKey:\s*")[^"]*"'), | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.