Skip to content

Commit 6dbb9be

Browse files
authored
Merge pull request #13 from Semih702/ci/helm-regression-contract-tests
ci: add OpenAI-compatible contract tests to helm regression pipeline
2 parents a6f1247 + 6b8d85b commit 6dbb9be

11 files changed

Lines changed: 438 additions & 41 deletions

File tree

.github/workflows/helm-regression.yaml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,11 +78,13 @@ jobs:
7878
run: |
7979
docker build -t llm-proxy:ci ./proxy
8080
docker build -t llm-collector:ci ./collector
81+
docker build -t mock-openai:ci ./tests/contract/mock-openai
8182
8283
- name: Load images into kind
8384
run: |
8485
kind load docker-image llm-proxy:ci --name chart-testing
8586
kind load docker-image llm-collector:ci --name chart-testing
87+
kind load docker-image mock-openai:ci --name chart-testing
8688
8789
- name: Create dummy secret for CI
8890
run: |
@@ -92,6 +94,12 @@ jobs:
9294
--from-literal=UPSTREAM_OPENAI_API_KEY=dummy-key \
9395
--dry-run=client -o yaml | kubectl apply -f -
9496
97+
- name: Deploy mock OpenAI upstream
98+
run: |
99+
set -euo pipefail
100+
kubectl apply -n "${NAMESPACE}" -f tests/contract/k8s/mock-openai.yaml
101+
kubectl rollout status deployment/mock-openai -n "${NAMESPACE}" --timeout=180s
102+
95103
- name: Install chart into kind (default)
96104
run: |
97105
set -euo pipefail
@@ -104,6 +112,7 @@ jobs:
104112
--set collector.image.repository=llm-collector \
105113
--set collector.image.tag=ci \
106114
--set collector.image.pullPolicy=IfNotPresent \
115+
--set proxy.env.UPSTREAM_OPENAI_BASE_URL="http://mock-openai:8080" \
107116
-n "${NAMESPACE}" --create-namespace
108117
109118
kubectl get all -n "${NAMESPACE}"
@@ -133,3 +142,10 @@ jobs:
133142
134143
kubectl wait --for=condition=Available deployment/llm-proxy -n "${NAMESPACE}" --timeout=180s
135144
kubectl wait --for=condition=Available deployment/llm-collector -n "${NAMESPACE}" --timeout=180s
145+
146+
- name: Contract tests (OpenAI-compatible contract)
147+
shell: bash
148+
run: |
149+
chmod +x tests/contract/ci-contract.sh
150+
NAMESPACE="${NAMESPACE}" ./tests/contract/ci-contract.sh
151+

README.md

Lines changed: 71 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -230,64 +230,84 @@ Collector delivery uses a short HTTP timeout and never blocks the request path;
230230

231231
---
232232

233-
CI Regression & Chart Testing (for Contributors)
233+
## CI Regression & Chart Testing (for Contributors)
234234

235235
This repository includes a mandatory CI regression pipeline to ensure Helm chart correctness,
236236
Kubernetes compatibility, and safe upgrade behavior.
237237

238238
All pull requests that modify Helm charts or CI workflows must pass this pipeline before being merged.
239239

240-
What is validated in CI?
240+
---
241+
242+
### What is validated in CI?
241243

242244
The helm-regression workflow performs the following checks, in order:
243245

244246
1. Helm chart linting
245-
- Runs helm lint on the chart
246-
- Catches common issues:
247-
- Invalid chart metadata
248-
- Obvious template mistakes
249-
- Missing required values
247+
- Runs `helm lint` on the chart
248+
- Catches common issues:
249+
- Invalid chart metadata
250+
- Obvious template mistakes
251+
- Missing required values
250252

251253
2. Template rendering (dry-run)
252-
- Renders manifests using:
253-
- values-ci.yaml (required)
254-
- values-test.yaml (optional, if present)
255-
- Ensures templates render successfully without a live cluster
254+
- Renders manifests using:
255+
- `values-ci.yaml` (required)
256+
- `values-test.yaml` (optional, if present)
257+
- Ensures templates render successfully without a live cluster
256258

257259
3. Kubernetes schema validation (kubeconform)
258-
- Validates rendered YAML against official Kubernetes schemas
259-
- Uses strict mode
260-
- Catches:
261-
- Invalid API versions
262-
- Invalid fields
263-
- Structural mismatches that Helm itself does not detect
260+
- Validates rendered YAML against official Kubernetes schemas
261+
- Uses strict mode
262+
- Catches:
263+
- Invalid API versions
264+
- Invalid fields
265+
- Structural mismatches that Helm itself does not detect
264266

265267
4. Helm unit tests
266-
- Runs helm unittest against the chart
267-
- Validates:
268-
- Expected resources are created
269-
- Correct values are applied
270-
- Conditional logic behaves as intended
268+
- Runs `helm unittest` against the chart
269+
- Validates:
270+
- Expected resources are created
271+
- Correct values are applied
272+
- Conditional logic behaves as intended
271273

272274
5. In-cluster install & upgrade test (kind)
273-
- Spins up a real Kubernetes cluster using kind
274-
- Builds proxy and collector images locally
275-
- Loads images into the cluster
276-
- Installs the chart using helm upgrade --install
277-
- Verifies:
278-
- Pods start successfully
279-
- Deployments become ready
280-
- Performs a real Helm upgrade
281-
- Uses values-test.yaml if present
282-
- Ensures upgrades do not break running workloads
275+
- Spins up a real Kubernetes cluster using kind
276+
- Builds proxy and collector images locally
277+
- Loads images into the cluster
278+
- Installs the chart using `helm upgrade --install`
279+
- Verifies:
280+
- Pods start successfully
281+
- Deployments become ready
282+
- Performs a real Helm upgrade:
283+
- Uses `values-test.yaml` if present
284+
- Ensures upgrades do not break running workloads
283285

284286
6. Smoke checks
285-
- Confirms:
286-
- Pods are running
287-
- Services are created
288-
- Deployments reach Available condition
287+
- Confirms:
288+
- Pods are running
289+
- Services are created
290+
- Deployments reach Available condition
291+
292+
7. OpenAI-compatible contract tests
293+
- Deploys a lightweight in-cluster mock OpenAI upstream
294+
- Configures the gateway to point to the mock via `UPSTREAM_OPENAI_BASE_URL`
295+
- Sends real HTTP requests to the gateway service
296+
- Validates the public OpenAI-compatible contract, including:
297+
- Chat completions (non-streaming)
298+
- VLM-style payloads (`image_url` content)
299+
- Streaming (SSE) passthrough with `[DONE]`
300+
- Presence of `usage` fields in responses
301+
302+
These tests intentionally validate the API contract and behavior,
303+
not model semantics or response quality.
304+
305+
They ensure that changes to the proxy, chart, or deployment logic
306+
do not silently break OpenAI compatibility.
307+
308+
---
289309

290-
Files contributors should be aware of:
310+
### Files contributors should be aware of
291311

292312
- charts/llm-gateway/values-ci.yaml
293313
Required for CI. Used for deterministic, non-secret test installs.
@@ -298,7 +318,13 @@ Files contributors should be aware of:
298318
- .github/workflows/helm-regression.yml
299319
CI definition. Any change here is also gated by this workflow.
300320

301-
Running key checks locally (recommended):
321+
- tests/contract/
322+
Contains OpenAI-compatible contract tests executed in CI and runnable locally.
323+
See tests/contract/README.md for details.
324+
325+
---
326+
327+
### Running key checks locally (recommended)
302328

303329
helm lint charts/llm-gateway
304330

@@ -311,7 +337,9 @@ For full parity with CI (optional but ideal):
311337
- Build images locally
312338
- Install the chart with helm upgrade --install
313339

314-
CI expectations:
340+
---
341+
342+
### CI expectations
315343

316344
- CI failures must be fixed, not bypassed
317345
- Do not disable schema validation or tests to make CI green
@@ -325,13 +353,15 @@ This pipeline exists to ensure the Helm chart remains:
325353
- Kubernetes-version compatible
326354
- Predictable across environments
327355

356+
---
357+
328358
## Planned next steps
329359

330360
### Regression & compatibility test suite
331361
- Introduce a minimal regression test suite to validate the gateway’s public contract ✅
332362
- Focus on high-risk areas:
333-
- OpenAI-compatible request/response schemas
334-
- Streaming (SSE) pass-through behavior
363+
- OpenAI-compatible request/response schemas
364+
- Streaming (SSE) pass-through behavior
335365
- Error and status code mapping
336366
- Metering event emission (usage present vs missing)
337367
- Use a mock upstream and lightweight in-cluster setup (e.g., kind or docker-compose) ✅

tests/contract/README.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# Contract tests
2+
3+
These tests validate the LLM Gateway's public OpenAI-compatible contract in a deterministic way.
4+
5+
## Scope
6+
- Chat completions (non-stream)
7+
- VLM-style payloads (messages content with `image_url`)
8+
- Streaming passthrough (SSE) with `[DONE]`
9+
10+
## Design
11+
- A lightweight in-cluster `mock-openai` acts as upstream.
12+
- The gateway is deployed to a kind cluster via the Helm chart.
13+
- Tests send HTTP requests to the gateway service and assert the response shape and streaming behavior (not model semantics).
14+
15+
## Files
16+
- `mock-openai/`: mock upstream server implementation + Dockerfile
17+
- `k8s/`: Kubernetes manifests for the mock upstream
18+
- `fixtures/`: request payload fixtures
19+
20+
## Adding a new test
21+
1. Add a new request fixture under `fixtures/`.
22+
2. Extend `mock-openai/main.go` if a new upstream behavior is needed.
23+
3. Extend the contract test runner (ci-contract.sh) with new assertions.
24+
25+
## CI usage
26+
27+
In CI, the helm-regression workflow:
28+
- Creates a kind cluster
29+
- Builds and loads images (llm-proxy, llm-collector, mock-openai)
30+
- Deploys mock-openai
31+
- Installs the Helm chart with UPSTREAM_OPENAI_BASE_URL pointing to mock-openai
32+
- Runs tests/contract/ci-contract.sh
33+
34+
The CI runner assumes:
35+
- A reachable Kubernetes cluster
36+
- llm-gateway already installed
37+
- mock-openai running
38+
- Proxy service reachable inside the cluster
39+
40+
It does not perform any setup.
41+
42+
## Local usage
43+
44+
Make scripts executable:
45+
chmod +x tests/contract/local-run.sh
46+
chmod +x tests/contract/ci-contract.sh
47+
48+
Run:
49+
./tests/contract/local-run.sh
50+
51+
This mirrors the CI workflow:
52+
- Creates or reuses a kind cluster
53+
- Builds and loads images
54+
- Deploys mock-openai
55+
- Installs the chart pointing to the mock upstream
56+
- Runs the same CI contract tests

tests/contract/ci-contract.sh

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
NAMESPACE="${NAMESPACE:-llm-system}"
5+
PROXY_SVC="${PROXY_SVC:-llm-proxy}"
6+
PROXY_PORT="${PROXY_PORT:-8080}"
7+
8+
need() { command -v "$1" >/dev/null 2>&1 || { echo "Missing: $1" >&2; exit 1; }; }
9+
need kubectl
10+
11+
echo "[contract] namespace=${NAMESPACE} target=http://${PROXY_SVC}:${PROXY_PORT}"
12+
13+
# run inside cluster (no port-forward flake)
14+
kubectl run -n "${NAMESPACE}" contract-curl --rm -i --restart=Never \
15+
--image=curlimages/curl:8.6.0 \
16+
-- sh -lc "
17+
set -e
18+
19+
echo '--- normal ---'
20+
cat <<'JSON' | curl -sS http://${PROXY_SVC}:${PROXY_PORT}/v1/chat/completions \
21+
-H 'Content-Type: application/json' \
22+
-H 'Authorization: Bearer dummy' \
23+
-d @- | tee /tmp/normal.json >/dev/null
24+
{
25+
\"model\": \"gpt-4o-mini\",
26+
\"messages\": [{\"role\":\"user\",\"content\":\"hi\"}]
27+
}
28+
JSON
29+
grep -q 'ok-from-mock' /tmp/normal.json
30+
grep -q '\"usage\"' /tmp/normal.json
31+
32+
echo '--- vlm ---'
33+
cat <<'JSON' | curl -sS http://${PROXY_SVC}:${PROXY_PORT}/v1/chat/completions \
34+
-H 'Content-Type: application/json' \
35+
-H 'Authorization: Bearer dummy' \
36+
-d @- | tee /tmp/vlm.json >/dev/null
37+
{
38+
\"model\": \"gpt-4o-mini\",
39+
\"messages\": [
40+
{
41+
\"role\": \"user\",
42+
\"content\": [
43+
{\"type\":\"text\",\"text\":\"what is in this image?\"},
44+
{\"type\":\"image_url\",\"image_url\":{\"url\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO2Xc+UAAAAASUVORK5CYII=\"}}
45+
]
46+
}
47+
]
48+
}
49+
JSON
50+
grep -q 'ok-from-mock' /tmp/vlm.json
51+
grep -q '\"usage\"' /tmp/vlm.json
52+
53+
echo '--- stream ---'
54+
cat <<'JSON' | curl -sS -N http://${PROXY_SVC}:${PROXY_PORT}/v1/chat/completions \
55+
-H 'Content-Type: application/json' \
56+
-H 'Authorization: Bearer dummy' \
57+
-d @- | tee /tmp/stream.txt >/dev/null
58+
{
59+
\"model\": \"gpt-4o-mini\",
60+
\"stream\": true,
61+
\"messages\": [{\"role\":\"user\",\"content\":\"stream pls\"}]
62+
}
63+
JSON
64+
grep -q '\\[DONE\\]' /tmp/stream.txt
65+
66+
echo 'OK: contract tests passed.'
67+
"
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"model": "gpt-4o-mini",
3+
"messages": [
4+
{
5+
"role": "user",
6+
"content": "hi from llm-gateway!"
7+
}
8+
]
9+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"model": "gpt-4o-mini",
3+
"stream": true,
4+
"messages": [
5+
{
6+
"role": "user",
7+
"content": "may the stream be with you!"
8+
}
9+
]
10+
}

tests/contract/fixtures/vlm.json

Lines changed: 20 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
apiVersion: apps/v1
2+
kind: Deployment
3+
metadata:
4+
name: mock-openai
5+
spec:
6+
replicas: 1
7+
selector:
8+
matchLabels:
9+
app: mock-openai
10+
template:
11+
metadata:
12+
labels:
13+
app: mock-openai
14+
spec:
15+
containers:
16+
- name: mock-openai
17+
image: mock-openai:ci
18+
imagePullPolicy: IfNotPresent
19+
ports:
20+
- containerPort: 8080
21+
resources:
22+
requests:
23+
cpu: 10m
24+
memory: 32Mi
25+
limits:
26+
cpu: 100m
27+
memory: 128Mi
28+
---
29+
apiVersion: v1
30+
kind: Service
31+
metadata:
32+
name: mock-openai
33+
spec:
34+
selector:
35+
app: mock-openai
36+
ports:
37+
- name: http
38+
port: 8080
39+
targetPort: 8080

0 commit comments

Comments
 (0)