5a558eb09e
Backend Tests / discover-tests (push) Waiting to run
Backend Tests / ${{ matrix.name }} (push) Blocked by required conditions
Build and Publish SDK / build-and-publish (push) Waiting to run
Build Opik Docker Images / set-version (push) Waiting to run
Build Opik Docker Images / build-backend (push) Blocked by required conditions
Build Opik Docker Images / build-sandbox-executor-python (push) Blocked by required conditions
Build Opik Docker Images / build-python-backend (push) Blocked by required conditions
Build Opik Docker Images / build-frontend (push) Blocked by required conditions
Build Opik Docker Images / create-git-tag (push) Blocked by required conditions
ClickHouse Migration Cluster Check / validate-clickhouse-migrations (push) Waiting to run
Docs - Publish / run (push) Waiting to run
E2E Tests - Post Merge (v2) / 🧪 E2E v2 Tests (${{ github.event.inputs.tier || 't1' }}) (push) Waiting to run
E2E Tests - Post Merge (v2) / 📢 Slack Notification (push) Blocked by required conditions
Frontend Unit Tests / Test on Node 20 (push) Waiting to run
Guardrails E2E Tests / Select Python version matrix (push) Waiting to run
Guardrails E2E Tests / Guardrails E2E Tests ${{matrix.python_version}} (push) Blocked by required conditions
Guardrails E2E Tests / 📢 Slack Notification (push) Blocked by required conditions
Guardrails Backend Unit Tests / Guardrails Backend Unit Tests (push) Waiting to run
Guardrails Backend Unit Tests / 📢 Slack Notification (push) Blocked by required conditions
SDK Library Integration Tests Runner / Check Secrets (push) Waiting to run
SDK Library Integration Tests Runner / Missed OpenAI API Key Warning (push) Blocked by required conditions
SDK Library Integration Tests Runner / Build (push) Blocked by required conditions
SDK Library Integration Tests Runner / openai_tests (push) Blocked by required conditions
SDK Library Integration Tests Runner / langchain_tests (push) Blocked by required conditions
SDK Library Integration Tests Runner / langchain_legacy_tests (push) Blocked by required conditions
SDK Library Integration Tests Runner / llama_index_tests (push) Blocked by required conditions
SDK Library Integration Tests Runner / anthropic_tests (push) Blocked by required conditions
SDK Library Integration Tests Runner / mistral_tests (push) Blocked by required conditions
SDK Library Integration Tests Runner / groq_tests (push) Blocked by required conditions
SDK Library Integration Tests Runner / aisuite_tests (push) Blocked by required conditions
SDK Library Integration Tests Runner / haystack_tests (push) Blocked by required conditions
SDK Library Integration Tests Runner / dspy_tests (push) Blocked by required conditions
SDK Library Integration Tests Runner / crewai_v0_tests (push) Blocked by required conditions
SDK Library Integration Tests Runner / crewai_v1_tests (push) Blocked by required conditions
SDK Library Integration Tests Runner / genai_tests (push) Blocked by required conditions
SDK Library Integration Tests Runner / adk_tests (push) Blocked by required conditions
SDK Library Integration Tests Runner / adk_legacy_1_3_0_tests (push) Blocked by required conditions
SDK Library Integration Tests Runner / evaluation_metrics_tests (push) Blocked by required conditions
SDK Library Integration Tests Runner / bedrock_tests (push) Blocked by required conditions
SDK Library Integration Tests Runner / litellm_tests (push) Blocked by required conditions
SDK Library Integration Tests Runner / harbor_tests (push) Blocked by required conditions
SDK Library Integration Tests Runner / Slack Notification (push) Blocked by required conditions
Lint Opik Helm Chart / lint-helm-chart (Helm v3.21.0) (push) Waiting to run
Lint Opik Helm Chart / lint-helm-chart (Helm v4.2.0) (push) Waiting to run
Lint Opik Helm Chart / unittest-helm-chart (push) Waiting to run
Lint Opik Helm Chart / render-equality (push) Waiting to run
Opik Optimizer - Unit Tests / Opik Optimizer Unit Tests Python ${{matrix.python_version}} (push) Waiting to run
Python BE E2E Tests / Python BE E2E (push) Waiting to run
Python Backend Tests / run-python-backend-tests (push) Waiting to run
Python SDK Unit Tests / Python SDK Unit Tests ${{matrix.python_version}} (push) Waiting to run
Release Drafter / update_release_draft (push) Waiting to run
SDK E2E Libraries Integration Tests / Check Secrets (push) Waiting to run
SDK E2E Libraries Integration Tests / Missed OpenAI API Key Warning (push) Blocked by required conditions
SDK E2E Libraries Integration Tests / build-opik (push) Blocked by required conditions
SDK E2E Libraries Integration Tests / E2E Lib Integration Python ${{matrix.python_version}} (push) Blocked by required conditions
TypeScript SDK Integration Build & Publish / build-and-publish (opik-gemini) (push) Waiting to run
TypeScript SDK Integration Build & Publish / build-and-publish (opik-langchain) (push) Waiting to run
TypeScript SDK Integration Build & Publish / build-and-publish (opik-openai) (push) Waiting to run
TypeScript SDK Integration Build & Publish / build-and-publish (opik-otel) (push) Waiting to run
TypeScript SDK Integration Build & Publish / build-and-publish (opik-vercel) (push) Waiting to run
TypeScript SDK Library Integration Tests / Check Secrets (push) Waiting to run
TypeScript SDK Library Integration Tests / opik-vercel (Vercel AI SDK / eve) (push) Blocked by required conditions
TypeScript SDK Build & Publish / build-and-publish (push) Waiting to run
TypeScript SDK Unit Tests / Test on Node ${{ matrix.node-version }} (push) Waiting to run
TypeScript SDK Compatibility V1.x E2E Tests / Select Node version matrix (push) Has been cancelled
TypeScript SDK Compatibility V1.x E2E Tests / TypeScript SDK Compatibility V1.x E2E Tests Node ${{matrix.node_version}} (push) Has been cancelled
TypeScript SDK E2E Tests / TypeScript SDK E2E Tests Node ${{matrix.node_version}} (push) Has been cancelled
Opik Optimizer - E2E Tests / build-opik (push) Has been cancelled
TypeScript SDK Compatibility V1.x E2E Tests / build-opik (push) Has been cancelled
Python SDK E2E Tests / Select Python version matrix (push) Has been cancelled
Python SDK E2E Tests / Python SDK E2E Tests ${{matrix.python_version}} (push) Has been cancelled
Python SDK E2E Tests / build-opik (push) Has been cancelled
Python SDK Compatibility V1.x E2E Tests / Select Python version matrix (push) Has been cancelled
Python SDK Compatibility V1.x E2E Tests / Python SDK Compatibility V1.x E2E Tests ${{matrix.python_version}} (push) Has been cancelled
Python SDK Compatibility V1.x E2E Tests / build-opik (push) Has been cancelled
TypeScript SDK E2E Tests / Select Node version matrix (push) Has been cancelled
TypeScript SDK E2E Tests / build-opik (push) Has been cancelled
Opik Optimizer - E2E Tests / Opik Optimizer E2E Tests Python ${{matrix.python_version}} (push) Has been cancelled
Opik Optimizer - E2E Tests / Opik Optimizer Integration Smoke Tests (push) Has been cancelled
🐙 Code Quality / detect (push) Has been cancelled
🐙 Code Quality / lint (${{ matrix.leg.name }}) (push) Has been cancelled
🐙 Code Quality / summary (push) Has been cancelled
236 lines
8.5 KiB
Python
236 lines
8.5 KiB
Python
"""
|
|
Contract tests for Java-Python RQ integration.
|
|
|
|
These tests verify that:
|
|
1. Java writes jobs with plain JSON 'data' (no zlib) in RQ format
|
|
2. Java enqueues job IDs into the RQ list key: 'rq:queue:<queue_name>'
|
|
3. Python RQ (JSONSerializer + default Job) can process Java-created jobs
|
|
4. Job format matches RQ expectations
|
|
"""
|
|
|
|
import pytest
|
|
fakeredis = pytest.importorskip("fakeredis")
|
|
import json
|
|
import uuid
|
|
from datetime import datetime
|
|
|
|
|
|
@pytest.fixture
|
|
def redis_conn():
|
|
"""Mocked Redis using fakeredis (no external service)."""
|
|
return fakeredis.FakeRedis(decode_responses=False)
|
|
|
|
|
|
def create_java_style_job(redis_conn, job_id=None, function='test_function', args=None, kwargs=None):
|
|
"""
|
|
Create a job in Redis using the same format as Java RqPublisher.
|
|
|
|
Simulates Java by:
|
|
- Writing 'rq:job:{id}' hash with 'data' as plain JSON: [func, null, args, kwargs]
|
|
- Pushing the job id onto the RQ list: 'rq:queue:<queue_name>'
|
|
"""
|
|
if job_id is None:
|
|
job_id = str(uuid.uuid4())
|
|
if args is None:
|
|
args = ['test_arg']
|
|
if kwargs is None:
|
|
kwargs = {}
|
|
|
|
queue_name = 'opik:optimizer-cloud'
|
|
job_key = f'rq:job:{job_id}'
|
|
|
|
# Create data field: [function, null, args, kwargs] as plain JSON string
|
|
data_array = [function, None, args, kwargs]
|
|
json_data = json.dumps(data_array)
|
|
|
|
# Create HASH fields
|
|
now = datetime.utcnow().isoformat() + 'Z'
|
|
job_fields = {
|
|
b'data': json_data.encode('utf-8'),
|
|
b'created_at': now.encode('utf-8'),
|
|
b'enqueued_at': now.encode('utf-8'),
|
|
b'status': b'queued',
|
|
b'origin': queue_name.encode('utf-8'),
|
|
b'timeout': b'3600'
|
|
}
|
|
|
|
# Store in Redis
|
|
redis_conn.hset(job_key, mapping=job_fields)
|
|
redis_conn.expire(job_key, 3600)
|
|
redis_conn.rpush(f'rq:queue:{queue_name}', job_id)
|
|
|
|
return job_id, queue_name
|
|
|
|
|
|
class TestJavaPythonRqContract:
|
|
"""Test Java-Python RQ integration contract."""
|
|
|
|
def test_java_plain_json_data_format(self):
|
|
"""Verify Java contract uses plain JSON string for 'data'."""
|
|
test_data = ["function_name", None, ["arg1", "arg2"], {"key": "value"}]
|
|
json_str = json.dumps(test_data)
|
|
# Assert it's valid JSON and round-trips
|
|
parsed = json.loads(json_str)
|
|
assert parsed == test_data
|
|
|
|
def test_python_can_read_java_created_job(self, redis_conn):
|
|
"""Verify Python can read job created by Java."""
|
|
# Given: Job created with Java format
|
|
job_id, queue_name = create_java_style_job(
|
|
redis_conn,
|
|
function='opik_backend.rq_worker.process_optimizer_job',
|
|
args=['Test message']
|
|
)
|
|
|
|
try:
|
|
# When: Python reads the job
|
|
job_key = f'rq:job:{job_id}'
|
|
job_data = redis_conn.hgetall(job_key)
|
|
|
|
# Then: All fields are present
|
|
assert b'data' in job_data
|
|
assert b'created_at' in job_data
|
|
assert b'enqueued_at' in job_data
|
|
assert b'status' in job_data
|
|
assert b'origin' in job_data
|
|
assert b'timeout' in job_data
|
|
|
|
# And: Data is plain JSON
|
|
parsed = json.loads(job_data[b'data'].decode('utf-8'))
|
|
|
|
# And: Data has correct RQ format
|
|
assert len(parsed) == 4
|
|
assert parsed[0] == 'opik_backend.rq_worker.process_optimizer_job'
|
|
assert parsed[1] is None
|
|
assert parsed[2] == ['Test message']
|
|
assert parsed[3] == {}
|
|
|
|
# And: Status is correct
|
|
assert job_data[b'status'] == b'queued'
|
|
|
|
finally:
|
|
# Cleanup
|
|
redis_conn.delete(f'rq:job:{job_id}')
|
|
redis_conn.delete(queue_name)
|
|
|
|
def test_rq_job_format_matches_expectations(self):
|
|
"""Verify RQ job 'data' array structure."""
|
|
# Given: Expected RQ format
|
|
expected_format = {
|
|
'function': str, # Import path to Python function
|
|
'callback': type(None),# Result callback placeholder (RQ uses None)
|
|
'args': list, # Positional arguments
|
|
'kwargs': dict # Keyword arguments
|
|
}
|
|
|
|
# When: Create job data
|
|
job_data = [
|
|
'opik_backend.rq_worker.process_optimizer_job',
|
|
None,
|
|
['message'],
|
|
{}
|
|
]
|
|
|
|
# Then: Format matches expectations
|
|
assert isinstance(job_data[0], str)
|
|
assert job_data[1] is None
|
|
assert isinstance(job_data[2], list)
|
|
assert isinstance(job_data[3], dict)
|
|
|
|
def test_data_is_json_not_compressed(self):
|
|
"""Ensure contract uses plain JSON, not zlib compression."""
|
|
arr = ["f", None, ["x"], {}]
|
|
json_str = json.dumps(arr)
|
|
# No compression; just JSON presence check
|
|
assert json.loads(json_str) == arr
|
|
|
|
def test_timestamp_format_compatibility(self):
|
|
"""Verify timestamp format is ISO-8601."""
|
|
# Given: Java Instant format
|
|
java_timestamp = "2025-10-14T10:57:05.638222Z"
|
|
|
|
# When: Parse with Python
|
|
# This should not raise an exception
|
|
from datetime import datetime
|
|
parsed = datetime.fromisoformat(java_timestamp.replace('Z', '+00:00'))
|
|
|
|
# Then: Successfully parsed
|
|
assert parsed.year == 2025
|
|
assert parsed.month == 10
|
|
assert parsed.day == 14
|
|
|
|
def test_rq_api_with_fakeredis_works(self, redis_conn):
|
|
"""Use RQ Queue + SimpleWorker (JSONSerializer) over fakeredis to validate processing API."""
|
|
from rq import Queue
|
|
from rq.serializers import JSONSerializer
|
|
from rq.worker import SimpleWorker
|
|
from rq.job import Job
|
|
|
|
# Create queue using mocked Redis
|
|
queue_name = 'opik:optimizer-cloud'
|
|
serializer = JSONSerializer()
|
|
q = Queue(queue_name, connection=redis_conn, serializer=serializer)
|
|
|
|
# Create a proper job message matching the OptimizationJobContext format
|
|
job_message = {
|
|
'optimization_id': str(uuid.uuid4()),
|
|
'workspace_name': 'test-workspace',
|
|
'opik_api_key': 'test-key',
|
|
'config': {
|
|
'dataset_name': 'test-dataset',
|
|
'prompt': {
|
|
'messages': [
|
|
{'role': 'system', 'content': 'You are a helpful assistant'},
|
|
{'role': 'user', 'content': 'Answer: {question}'}
|
|
]
|
|
},
|
|
'llm_model': {
|
|
'model': 'gpt-4',
|
|
'parameters': {'temperature': 0.7}
|
|
},
|
|
'evaluation': {
|
|
'metrics': [
|
|
{
|
|
'type': 'equals',
|
|
'parameters': {}
|
|
}
|
|
]
|
|
},
|
|
'optimizer': {
|
|
'type': 'gepa',
|
|
'parameters': {}
|
|
}
|
|
}
|
|
}
|
|
|
|
# Enqueue our worker function by reference with proper job message
|
|
from opik_backend.rq_worker import process_optimizer_job
|
|
job = q.enqueue(process_optimizer_job, job_message)
|
|
|
|
# Process with SimpleWorker (no forking) against the same mocked Redis
|
|
worker = SimpleWorker([q], connection=redis_conn, serializer=serializer)
|
|
worker.work(burst=True)
|
|
|
|
# Fetch job and verify result
|
|
fetched = Job.fetch(job.id, connection=redis_conn, serializer=serializer)
|
|
|
|
# The job will fail because we don't have a real dataset or LLM keys,
|
|
# but it should at least parse the message correctly and not fail with KeyError
|
|
# Check that it got past the message parsing stage
|
|
if not fetched.is_finished:
|
|
# If it failed, check that it's not due to missing job message fields
|
|
exc_info = fetched.latest_result()
|
|
error_msg = str(exc_info.exc_string) if hasattr(exc_info, 'exc_string') else str(fetched.exc_info)
|
|
# Should not fail with KeyError for optimization_id, workspace_name, or config
|
|
assert 'KeyError' not in error_msg or 'optimization_id' not in error_msg, \
|
|
f"Job failed with message parsing error: {error_msg}"
|
|
# It's OK to fail for other reasons (missing dataset, API keys, etc.)
|
|
else:
|
|
# If it succeeded (unlikely without real resources), that's also OK
|
|
val = fetched.return_value
|
|
assert val is not None
|
|
|
|
|
|
|
|
|