Skip to content

Commit 77d95d6

Browse files
authored
add simple agent test
1 parent 5359f91 commit 77d95d6

File tree

7 files changed

+758
-0
lines changed

7 files changed

+758
-0
lines changed
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
name: Secure Integration test
2+
3+
on:
4+
pull_request:
5+
pull_request_target:
6+
branches: main
7+
8+
jobs:
9+
authorization-check:
10+
permissions: read-all
11+
runs-on: ubuntu-latest
12+
outputs:
13+
approval-env: ${{ steps.collab-check.outputs.result }}
14+
steps:
15+
- name: Collaborator Check
16+
uses: actions/github-script@v7
17+
id: collab-check
18+
with:
19+
result-encoding: string
20+
script: |
21+
try {
22+
const permissionResponse = await github.rest.repos.getCollaboratorPermissionLevel({
23+
owner: context.repo.owner,
24+
repo: context.repo.repo,
25+
username: context.payload.pull_request.user.login,
26+
});
27+
const permission = permissionResponse.data.permission;
28+
const hasWriteAccess = ['write', 'admin'].includes(permission);
29+
if (!hasWriteAccess) {
30+
console.log(`User ${context.payload.pull_request.user.login} does not have write access to the repository (permission: ${permission})`);
31+
return "manual-approval"
32+
} else {
33+
console.log(`Verifed ${context.payload.pull_request.user.login} has write access. Auto Approving PR Checks.`)
34+
return "auto-approve"
35+
}
36+
} catch (error) {
37+
console.log(`${context.payload.pull_request.user.login} does not have write access. Requiring Manual Approval to run PR Checks.`)
38+
return "manual-approval"
39+
}
40+
check-access-and-checkout:
41+
runs-on: ubuntu-latest
42+
needs: authorization-check
43+
environment: ${{ needs.authorization-check.outputs.approval-env }}
44+
permissions:
45+
id-token: write
46+
pull-requests: read
47+
contents: read
48+
steps:
49+
- name: Configure Credentials
50+
uses: aws-actions/configure-aws-credentials@v4
51+
with:
52+
role-to-assume: ${{ secrets.AGENTCORE_INTEG_TEST_ROLE }}
53+
aws-region: us-west-2
54+
mask-aws-account-id: true
55+
- name: Checkout head commit
56+
uses: actions/checkout@v4
57+
with:
58+
ref: ${{ github.event.pull_request.head.sha }} # Pull the commit from the forked repo
59+
persist-credentials: false # Don't persist credentials for subsequent actions
60+
- name: Set up Python
61+
uses: actions/setup-python@v5
62+
with:
63+
python-version: '3.10'
64+
- name: Install dependencies
65+
run: |
66+
pip install -e .
67+
pip install --no-cache-dir pytest requests strands-agents
68+
- name: Run integration tests
69+
env:
70+
AWS_REGION: us-west-2
71+
id: tests
72+
run: |
73+
pytest tests_integ/runtime -s --log-cli-level=INFO

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,4 +142,5 @@ dev = [
142142
"pytest-cov>=6.0.0",
143143
"ruff>=0.12.0",
144144
"wheel>=0.45.1",
145+
"strands-agents>=1.1.0",
145146
]

tests_integ/runtime/__init__.py

Whitespace-only changes.

tests_integ/runtime/base_test.py

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import logging
2+
import os
3+
import subprocess
4+
import threading
5+
import time
6+
from abc import ABC, abstractmethod
7+
from contextlib import contextmanager
8+
from subprocess import Popen
9+
from typing import IO, Generator
10+
11+
logger = logging.getLogger("sdk-runtime-base-test")
12+
13+
AGENT_SERVER_ENDPOINT = "http://127.0.0.1:8080"
14+
15+
16+
class BaseSDKRuntimeTest(ABC):
17+
def run(self, tmp_path) -> None:
18+
original_dir = os.getcwd()
19+
try:
20+
os.chdir(tmp_path)
21+
22+
self.setup()
23+
24+
logger.info("Running test...")
25+
self.run_test()
26+
27+
finally:
28+
os.chdir(original_dir)
29+
30+
def setup(self) -> None:
31+
return
32+
33+
@abstractmethod
34+
def run_test(self) -> None:
35+
raise NotImplementedError
36+
37+
38+
@contextmanager
39+
def start_agent_server(agent_module, timeout=5) -> Generator[Popen, None, None]:
40+
logger.info("Starting agent server...")
41+
start_time = time.time()
42+
43+
try:
44+
agent_server = Popen(
45+
["python", "-m", agent_module], text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT
46+
)
47+
48+
while time.time() - start_time < timeout:
49+
if agent_server.stdout is None:
50+
raise RuntimeError("Agent server has no configured output")
51+
52+
if agent_server.poll() is not None:
53+
out = agent_server.stdout.read()
54+
raise RuntimeError(f"Error when running agent server: {out}")
55+
56+
line = agent_server.stdout.readline()
57+
while line:
58+
line = line.strip()
59+
if line:
60+
logger.info(line)
61+
if "Uvicorn running on http://127.0.0.1:8080" in line:
62+
_start_logging_thread(agent_server.stdout)
63+
yield agent_server
64+
return
65+
line = agent_server.stdout.readline()
66+
67+
time.sleep(0.5)
68+
raise TimeoutError(f"Agent server did not start within {timeout} seconds")
69+
finally:
70+
_stop_agent_server(agent_server)
71+
72+
73+
def _stop_agent_server(agent_server: Popen) -> None:
74+
logger.info("Stopping agent server...")
75+
if agent_server.poll() is None: # Process is still running
76+
logger.info("Terminating agent server process...")
77+
agent_server.terminate()
78+
79+
# Wait for graceful shutdown
80+
try:
81+
agent_server.wait(timeout=5)
82+
except subprocess.TimeoutExpired:
83+
logger.warning("Agent server didn't terminate, force killing...")
84+
agent_server.kill()
85+
agent_server.wait()
86+
finally:
87+
if agent_server.stdout:
88+
agent_server.stdout.close()
89+
logger.info("Agent server terminated")
90+
91+
92+
def _start_logging_thread(stdout: IO[str]):
93+
def log_server_output():
94+
logger.info("Server logging thread started")
95+
# thread is stopped when stdout is closed
96+
for line in iter(stdout.readline, ""):
97+
if line.strip():
98+
logger.info(line.strip())
99+
logger.info("Server logging thread stopped")
100+
101+
logging_thread = threading.Thread(target=log_server_output, daemon=True, name="AgentServerLogger")
102+
logging_thread.start()
103+
return logging_thread

tests_integ/runtime/http_client.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import json
2+
import logging
3+
4+
import requests
5+
6+
7+
class HttpClient:
8+
"""Local HTTP client for invoking endpoints."""
9+
10+
def __init__(self, endpoint: str):
11+
"""Initialize the local client with the given endpoint."""
12+
self.endpoint = endpoint
13+
self.logger = logging.getLogger("sdk-runtime-test-http-client")
14+
15+
def invoke_endpoint(self, payload: str):
16+
"""Invoke the endpoint with the given parameters."""
17+
self.logger.info("Sending request to agent with payload: %s", payload)
18+
19+
url = f"{self.endpoint}/invocations"
20+
21+
headers = {
22+
"Content-Type": "application/json",
23+
}
24+
25+
try:
26+
body = json.loads(payload) if isinstance(payload, str) else payload
27+
except json.JSONDecodeError:
28+
# Fallback for non-JSON strings - wrap in payload object
29+
self.logger.warning("Failed to parse payload as JSON, wrapping in payload object")
30+
body = {"message": payload}
31+
32+
try:
33+
# Make request with timeout
34+
return requests.post(url, headers=headers, json=body, timeout=100, stream=True).text
35+
except requests.exceptions.RequestException as e:
36+
self.logger.error("Failed to invoke agent endpoint: %s", str(e))
37+
raise
38+
39+
def ping(self):
40+
self.logger.info("Pinging agent server")
41+
42+
url = f"{self.endpoint}/ping"
43+
try:
44+
return requests.get(url, timeout=2).text
45+
except requests.exceptions.RequestException as e:
46+
self.logger.error("Failed to ping agent endpoint: %s", str(e))
47+
raise
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import logging
2+
import textwrap
3+
4+
from tests_integ.runtime.base_test import AGENT_SERVER_ENDPOINT, BaseSDKRuntimeTest, start_agent_server
5+
from tests_integ.runtime.http_client import HttpClient
6+
7+
logger = logging.getLogger("sdk-runtime-simple-agent-test")
8+
9+
10+
class TestSDKSimpleAgent(BaseSDKRuntimeTest):
11+
def setup(self):
12+
self.agent_module = "agent"
13+
with open(self.agent_module + ".py", "w") as file:
14+
content = textwrap.dedent("""
15+
from bedrock_agentcore import BedrockAgentCoreApp
16+
from strands import Agent
17+
18+
app = BedrockAgentCoreApp()
19+
agent = Agent()
20+
21+
@app.entrypoint
22+
async def agent_invocation(payload):
23+
return agent(payload.get("message"))
24+
25+
app.run()
26+
""").strip()
27+
file.write(content)
28+
29+
def run_test(self):
30+
with start_agent_server(self.agent_module):
31+
client = HttpClient(AGENT_SERVER_ENDPOINT)
32+
33+
ping_response = client.ping()
34+
logger.info(ping_response)
35+
assert "Healthy" in ping_response
36+
37+
response = client.invoke_endpoint("tell me a joke")
38+
logger.info(response)
39+
assert "Because they make up everything!" in response
40+
41+
42+
def test(tmp_path):
43+
TestSDKSimpleAgent().run(tmp_path)

0 commit comments

Comments
 (0)