Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .github/agent-e2e/known-failures-python.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"_README": "conductor-oss known failures for the agent python e2e suite (bundle: conductor-ai-e2e-python-*, from conductor-oss/python-sdk), consumed by known_failures_plugin.py via E2E_KNOWN_FAILURES. Each key is a pytest node-id (or a '<file>::<Class>::<test>' suffix); the value is the reason + tracking link. The gate stays green by xfail-ing these; a key that matches nothing is a harmless no-op (the test still runs and the gate still catches a real break), and a fixed bug turns its test XPASS — remove the entry then. Do NOT list flaky tests (rerun handles those).",
"_HOWTO": "Reconcile from the run's results/junit-e2e.xml. A value is either a reason string (test still RUNS so it XPASSes when fixed) or an object {\"reason\":..., \"run\":false} to xfail WITHOUT executing (use for a deterministic hang that would burn CI time; un-list it manually when fixed). Example: \"test_suite4_mcp_tools.py::TestSuite4McpTools::test_mcp_lifecycle\": \"reason — conductor-oss/conductor#NNNN\"",
"_CONTEXT_cli_skills": "The 3 Suite16 cli-skills entries below share one root cause: #1288 (commit 011040107, merged 2026-07-15, first in v3.32.0-rc.9) restructured the agentspan module and CHANGED the SkillController gate from @ConditionalOnProperty(\"agentspan.embedded\") [1 prop] to @ConditionalOnProperty({\"agentspan.embedded\",\"agentspan.skills.enabled\"}) [2 props]. That flipped the /api/skills API from on-by-default-with-embedded (rc.8, which python-sdk pins → tests pass) to off-by-default (main → POST /api/skills/register returns 404 → tests fail). Deliberate mainline change; e2e was not updated to opt in, so coverage regressed silently. Decision pending in #1353: (a) opt-in intended → boot with agentspan.skills.enabled=true, or (b) unintended → fix the gate (matchIfMissing/drop the prop). Remove these entries once resolved (they XPASS).",

"test_suite16_cli_skills.py::TestSuite16CliSkills::test_cli_skill_register_list_get_pull_and_delete": "POST /api/skills/register -> 404 on main: #1288 (rc.9) gated SkillController behind agentspan.skills.enabled (off by default) in addition to agentspan.embedded; served on rc.8 (python-sdk's pin) with embedded alone. Tracked in conductor-oss/conductor#1353. See _CONTEXT_cli_skills.",
"test_suite16_cli_skills.py::TestSuite16CliSkills::test_registered_cross_skill_dependency_versions_are_pinned": "POST /api/skills/register -> 404 on main: skills API off-by-default since #1288 (rc.9) added the agentspan.skills.enabled gate. Tracked in conductor-oss/conductor#1353. See _CONTEXT_cli_skills.",
"test_suite16_cli_skills.py::TestSuite16CliSkills::test_cli_skill_run_registered_executes_downloaded_script_worker": "POST /api/skills/register -> 404 on main: skills API off-by-default since #1288 (rc.9) added the agentspan.skills.enabled gate. Tracked in conductor-oss/conductor#1353. See _CONTEXT_cli_skills.",

"test_suite14_stateful_domain.py::TestSuite14StatefulDomain::test_stateful_swarm_handoff_completes": {
"run": false,
"reason": "Stateful swarm handoff hangs (workflow stuck RUNNING ~908s, deterministic): #1356 (3ea601884, 'Enhances A2A/AgentSpan execution') stopped registering the swarm transfer/handoff task defs (now compiler-owned INLINE), but the pinned SDK bundle (2.0.0-rc2, the latest release) still PUTs them -> MetadataServiceImpl.updateTaskDef NotFound ('No such task by name <agent>_transfer_to_<other>') -> agents can't hand off -> workflow never completes. Passed pre-#1356 (2026-07-17). Server/SDK contract break; no newer SDK to bump to. run:false so the ~15min hang doesn't burn CI every run — un-list manually when conductor-oss/conductor#1363 is fixed."
}
}
79 changes: 79 additions & 0 deletions .github/agent-e2e/known_failures_plugin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
"""Known-failure xfail loader for the agent (conductor-ai) python e2e suite.

The e2e suite is shared: the SAME tests (this repo's python SDK e2e, shipped as the
`conductor-ai-e2e-python-*` bundle from conductor-oss/python-sdk) run against multiple
targets. A test can be a known failure on one target and pass on another, so skip lists are
kept per target and selected via the E2E_KNOWN_FAILURES env var. This file is the loader; the
conductor-oss list lives in known-failures-python.json (empty when the suite is green).

It runs as an external pytest plugin (`-p known_failures_plugin`) so it composes with the
downloaded bundle's own conftest WITHOUT modifying the bundle.

Point E2E_KNOWN_FAILURES at a JSON object mapping a test node-id (or a "<file>::<test>" suffix
of one) to a human-readable reason. Matched tests are marked xfail(strict=False, run=True):
they still RUN, a failure reports as XFAIL (green), and a fix XPASSes — the signal to delete
the entry. Keys that match nothing are harmless no-ops (the test runs and the gate still
catches a real break), so a stale entry can never silently hide a regression. Keys beginning
with "_" (e.g. "_README") are treated as comments and ignored.
"""

import json
import os

import pytest


def _load_known_failures():
"""Return {nodeid_suffix: (reason, run)}.

Each JSON value may be either a plain string (the reason; the test still
RUNS so a fix XPASSes) or an object {"reason": ..., "run": false} to xfail
WITHOUT executing the test — use run:false for a deterministic hang that
would otherwise burn CI time every run (you then un-list it manually when
fixed, since a non-run xfail can't XPASS). Keys starting with "_" are
comments and ignored.
"""
path = os.environ.get("E2E_KNOWN_FAILURES")
if not path or not os.path.exists(path):
return {}
with open(path) as f:
data = json.load(f)
out = {}
for k, v in data.items():
if k.startswith("_"):
continue
if isinstance(v, dict):
out[k] = (str(v.get("reason", "")), bool(v.get("run", True)))
else:
out[k] = (str(v), True)
return out


def _matches(nodeid, suffix):
# The suite appends an xdist loadgroup label as "@<group>" to some node-ids
# (e.g. test_mcp_lifecycle@credentials). Match against both the raw node-id and
# the label-stripped base so entries can be written either way.
for nid in (nodeid, nodeid.split("@", 1)[0]):
for suf in (suffix, suffix.split("@", 1)[0]):
if nid == suf or nid.endswith("::" + suf) or nid.endswith(suf):
return True
return False


def pytest_collection_modifyitems(config, items):
known = _load_known_failures()
if not known:
return
matched = 0
for item in items:
for suffix, (reason, run) in known.items():
if _matches(item.nodeid, suffix):
item.add_marker(pytest.mark.xfail(reason=reason, strict=False, run=run))
matched += 1
break
reporter = config.pluginmanager.get_plugin("terminalreporter")
if reporter is not None:
reporter.write_line(
f"[known-failures] xfail-marked {matched} item(s) from "
f"{os.environ.get('E2E_KNOWN_FAILURES')}"
)
132 changes: 132 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ jobs:
outputs:
persistence: ${{ steps.filter.outputs.persistence }}
ui: ${{ steps.filter.outputs.ui }}
agent: ${{ steps.filter.outputs.agent }}
steps:
- uses: actions/checkout@v7
with:
Expand Down Expand Up @@ -50,6 +51,14 @@ jobs:
ui:
- 'ui/**'
- 'ui-next/**'
agent:
- 'ai/**'
- 'conductor-agentspan/**'
- 'server/**'
- 'core/**'
- 'sqlite-persistence/**'
- '.github/agent-e2e/**'
- '.github/workflows/ci.yml'

build:
runs-on: ubuntu-latest
Expand Down Expand Up @@ -458,3 +467,126 @@ jobs:

- name: Build UI
run: NODE_OPTIONS=--max-old-space-size=4096 pnpm build

# Boots the conductor server built from THIS commit in SQLite mode (the default — no external
# DB) and runs the released python SDK agent e2e suite against it, so a server change can't
# silently break the SDK before a release. The suite + bundle come from conductor-oss/python-sdk
# (conductor-ai-e2e-python-<version>); the server auto-configures the openai provider from
# OPENAI_API_KEY (conductor.ai.openai.api-key), so no manual integration setup is needed.
# Known failures (none today) are xfail-ed via .github/agent-e2e/ so the lane can gate while
# any future gap is fixed. Runs on push/dispatch, and on PRs that touch agent-relevant paths.
python-sdk-e2e:
name: Python SDK E2E (SQLite)
needs: detect-changes
if: ${{ github.event_name != 'pull_request' || needs.detect-changes.outputs.agent == 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 40
permissions:
contents: read
checks: write # publish the junit test-report check
env:
# Pinned python-sdk release bundle (conductor-python[agents] resolves to the same version).
# python-sdk release tags carry no `v` prefix. Bump deliberately.
CONDUCTOR_PY_E2E_BUNDLE_VERSION: "2.0.0-rc2"
# Pinned agentspan CLI release — a few suites (cli-skills, credential lifecycle) shell
# out to the `agentspan` binary. Independent of the server pin; bump deliberately.
AGENTSPAN_CLI_VERSION: "0.4.4"
# Named `agentspan-cli`, not `agentspan` — the repo already has an `agentspan/`
# module directory that `gh release download --output agentspan` would collide with.
AGENTSPAN_CLI_PATH: ${{ github.workspace }}/agentspan-cli
AGENTSPAN_SERVER_URL: http://localhost:8080/api
AGENTSPAN_LLM_MODEL: openai/gpt-4o-mini
MCP_TESTKIT_URL: http://localhost:3001
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
E2E_KNOWN_FAILURES: ${{ github.workspace }}/.github/agent-e2e/known-failures-python.json
steps:
- uses: actions/checkout@v7

- name: Set up Zulu JDK 21
uses: actions/setup-java@v4
with:
distribution: zulu
java-version: "21"

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Build server boot jar
run: ./gradlew :conductor-server:bootJar -x test --no-daemon

- name: Start mcp-testkit (best effort)
run: |
python -m pip install --quiet mcp-testkit || { echo "::warning::mcp-testkit install failed — MCP suites will skip"; exit 0; }
nohup mcp-testkit --transport http --port 3001 > mcp-testkit.log 2>&1 &
for i in $(seq 1 15); do curl -sf http://localhost:3001/ >/dev/null 2>&1 && break; sleep 1; done
echo "mcp-testkit started (or unavailable; suites skip gracefully)"

- name: Start conductor server (SQLite)
run: |
JAR="$(ls server/build/libs/conductor-server-*-boot.jar | head -1)"
echo "booting $JAR"
nohup java -jar "$JAR" --server.port=8080 > conductor-server.log 2>&1 &
for i in $(seq 1 60); do
if curl -sf http://localhost:8080/health >/dev/null 2>&1; then
echo "server healthy after ~$((i * 3))s"; break
fi
if [ "$i" -eq 60 ]; then echo "::error::server failed to become healthy within 180s"; tail -100 conductor-server.log; exit 1; fi
sleep 3
done

- name: Fetch python e2e bundle
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
V="${CONDUCTOR_PY_E2E_BUNDLE_VERSION}"
NAME="conductor-ai-e2e-python-${V}"
gh release download "${V}" --repo conductor-oss/python-sdk \
--pattern "${NAME}.tar.gz" --pattern "${NAME}.tar.gz.sha256" --dir .
echo "$(cat "${NAME}.tar.gz.sha256") ${NAME}.tar.gz" | sha256sum -c -
tar -xzf "${NAME}.tar.gz"
echo "BUNDLE_DIR=${NAME}" >> "$GITHUB_ENV"

- name: Download AgentSpan CLI
# A few suites (cli-skills, credential lifecycle) shell out to the `agentspan`
# binary; without it they error with FileNotFoundError instead of skipping.
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release download "v${AGENTSPAN_CLI_VERSION}" --repo agentspan-ai/agentspan \
--pattern "agentspan_linux_amd64" --output "$AGENTSPAN_CLI_PATH" --clobber
chmod +x "$AGENTSPAN_CLI_PATH"

# `run.sh` forwards extra args to pytest, so `-p known_failures_plugin` loads the
# orkes-style xfail plugin (found via PYTHONPATH) without editing the bundle. Gating:
# a genuine failure fails the job; entries in known-failures-python.json (none today)
# are xfail-ed green.
- name: Run python e2e (SQLite; known failures xfail-ed)
env:
PYTHONPATH: ${{ github.workspace }}/.github/agent-e2e
working-directory: ${{ env.BUNDLE_DIR }}
run: bash run.sh -p known_failures_plugin

- name: Publish test report
if: always()
continue-on-error: true # gating is the e2e run step's exit code; a checks-API hiccup must not fail the job
uses: mikepenz/action-junit-report@v4
with:
report_paths: "${{ env.BUNDLE_DIR }}/results/junit-e2e.xml"
check_name: "Python SDK E2E (SQLite)"
fail_on_failure: false
require_tests: false

- name: Upload results + server log
if: always()
uses: actions/upload-artifact@v7
with:
name: python-sdk-e2e-results
path: |
${{ env.BUNDLE_DIR }}/results/**
conductor-server.log
mcp-testkit.log
if-no-files-found: ignore
retention-days: 7
Loading