Skip to content

Commit b581a5e

Browse files
[CCOR-13193] Run Integration(e2e) tests on PR-to-main (phase 1: test with sdkdev v5 server) (#422)
1 parent 7481aed commit b581a5e

19 files changed

Lines changed: 1535 additions & 433 deletions

.github/workflows/pull_request.yml

Lines changed: 49 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
11
name: Continuous Integration
22

3-
on: [pull_request, workflow_dispatch]
3+
on:
4+
push:
5+
branches:
6+
- main
7+
pull_request:
8+
branches:
9+
- main
10+
workflow_dispatch:
411

512
concurrency:
6-
group: ${{ github.workflow }}-${{ github.ref }}
13+
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
714
cancel-in-progress: true
815

916
jobs:
@@ -46,9 +53,6 @@ jobs:
4653
id: unit_tests
4754
continue-on-error: true
4855
env:
49-
CONDUCTOR_AUTH_KEY: ${{ secrets.CONDUCTOR_AUTH_KEY }}
50-
CONDUCTOR_AUTH_SECRET: ${{ secrets.CONDUCTOR_AUTH_SECRET }}
51-
CONDUCTOR_SERVER_URL: ${{ secrets.CONDUCTOR_SERVER_URL }}
5256
COVERAGE_FILE: ${{ env.COVERAGE_DIR }}/.coverage.unit
5357
run: |
5458
coverage run -m pytest tests/unit -v
@@ -57,9 +61,6 @@ jobs:
5761
id: bc_tests
5862
continue-on-error: true
5963
env:
60-
CONDUCTOR_AUTH_KEY: ${{ secrets.CONDUCTOR_AUTH_KEY }}
61-
CONDUCTOR_AUTH_SECRET: ${{ secrets.CONDUCTOR_AUTH_SECRET }}
62-
CONDUCTOR_SERVER_URL: ${{ secrets.CONDUCTOR_SERVER_URL }}
6364
COVERAGE_FILE: ${{ env.COVERAGE_DIR }}/.coverage.bc
6465
run: |
6566
coverage run -m pytest tests/backwardcompatibility -v
@@ -68,9 +69,6 @@ jobs:
6869
id: serdeser_tests
6970
continue-on-error: true
7071
env:
71-
CONDUCTOR_AUTH_KEY: ${{ secrets.CONDUCTOR_AUTH_KEY }}
72-
CONDUCTOR_AUTH_SECRET: ${{ secrets.CONDUCTOR_AUTH_SECRET }}
73-
CONDUCTOR_SERVER_URL: ${{ secrets.CONDUCTOR_SERVER_URL }}
7472
COVERAGE_FILE: ${{ env.COVERAGE_DIR }}/.coverage.serdeser
7573
run: |
7674
coverage run -m pytest tests/serdesertest -v
@@ -103,4 +101,43 @@ jobs:
103101

104102
- name: Check test results
105103
if: steps.unit_tests.outcome == 'failure' || steps.bc_tests.outcome == 'failure' || steps.serdeser_tests.outcome == 'failure' || steps.agent_base_import.outcome == 'failure'
106-
run: exit 1
104+
run: exit 1
105+
106+
integration-test:
107+
runs-on: ubuntu-latest
108+
timeout-minutes: 30
109+
strategy:
110+
fail-fast: false
111+
matrix:
112+
bucket: [test-all, long-sync, long-async, core]
113+
name: integration-test (${{ matrix.bucket }})
114+
env:
115+
CONDUCTOR_SERVER_URL: ${{ vars.SDKDEV_V5_SERVER_URL }}
116+
CONDUCTOR_AUTH_KEY: ${{ vars.SDKDEV_V5_AUTH_KEY }}
117+
CONDUCTOR_AUTH_SECRET: ${{ secrets.SDKDEV_V5_AUTH_SECRET }}
118+
# Force HTTP/1.1 for integration tests: long-lived HTTP/2 connections to
119+
# the shared dev server (through its proxy/LB) intermittently stall
120+
# mid-stream, surfacing as httpcore.ReadTimeout -> ApiException(0). HTTP/1.1
121+
# uses one request per pooled connection, avoiding that failure class.
122+
CONDUCTOR_HTTP2_ENABLED: "false"
123+
steps:
124+
- name: Checkout code
125+
uses: actions/checkout@v4
126+
127+
- name: Set up Python
128+
uses: actions/setup-python@v5
129+
with:
130+
python-version: '3.12'
131+
cache: 'pip'
132+
133+
- name: Install dependencies
134+
run: |
135+
python -m pip install --upgrade pip
136+
pip install -e .
137+
pip install pytest
138+
139+
- name: Run integration tests
140+
run: >-
141+
bash scripts/run_integration_tests.sh --bucket=${{ matrix.bucket }}
142+
-s --log-cli-level=INFO
143+
--log-cli-format='%(asctime)s %(levelname)s %(name)s: %(message)s'

pyproject.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,10 @@ markers = [
197197
"sse: requires SSE streaming (AGENTSPAN_STREAMING_ENABLED=true)",
198198
"agent_correctness: marks tests as agent correctness tests",
199199
"semantic: marks tests that use an LLM judge for semantic assertions",
200+
"slow_sync: long-running sync lease-extension tests (~90s)",
201+
"slow_async: long-running async lease-extension tests (~90s)",
202+
"slow_test_all: long-running aggregate workflow-client test_all (~83s)",
203+
"server_timeout_unreliable: depends on server-side task timeout firing in a bounded window; excluded from CI",
200204
]
201205

202206
[tool.coverage.run]

scripts/run_integration_tests.sh

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
#!/usr/bin/env bash
2+
#
3+
# Run the SDK integration test suite locally, mirroring the `integration-test`
4+
# job in .github/workflows/pull_request.yml.
5+
#
6+
# The AI/agentic integration tests are excluded on purpose: they target a
7+
# dedicated AI-enabled server (test_ai_task_types.py and test_ai_examples.py
8+
# hardcode http://localhost:7001/api, and test_agentic_workflows.py needs an
9+
# `openai` LLM provider configured on the server), so they don't run against
10+
# the standard test server this suite targets. The entire tests/integration/ai/
11+
# suite is excluded for the same reason: it drives the agent runtime (POST
12+
# /api/agent/start plus an LLM provider), which the standard server doesn't
13+
# expose (it 404s /api/agent/start).
14+
#
15+
# The performance test (test_update_task_v2_perf.py) is also excluded by
16+
# default: it submits ~1000 workflows and takes several minutes. Pass
17+
# --with-perf to include it.
18+
#
19+
# Server connection is read from the environment (see
20+
# src/conductor/client/configuration/configuration.py):
21+
# CONDUCTOR_SERVER_URL (required; defaults to http://localhost:8080/api)
22+
# CONDUCTOR_AUTH_KEY (optional; needed for Orkes/authenticated servers)
23+
# CONDUCTOR_AUTH_SECRET (optional; needed for Orkes/authenticated servers)
24+
#
25+
# Usage:
26+
# export CONDUCTOR_SERVER_URL="http://localhost:8080/api"
27+
# ./scripts/run_integration_tests.sh
28+
# ./scripts/run_integration_tests.sh --with-perf # also run the perf test
29+
#
30+
# Buckets (--bucket=<name>): the slowest tests are split into their own buckets
31+
# so they can run as separate parallel CI jobs and are skipped by default
32+
# locally. Each slow bucket is path-scoped so it only imports its own module.
33+
# core (default) everything except the slow buckets below
34+
# long-sync sync lease-extension tests (test_lease_extension.py, ~90s)
35+
# long-async async lease-extension tests (test_async_lease_extension.py, ~90s)
36+
# test-all aggregate workflow-client test_all (test_workflow_client_intg.py, ~83s)
37+
# all the full suite (no bucket filtering) — for a complete local run
38+
#
39+
# The long-* buckets deselect the no-heartbeat test_02 cases (marker
40+
# server_timeout_unreliable): those assert a server-side task timeout that the
41+
# sdkdev server doesn't reliably fire on a CI-bounded timeline. Use --bucket=all
42+
# (or target the test directly) to run them anyway.
43+
#
44+
# ./scripts/run_integration_tests.sh # fast: skips slow buckets
45+
# ./scripts/run_integration_tests.sh --bucket=long-sync
46+
# ./scripts/run_integration_tests.sh --bucket=all # run everything
47+
#
48+
# Any other arguments are passed straight through to pytest, which is handy for
49+
# targeting a subset of tests or getting more detail on failures:
50+
#
51+
# # run a subset (by keyword or path) with live output
52+
# ./scripts/run_integration_tests.sh -s -k lease
53+
# ./scripts/run_integration_tests.sh tests/integration/test_lease_extension.py
54+
#
55+
# # short tracebacks + a one-line reason for every failure/skip, with live logs
56+
# ./scripts/run_integration_tests.sh -ra --tb=short --log-cli-level=INFO
57+
#
58+
# # watch a slow bucket live: -s streams print() and the worker/child-process
59+
# # task logs; --log-cli-level=INFO streams the main-process logger.info lines
60+
# ./scripts/run_integration_tests.sh --bucket=long-sync -s --log-cli-level=INFO
61+
#
62+
# # same, but with timestamped live logs (what CI uses)
63+
# ./scripts/run_integration_tests.sh --bucket=long-sync -s \
64+
# --log-cli-level=INFO \
65+
# --log-cli-format='%(asctime)s %(levelname)s %(name)s: %(message)s'
66+
#
67+
# # stop at the first failure instead of waiting for the whole suite
68+
# ./scripts/run_integration_tests.sh -x --tb=long
69+
#
70+
# # re-run just specific tests, e.g. the ones that failed
71+
# ./scripts/run_integration_tests.sh -s -k "upsert_group or create_role"
72+
#
73+
# # show the 15 slowest tests (setup/call/teardown timings) after the run
74+
# ./scripts/run_integration_tests.sh --durations=15
75+
76+
set -euo pipefail
77+
78+
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
79+
cd "$REPO_ROOT"
80+
81+
# Slow-test files that get their own buckets / CI jobs (see --bucket above).
82+
LEASE_SYNC=tests/integration/test_lease_extension.py
83+
LEASE_ASYNC=tests/integration/test_async_lease_extension.py
84+
TEST_ALL_FILE=tests/integration/test_workflow_client_intg.py
85+
86+
# The perf test is skipped by default; --with-perf opts back in.
87+
perf_ignore=(--ignore=tests/integration/test_update_task_v2_perf.py)
88+
bucket="core"
89+
pytest_args=()
90+
for arg in "$@"; do
91+
case "$arg" in
92+
--with-perf) perf_ignore=() ;;
93+
--bucket=*) bucket="${arg#*=}" ;;
94+
*) pytest_args+=("$arg") ;;
95+
esac
96+
done
97+
98+
# The AI/agentic tests always target a dedicated server (see header) and are
99+
# never part of these buckets. The tests/integration/ai/ suite drives the agent
100+
# runtime (POST /api/agent/start, an LLM provider, SSE streaming): the standard
101+
# sdkdev server has no agent API and returns 404 for every one of them, so the
102+
# whole directory is excluded here rather than file-by-file.
103+
ai_ignore=(
104+
--ignore=tests/integration/test_ai_task_types.py
105+
--ignore=tests/integration/test_ai_examples.py
106+
--ignore=tests/integration/test_agentic_workflows.py
107+
--ignore=tests/integration/ai
108+
)
109+
110+
# Build the target paths + selection for the chosen bucket. The slow buckets are
111+
# path-scoped so each job only imports its own module: this keeps
112+
# scan_for_annotated_workers from starting unrelated workers and lets the four
113+
# buckets run in parallel against one server without stealing each other's tasks.
114+
case "$bucket" in
115+
core)
116+
targets=(tests/integration)
117+
select=("${ai_ignore[@]}" ${perf_ignore[@]+"${perf_ignore[@]}"} \
118+
--ignore="$LEASE_SYNC" --ignore="$LEASE_ASYNC" --ignore="$TEST_ALL_FILE")
119+
;;
120+
all)
121+
targets=(tests/integration)
122+
select=("${ai_ignore[@]}" ${perf_ignore[@]+"${perf_ignore[@]}"})
123+
;;
124+
# The slow buckets below target a single specific file, so the perf test is
125+
# never in scope and perf_ignore is unnecessary here. This also means
126+
# --with-perf has no effect on these buckets; the perf test is only reachable
127+
# via the core/all buckets.
128+
# The no-heartbeat "server should time the task out" cases (test_02) are
129+
# deselected here: the sdkdev server does not reliably time a task out on a
130+
# CI-bounded timeline, so they flake for reasons unrelated to the SDK. They
131+
# carry the server_timeout_unreliable marker and still run under --bucket=all.
132+
long-sync)
133+
targets=("$LEASE_SYNC")
134+
select=(-m "slow_sync and not server_timeout_unreliable")
135+
;;
136+
long-async)
137+
targets=("$LEASE_ASYNC")
138+
select=(-m "slow_async and not server_timeout_unreliable")
139+
;;
140+
test-all)
141+
targets=("$TEST_ALL_FILE")
142+
select=(-m slow_test_all)
143+
;;
144+
*)
145+
echo "Unknown --bucket='$bucket' (expected: core, long-sync, long-async, test-all, all)" >&2
146+
exit 2
147+
;;
148+
esac
149+
150+
# Note: the "${arr[@]+"${arr[@]}"}" form is required so empty arrays don't trip
151+
# "unbound variable" under `set -u` on bash 3.2 (the default macOS bash).
152+
exec python3 -m pytest -v \
153+
"${targets[@]}" \
154+
${select[@]+"${select[@]}"} \
155+
${pytest_args[@]+"${pytest_args[@]}"}

src/conductor/client/http/models/service_method.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ class ServiceMethod:
1515
'input_type': 'str',
1616
'output_type': 'str',
1717
'request_params': 'list[RequestParam]',
18-
'example_input': 'dict'
18+
'example_input': 'object'
1919
}
2020

2121
attribute_map = {

src/conductor/client/http/models/upsert_user_request.py

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import re # noqa: F401
33
import six
44
from dataclasses import dataclass, field, InitVar
5-
from typing import List, Optional
5+
from typing import Dict, List, Optional
66
from enum import Enum
77

88

@@ -30,41 +30,50 @@ class UpsertUserRequest:
3030
name: InitVar[Optional[str]] = None
3131
roles: InitVar[Optional[List[str]]] = None
3232
groups: InitVar[Optional[List[str]]] = None
33+
contact_information: InitVar[Optional[Dict[str, str]]] = None
3334

3435
_name: str = field(default=None, init=False)
3536
_roles: List[str] = field(default=None, init=False)
3637
_groups: List[str] = field(default=None, init=False)
38+
_contact_information: Dict[str, str] = field(default=None, init=False)
3739

3840
swagger_types = {
3941
'name': 'str',
4042
'roles': 'list[str]',
41-
'groups': 'list[str]'
43+
'groups': 'list[str]',
44+
'contact_information': 'dict(str, str)'
4245
}
4346

4447
attribute_map = {
4548
'name': 'name',
4649
'roles': 'roles',
47-
'groups': 'groups'
50+
'groups': 'groups',
51+
'contact_information': 'contactInformation'
4852
}
4953

50-
def __init__(self, name=None, roles=None, groups=None): # noqa: E501
54+
def __init__(self, name=None, roles=None, groups=None, contact_information=None): # noqa: E501
5155
"""UpsertUserRequest - a model defined in Swagger""" # noqa: E501
5256
self._name = None
5357
self._roles = None
5458
self._groups = None
59+
self._contact_information = None
5560
self.discriminator = None
5661
self.name = name
5762
if roles is not None:
5863
self.roles = roles
5964
if groups is not None:
6065
self.groups = groups
66+
if contact_information is not None:
67+
self.contact_information = contact_information
6168

62-
def __post_init__(self, name, roles, groups):
69+
def __post_init__(self, name, roles, groups, contact_information):
6370
self.name = name
6471
if roles is not None:
6572
self.roles = roles
6673
if groups is not None:
6774
self.groups = groups
75+
if contact_information is not None:
76+
self.contact_information = contact_information
6877

6978
@property
7079
def name(self):
@@ -139,6 +148,29 @@ def groups(self, groups):
139148

140149
self._groups = groups
141150

151+
@property
152+
def contact_information(self):
153+
"""Gets the contact_information of this UpsertUserRequest. # noqa: E501
154+
155+
User's contact information, e.g. {"email": "user@example.com"} # noqa: E501
156+
157+
:return: The contact_information of this UpsertUserRequest. # noqa: E501
158+
:rtype: dict(str, str)
159+
"""
160+
return self._contact_information
161+
162+
@contact_information.setter
163+
def contact_information(self, contact_information):
164+
"""Sets the contact_information of this UpsertUserRequest.
165+
166+
User's contact information, e.g. {"email": "user@example.com"} # noqa: E501
167+
168+
:param contact_information: The contact_information of this UpsertUserRequest. # noqa: E501
169+
:type: dict(str, str)
170+
"""
171+
172+
self._contact_information = contact_information
173+
142174
def to_dict(self):
143175
"""Returns the model properties as a dict"""
144176
result = {}

0 commit comments

Comments
 (0)