chore: import upstream snapshot with attribution
Continuous Integration / Pre-commit Linter (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.10) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.11) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.12) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.13) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.10) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.11) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.12) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.13) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.14) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.10) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.11) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.12) (push) Has been cancelled
Copybara PR Handler / close-imported-pr (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.13) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.14) (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:25:13 +08:00
commit ec2b666284
2231 changed files with 491535 additions and 0 deletions
+67
View File
@@ -0,0 +1,67 @@
# Remote Integration Tests for `/apps/{app_name}/trigger/*` Endpoints
End-to-end tests that verify Pub/Sub push, and
Eventarc triggers against a real ADK agent deployed to Cloud Run.
## Workflow
This workflow is split into three independent phases.
### Phase 1: Deploy Agent
Build the ADK package and deploy the echo agent to Cloud Run. This ensures that
the service and its identity exist before any infrastructure wiring occurs.
```bash
export GCP_PROJECT_ID=your-project-id
export SUFFIX=ea786 # Pick a unique suffix
export SERVICE_NAME=adk-trigger-test-$SUFFIX
# 1. Build local ADK wheel
uv build --wheel --out-dir tests/remote/triggers/test_agent/wheels/
# 2. Deploy Agent
gcloud run deploy $SERVICE_NAME \
--source=tests/remote/triggers/test_agent \
--project="$GCP_PROJECT_ID" \
--region="us-central1" \
--port=8080 \
--quiet
```
### Phase 2: Wire Infrastructure (Terraform)
Run Terraform to create the supporting infrastructure (IAM roles, Pub/Sub
topics).
```bash
cd tests/remote/triggers/terraform
terraform init
terraform apply \
-var=project_id=$GCP_PROJECT_ID \
-var=service_name=$SERVICE_NAME \
-var=suffix=$SUFFIX
```
### Phase 3: Run Tests (Pytest)
```bash
# Run Tests from the project root
export GCP_PROJECT_ID=your-project-id
export SUFFIX=ea786
pytest tests/remote/triggers/ -v -s
```
## Cleanup
```bash
# 1. Destroy infrastructure
cd tests/remote/triggers/terraform
terraform destroy \
-var=project_id=$GCP_PROJECT_ID \
-var=service_name=$SERVICE_NAME \
-var=suffix=$SUFFIX
# 2. Delete Cloud Run service
gcloud run services delete $SERVICE_NAME --project=$GCP_PROJECT_ID --region=us-central1 --quiet
```
+169
View File
@@ -0,0 +1,169 @@
# Copyright 2026 Google LLC
#
# 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.
"""Shared pytest fixtures for remote trigger integration tests.
Exposes GCP resource references by reading current Terraform state.
Environment variables:
GCP_PROJECT_ID : GCP project.
GCP_REGION : GCP region (default: ``us-central1``).
ADK_TERRAFORM_BIN : Path to terraform binary (default: ``terraform``).
ADK_TERRAFORM_CWD : Directory to run terraform from (default:
``tests/remote/terraform``).
ADK_TERRAFORM_ARGS: Extra arguments for terraform commands.
"""
from __future__ import annotations
import json
import os
import shlex
import subprocess
import time
import pytest
import requests
TERRAFORM_DIR = os.path.join(os.path.dirname(__file__), "terraform")
def _get_project_id() -> str | None:
"""Return GCP project ID from env or gcloud config."""
project = os.environ.get("GCP_PROJECT_ID")
if project:
return project
try:
result = subprocess.run(
["gcloud", "config", "get-value", "project"],
capture_output=True,
text=True,
check=False,
)
return result.stdout.strip() or None
except FileNotFoundError:
return None
def _get_identity_token(audience: str) -> str | None:
"""Fetch an identity token for the given audience via gcloud."""
try:
result = subprocess.run(
[
"gcloud",
"auth",
"print-identity-token",
f"--audiences={audience}",
],
capture_output=True,
text=True,
check=True,
)
return result.stdout.strip()
except (FileNotFoundError, subprocess.CalledProcessError):
return None
# ---------------------------------------------------------------------------
# Infrastructure Fixtures
# ---------------------------------------------------------------------------
@pytest.fixture(scope="session")
def terraform_outputs():
"""Read Terraform outputs from the current state."""
project = _get_project_id()
if not project:
pytest.skip(
"GCP_PROJECT_ID not set and no gcloud default project configured"
)
tf_bin = os.environ.get("ADK_TERRAFORM_BIN", "terraform")
tf_cwd = os.environ.get("ADK_TERRAFORM_CWD", TERRAFORM_DIR)
tf_args = shlex.split(os.environ.get("ADK_TERRAFORM_ARGS", ""))
try:
# Build the command using provided overrides
cmd = [tf_bin] + tf_args + ["output", "-json"]
result = subprocess.run(
cmd,
cwd=tf_cwd,
capture_output=True,
text=True,
check=True,
)
raw = json.loads(result.stdout)
return {k: v["value"] for k, v in raw.items()}
except (
subprocess.CalledProcessError,
FileNotFoundError,
json.JSONDecodeError,
) as e:
pytest.fail(
"Failed to read Terraform outputs. Ensure 'terraform apply' has been"
f" run successfully.\nCommand: {' '.join(cmd)}\nCWD:"
f" {tf_cwd}\nError: {e}"
)
@pytest.fixture(scope="session")
def cloud_run_url(terraform_outputs) -> str:
"""Base URL of the deployed Cloud Run service."""
return terraform_outputs["cloud_run_url"]
@pytest.fixture(scope="session")
def pubsub_topic(terraform_outputs) -> str:
"""Fully qualified Pub/Sub topic name."""
return terraform_outputs["pubsub_topic"]
@pytest.fixture(scope="session")
def pubsub_topic_short(terraform_outputs) -> str:
"""Short Pub/Sub topic name."""
return terraform_outputs["pubsub_topic_short"]
@pytest.fixture(scope="session")
def eventarc_topic(terraform_outputs) -> str:
"""Fully qualified Eventarc source topic name."""
return terraform_outputs["eventarc_topic"]
@pytest.fixture(scope="session")
def eventarc_topic_short(terraform_outputs) -> str:
"""Short Eventarc source topic name."""
return terraform_outputs["eventarc_topic_short"]
@pytest.fixture(scope="session")
def project_id(terraform_outputs) -> str:
"""GCP project ID."""
return terraform_outputs["project_id"]
@pytest.fixture(scope="session")
def auth_headers(cloud_run_url) -> dict[str, str]:
"""Authorization headers for direct HTTP calls to Cloud Run."""
try:
resp = requests.get(cloud_run_url, timeout=10)
if resp.status_code != 403:
return {}
except requests.RequestException:
pass
token = _get_identity_token(cloud_run_url)
if token:
return {"Authorization": f"Bearer {token}"}
return {}
@@ -0,0 +1,35 @@
# Copyright 2026 Google LLC
#
# 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.
# ---------------------------------------------------------------------------
# ADK Agent for trigger testing.
#
# This configuration references a Cloud Run service that has been deployed
# manually (e.g. via `gcloud run deploy`) before running Terraform.
# ---------------------------------------------------------------------------
locals {
service_name = var.service_name != null ? var.service_name : "${local.name_prefix}-${local.suffix}"
}
# Read the service back as a data source so other resources can reference
# its URL and attributes.
data "google_cloud_run_v2_service" "trigger_agent" {
name = local.service_name
location = var.region
project = var.project_id
}
# No longer using resource "google_cloud_run_v2_service" to avoid
# deployment conflicts with local tools.
+120
View File
@@ -0,0 +1,120 @@
# Copyright 2026 Google LLC
#
# 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.
# ---------------------------------------------------------------------------
# Eventarc: separate Pub/Sub topic as event source → Cloud Run /trigger/eventarc
# ---------------------------------------------------------------------------
# A dedicated topic that acts as the Eventarc event source.
# Publishing to this topic triggers the Eventarc → Cloud Run pipeline.
resource "google_pubsub_topic" "eventarc_source" {
name = "${local.name_prefix}-eventarc-${local.suffix}"
project = var.project_id
depends_on = [google_project_service.apis]
}
resource "google_eventarc_trigger" "trigger_test" {
name = "${local.name_prefix}-${local.suffix}"
location = var.region
project = var.project_id
matching_criteria {
attribute = "type"
value = "google.cloud.pubsub.topic.v1.messagePublished"
}
transport {
pubsub {
topic = google_pubsub_topic.eventarc_source.id
}
}
destination {
cloud_run_service {
service = data.google_cloud_run_v2_service.trigger_agent.name
path = "/apps/trigger_echo_agent/trigger/eventarc"
region = var.region
}
}
service_account = google_service_account.eventarc_invoker.email
depends_on = [
google_project_iam_member.eventarc_event_receiver,
google_project_service.apis,
]
}
# ---------------------------------------------------------------------------
# GCS Trigger Test
# ---------------------------------------------------------------------------
resource "random_id" "bucket_suffix" {
byte_length = 4
}
resource "google_storage_bucket" "trigger_test_bucket" {
name = "${local.name_prefix}-bucket-${local.suffix}-${random_id.bucket_suffix.hex}"
location = var.region
project = var.project_id
force_destroy = true
uniform_bucket_level_access = true
depends_on = [google_project_service.apis]
}
data "google_storage_project_service_account" "gcs_account" {
project = var.project_id
}
resource "google_project_iam_member" "gcs_pubsub_publisher" {
project = var.project_id
role = "roles/pubsub.publisher"
member = "serviceAccount:${data.google_storage_project_service_account.gcs_account.email_address}"
depends_on = [data.google_storage_project_service_account.gcs_account]
}
resource "google_eventarc_trigger" "gcs_trigger" {
name = "${local.name_prefix}-gcs-${local.suffix}"
location = var.region
project = var.project_id
matching_criteria {
attribute = "type"
value = "google.cloud.storage.object.v1.finalized"
}
matching_criteria {
attribute = "bucket"
value = google_storage_bucket.trigger_test_bucket.name
}
destination {
cloud_run_service {
service = data.google_cloud_run_v2_service.trigger_agent.name
path = "/apps/trigger_echo_agent/trigger/eventarc"
region = var.region
}
}
service_account = google_service_account.eventarc_invoker.email
depends_on = [
google_project_iam_member.eventarc_event_receiver,
google_project_iam_member.gcs_pubsub_publisher,
google_project_service.apis,
]
}
+80
View File
@@ -0,0 +1,80 @@
# Copyright 2026 Google LLC
#
# 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.
# ---------------------------------------------------------------------------
# IAM bindings and Service Accounts for Cloud Run invokers.
# ---------------------------------------------------------------------------
# Service account for the Cloud Run agent itself.
resource "google_service_account" "cloud_run" {
account_id = "${local.name_prefix}-run-${local.suffix}"
display_name = "ADK Trigger Test - Cloud Run Agent"
project = var.project_id
}
# Service account for Pub/Sub to invoke Cloud Run.
resource "google_service_account" "pubsub_invoker" {
account_id = "${local.name_prefix}-ps-${local.suffix}"
display_name = "ADK Trigger Test - Pub/Sub Invoker"
project = var.project_id
}
# Service account for Eventarc to invoke Cloud Run.
resource "google_service_account" "eventarc_invoker" {
account_id = "${local.name_prefix}-ea-${local.suffix}"
display_name = "ADK Trigger Test - Eventarc Invoker"
project = var.project_id
}
resource "google_cloud_run_v2_service_iam_member" "pubsub_invoker" {
name = data.google_cloud_run_v2_service.trigger_agent.name
location = var.region
project = var.project_id
role = "roles/run.invoker"
member = "serviceAccount:${google_service_account.pubsub_invoker.email}"
}
resource "google_cloud_run_v2_service_iam_member" "eventarc_invoker" {
name = data.google_cloud_run_v2_service.trigger_agent.name
location = var.region
project = var.project_id
role = "roles/run.invoker"
member = "serviceAccount:${google_service_account.eventarc_invoker.email}"
}
# Eventarc requires the receiver role on the invoker service account.
resource "google_project_iam_member" "eventarc_event_receiver" {
project = var.project_id
role = "roles/eventarc.eventReceiver"
member = "serviceAccount:${google_service_account.eventarc_invoker.email}"
}
data "google_project" "project" {
project_id = var.project_id
}
# Grant the Pub/Sub service agent permission to create OIDC tokens for the pubsub_invoker SA
resource "google_service_account_iam_member" "pubsub_token_creator" {
service_account_id = google_service_account.pubsub_invoker.name
role = "roles/iam.serviceAccountTokenCreator"
member = "serviceAccount:service-${data.google_project.project.number}@gcp-sa-pubsub.iam.gserviceaccount.com"
}
# Grant the Pub/Sub service agent permission to create OIDC tokens for the eventarc_invoker SA
resource "google_service_account_iam_member" "eventarc_token_creator" {
service_account_id = google_service_account.eventarc_invoker.name
role = "roles/iam.serviceAccountTokenCreator"
member = "serviceAccount:service-${data.google_project.project.number}@gcp-sa-pubsub.iam.gserviceaccount.com"
}
+64
View File
@@ -0,0 +1,64 @@
# Copyright 2026 Google LLC
#
# 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.
terraform {
required_version = ">= 1.0.0"
required_providers {
google = {
source = "hashicorp/google"
version = ">= 5.0.0"
}
random = {
source = "hashicorp/random"
version = ">= 3.0.0"
}
}
}
provider "google" {
project = var.project_id
region = var.region
default_labels = {
goog-terraform-provisioned = "true"
}
}
# Random suffix to avoid resource name collisions across parallel test runs.
resource "random_id" "suffix" {
byte_length = 4
}
locals {
suffix = var.suffix != null ? var.suffix : random_id.suffix.hex
name_prefix = "adk-trigger-test"
required_apis = [
"run.googleapis.com",
"pubsub.googleapis.com",
"cloudbuild.googleapis.com",
"eventarc.googleapis.com",
"logging.googleapis.com",
]
}
resource "google_project_service" "apis" {
for_each = toset(local.required_apis)
project = var.project_id
service = each.value
disable_on_destroy = false
}
@@ -0,0 +1,56 @@
# Copyright 2026 Google LLC
#
# 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.
output "cloud_run_url" {
description = "Base URL of the deployed Cloud Run trigger-echo-agent service."
value = data.google_cloud_run_v2_service.trigger_agent.uri
}
output "pubsub_topic" {
description = "Fully qualified Pub/Sub topic name for direct-push tests."
value = google_pubsub_topic.trigger_test.id
}
output "pubsub_topic_short" {
description = "Short Pub/Sub topic name."
value = google_pubsub_topic.trigger_test.name
}
output "eventarc_topic" {
description = "Fully qualified Pub/Sub topic name that fires the Eventarc trigger."
value = google_pubsub_topic.eventarc_source.id
}
output "eventarc_topic_short" {
description = "Short Eventarc source topic name."
value = google_pubsub_topic.eventarc_source.name
}
output "project_id" {
description = "GCP project ID used for test resources."
value = var.project_id
}
output "region" {
description = "GCP region used for test resources."
value = var.region
}
output "suffix" {
description = "The random suffix used for resource naming."
value = local.suffix
}
+54
View File
@@ -0,0 +1,54 @@
# Copyright 2026 Google LLC
#
# 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.
# ---------------------------------------------------------------------------
# Pub/Sub topic + push subscription pointing to /trigger/pubsub
# ---------------------------------------------------------------------------
resource "google_pubsub_topic" "trigger_test" {
name = "${local.name_prefix}-${local.suffix}"
project = var.project_id
depends_on = [google_project_service.apis]
}
resource "google_pubsub_subscription" "trigger_push" {
name = "${local.name_prefix}-push-${local.suffix}"
project = var.project_id
topic = google_pubsub_topic.trigger_test.id
push_config {
push_endpoint = "${data.google_cloud_run_v2_service.trigger_agent.uri}/apps/trigger_echo_agent/trigger/pubsub"
oidc_token {
service_account_email = google_service_account.pubsub_invoker.email
audience = data.google_cloud_run_v2_service.trigger_agent.uri
}
}
# Short ack deadline for faster test feedback.
ack_deadline_seconds = 30
# Retry policy for failed pushes.
retry_policy {
minimum_backoff = "10s"
maximum_backoff = "60s"
}
# Let the subscription persist until Terraform destroys it.
# (default retention of 604800s is fine for tests)
expiration_policy {
ttl = ""
}
}
@@ -0,0 +1,42 @@
# Copyright 2026 Google LLC
#
# 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.
variable "project_id" {
description = "GCP project ID for test resources."
type = string
}
variable "region" {
description = "GCP region for Cloud Run and related resources."
type = string
default = "us-central1"
}
variable "simulate_429_count" {
description = "Number of 429 errors the echo agent simulates before succeeding (0 = disabled)."
type = number
default = 0
}
variable "service_name" {
description = "Optional name of a pre-deployed Cloud Run service. If provided, Terraform will use it instead of generating a name from the suffix."
type = string
default = null
}
variable "suffix" {
description = "Optional suffix for resource naming. If not provided, a random one will be generated."
type = string
default = null
}
@@ -0,0 +1,24 @@
FROM python:3.11-slim
WORKDIR /app
# Install the local ADK wheel (built from the repo under test).
# Install the .whl file directly so pip uses it instead of PyPI.
# Dependencies are resolved from PyPI automatically.
COPY wheels/ /tmp/wheels/
SHELL ["/bin/bash", "-c"]
RUN pip install --no-cache-dir /tmp/wheels/google_adk-*.whl \
&& rm -rf /tmp/wheels
# Copy the agent into the expected adk layout:
# /app/agents/trigger_echo_agent/__init__.py
# /app/agents/trigger_echo_agent/agent.py
COPY __init__.py agents/trigger_echo_agent/__init__.py
COPY agent.py agents/trigger_echo_agent/agent.py
ENV PORT=8080
ENV HOST=0.0.0.0
EXPOSE 8080
CMD ["adk", "api_server", "--host=0.0.0.0", "--port=8080", "--trigger_sources=pubsub,eventarc", "/app/agents"]
@@ -0,0 +1,13 @@
# Copyright 2026 Google LLC
#
# 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.
+86
View File
@@ -0,0 +1,86 @@
# Copyright 2026 Google LLC
#
# 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.
"""Echo agent for remote trigger integration tests.
Uses a before_model_callback to echo user input without calling the LLM,
making tests fast, deterministic, and free of LLM model quota usage.
Supports optional 429 simulation via the SIMULATE_429_COUNT environment
variable: when set to N > 0, the first N invocations per session will raise
a RuntimeError containing "429 RESOURCE_EXHAUSTED" before succeeding.
"""
import os
from google.adk.agents.callback_context import CallbackContext
from google.adk.agents.llm_agent import Agent
from google.adk.models.llm_request import LlmRequest
from google.adk.models.llm_response import LlmResponse
from google.genai import types
# Track 429 simulation state across invocations within a process.
# Keyed by session_id to allow per-session failure counts.
_429_counter: dict[str, int] = {}
def before_model_callback(
callback_context: CallbackContext,
llm_request: LlmRequest,
) -> LlmResponse:
"""Echo user input back without calling the LLM.
If SIMULATE_429_COUNT is set, raises a transient error for the first N
invocations (per session) to exercise the retry-with-backoff logic in
the trigger endpoints.
"""
fail_count = int(os.environ.get("SIMULATE_429_COUNT", "0"))
session = callback_context.session
session_id = session.id if session else "default"
if fail_count > 0:
current = _429_counter.get(session_id, 0)
if current < fail_count:
_429_counter[session_id] = current + 1
raise RuntimeError("429 RESOURCE_EXHAUSTED: simulated quota exceeded")
# Extract the most recent user message from the session events.
user_text = ""
if session and session.events:
for event in reversed(session.events):
if event.content and event.content.role == "user" and event.content.parts:
user_text = event.content.parts[0].text or ""
break
# Fall back to the current LLM request contents if no session event found.
if not user_text and llm_request.contents:
for content in reversed(llm_request.contents):
if content.role == "user" and content.parts:
user_text = content.parts[0].text or ""
break
return LlmResponse(
content=types.Content(
role="model",
parts=[types.Part(text=f"ECHO: {user_text}")],
),
)
root_agent = Agent(
model="gemini-2.5-flash",
name="trigger_echo_agent",
instruction="Echo agent for trigger testing.",
before_model_callback=before_model_callback,
)
@@ -0,0 +1,125 @@
# Copyright 2026 Google LLC
#
# 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.
"""Remote integration tests for the /apps/trigger_echo_agent/trigger/eventarc endpoint.
Tests cover:
/apps/trigger_echo_agent/trigger/eventarc on Cloud Run.
2. Full pipeline tests — publish to the Eventarc source topic and
verify the request reached Cloud Run via Cloud Logging.
Prerequisites:
- GCP project with Eventarc, Pub/Sub, Cloud Run, and Cloud Logging
APIs enabled.
- ``gcloud`` CLI authenticated.
- Terraform >= 1.5 installed.
Run:
GCP_PROJECT_ID=my-project pytest tests/remote/test_trigger_eventarc.py -v -s
"""
from __future__ import annotations
import datetime
import json
import time
import uuid
from google.cloud import logging as cloud_logging
from google.cloud import pubsub_v1
import pytest
import requests
# ---------------------------------------------------------------------------
# Full Eventarc pipeline tests
# ---------------------------------------------------------------------------
class TestEventarcPipeline:
"""Test the full Pub/Sub → Eventarc → Cloud Run pipeline.
Verification is done by checking Cloud Run request logs for successful
POST requests to /apps/trigger_echo_agent/trigger/eventarc.
"""
@pytest.fixture(autouse=True)
def _setup(self, eventarc_topic, project_id):
self.publisher = pubsub_v1.PublisherClient()
self.topic_path = eventarc_topic
self.project_id = project_id
self.logging_client = cloud_logging.Client(project=project_id)
def _publish_event(self, data: str, wait_seconds: int = 45) -> None:
"""Publish a message to the Eventarc source topic and wait."""
future = self.publisher.publish(
self.topic_path,
data.encode("utf-8"),
)
future.result(timeout=30)
time.sleep(wait_seconds)
def _count_successful_requests(
self,
path: str,
since: datetime.datetime,
) -> int:
"""Count successful HTTP requests to the given path in Cloud Run logs."""
timestamp = since.strftime("%Y-%m-%dT%H:%M:%SZ")
filter_str = (
'resource.type="cloud_run_revision" '
f'httpRequest.requestUrl:"{path}" '
"httpRequest.status=200 "
f'timestamp>="{timestamp}"'
)
entries = list(
self.logging_client.list_entries(
filter_=filter_str,
page_size=50,
)
)
return len(entries)
def test_eventarc_pubsub_trigger(self):
"""Publish to Eventarc source topic and verify Cloud Run processes it."""
start_time = datetime.datetime.now(datetime.timezone.utc)
self._publish_event(json.dumps({"test": "eventarc-pipeline"}))
count = self._count_successful_requests(
"/apps/trigger_echo_agent/trigger/eventarc", start_time
)
assert count >= 1, (
"Expected at least 1 successful request to"
f" /apps/trigger_echo_agent/trigger/eventarc, found {count}"
)
def test_eventarc_multiple_events(self):
"""Publish multiple events and verify they are processed."""
start_time = datetime.datetime.now(datetime.timezone.utc)
for i in range(3):
future = self.publisher.publish(
self.topic_path,
json.dumps({"seq": i}).encode("utf-8"),
)
future.result(timeout=30)
# Eventarc delivery + log ingestion can be slow; wait longer.
time.sleep(90)
count = self._count_successful_requests(
"/apps/trigger_echo_agent/trigger/eventarc", start_time
)
assert count >= 1, (
"Expected at least 1 successful request to"
f" /apps/trigger_echo_agent/trigger/eventarc, found {count}"
)
@@ -0,0 +1,166 @@
# Copyright 2026 Google LLC
#
# 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.
"""Remote integration tests for the /apps/trigger_echo_agent/trigger/pubsub endpoint.
Tests cover:
2. Full pipeline tests — publish to a Pub/Sub topic with a push
subscription pointing at the Cloud Run service, then verify via
Cloud Logging that the requests reached the service.
Prerequisites:
- GCP project with Pub/Sub, Cloud Run, and Cloud Logging APIs enabled.
- ``gcloud`` CLI authenticated.
- Terraform >= 1.5 installed.
Run:
GCP_PROJECT_ID=my-project pytest tests/remote/test_trigger_pubsub.py -v -s
"""
from __future__ import annotations
import base64
import datetime
import json
import time
from google.cloud import logging as cloud_logging
from google.cloud import pubsub_v1
import pytest
import requests
# ---------------------------------------------------------------------------
# Full Pub/Sub pipeline tests
# ---------------------------------------------------------------------------
class TestPubSubPipeline:
"""Test the full Pub/Sub → push subscription → Cloud Run pipeline.
Verification is done by checking Cloud Run request logs for successful
POST requests to /apps/trigger_echo_agent/trigger/pubsub. We look for
httpRequest entries with
status 200 that arrived after we published.
"""
@pytest.fixture(autouse=True)
def _setup(self, pubsub_topic, project_id, cloud_run_url):
self.publisher = pubsub_v1.PublisherClient()
self.topic_path = pubsub_topic
self.project_id = project_id
self.cloud_run_url = cloud_run_url
self.logging_client = cloud_logging.Client(project=project_id)
def _publish_and_wait(
self,
data: str,
attributes: dict[str, str] | None = None,
wait_seconds: int = 30,
) -> None:
"""Publish a message and wait for it to be delivered."""
future = self.publisher.publish(
self.topic_path,
data.encode("utf-8"),
**(attributes or {}),
)
future.result(timeout=30)
time.sleep(wait_seconds)
def _count_successful_requests(
self,
path: str,
since: datetime.datetime,
) -> int:
"""Count successful HTTP requests to the given path in Cloud Run logs."""
timestamp = since.strftime("%Y-%m-%dT%H:%M:%SZ")
filter_str = (
'resource.type="cloud_run_revision" '
f'httpRequest.requestUrl:"{path}" '
"httpRequest.status=200 "
f'timestamp>="{timestamp}"'
)
entries = list(
self.logging_client.list_entries(
filter_=filter_str,
page_size=200,
)
)
return len(entries)
def test_publish_text_message(self):
"""Publish a text message and verify it reaches Cloud Run."""
start_time = datetime.datetime.now(datetime.timezone.utc)
self._publish_and_wait("hello-pipeline-test")
count = self._count_successful_requests(
"/apps/trigger_echo_agent/trigger/pubsub", start_time
)
assert count >= 1, (
"Expected at least 1 successful request to"
f" /apps/trigger_echo_agent/trigger/pubsub, found {count}"
)
def test_publish_json_message(self):
"""Publish a JSON message and verify processing."""
start_time = datetime.datetime.now(datetime.timezone.utc)
data = json.dumps({"type": "json", "value": 42})
self._publish_and_wait(data)
count = self._count_successful_requests(
"/apps/trigger_echo_agent/trigger/pubsub", start_time
)
assert count >= 1, (
"Expected at least 1 successful request to"
f" /apps/trigger_echo_agent/trigger/pubsub, found {count}"
)
def test_publish_with_attributes(self):
"""Publish a message with attributes and verify processing."""
start_time = datetime.datetime.now(datetime.timezone.utc)
self._publish_and_wait(
"attr-pipeline-test",
attributes={"test_attr": "pytest"},
)
count = self._count_successful_requests(
"/apps/trigger_echo_agent/trigger/pubsub", start_time
)
assert count >= 1, (
"Expected at least 1 successful request to"
f" /apps/trigger_echo_agent/trigger/pubsub, found {count}"
)
def test_high_volume(self):
"""Publish 20 messages and verify they are processed."""
start_time = datetime.datetime.now(datetime.timezone.utc)
n = 20
futures = []
for i in range(n):
future = self.publisher.publish(
self.topic_path,
f"volume-test-{i}".encode("utf-8"),
)
futures.append(future)
for f in futures:
f.result(timeout=30)
# Wait for push delivery.
time.sleep(15)
count = self._count_successful_requests(
"/apps/trigger_echo_agent/trigger/pubsub", start_time
)
# Allow some tolerance — at least 80% should arrive.
assert (
count >= n * 0.8
), f"Expected at least {int(n * 0.8)} successful requests, found {count}"