Skip to content

Commit b8efa4d

Browse files
authored
test(messages): add live Claude Agent SDK smoke test against /v1/messages (#6010)
## What Adds a live smoke test that drives the upstream [`claude-agent-sdk`](https://github.com/anthropics/claude-agent-sdk-python) Python package against a local OGX server, exercising the Anthropic Messages (`/v1/messages`) path end to end through a real client. This is the SDK companion to the Claude Code CLI smoke test merged in #5986 (RHAIENG-5377). The SDK does not speak HTTP itself — it spawns the Claude Code CLI as a subprocess and parses its streamed session output. So this covers a distinct client surface (the SDK's session machinery: message streaming, `ResultMessage` parsing) on top of the same path: full system prompt, tool definitions, and an inline system-role message that the server must accept and dispatch to the backing provider. ## Why live, not record/replay The SDK drives the real CLI, which bakes the working directory, date, and platform into every request body. The recording system keys on SHA256 of the request body, so those hashes are not reproducible across runs or machines — record/replay is not viable. The test runs live against Ollama and self-skips unless **both** the `claude-agent-sdk` package and the `claude` binary are available. ## What it asserts Integration health, not answer quality — a small local model driving the Claude Code harness cannot be relied on to produce specific text. The test checks the parsed `ResultMessage`: - `subtype == "success"` and `is_error is False` - the backing model present in `model_usage` A regression like a rejected inline system-role message would surface as a session error and be caught here. ## Changes - New test: `tests/integration/messages/test_claude_agent_sdk.py` - New CI workflow: `.github/workflows/integration-tests-messages-sdk.yml` (provisions Ollama, installs the pinned CLI, runs the test live) - `claude-agent-sdk>=0.2.0` added to the `test` dependency group (locked to 0.2.87) ## Test plan Ran live against local Ollama using the same CI command: ``` ./scripts/integration-tests.sh \ --stack-config server:ci-tests --setup ollama --suite messages \ --inference-mode live --pattern test_claude_agent_sdk_smoke ``` Result: ``` tests/integration/messages/test_claude_agent_sdk.py::test_claude_agent_sdk_smoke[txt=ollama/llama3.2:3b-instruct-fp16] PASSED 1 passed, 20 deselected in 36.45s ``` 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Charlie Doern <cdoern@redhat.com>
1 parent 9690317 commit b8efa4d

3 files changed

Lines changed: 138 additions & 17 deletions

File tree

.github/workflows/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ OGX uses GitHub Actions for Continuous Integration (CI). Below is a table detail
1717
| Integration Auth Tests | [integration-auth-tests.yml](integration-auth-tests.yml) | Run the integration test suite with Kubernetes authentication |
1818
| Integration Responses, Conversations & Prompts Auth Tests | [integration-responses-conversations-auth-tests.yml](integration-responses-conversations-auth-tests.yml) | Run responses, conversations, and prompts auth tests with Kubernetes authentication |
1919
| SqlStore Integration Tests | [integration-sql-store-tests.yml](integration-sql-store-tests.yml) | Run the integration test suite with SqlStore |
20-
| Messages API - Claude Code CLI Smoke Test | [integration-tests-messages-cli.yml](integration-tests-messages-cli.yml) | Drive the real Claude Code CLI against /v1/messages (live, Ollama) |
20+
| Messages API - Claude Code Client Smoke Tests | [integration-tests-messages-clients.yml](integration-tests-messages-clients.yml) | Drive the Claude Code CLI and Agent SDK against /v1/messages (live, Ollama) |
2121
| Integration Tests (Replay) | [integration-tests.yml](integration-tests.yml) | Run the integration test suites from tests/integration in replay mode |
2222
| Vector IO Integration Tests | [integration-vector-io-tests.yml](integration-vector-io-tests.yml) | Run the integration test suite with various VectorIO providers |
2323
| OpenAPI Generator SDK Validation | [openapi-generator-validation.yml](openapi-generator-validation.yml) | Validate OpenAPI Generator SDK generation |

.github/workflows/integration-tests-messages-cli.yml renamed to .github/workflows/integration-tests-messages-clients.yml

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
name: Messages API - Claude Code CLI Smoke Test
1+
name: Messages API - Claude Code Client Smoke Tests
22

3-
run-name: Drive the real Claude Code CLI against /v1/messages (live, Ollama)
3+
run-name: Drive the Claude Code CLI and Agent SDK against /v1/messages (live, Ollama)
44

55
on:
66
push:
@@ -18,7 +18,7 @@ on:
1818
- 'tests/integration/messages/**'
1919
- 'uv.lock'
2020
- 'pyproject.toml'
21-
- '.github/workflows/integration-tests-messages-cli.yml'
21+
- '.github/workflows/integration-tests-messages-clients.yml'
2222
- '.github/actions/setup-test-environment/action.yml'
2323
- '.github/actions/run-and-record-tests/action.yml'
2424
- 'scripts/integration-tests.sh'
@@ -36,27 +36,31 @@ permissions:
3636
contents: read
3737

3838
env:
39-
# Pinned for reproducibility. The CLI bakes the cwd, date, and platform into
40-
# every request body, so its traffic cannot be recorded/replayed; this test
41-
# runs live against Ollama instead. Bumping only changes the client behavior
42-
# under test, not any committed recordings.
39+
# Pinned for reproducibility. Both clients ultimately drive the Claude Code
40+
# CLI, which bakes the cwd, date, and platform into every request body, so
41+
# their traffic cannot be recorded/replayed; these tests run live against
42+
# Ollama instead. Bumping only changes the client behavior under test.
4343
CLAUDE_CODE_CLI_VERSION: '2.1.159'
44+
# The Agent SDK is installed at workflow time rather than as a project
45+
# dependency, since only this one live smoke test uses it; it spawns the same
46+
# CLI. The SDK test self-skips if the package is unavailable.
47+
CLAUDE_AGENT_SDK_VERSION: '0.2.87'
4448

4549
jobs:
46-
claude-code-cli-smoke:
47-
name: Claude Code CLI smoke (ollama, live)
50+
claude-code-client-smoke:
51+
name: Claude Code CLI + Agent SDK smoke (ollama, live)
4852
runs-on: ubuntu-latest
49-
# CPU-only runners generate slowly; the live CLI session can take several
50-
# minutes against the Ollama model. Keep ample headroom over the test's own
51-
# 600s subprocess timeout plus environment setup.
53+
# CPU-only runners generate slowly; the live sessions can take several
54+
# minutes against the Ollama model. Keep ample headroom over the tests' own
55+
# 600s timeouts plus environment setup.
5256
timeout-minutes: 30
5357

5458
steps:
5559
- name: Checkout repository
5660
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
5761

58-
# Live mode (not replay) so the Ollama backend is provisioned and the
59-
# real CLI traffic reaches a real model.
62+
# Live mode (not replay) so the Ollama backend is provisioned and the real
63+
# client traffic reaches a real model.
6064
- name: Setup test environment
6165
uses: ./.github/actions/setup-test-environment
6266
with:
@@ -66,17 +70,26 @@ jobs:
6670
suite: 'messages'
6771
inference-mode: 'live'
6872

73+
# Both clients need the Claude Code CLI at runtime: the CLI test invokes it
74+
# directly, and the SDK spawns it as a subprocess.
6975
- name: Install Claude Code CLI
7076
run: |
7177
curl -fsSL https://claude.ai/install.sh | bash -s -- "${CLAUDE_CODE_CLI_VERSION}"
7278
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
7379
"$HOME/.local/bin/claude" --version
7480
75-
- name: Run Claude Code CLI smoke test
81+
- name: Install Claude Agent SDK
82+
run: uv pip install "claude-agent-sdk==${CLAUDE_AGENT_SDK_VERSION}"
83+
84+
- name: Run Claude Code client smoke tests
7685
uses: ./.github/actions/run-and-record-tests
7786
with:
7887
stack-config: 'server:ci-tests'
7988
setup: 'ollama'
8089
suite: 'messages'
8190
inference-mode: 'live'
82-
pattern: 'test_claude_code_cli_smoke'
91+
# Selects both test_claude_code_cli_smoke and test_claude_agent_sdk_smoke.
92+
# Must stay a single shell word: run-and-record-tests passes --pattern
93+
# unquoted, so a -k expression with spaces would be split into separate
94+
# arguments.
95+
pattern: 'test_claude'
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
# Copyright (c) The OGX Contributors.
2+
# All rights reserved.
3+
#
4+
# This source code is licensed under the terms described in the LICENSE file in
5+
# the root directory of this source tree.
6+
7+
"""Smoke test: drive the Claude Agent SDK against the OGX Messages API.
8+
9+
Points the SDK at a local OGX server and runs a single prompt through the
10+
upstream `claude-agent-sdk` Python package (github.com/anthropics/
11+
claude-agent-sdk-python). The SDK does not speak HTTP itself: it spawns the
12+
Claude Code CLI as a subprocess and parses its streamed session output. This
13+
exercises a different client surface than the CLI smoke test -- the SDK's
14+
session machinery (message streaming, ResultMessage parsing) on top of the same
15+
end-to-end path through /v1/messages: full system prompt, tool definitions, and
16+
an inline system-role message that the server must accept and dispatch to the
17+
backing provider.
18+
19+
This runs LIVE against a real backend, not in replay mode. The SDK drives the
20+
real CLI, which bakes the working directory, date, and platform into every
21+
request body, so the request-body hashes the recording system keys on are not
22+
reproducible across runs or machines; recording/replay is therefore not viable.
23+
24+
The test self-skips unless both the `claude-agent-sdk` package and the `claude`
25+
binary are available, since the SDK requires the CLI at runtime.
26+
"""
27+
28+
import asyncio
29+
import importlib.util
30+
import shutil
31+
32+
import pytest
33+
34+
CLAUDE_CLI = shutil.which("claude")
35+
HAS_SDK = importlib.util.find_spec("claude_agent_sdk") is not None
36+
37+
pytestmark = pytest.mark.skipif(
38+
CLAUDE_CLI is None or not HAS_SDK,
39+
reason="claude-agent-sdk and the claude CLI must both be installed to run",
40+
)
41+
42+
43+
def _run_query(prompt: str, base_url: str, model: str, cwd: str) -> list:
44+
"""Run a single Agent SDK query() to completion and return all messages."""
45+
from claude_agent_sdk import ClaudeAgentOptions, query
46+
47+
options = ClaudeAgentOptions(
48+
model=model,
49+
# Run from an isolated directory so the spawned CLI does not pick up
50+
# repo-local context, which matters while permissions are bypassed.
51+
cwd=cwd,
52+
# Passed to the spawned CLI subprocess so it reaches OGX instead of
53+
# api.anthropic.com.
54+
env={
55+
"ANTHROPIC_BASE_URL": base_url,
56+
"ANTHROPIC_API_KEY": "dummy",
57+
"ANTHROPIC_MODEL": model,
58+
},
59+
# The prompt is pure Q&A and triggers no tools, but bypass permissions
60+
# so the non-interactive session can never stall on a permission prompt.
61+
permission_mode="bypassPermissions",
62+
)
63+
64+
messages: list = []
65+
66+
async def _collect() -> None:
67+
async for message in query(prompt=prompt, options=options):
68+
messages.append(message)
69+
70+
# Generous: the SDK drives the real CLI, which makes several large-context
71+
# calls, and a small model on a CPU-only CI runner generates slowly (tens of
72+
# seconds each). Bound it so a hung session fails loudly instead of riding
73+
# the job timeout.
74+
asyncio.run(asyncio.wait_for(_collect(), timeout=600))
75+
return messages
76+
77+
78+
def test_claude_agent_sdk_smoke(messages_base_url, text_model_id, tmp_path):
79+
"""Claude Agent SDK completes a session against /v1/messages without error.
80+
81+
The smoke signal is integration health, not answer quality: the SDK drives a
82+
full agentic session (system prompt, tools, inline system message) against
83+
OGX, OGX routes it to the backing model, and the session terminates with a
84+
successful ResultMessage. We deliberately do not assert on the model's text
85+
output -- a small local model driving the Claude Code harness cannot be
86+
relied on to produce a specific answer, but a regression like a rejected
87+
system-role message (which would surface as a session error) is caught here.
88+
"""
89+
from claude_agent_sdk import ResultMessage
90+
91+
prompt = "What is the capital of France? Reply with only the city name and nothing else."
92+
base_url = str(messages_base_url).rstrip("/")
93+
94+
messages = _run_query(prompt, base_url, text_model_id, cwd=str(tmp_path))
95+
96+
results = [m for m in messages if isinstance(m, ResultMessage)]
97+
assert results, f"Agent SDK session produced no ResultMessage; got: {[type(m).__name__ for m in messages]}"
98+
99+
result = results[-1]
100+
assert result.subtype == "success" and not result.is_error, (
101+
f"Agent SDK session reported an error talking to /v1/messages: "
102+
f"subtype={result.subtype} is_error={result.is_error} errors={result.errors}"
103+
)
104+
# Confirm the request actually reached the backing model through /v1/messages.
105+
model_usage = result.model_usage or {}
106+
assert text_model_id in model_usage, (
107+
f"Expected model {text_model_id} in ResultMessage.model_usage; got: {list(model_usage)}"
108+
)

0 commit comments

Comments
 (0)