From c5d487af8aa52d9c5ffce1c30c8c310a768e7eb1 Mon Sep 17 00:00:00 2001 From: Chris Hagglund Date: Thu, 13 Aug 2026 12:37:33 -0600 Subject: [PATCH 01/10] add test against oss on push of pr-to-main --- .github/workflows/ci.yml | 57 ++++++++++ CONTRIBUTING.md | 41 +++++++ .../client/http/SchedulerResource.java | 9 +- scripts/docker-compose-oss.yaml | 28 +++++ scripts/run-integration-oss.sh | 106 ++++++++++++++++++ .../client/ServiceRegistryClientTest.java | 3 + .../conductor/client/WorkflowRetryTest.java | 3 + .../client/http/AuthorizationClientTests.java | 3 + .../client/http/EnvironmentClientTests.java | 3 + .../client/http/EventClientTests.java | 6 +- .../client/http/MetadataClientTests.java | 42 ++++++- .../client/http/PromptClientTests.java | 3 + .../client/http/SchedulerClientTests.java | 5 + .../client/http/SchemaClientTests.java | 3 + .../client/http/SecretClientTests.java | 3 + .../http/ServiceRegistryClientTests.java | 3 + .../client/http/TaskClientTests.java | 34 +++++- .../client/http/TokenClientTest.java | 3 + .../client/http/WorkflowClientTests.java | 5 + .../client/http/WorkflowStateUpdateTests.java | 5 + .../orkes/conductor/client/util/TestUtil.java | 25 +++++ .../orkes/conductor/sdk/WorkflowSDKTests.java | 14 ++- 22 files changed, 388 insertions(+), 16 deletions(-) create mode 100644 scripts/docker-compose-oss.yaml create mode 100755 scripts/run-integration-oss.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ecca05d7a..ef7d836a9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,6 +8,11 @@ on: branches: - main workflow_dispatch: + inputs: + oss_conductor_version: + description: 'OSS Conductor image tag (falls back to E2E_TEST_OSS_CONDUCTOR_VERSION org var)' + required: false + type: string concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -153,3 +158,55 @@ jobs: - name: Check Tests Status if: steps.tests.outcome == 'failure' run: exit 1 + + integration-tests-oss: + runs-on: ubuntu-latest + name: Integration Tests (OSS) + timeout-minutes: 30 + env: + CONDUCTOR_SERVER_URL: http://localhost:8080/api + CONDUCTOR_SERVER_TYPE: oss + OSS_CONDUCTOR_VERSION: ${{ inputs.oss_conductor_version || vars.E2E_TEST_OSS_CONDUCTOR_VERSION }} + + steps: + - name: Verify OSS Conductor version is set + run: | + if [ -z "$OSS_CONDUCTOR_VERSION" ]; then + echo "::error::No Conductor OSS image tag resolved. Set the E2E_TEST_OSS_CONDUCTOR_VERSION organization variable (and ensure its repository access policy includes this repo), or pass the oss_conductor_version input via workflow_dispatch." + exit 1 + fi + echo "Using conductoross/conductor:$OSS_CONDUCTOR_VERSION" + + - name: Checkout + uses: actions/checkout@v6 + + - name: Set up Zulu JDK 21 + uses: actions/setup-java@v5 + with: + distribution: "zulu" + java-version: "21" + + - name: Start Conductor OSS stack + run: docker compose -f scripts/docker-compose-oss.yaml up -d + + - name: Wait for Conductor to be healthy + run: timeout 120 bash -c 'until curl -sf http://localhost:8080/health; do sleep 5; done' + + - name: Run integration tests (OSS) + id: integration_tests + continue-on-error: true + run: ./gradlew :tests:test -PIntegrationTests + + - name: Dump Conductor logs + if: failure() || steps.integration_tests.outcome == 'failure' + run: docker compose -f scripts/docker-compose-oss.yaml logs conductor-server + + - name: Publish Test Report + if: always() + uses: mikepenz/action-junit-report@v6 + with: + report_paths: '**/tests/build/test-results/test/TEST-*.xml' + + - name: Check Integration Tests Status + if: steps.integration_tests.outcome == 'failure' + run: exit 1 \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7bfa1458c..bb0172b6d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -23,6 +23,47 @@ Run the SDK test suite: ./gradlew test jacocoTestReport ``` +### Running the OSS integration suite locally + +The `tests` module also has an integration suite (`-PIntegrationTests`) that runs against a +real Conductor server, separate from the unit suite above. `scripts/run-integration-oss.sh` +mirrors the `integration-tests-oss` job in `ci.yml`: it starts a local Conductor OSS + +Postgres stack (defined in `scripts/docker-compose-oss.yaml`), waits for `/health`, runs the +integration suite, and tears the stack down on exit. + +```shell +scripts/run-integration-oss.sh # against `latest` +scripts/run-integration-oss.sh --version 3.32.0-rc18 +scripts/run-integration-oss.sh --keep-up # leave the stack running afterwards +scripts/run-integration-oss.sh --include-gated # also run tests normally skipped as Orkes-only +``` + +The script always prints the resolved `conductoross/conductor` tag and pulls it before +starting the stack, since `latest` (the local default) is a mutable tag — without an +explicit pull, `docker compose up` would silently reuse a stale cached image instead of +fetching the current one. It also always runs Gradle with `--rerun-tasks`, since the `test` +task's up-to-date check doesn't account for env vars like `CONDUCTOR_SERVER_TYPE` or the +state of the live server underneath — without it, a rerun after changing gating or switching +server versions could silently report a stale cached result instead of executing anything. + +The script doesn't pin a JDK itself, but CI runs on Zulu 21. If your local default JDK is +newer (e.g. 23) you may hit `Unsupported class file major version` errors compiling tests — +set `JAVA_HOME` explicitly to match CI: + +```shell +JAVA_HOME=/Library/Java/JavaVirtualMachines/zulu-21.jdk/Contents/Home ./scripts/run-integration-oss.sh +``` + +Tests annotated `@DisabledIfEnvironmentVariable(named = "CONDUCTOR_SERVER_TYPE", matches = +"oss")` skip themselves against plain OSS because they exercise Orkes-managed-only features +(e.g. the Service Registry, Authorization, Prompts/Integrations, Environment Variables, and +Secrets APIs, plus a handful of task/workflow endpoints OSS doesn't implement or that hit +known Postgres-persistence bugs). Each annotation's `disabledReason` documents the specific, +empirically-confirmed gap — treat those as the source of truth rather than a list here, since +they can drift as OSS gains features. If you add or remove that annotation, re-verify against +a freshly-pulled image first: a test that fails against a stale local image may pass against +current OSS, and vice versa. + Compile the maintained agent examples when changing their APIs or documentation: ```shell diff --git a/conductor-client/src/main/java/io/orkes/conductor/client/http/SchedulerResource.java b/conductor-client/src/main/java/io/orkes/conductor/client/http/SchedulerResource.java index fe9d7db38..71e0d030b 100644 --- a/conductor-client/src/main/java/io/orkes/conductor/client/http/SchedulerResource.java +++ b/conductor-client/src/main/java/io/orkes/conductor/client/http/SchedulerResource.java @@ -167,14 +167,19 @@ public void resumeSchedule(String name) { /** * Enterprise scheduler endpoints accept GET while OSS accepts PUT. Retry only * a method-not-allowed response so application and authentication failures - * retain their original behavior. + * retain their original behavior. Orkes Enterprise reports this as a proper + * 405; plain OSS Conductor instead reports it as a 500 with a "Request + * method '...' is not supported" message (confirmed empirically) -- treat + * both as a signal to retry with PUT. */ private void executeGetThenPutOnMethodNotAllowed( ConductorClientRequest getRequest, ConductorClientRequest putRequest) { try { client.execute(getRequest); } catch (ConductorClientException e) { - if (e.getStatus() != 405) { + if (e.getStatus() != 405 + && !(e.getStatus() == 500 && e.getMessage() != null + && e.getMessage().contains("is not supported"))) { throw e; } client.execute(putRequest); diff --git a/scripts/docker-compose-oss.yaml b/scripts/docker-compose-oss.yaml new file mode 100644 index 000000000..efc517329 --- /dev/null +++ b/scripts/docker-compose-oss.yaml @@ -0,0 +1,28 @@ +services: + conductor-server: + image: conductoross/conductor:${OSS_CONDUCTOR_VERSION:-latest} + environment: + - CONFIG_PROP=config-postgres.properties + ports: + - "8080:8080" + healthcheck: + test: ["CMD", "curl", "-I", "-XGET", "http://localhost:8080/health"] + interval: 10s + timeout: 10s + retries: 20 + links: + - conductor-postgres:postgresdb + depends_on: + conductor-postgres: + condition: service_healthy + + conductor-postgres: + image: postgres:16 + environment: + - POSTGRES_USER=conductor + - POSTGRES_PASSWORD=conductor + healthcheck: + test: timeout 5 bash -c 'cat < /dev/null > /dev/tcp/localhost/5432' + interval: 5s + timeout: 5s + retries: 12 diff --git a/scripts/run-integration-oss.sh b/scripts/run-integration-oss.sh new file mode 100755 index 000000000..1a17b6d3f --- /dev/null +++ b/scripts/run-integration-oss.sh @@ -0,0 +1,106 @@ +#!/usr/bin/env bash +# +# Spin up a local Conductor OSS stack and run the `tests` module's +# integration suite against it, mirroring the `integration-tests-oss` job in +# .github/workflows/integration-tests-oss.yml. Orkes-Enterprise-only test +# classes are annotated with +# @DisabledIfEnvironmentVariable(named = "CONDUCTOR_SERVER_TYPE", matches = "oss") +# so they skip themselves when it's set (see the individual test files for +# the empirically-confirmed gaps). +# +# The stack (Conductor OSS + Postgres) is defined in +# scripts/docker-compose-oss.yaml and is torn down automatically on exit. The +# image is always pulled before starting, since `latest` (the local default) +# is a mutable tag and a cached copy would otherwise go stale silently. +# +# Usage: +# scripts/run-integration-oss.sh [--keep-up] [--version ] [--include-gated] [-- gradle args] +# Examples: +# scripts/run-integration-oss.sh +# scripts/run-integration-oss.sh --version 3.32.0-rc18 +# scripts/run-integration-oss.sh --keep-up +# scripts/run-integration-oss.sh --include-gated # also run tests normally skipped as Orkes-only +# scripts/run-integration-oss.sh -- --tests "*WorkflowClientTests" +set -euo pipefail + +KEEP_UP=0 +INCLUDE_GATED=0 +extra=() + +while [[ $# -gt 0 ]]; do + case "$1" in + --keep-up) KEEP_UP=1; shift ;; + --version) OSS_CONDUCTOR_VERSION="${2:?--version needs a tag}"; shift 2 ;; + --include-gated) INCLUDE_GATED=1; shift ;; + -h|--help) + echo "Usage: $0 [--keep-up] [--version ] [--include-gated] [-- gradle args]" + exit 0 + ;; + --) shift; extra=("$@"); break ;; + *) echo "Unknown argument: $1" >&2; exit 1 ;; + esac +done + +export OSS_CONDUCTOR_VERSION="${OSS_CONDUCTOR_VERSION:-latest}" + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" +COMPOSE_FILE="${SCRIPT_DIR}/docker-compose-oss.yaml" +cd "${REPO_ROOT}" + +compose() { docker compose -f "${COMPOSE_FILE}" "$@"; } + +cleanup() { + if [[ "${KEEP_UP}" == "1" ]]; then + echo "--keep-up set: leaving the OSS stack running. Tear down with:" + echo " docker compose -f ${COMPOSE_FILE} down -v" + return + fi + echo "Tearing down Conductor OSS stack..." + compose down -v || true +} +trap cleanup EXIT + +echo "Using conductoross/conductor:${OSS_CONDUCTOR_VERSION}" + +# `docker compose up` only pulls an image when it is missing locally, so a +# previously-cached `latest` (or any other mutable tag) would silently be +# reused instead of getting the current version. Pull unconditionally so the +# stack always reflects the tag we just printed. +echo "Pulling conductoross/conductor:${OSS_CONDUCTOR_VERSION} to ensure it's current..." +compose pull conductor-server + +echo "Starting Conductor OSS stack..." +compose up -d + +echo "Waiting for Conductor to be healthy..." +HEALTH_TIMEOUT="${HEALTH_TIMEOUT:-180}" +deadline=$(( SECONDS + HEALTH_TIMEOUT )) +until curl -sf http://localhost:8080/health >/dev/null 2>&1; do + if (( SECONDS >= deadline )); then + echo "Error: Conductor did not become healthy within ${HEALTH_TIMEOUT}s." >&2 + compose logs conductor-server || true + exit 1 + fi + sleep 5 +done +echo "Conductor is up." + +export CONDUCTOR_SERVER_URL="http://localhost:8080/api" + +if [[ "${INCLUDE_GATED}" == "1" ]]; then + echo "--include-gated set: leaving CONDUCTOR_SERVER_TYPE unset, so tests normally" \ + "skipped as Orkes-only will run against OSS too." + unset CONDUCTOR_SERVER_TYPE || true +else + export CONDUCTOR_SERVER_TYPE="oss" +fi + + +# --rerun-tasks: the `test` task's up-to-date check only considers the compiled +# test classpath, not env vars like CONDUCTOR_SERVER_URL/CONDUCTOR_SERVER_TYPE +# or the state of the live server underneath. Without this, Gradle can report +# BUILD SUCCESSFUL while silently reusing a stale cached result from a +# previous run against a different server/tag/gating state instead of +# actually executing anything. +./gradlew :tests:test -PIntegrationTests --rerun-tasks ${extra[@]+"${extra[@]}"} diff --git a/tests/src/test/java/io/orkes/conductor/client/ServiceRegistryClientTest.java b/tests/src/test/java/io/orkes/conductor/client/ServiceRegistryClientTest.java index 76922097e..64fc95d0f 100644 --- a/tests/src/test/java/io/orkes/conductor/client/ServiceRegistryClientTest.java +++ b/tests/src/test/java/io/orkes/conductor/client/ServiceRegistryClientTest.java @@ -19,6 +19,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable; import com.netflix.conductor.common.model.OrkesCircuitBreakerConfig; import com.netflix.conductor.common.model.ServiceMethod; @@ -30,6 +31,8 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; +@DisabledIfEnvironmentVariable(named = "CONDUCTOR_SERVER_TYPE", matches = "oss", + disabledReason = "the Service Registry API (/registry/service) is not implemented by plain OSS Conductor, confirmed empirically (404 'No static resource api/registry/service')") public class ServiceRegistryClientTest { private static final String PROTO_FILENAME = "compiled.bin"; diff --git a/tests/src/test/java/io/orkes/conductor/client/WorkflowRetryTest.java b/tests/src/test/java/io/orkes/conductor/client/WorkflowRetryTest.java index e3793c077..739367075 100644 --- a/tests/src/test/java/io/orkes/conductor/client/WorkflowRetryTest.java +++ b/tests/src/test/java/io/orkes/conductor/client/WorkflowRetryTest.java @@ -18,6 +18,7 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable; import com.netflix.conductor.common.metadata.tasks.TaskDef; import com.netflix.conductor.common.metadata.tasks.TaskResult; @@ -36,6 +37,8 @@ import lombok.extern.slf4j.Slf4j; @Slf4j +@DisabledIfEnvironmentVariable(named = "CONDUCTOR_SERVER_TYPE", matches = "oss", + disabledReason = "workflowClient.uploadCompletedWorkflows() (/workflow/document-store/upload) is not implemented by plain OSS Conductor, confirmed empirically (404 'No static resource api/workflow/document-store/upload')") public class WorkflowRetryTest { private final OrkesMetadataClient metadataClient; private final OrkesWorkflowClient workflowClient; diff --git a/tests/src/test/java/io/orkes/conductor/client/http/AuthorizationClientTests.java b/tests/src/test/java/io/orkes/conductor/client/http/AuthorizationClientTests.java index d40e829bc..fa896a45d 100644 --- a/tests/src/test/java/io/orkes/conductor/client/http/AuthorizationClientTests.java +++ b/tests/src/test/java/io/orkes/conductor/client/http/AuthorizationClientTests.java @@ -25,6 +25,7 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable; import com.netflix.conductor.client.exception.ConductorClientException; import com.netflix.conductor.common.metadata.workflow.WorkflowDef; @@ -49,6 +50,8 @@ import io.orkes.conductor.client.util.ClientTestUtil; import io.orkes.conductor.client.util.Commons; +@DisabledIfEnvironmentVariable(named = "CONDUCTOR_SERVER_TYPE", matches = "oss", + disabledReason = "the Authorization APIs (applications/users/groups/roles/permissions) are not implemented by plain OSS Conductor, confirmed empirically (404 'No static resource api/applications|users|groups|...')") public class AuthorizationClientTests { private static AuthorizationClient authorizationClient; private static String applicationId; diff --git a/tests/src/test/java/io/orkes/conductor/client/http/EnvironmentClientTests.java b/tests/src/test/java/io/orkes/conductor/client/http/EnvironmentClientTests.java index 6dd0e3359..d1de9535b 100644 --- a/tests/src/test/java/io/orkes/conductor/client/http/EnvironmentClientTests.java +++ b/tests/src/test/java/io/orkes/conductor/client/http/EnvironmentClientTests.java @@ -19,11 +19,14 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable; import java.util.List; import java.util.Optional; import java.util.UUID; +@DisabledIfEnvironmentVariable(named = "CONDUCTOR_SERVER_TYPE", matches = "oss", + disabledReason = "environment variable writes are not supported by plain OSS Conductor, confirmed empirically: OSS added read-only GET /environment in 3.32.0-rc.9 but PUT /environment/{key} still 405s ('Request method 'PUT' is not supported')") public class EnvironmentClientTests { private static EnvironmentClient envClient; diff --git a/tests/src/test/java/io/orkes/conductor/client/http/EventClientTests.java b/tests/src/test/java/io/orkes/conductor/client/http/EventClientTests.java index c1e21d90b..47c2d28af 100644 --- a/tests/src/test/java/io/orkes/conductor/client/http/EventClientTests.java +++ b/tests/src/test/java/io/orkes/conductor/client/http/EventClientTests.java @@ -35,7 +35,11 @@ void testEventHandler() { try { eventClient.unregisterEventHandler(EVENT_NAME); } catch (ConductorClientException e) { - if (e.getStatus() != 404) { + // Best-effort cleanup: tolerate "doesn't exist" regardless of how the + // server reports it. Orkes Enterprise returns 404; plain OSS Conductor + // returns a 500 with a "not found" message instead (confirmed + // empirically) -- treat both as success for this purpose. + if (e.getStatus() != 404 && !e.getMessage().contains("not found")) { throw e; } } diff --git a/tests/src/test/java/io/orkes/conductor/client/http/MetadataClientTests.java b/tests/src/test/java/io/orkes/conductor/client/http/MetadataClientTests.java index 1e91169d9..837e276ef 100644 --- a/tests/src/test/java/io/orkes/conductor/client/http/MetadataClientTests.java +++ b/tests/src/test/java/io/orkes/conductor/client/http/MetadataClientTests.java @@ -16,6 +16,7 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable; import com.netflix.conductor.client.exception.ConductorClientException; import com.netflix.conductor.common.metadata.tasks.TaskDef; @@ -38,7 +39,11 @@ void taskDefinition() { try { metadataClient.unregisterTaskDef(Commons.TASK_NAME); } catch (ConductorClientException e) { - if (e.getStatus() != 404) { + // Best-effort cleanup: tolerate "doesn't exist" regardless of how the + // server reports it. Orkes Enterprise returns 404; plain OSS Conductor + // returns a 500 with a "No such task definition" message instead + // (confirmed empirically) -- treat both as success for this purpose. + if (e.getStatus() != 404 && !e.getMessage().contains("No such task definition")) { throw e; } } @@ -54,16 +59,41 @@ void workflow() { try { metadataClient.unregisterWorkflowDef(Commons.WORKFLOW_NAME, Commons.WORKFLOW_VERSION); } catch (ConductorClientException e) { - if (e.getStatus() != 404) { + // Best-effort cleanup: tolerate "doesn't exist" regardless of how the + // server reports it. Orkes Enterprise returns 404; plain OSS Conductor + // returns a 500 with a "No such workflow definition" message instead + // (confirmed empirically) -- treat both as success for this purpose. + if (e.getStatus() != 404 && !e.getMessage().contains("No such workflow definition")) { throw e; } } metadataClient.registerTaskDefs(List.of(Commons.getTaskDef())); WorkflowDef workflowDef = WorkflowUtil.getWorkflowDef(); - metadataClient.registerWorkflowDef(workflowDef); + try { + metadataClient.registerWorkflowDef(workflowDef); + } catch (ConductorClientException e) { + // Commons.WORKFLOW_NAME/VERSION is shared fixture data used by several + // test classes in this suite; tolerate an "already exists" collision + // here since the update/overwrite calls below re-establish the + // intended definition regardless of which class registered it first. + if (e.getStatus() != 500 || !e.getMessage().contains("already exists")) { + throw e; + } + } metadataClient.updateWorkflowDefs(List.of(workflowDef)); metadataClient.updateWorkflowDefs(List.of(workflowDef), true); - metadataClient.registerWorkflowDef(workflowDef, true); + try { + metadataClient.registerWorkflowDef(workflowDef, true); + } catch (ConductorClientException e) { + // The overwrite=true query param on POST /metadata/workflow is not + // honored by plain OSS Conductor, confirmed empirically (it still + // rejects an existing name+version instead of overwriting); the + // updateWorkflowDefs(..., true) call above already re-established + // the intended definition. + if (e.getStatus() != 500 || !e.getMessage().contains("already exists")) { + throw e; + } + } ((OrkesMetadataClient) metadataClient) .getWorkflowDefWithMetadata(Commons.WORKFLOW_NAME, Commons.WORKFLOW_VERSION); WorkflowDef receivedWorkflowDef = metadataClient.getWorkflowDef(Commons.WORKFLOW_NAME, @@ -73,6 +103,8 @@ void workflow() { } @Test + @DisabledIfEnvironmentVariable(named = "CONDUCTOR_SERVER_TYPE", matches = "oss", + disabledReason = "task tagging (/metadata/task/{name}/tags) is not implemented by plain OSS Conductor, confirmed empirically (404 'No static resource api/metadata/task/{name}/tags')") void tagTask() throws Exception { metadataClient.registerTaskDefs(List.of(Commons.getTaskDef())); try { @@ -98,6 +130,8 @@ void tagTask() throws Exception { } @Test + @DisabledIfEnvironmentVariable(named = "CONDUCTOR_SERVER_TYPE", matches = "oss", + disabledReason = "workflow tagging (/metadata/workflow/{name}/tags) is not implemented by plain OSS Conductor, confirmed empirically (the {version} path segment ends up matching the literal string \"tags\" instead, a server-side routing collision)") void tagWorkflow() { TagObject tagObject = Commons.getTagObject(); try { diff --git a/tests/src/test/java/io/orkes/conductor/client/http/PromptClientTests.java b/tests/src/test/java/io/orkes/conductor/client/http/PromptClientTests.java index 53d427232..2ce260590 100644 --- a/tests/src/test/java/io/orkes/conductor/client/http/PromptClientTests.java +++ b/tests/src/test/java/io/orkes/conductor/client/http/PromptClientTests.java @@ -20,6 +20,7 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable; import com.netflix.conductor.client.exception.ConductorClientException; @@ -32,6 +33,8 @@ import org.conductoross.conductor.client.model.ai.PromptTemplate; import io.orkes.conductor.client.util.ClientTestUtil; +@DisabledIfEnvironmentVariable(named = "CONDUCTOR_SERVER_TYPE", matches = "oss", + disabledReason = "the Prompts and Integrations APIs (/prompts, /integrations) are not implemented by plain OSS Conductor, confirmed empirically (404 'No static resource api/prompts|integrations/...')") public class PromptClientTests { private static final String PROMPT_NAME = "test-sdk-java-prompt"; private static final String PROMPT_DESCRIPTION = "Test prompt for Java SDK"; diff --git a/tests/src/test/java/io/orkes/conductor/client/http/SchedulerClientTests.java b/tests/src/test/java/io/orkes/conductor/client/http/SchedulerClientTests.java index f86c332d2..9bbc030a7 100644 --- a/tests/src/test/java/io/orkes/conductor/client/http/SchedulerClientTests.java +++ b/tests/src/test/java/io/orkes/conductor/client/http/SchedulerClientTests.java @@ -16,6 +16,7 @@ import java.util.UUID; import org.junit.jupiter.api.*; +import org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable; import com.netflix.conductor.common.model.BulkResponse; @@ -51,6 +52,10 @@ void afterEach() { } @Test + @DisabledIfEnvironmentVariable(named = "CONDUCTOR_SERVER_TYPE", matches = "oss", + disabledReason = "GET /scheduler/search does not 404 on plain OSS Conductor, but confirmed empirically to " + + "always return zero results (even after polling for 30s) -- schedules aren't surfaced via search " + + "on plain OSS the way they are on Orkes Enterprise") void testMethods() { schedulerClient.deleteSchedule(SCHEDULE_1); Assertions.assertTrue(schedulerClient.getNextFewSchedules(CRON_EXPRESSION_1, 0L, 0L, 0).isEmpty()); diff --git a/tests/src/test/java/io/orkes/conductor/client/http/SchemaClientTests.java b/tests/src/test/java/io/orkes/conductor/client/http/SchemaClientTests.java index b74b318fc..8dd145160 100644 --- a/tests/src/test/java/io/orkes/conductor/client/http/SchemaClientTests.java +++ b/tests/src/test/java/io/orkes/conductor/client/http/SchemaClientTests.java @@ -17,6 +17,7 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable; import com.netflix.conductor.client.exception.ConductorClientException; import com.netflix.conductor.common.metadata.SchemaDef; @@ -24,6 +25,8 @@ import io.orkes.conductor.client.SchemaClient; import io.orkes.conductor.client.util.ClientTestUtil; +@DisabledIfEnvironmentVariable(named = "CONDUCTOR_SERVER_TYPE", matches = "oss", + disabledReason = "the Schema API (/schema) is not implemented by plain OSS Conductor, confirmed empirically (404 'No static resource api/schema')") public class SchemaClientTests { private static final String SCHEMA_NAME = "test-sdk-java-schema"; diff --git a/tests/src/test/java/io/orkes/conductor/client/http/SecretClientTests.java b/tests/src/test/java/io/orkes/conductor/client/http/SecretClientTests.java index 1a8daecf8..33ce31ee7 100644 --- a/tests/src/test/java/io/orkes/conductor/client/http/SecretClientTests.java +++ b/tests/src/test/java/io/orkes/conductor/client/http/SecretClientTests.java @@ -16,6 +16,7 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable; import com.netflix.conductor.client.exception.ConductorClientException; @@ -24,6 +25,8 @@ import io.orkes.conductor.client.util.ClientTestUtil; +@DisabledIfEnvironmentVariable(named = "CONDUCTOR_SERVER_TYPE", matches = "oss", + disabledReason = "secret writes are not supported by plain OSS Conductor, confirmed empirically: its env-var-backed secrets DAO is read-only, so putSecret() 501s ('env-backed secrets are read-only')") public class SecretClientTests { private final String SECRET_NAME = "test-sdk-java-secret_name"; private final String SECRET_KEY = "test-sdk-java-secret_key"; diff --git a/tests/src/test/java/io/orkes/conductor/client/http/ServiceRegistryClientTests.java b/tests/src/test/java/io/orkes/conductor/client/http/ServiceRegistryClientTests.java index d5253fd8a..a09ba6c85 100644 --- a/tests/src/test/java/io/orkes/conductor/client/http/ServiceRegistryClientTests.java +++ b/tests/src/test/java/io/orkes/conductor/client/http/ServiceRegistryClientTests.java @@ -18,6 +18,7 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable; import com.netflix.conductor.client.exception.ConductorClientException; import com.netflix.conductor.common.model.CircuitBreakerTransitionResponse; @@ -32,6 +33,8 @@ import static org.junit.jupiter.api.Assertions.assertNull; +@DisabledIfEnvironmentVariable(named = "CONDUCTOR_SERVER_TYPE", matches = "oss", + disabledReason = "the Service Registry API (/registry/service) is not implemented by plain OSS Conductor, confirmed empirically (404 'No static resource api/registry/service')") public class ServiceRegistryClientTests { private static final String SERVICE_NAME = "test-sdk-java-service"; private static final String SERVICE_URI = "localhost:50051"; diff --git a/tests/src/test/java/io/orkes/conductor/client/http/TaskClientTests.java b/tests/src/test/java/io/orkes/conductor/client/http/TaskClientTests.java index 4efb61f59..ef13c06d6 100644 --- a/tests/src/test/java/io/orkes/conductor/client/http/TaskClientTests.java +++ b/tests/src/test/java/io/orkes/conductor/client/http/TaskClientTests.java @@ -30,6 +30,7 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable; import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; import org.testcontainers.shaded.com.google.common.util.concurrent.Uninterruptibles; @@ -146,6 +147,8 @@ public void testUpdateByRefName() { } @Test + @DisabledIfEnvironmentVariable(named = "CONDUCTOR_SERVER_TYPE", matches = "oss", + disabledReason = "the sync task-update endpoint (POST /tasks/{workflowId}/{taskRefName}/{status}/sync) never returns the updated workflow on plain OSS Conductor, confirmed empirically (caller times out waiting for terminal status)") public void testUpdateByRefNameSync() { StartWorkflowRequest request = new StartWorkflowRequest(); request.setName(workflowName); @@ -331,6 +334,8 @@ private void completeWorkflow(String workflowId) throws Exception { } @Test + @DisabledIfEnvironmentVariable(named = "CONDUCTOR_SERVER_TYPE", matches = "oss", + disabledReason = "the SYNCHRONOUS/REGION_DURABLE consistency + BLOCKING_* return-strategy signal API (POST /tasks/{workflowId}/{status}/signal/sync) is not implemented by plain OSS Conductor, confirmed empirically") void testSyncTargetWorkflow() throws Exception { String workflowId = startComplexWorkflow(Consistency.SYNCHRONOUS, ReturnStrategy.TARGET_WORKFLOW); @@ -345,6 +350,8 @@ void testSyncTargetWorkflow() throws Exception { } @Test + @DisabledIfEnvironmentVariable(named = "CONDUCTOR_SERVER_TYPE", matches = "oss", + disabledReason = "the SYNCHRONOUS/REGION_DURABLE consistency + BLOCKING_* return-strategy signal API (POST /tasks/{workflowId}/{status}/signal/sync) is not implemented by plain OSS Conductor, confirmed empirically") void testSyncBlockingWorkflow() throws Exception { String workflowId = startComplexWorkflow(Consistency.SYNCHRONOUS, ReturnStrategy.BLOCKING_WORKFLOW); @@ -359,6 +366,8 @@ void testSyncBlockingWorkflow() throws Exception { } @Test + @DisabledIfEnvironmentVariable(named = "CONDUCTOR_SERVER_TYPE", matches = "oss", + disabledReason = "the SYNCHRONOUS/REGION_DURABLE consistency + BLOCKING_* return-strategy signal API (POST /tasks/{workflowId}/{status}/signal/sync) is not implemented by plain OSS Conductor, confirmed empirically") void testSyncBlockingTask() throws Exception { String workflowId = startComplexWorkflow(Consistency.SYNCHRONOUS, ReturnStrategy.BLOCKING_TASK); @@ -373,6 +382,8 @@ void testSyncBlockingTask() throws Exception { } @Test + @DisabledIfEnvironmentVariable(named = "CONDUCTOR_SERVER_TYPE", matches = "oss", + disabledReason = "the SYNCHRONOUS/REGION_DURABLE consistency + BLOCKING_* return-strategy signal API (POST /tasks/{workflowId}/{status}/signal/sync) is not implemented by plain OSS Conductor, confirmed empirically") void testSyncBlockingTaskInput() throws Exception { String workflowId = startComplexWorkflow(Consistency.SYNCHRONOUS, ReturnStrategy.BLOCKING_TASK_INPUT); @@ -390,7 +401,10 @@ void testSyncBlockingTaskInput() throws Exception { private static final String REGION_DURABLE_ENABLED = "CONDUCTOR_REGION_DURABLE_ENABLED"; @Test - @EnabledIfEnvironmentVariable(named = REGION_DURABLE_ENABLED, matches = "true") + @EnabledIfEnvironmentVariable(named = REGION_DURABLE_ENABLED, matches = "true", + disabledReason = "target server has no region replication configured") + @DisabledIfEnvironmentVariable(named = "CONDUCTOR_SERVER_TYPE", matches = "oss", + disabledReason = "the SYNCHRONOUS/REGION_DURABLE consistency + BLOCKING_* return-strategy signal API (POST /tasks/{workflowId}/{status}/signal/sync) is not implemented by plain OSS Conductor, confirmed empirically") void testDurableTargetWorkflow() throws Exception { String workflowId = startComplexWorkflow(Consistency.REGION_DURABLE, ReturnStrategy.TARGET_WORKFLOW); @@ -405,7 +419,10 @@ void testDurableTargetWorkflow() throws Exception { } @Test - @EnabledIfEnvironmentVariable(named = REGION_DURABLE_ENABLED, matches = "true") + @EnabledIfEnvironmentVariable(named = REGION_DURABLE_ENABLED, matches = "true", + disabledReason = "target server has no region replication configured") + @DisabledIfEnvironmentVariable(named = "CONDUCTOR_SERVER_TYPE", matches = "oss", + disabledReason = "the SYNCHRONOUS/REGION_DURABLE consistency + BLOCKING_* return-strategy signal API (POST /tasks/{workflowId}/{status}/signal/sync) is not implemented by plain OSS Conductor, confirmed empirically") void testDurableBlockingWorkflow() throws Exception { String workflowId = startComplexWorkflow(Consistency.REGION_DURABLE, ReturnStrategy.BLOCKING_WORKFLOW); @@ -420,6 +437,8 @@ void testDurableBlockingWorkflow() throws Exception { } @Test + @DisabledIfEnvironmentVariable(named = "CONDUCTOR_SERVER_TYPE", matches = "oss", + disabledReason = "the SYNCHRONOUS/REGION_DURABLE consistency + BLOCKING_* return-strategy signal API (POST /tasks/{workflowId}/{status}/signal/sync) is not implemented by plain OSS Conductor, confirmed empirically") void testDurableBlockingTask() throws Exception { String workflowId = startComplexWorkflow(Consistency.DURABLE, ReturnStrategy.BLOCKING_TASK); @@ -434,7 +453,10 @@ void testDurableBlockingTask() throws Exception { } @Test - @EnabledIfEnvironmentVariable(named = REGION_DURABLE_ENABLED, matches = "true") + @EnabledIfEnvironmentVariable(named = REGION_DURABLE_ENABLED, matches = "true", + disabledReason = "target server has no region replication configured") + @DisabledIfEnvironmentVariable(named = "CONDUCTOR_SERVER_TYPE", matches = "oss", + disabledReason = "the SYNCHRONOUS/REGION_DURABLE consistency + BLOCKING_* return-strategy signal API (POST /tasks/{workflowId}/{status}/signal/sync) is not implemented by plain OSS Conductor, confirmed empirically") void testDurableBlockingTaskInput() throws Exception { String workflowId = startComplexWorkflow(Consistency.REGION_DURABLE, ReturnStrategy.BLOCKING_TASK_INPUT); @@ -449,6 +471,8 @@ void testDurableBlockingTaskInput() throws Exception { } @Test + @DisabledIfEnvironmentVariable(named = "CONDUCTOR_SERVER_TYPE", matches = "oss", + disabledReason = "the SYNCHRONOUS/REGION_DURABLE consistency + BLOCKING_* return-strategy signal API (POST /tasks/{workflowId}/{status}/signal/sync) is not implemented by plain OSS Conductor, confirmed empirically") void testDefaultReturnStrategy() throws Exception { String workflowId = startComplexWorkflow(Consistency.SYNCHRONOUS, ReturnStrategy.TARGET_WORKFLOW); @@ -727,6 +751,8 @@ void testRequeuePendingTasksByTaskType() { // ==================== Search Tests ==================== @Test + @DisabledIfEnvironmentVariable(named = "CONDUCTOR_SERVER_TYPE", matches = "oss", + disabledReason = "GET /tasks/search fails on plain OSS Conductor with a Postgres persistence layer, confirmed empirically (ERROR: column \"workflow_id\" does not exist)") void testSearchTasks() { StartWorkflowRequest request = new StartWorkflowRequest(); request.setName(workflowName); @@ -763,6 +789,8 @@ void testSearchV2Tasks() { } @Test + @DisabledIfEnvironmentVariable(named = "CONDUCTOR_SERVER_TYPE", matches = "oss", + disabledReason = "GET /tasks/search fails on plain OSS Conductor with a Postgres persistence layer, confirmed empirically (ERROR: column \"workflow_id\" does not exist)") void testPaginatedSearchTasks() { StartWorkflowRequest request = new StartWorkflowRequest(); request.setName(workflowName); diff --git a/tests/src/test/java/io/orkes/conductor/client/http/TokenClientTest.java b/tests/src/test/java/io/orkes/conductor/client/http/TokenClientTest.java index 06b2f25dc..7f6ed98db 100644 --- a/tests/src/test/java/io/orkes/conductor/client/http/TokenClientTest.java +++ b/tests/src/test/java/io/orkes/conductor/client/http/TokenClientTest.java @@ -16,11 +16,14 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable; import io.orkes.conductor.client.model.GenerateTokenRequest; import io.orkes.conductor.client.model.TokenResponse; import io.orkes.conductor.client.util.ClientTestUtil; +@DisabledIfEnvironmentVariable(named = "CONDUCTOR_SERVER_TYPE", matches = "oss", + disabledReason = "the auth Token API (/token) is not implemented by plain OSS Conductor (which has no authentication layer), confirmed empirically (404 'No static resource api/token')") public class TokenClientTest { public static OrkesTokenClient tokenClient; diff --git a/tests/src/test/java/io/orkes/conductor/client/http/WorkflowClientTests.java b/tests/src/test/java/io/orkes/conductor/client/http/WorkflowClientTests.java index a8d906940..c4c5e8e13 100644 --- a/tests/src/test/java/io/orkes/conductor/client/http/WorkflowClientTests.java +++ b/tests/src/test/java/io/orkes/conductor/client/http/WorkflowClientTests.java @@ -22,6 +22,7 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable; import com.netflix.conductor.common.metadata.tasks.TaskDef; import com.netflix.conductor.common.metadata.tasks.TaskResult; @@ -102,6 +103,8 @@ public void startWorkflow() { } @Test + @DisabledIfEnvironmentVariable(named = "CONDUCTOR_SERVER_TYPE", matches = "oss", + disabledReason = "batch correlation-id search (POST /workflow/correlated/batch) is not implemented by plain OSS Conductor, confirmed empirically (404 'No static resource api/workflow/correlated/batch')") public void testSearchByCorrelationIds() { List correlationIds = new ArrayList<>(); Set workflowNames = new HashSet<>(); @@ -188,6 +191,8 @@ public void testSkipTaskFromWorkflow() throws Exception { } @Test + @DisabledIfEnvironmentVariable(named = "CONDUCTOR_SERVER_TYPE", matches = "oss", + disabledReason = "POST /workflow/{workflowId}/variables is not implemented by plain OSS Conductor, confirmed empirically (404 'No static resource api/workflow/{id}/variables')") public void testUpdateVariables() { ConductorWorkflow workflow = new ConductorWorkflow<>(workflowExecutor); workflow.add(new SimpleTask("simple_task", "simple_task_ref")); diff --git a/tests/src/test/java/io/orkes/conductor/client/http/WorkflowStateUpdateTests.java b/tests/src/test/java/io/orkes/conductor/client/http/WorkflowStateUpdateTests.java index 286d428a5..0faad3086 100644 --- a/tests/src/test/java/io/orkes/conductor/client/http/WorkflowStateUpdateTests.java +++ b/tests/src/test/java/io/orkes/conductor/client/http/WorkflowStateUpdateTests.java @@ -22,6 +22,7 @@ import org.conductoross.conductor.common.model.WorkflowRun; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable; import com.netflix.conductor.client.exception.ConductorClientException; import com.netflix.conductor.common.metadata.tasks.Task; @@ -91,6 +92,8 @@ public String startWorkflow() { } @Test + @DisabledIfEnvironmentVariable(named = "CONDUCTOR_SERVER_TYPE", matches = "oss", + disabledReason = "POST /workflow/{workflowId}/state (updateWorkflow) is not implemented by plain OSS Conductor, confirmed empirically (404 'No static resource api/workflow/{id}/state')") public void test() { String workflowId = startWorkflow(); System.out.println(workflowId); @@ -135,6 +138,8 @@ public void test() { } @Test + @DisabledIfEnvironmentVariable(named = "CONDUCTOR_SERVER_TYPE", matches = "oss", + disabledReason = "workflow start idempotency keys are not honored by plain OSS Conductor, confirmed empirically (RETURN_EXISTING starts a brand-new run instead of returning the original workflowId)") public void testIdempotency() { StartWorkflowRequest startWorkflowRequest = new StartWorkflowRequest(); startWorkflowRequest.setName("sync_task_variable_updates"); diff --git a/tests/src/test/java/io/orkes/conductor/client/util/TestUtil.java b/tests/src/test/java/io/orkes/conductor/client/util/TestUtil.java index 65b13f72e..a0c4025b1 100644 --- a/tests/src/test/java/io/orkes/conductor/client/util/TestUtil.java +++ b/tests/src/test/java/io/orkes/conductor/client/util/TestUtil.java @@ -18,6 +18,7 @@ import java.time.Duration; import java.util.concurrent.Callable; import java.util.concurrent.TimeoutException; +import java.util.function.Predicate; import com.netflix.conductor.common.config.ObjectMapperProvider; import com.netflix.conductor.common.metadata.workflow.WorkflowDef; @@ -160,4 +161,28 @@ private static boolean isTerminalFailure(Workflow workflow) { return workflow.getStatus() == Workflow.WorkflowStatus.FAILED || workflow.getStatus() == Workflow.WorkflowStatus.TERMINATED; } + + /** + * Repeatedly invokes {@code supplier} until {@code condition} accepts its result, or the + * time budget is exhausted, sleeping {@code pollIntervalMs} between attempts. Useful for + * assertions against eventually-consistent state (e.g. search-index writes) instead of a + * single point-in-time check. + * + * @return the first result accepted by {@code condition} + * @throws TimeoutException if no result satisfies {@code condition} within maxWaitTimeMs + */ + public static T waitUntil(Callable supplier, Predicate condition, + long maxWaitTimeMs, long pollIntervalMs) throws Exception { + long endTime = System.currentTimeMillis() + maxWaitTimeMs; + T last = supplier.call(); + while (!condition.test(last)) { + if (System.currentTimeMillis() >= endTime) { + throw new TimeoutException( + String.format("Condition not met within %dms. Last value: %s", maxWaitTimeMs, last)); + } + Thread.sleep(pollIntervalMs); + last = supplier.call(); + } + return last; + } } diff --git a/tests/src/test/java/io/orkes/conductor/sdk/WorkflowSDKTests.java b/tests/src/test/java/io/orkes/conductor/sdk/WorkflowSDKTests.java index 452da6872..883855e31 100644 --- a/tests/src/test/java/io/orkes/conductor/sdk/WorkflowSDKTests.java +++ b/tests/src/test/java/io/orkes/conductor/sdk/WorkflowSDKTests.java @@ -14,9 +14,6 @@ import java.util.Map; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -33,12 +30,13 @@ import com.netflix.conductor.sdk.workflow.task.WorkerTask; import io.orkes.conductor.client.util.ClientTestUtil; +import io.orkes.conductor.client.util.TestUtil; public class WorkflowSDKTests { @Test - public void testCreateWorkflow() { + public void testCreateWorkflow() throws Exception { ConductorClient client = ClientTestUtil.getClient(); AnnotatedWorkerExecutor workerExecutor = new AnnotatedWorkerExecutor(new TaskClient(client), new WorkerConfiguration()); @@ -57,10 +55,14 @@ public void testCreateWorkflow() { CompletableFuture result = workflow.execute(Map.of("name", "orkes")); Assertions.assertNotNull(result); try { - Workflow executedWorkflow = result.get(3, TimeUnit.SECONDS); + // Poll with a time budget instead of a single point-in-time get(): worker + // registration + polling + task execution can take longer than a couple of + // seconds under load (e.g. running alongside the rest of the integration suite). + TestUtil.waitUntil(result::isDone, Boolean::booleanValue, 30_000, 3_000); + Workflow executedWorkflow = result.get(); Assertions.assertNotNull(executedWorkflow); Assertions.assertEquals(Workflow.WorkflowStatus.COMPLETED, executedWorkflow.getStatus()); - } catch (InterruptedException | ExecutionException | TimeoutException e) { + } catch (Exception e) { Assertions.fail(e.getMessage()); } } From 37da691ef7453d16d75c1e691a1d932a52d99a9c Mon Sep 17 00:00:00 2001 From: Chris Hagglund Date: Thu, 13 Aug 2026 12:50:00 -0600 Subject: [PATCH 02/10] give permission to update test report --- .github/workflows/ci.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ef7d836a9..87a16f1e7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,6 +18,10 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +permissions: + contents: read + checks: write + jobs: documentation-validation: runs-on: ubuntu-latest From d8fca483bf96c84097e8746d79a6b94044bad80a Mon Sep 17 00:00:00 2001 From: Chris Hagglund Date: Fri, 14 Aug 2026 09:34:09 -0600 Subject: [PATCH 03/10] increase timeout on a test that fails --- .github/workflows/ci.yml | 8 ++++++-- .../java/io/orkes/conductor/sdk/WorkflowSDKTests.java | 5 +++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 87a16f1e7..1a3317137 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -161,7 +161,9 @@ jobs: - name: Check Tests Status if: steps.tests.outcome == 'failure' - run: exit 1 + run: | + echo "::error::Tests failed. See the 'Run Tests' step above for the Gradle/test output, and the 'Publish Test Report' step's JUnit summary for which test(s) failed." + exit 1 integration-tests-oss: runs-on: ubuntu-latest @@ -213,4 +215,6 @@ jobs: - name: Check Integration Tests Status if: steps.integration_tests.outcome == 'failure' - run: exit 1 \ No newline at end of file + run: | + echo "::error::Integration tests (OSS) failed. See the 'Run integration tests (OSS)' step above for the Gradle/test output, the 'Dump Conductor logs' step for server-side logs, and the 'Publish Test Report' step's JUnit summary for which test(s) failed." + exit 1 \ No newline at end of file diff --git a/tests/src/test/java/io/orkes/conductor/sdk/WorkflowSDKTests.java b/tests/src/test/java/io/orkes/conductor/sdk/WorkflowSDKTests.java index 883855e31..0686fda89 100644 --- a/tests/src/test/java/io/orkes/conductor/sdk/WorkflowSDKTests.java +++ b/tests/src/test/java/io/orkes/conductor/sdk/WorkflowSDKTests.java @@ -57,8 +57,9 @@ public void testCreateWorkflow() throws Exception { try { // Poll with a time budget instead of a single point-in-time get(): worker // registration + polling + task execution can take longer than a couple of - // seconds under load (e.g. running alongside the rest of the integration suite). - TestUtil.waitUntil(result::isDone, Boolean::booleanValue, 30_000, 3_000); + // seconds under load (e.g. running alongside the rest of the integration suite, + // or on a shared/slower CI runner -- 30s was observed to be marginal in CI). + TestUtil.waitUntil(result::isDone, Boolean::booleanValue, 60_000, 3_000); Workflow executedWorkflow = result.get(); Assertions.assertNotNull(executedWorkflow); Assertions.assertEquals(Workflow.WorkflowStatus.COMPLETED, executedWorkflow.getStatus()); From a8f1409801d492f0c0e05f368f27da71ffdab3de Mon Sep 17 00:00:00 2001 From: Chris Hagglund Date: Fri, 14 Aug 2026 10:03:32 -0600 Subject: [PATCH 04/10] add helper for test tolernace of varying behavior between oss and enterprise for a few endpoints, but the 404 vs the oss-way explicitly --- .../client/http/EventClientTests.java | 11 +++--- .../client/http/MetadataClientTests.java | 36 ++++++------------- .../orkes/conductor/client/util/TestUtil.java | 28 +++++++++++++++ 3 files changed, 42 insertions(+), 33 deletions(-) diff --git a/tests/src/test/java/io/orkes/conductor/client/http/EventClientTests.java b/tests/src/test/java/io/orkes/conductor/client/http/EventClientTests.java index 47c2d28af..bbfdb195e 100644 --- a/tests/src/test/java/io/orkes/conductor/client/http/EventClientTests.java +++ b/tests/src/test/java/io/orkes/conductor/client/http/EventClientTests.java @@ -24,6 +24,7 @@ import io.orkes.conductor.client.util.ClientTestUtil; import io.orkes.conductor.client.util.Commons; +import io.orkes.conductor.client.util.TestUtil; public class EventClientTests { private static final String EVENT_NAME = "test_sdk_java_event_name"; @@ -35,13 +36,9 @@ void testEventHandler() { try { eventClient.unregisterEventHandler(EVENT_NAME); } catch (ConductorClientException e) { - // Best-effort cleanup: tolerate "doesn't exist" regardless of how the - // server reports it. Orkes Enterprise returns 404; plain OSS Conductor - // returns a 500 with a "not found" message instead (confirmed - // empirically) -- treat both as success for this purpose. - if (e.getStatus() != 404 && !e.getMessage().contains("not found")) { - throw e; - } + // Best-effort cleanup: tolerate "doesn't exist" in whichever shape the server + // we're running against actually reports it. + TestUtil.assertNotFoundOrRethrow(e, "not found"); } EventHandler eventHandler = getEventHandler(); eventClient.registerEventHandler(eventHandler); diff --git a/tests/src/test/java/io/orkes/conductor/client/http/MetadataClientTests.java b/tests/src/test/java/io/orkes/conductor/client/http/MetadataClientTests.java index 837e276ef..bfe82d8d0 100644 --- a/tests/src/test/java/io/orkes/conductor/client/http/MetadataClientTests.java +++ b/tests/src/test/java/io/orkes/conductor/client/http/MetadataClientTests.java @@ -39,13 +39,9 @@ void taskDefinition() { try { metadataClient.unregisterTaskDef(Commons.TASK_NAME); } catch (ConductorClientException e) { - // Best-effort cleanup: tolerate "doesn't exist" regardless of how the - // server reports it. Orkes Enterprise returns 404; plain OSS Conductor - // returns a 500 with a "No such task definition" message instead - // (confirmed empirically) -- treat both as success for this purpose. - if (e.getStatus() != 404 && !e.getMessage().contains("No such task definition")) { - throw e; - } + // Best-effort cleanup: tolerate "doesn't exist" in whichever shape the server + // we're running against actually reports it. + TestUtil.assertNotFoundOrRethrow(e, "No such task definition"); } TaskDef taskDef = Commons.getTaskDef(); metadataClient.registerTaskDefs(List.of(taskDef)); @@ -59,13 +55,9 @@ void workflow() { try { metadataClient.unregisterWorkflowDef(Commons.WORKFLOW_NAME, Commons.WORKFLOW_VERSION); } catch (ConductorClientException e) { - // Best-effort cleanup: tolerate "doesn't exist" regardless of how the - // server reports it. Orkes Enterprise returns 404; plain OSS Conductor - // returns a 500 with a "No such workflow definition" message instead - // (confirmed empirically) -- treat both as success for this purpose. - if (e.getStatus() != 404 && !e.getMessage().contains("No such workflow definition")) { - throw e; - } + // Best-effort cleanup: tolerate "doesn't exist" in whichever shape the server + // we're running against actually reports it. + TestUtil.assertNotFoundOrRethrow(e, "No such workflow definition"); } metadataClient.registerTaskDefs(List.of(Commons.getTaskDef())); WorkflowDef workflowDef = WorkflowUtil.getWorkflowDef(); @@ -82,18 +74,10 @@ void workflow() { } metadataClient.updateWorkflowDefs(List.of(workflowDef)); metadataClient.updateWorkflowDefs(List.of(workflowDef), true); - try { - metadataClient.registerWorkflowDef(workflowDef, true); - } catch (ConductorClientException e) { - // The overwrite=true query param on POST /metadata/workflow is not - // honored by plain OSS Conductor, confirmed empirically (it still - // rejects an existing name+version instead of overwriting); the - // updateWorkflowDefs(..., true) call above already re-established - // the intended definition. - if (e.getStatus() != 500 || !e.getMessage().contains("already exists")) { - throw e; - } - } + // Both Orkes Enterprise and plain OSS Conductor honor overwrite=true on an existing + // name+version and succeed outright (verified empirically against a freshly-pulled + // OSS image; an earlier assumption that OSS rejected this with a 500 no longer holds). + metadataClient.registerWorkflowDef(workflowDef, true); ((OrkesMetadataClient) metadataClient) .getWorkflowDefWithMetadata(Commons.WORKFLOW_NAME, Commons.WORKFLOW_VERSION); WorkflowDef receivedWorkflowDef = metadataClient.getWorkflowDef(Commons.WORKFLOW_NAME, diff --git a/tests/src/test/java/io/orkes/conductor/client/util/TestUtil.java b/tests/src/test/java/io/orkes/conductor/client/util/TestUtil.java index a0c4025b1..c52712fb1 100644 --- a/tests/src/test/java/io/orkes/conductor/client/util/TestUtil.java +++ b/tests/src/test/java/io/orkes/conductor/client/util/TestUtil.java @@ -20,6 +20,7 @@ import java.util.concurrent.TimeoutException; import java.util.function.Predicate; +import com.netflix.conductor.client.exception.ConductorClientException; import com.netflix.conductor.common.config.ObjectMapperProvider; import com.netflix.conductor.common.metadata.workflow.WorkflowDef; import com.netflix.conductor.common.run.Workflow; @@ -185,4 +186,31 @@ public static T waitUntil(Callable supplier, Predicate condition, } return last; } + + /** + * Whether the suite is currently running against plain OSS Conductor rather than Orkes + * Enterprise, per the same {@code CONDUCTOR_SERVER_TYPE} signal that + * {@code @DisabledIfEnvironmentVariable(named = "CONDUCTOR_SERVER_TYPE", matches = "oss")} + * checks for test gating. + */ + public static boolean isOssServer() { + return "oss".equals(System.getenv("CONDUCTOR_SERVER_TYPE")); + } + + /** + * Asserts a caught exception represents "resource doesn't exist", in the shape specific to + * whichever server type {@code CONDUCTOR_SERVER_TYPE} says we're running against: Orkes + * Enterprise reports a proper 404; plain OSS Conductor instead reports a 500 whose message + * contains {@code ossMessageSubstring} (empirically confirmed per endpoint). Anything else + * is rethrown, since it isn't the "doesn't exist" case this is meant to tolerate. + */ + public static void assertNotFoundOrRethrow(ConductorClientException e, String ossMessageSubstring) { + if (isOssServer()) { + if (e.getStatus() != 500 || e.getMessage() == null || !e.getMessage().contains(ossMessageSubstring)) { + throw e; + } + } else if (e.getStatus() != 404) { + throw e; + } + } } From 56cb1df2189b3ca5ae69405be1d660d3d6f007a3 Mon Sep 17 00:00:00 2001 From: Chris Hagglund Date: Fri, 14 Aug 2026 10:21:08 -0600 Subject: [PATCH 05/10] remove unneeded oss-vs-orkes tolerance --- .../conductor/client/http/MetadataClientTests.java | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/tests/src/test/java/io/orkes/conductor/client/http/MetadataClientTests.java b/tests/src/test/java/io/orkes/conductor/client/http/MetadataClientTests.java index bfe82d8d0..5210001b1 100644 --- a/tests/src/test/java/io/orkes/conductor/client/http/MetadataClientTests.java +++ b/tests/src/test/java/io/orkes/conductor/client/http/MetadataClientTests.java @@ -61,17 +61,7 @@ void workflow() { } metadataClient.registerTaskDefs(List.of(Commons.getTaskDef())); WorkflowDef workflowDef = WorkflowUtil.getWorkflowDef(); - try { - metadataClient.registerWorkflowDef(workflowDef); - } catch (ConductorClientException e) { - // Commons.WORKFLOW_NAME/VERSION is shared fixture data used by several - // test classes in this suite; tolerate an "already exists" collision - // here since the update/overwrite calls below re-establish the - // intended definition regardless of which class registered it first. - if (e.getStatus() != 500 || !e.getMessage().contains("already exists")) { - throw e; - } - } + metadataClient.registerWorkflowDef(workflowDef); metadataClient.updateWorkflowDefs(List.of(workflowDef)); metadataClient.updateWorkflowDefs(List.of(workflowDef), true); // Both Orkes Enterprise and plain OSS Conductor honor overwrite=true on an existing From 427d5d11b2706c402af1db56b14921fb47dfb9c6 Mon Sep 17 00:00:00 2001 From: Chris Hagglund Date: Fri, 14 Aug 2026 12:23:58 -0600 Subject: [PATCH 06/10] remove unnecessary retry utility, add a try/except to a thing that is scheduleAtFixedRate'd --- .../workflow/executor/WorkflowExecutor.java | 14 ++++++++--- .../orkes/conductor/client/util/TestUtil.java | 25 ------------------- .../orkes/conductor/sdk/WorkflowSDKTests.java | 10 +++----- 3 files changed, 13 insertions(+), 36 deletions(-) diff --git a/conductor-client/src/main/java/com/netflix/conductor/sdk/workflow/executor/WorkflowExecutor.java b/conductor-client/src/main/java/com/netflix/conductor/sdk/workflow/executor/WorkflowExecutor.java index 33654d78d..b88d85150 100644 --- a/conductor-client/src/main/java/com/netflix/conductor/sdk/workflow/executor/WorkflowExecutor.java +++ b/conductor-client/src/main/java/com/netflix/conductor/sdk/workflow/executor/WorkflowExecutor.java @@ -174,10 +174,16 @@ private void initMonitor() { for (Map.Entry> entry : runningWorkflowFutures.entrySet()) { String workflowId = entry.getKey(); CompletableFuture future = entry.getValue(); - Workflow workflow = workflowClient.getWorkflow(workflowId, true); - if (workflow.getStatus().isTerminal()) { - future.complete(workflow); - runningWorkflowFutures.remove(workflowId); + try { + Workflow workflow = workflowClient.getWorkflow(workflowId, true); + if (workflow.getStatus().isTerminal()) { + future.complete(workflow); + runningWorkflowFutures.remove(workflowId); + } + } catch (Exception e) { + // scheduleAtFixedRate silently kills all future ticks on any uncaught exception, so catch here instead of letting one transient error stop completion-tracking forever. + LOGGER.warn("Error polling workflow {} for completion; will retry on " + + "the next tick", workflowId, e); } } }, diff --git a/tests/src/test/java/io/orkes/conductor/client/util/TestUtil.java b/tests/src/test/java/io/orkes/conductor/client/util/TestUtil.java index c52712fb1..6827f61ad 100644 --- a/tests/src/test/java/io/orkes/conductor/client/util/TestUtil.java +++ b/tests/src/test/java/io/orkes/conductor/client/util/TestUtil.java @@ -18,7 +18,6 @@ import java.time.Duration; import java.util.concurrent.Callable; import java.util.concurrent.TimeoutException; -import java.util.function.Predicate; import com.netflix.conductor.client.exception.ConductorClientException; import com.netflix.conductor.common.config.ObjectMapperProvider; @@ -163,30 +162,6 @@ private static boolean isTerminalFailure(Workflow workflow) { || workflow.getStatus() == Workflow.WorkflowStatus.TERMINATED; } - /** - * Repeatedly invokes {@code supplier} until {@code condition} accepts its result, or the - * time budget is exhausted, sleeping {@code pollIntervalMs} between attempts. Useful for - * assertions against eventually-consistent state (e.g. search-index writes) instead of a - * single point-in-time check. - * - * @return the first result accepted by {@code condition} - * @throws TimeoutException if no result satisfies {@code condition} within maxWaitTimeMs - */ - public static T waitUntil(Callable supplier, Predicate condition, - long maxWaitTimeMs, long pollIntervalMs) throws Exception { - long endTime = System.currentTimeMillis() + maxWaitTimeMs; - T last = supplier.call(); - while (!condition.test(last)) { - if (System.currentTimeMillis() >= endTime) { - throw new TimeoutException( - String.format("Condition not met within %dms. Last value: %s", maxWaitTimeMs, last)); - } - Thread.sleep(pollIntervalMs); - last = supplier.call(); - } - return last; - } - /** * Whether the suite is currently running against plain OSS Conductor rather than Orkes * Enterprise, per the same {@code CONDUCTOR_SERVER_TYPE} signal that diff --git a/tests/src/test/java/io/orkes/conductor/sdk/WorkflowSDKTests.java b/tests/src/test/java/io/orkes/conductor/sdk/WorkflowSDKTests.java index 0686fda89..a9091edc1 100644 --- a/tests/src/test/java/io/orkes/conductor/sdk/WorkflowSDKTests.java +++ b/tests/src/test/java/io/orkes/conductor/sdk/WorkflowSDKTests.java @@ -14,6 +14,7 @@ import java.util.Map; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -30,7 +31,6 @@ import com.netflix.conductor.sdk.workflow.task.WorkerTask; import io.orkes.conductor.client.util.ClientTestUtil; -import io.orkes.conductor.client.util.TestUtil; public class WorkflowSDKTests { @@ -55,12 +55,8 @@ public void testCreateWorkflow() throws Exception { CompletableFuture result = workflow.execute(Map.of("name", "orkes")); Assertions.assertNotNull(result); try { - // Poll with a time budget instead of a single point-in-time get(): worker - // registration + polling + task execution can take longer than a couple of - // seconds under load (e.g. running alongside the rest of the integration suite, - // or on a shared/slower CI runner -- 30s was observed to be marginal in CI). - TestUtil.waitUntil(result::isDone, Boolean::booleanValue, 60_000, 3_000); - Workflow executedWorkflow = result.get(); + // WorkflowExecutor's monitor thread polls every 100ms (see initMonitor()), so 10s is a generous margin. + Workflow executedWorkflow = result.get(10, TimeUnit.SECONDS); Assertions.assertNotNull(executedWorkflow); Assertions.assertEquals(Workflow.WorkflowStatus.COMPLETED, executedWorkflow.getStatus()); } catch (Exception e) { From 22fb9d23ed8015a995dabac3ae5952630b7adae5 Mon Sep 17 00:00:00 2001 From: Chris Hagglund Date: Fri, 14 Aug 2026 12:41:39 -0600 Subject: [PATCH 07/10] restore schedulerresource to prior state --- .../orkes/conductor/client/http/SchedulerResource.java | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/conductor-client/src/main/java/io/orkes/conductor/client/http/SchedulerResource.java b/conductor-client/src/main/java/io/orkes/conductor/client/http/SchedulerResource.java index 71e0d030b..fe9d7db38 100644 --- a/conductor-client/src/main/java/io/orkes/conductor/client/http/SchedulerResource.java +++ b/conductor-client/src/main/java/io/orkes/conductor/client/http/SchedulerResource.java @@ -167,19 +167,14 @@ public void resumeSchedule(String name) { /** * Enterprise scheduler endpoints accept GET while OSS accepts PUT. Retry only * a method-not-allowed response so application and authentication failures - * retain their original behavior. Orkes Enterprise reports this as a proper - * 405; plain OSS Conductor instead reports it as a 500 with a "Request - * method '...' is not supported" message (confirmed empirically) -- treat - * both as a signal to retry with PUT. + * retain their original behavior. */ private void executeGetThenPutOnMethodNotAllowed( ConductorClientRequest getRequest, ConductorClientRequest putRequest) { try { client.execute(getRequest); } catch (ConductorClientException e) { - if (e.getStatus() != 405 - && !(e.getStatus() == 500 && e.getMessage() != null - && e.getMessage().contains("is not supported"))) { + if (e.getStatus() != 405) { throw e; } client.execute(putRequest); From 14cc5a8f18a636f5ab3d8b371229671724abd40d Mon Sep 17 00:00:00 2001 From: Chris Hagglund Date: Fri, 14 Aug 2026 12:48:44 -0600 Subject: [PATCH 08/10] remove unnecessary comments --- .../io/orkes/conductor/client/http/MetadataClientTests.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/src/test/java/io/orkes/conductor/client/http/MetadataClientTests.java b/tests/src/test/java/io/orkes/conductor/client/http/MetadataClientTests.java index 5210001b1..d7430a97b 100644 --- a/tests/src/test/java/io/orkes/conductor/client/http/MetadataClientTests.java +++ b/tests/src/test/java/io/orkes/conductor/client/http/MetadataClientTests.java @@ -64,9 +64,6 @@ void workflow() { metadataClient.registerWorkflowDef(workflowDef); metadataClient.updateWorkflowDefs(List.of(workflowDef)); metadataClient.updateWorkflowDefs(List.of(workflowDef), true); - // Both Orkes Enterprise and plain OSS Conductor honor overwrite=true on an existing - // name+version and succeed outright (verified empirically against a freshly-pulled - // OSS image; an earlier assumption that OSS rejected this with a 500 no longer holds). metadataClient.registerWorkflowDef(workflowDef, true); ((OrkesMetadataClient) metadataClient) .getWorkflowDefWithMetadata(Commons.WORKFLOW_NAME, Commons.WORKFLOW_VERSION); From 9b662598b7a3bd244328ce42deeacbcc725611b0 Mon Sep 17 00:00:00 2001 From: Chris Hagglund Date: Mon, 17 Aug 2026 09:38:20 -0600 Subject: [PATCH 09/10] attempt to improve flaky test by removing a redundant call to startPolling --- .../src/test/java/io/orkes/conductor/sdk/WorkflowSDKTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/src/test/java/io/orkes/conductor/sdk/WorkflowSDKTests.java b/tests/src/test/java/io/orkes/conductor/sdk/WorkflowSDKTests.java index a9091edc1..25d5d303f 100644 --- a/tests/src/test/java/io/orkes/conductor/sdk/WorkflowSDKTests.java +++ b/tests/src/test/java/io/orkes/conductor/sdk/WorkflowSDKTests.java @@ -40,8 +40,8 @@ public void testCreateWorkflow() throws Exception { ConductorClient client = ClientTestUtil.getClient(); AnnotatedWorkerExecutor workerExecutor = new AnnotatedWorkerExecutor(new TaskClient(client), new WorkerConfiguration()); + // initWorkers() already starts polling; a redundant extra startPolling() call here used to race with AnnotatedWorkerExecutor's own double-start (likely a real bug there) and could drop the first polled task. workerExecutor.initWorkers("io.orkes.conductor.sdk"); - workerExecutor.startPolling(); WorkflowExecutor executor = new WorkflowExecutor(client, workerExecutor); From c49f5477fb9fcd0c17f7dec2feed312eef59a963 Mon Sep 17 00:00:00 2001 From: Chris Hagglund Date: Mon, 17 Aug 2026 10:10:46 -0600 Subject: [PATCH 10/10] testing removal of redundant call to startPolling --- .../workflow/executor/task/AnnotatedWorkerExecutor.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/conductor-client/src/main/java/com/netflix/conductor/sdk/workflow/executor/task/AnnotatedWorkerExecutor.java b/conductor-client/src/main/java/com/netflix/conductor/sdk/workflow/executor/task/AnnotatedWorkerExecutor.java index e2ed6d107..349a38ba9 100644 --- a/conductor-client/src/main/java/com/netflix/conductor/sdk/workflow/executor/task/AnnotatedWorkerExecutor.java +++ b/conductor-client/src/main/java/com/netflix/conductor/sdk/workflow/executor/task/AnnotatedWorkerExecutor.java @@ -76,8 +76,9 @@ public AnnotatedWorkerExecutor(TaskClient taskClient, WorkerConfiguration worker * implementation */ public synchronized void initWorkers(String... basePackages) { + // scanWorkers() -> initWorkersFromClasses() -> initWorkersFromInstances() already calls startPolling(); + // an extra call here used to race with that one and could drop a task polled by the runner it replaces. scanWorkers(basePackages); - startPolling(); } public synchronized void initWorkersFromInstances(List workerInstances) { @@ -157,7 +158,10 @@ private void scanWorkers(String... basePackages) { initWorkersFromClasses(classes); } catch (Exception e) { - LOGGER.error("Error while scanning for workers: ", e); + // Rethrow (unchecked) rather than swallow: initWorkers() no longer has its own startPolling() + // fallback, so a swallowed failure here would otherwise leave the caller believing workers are + // running when none were ever started. + throw new RuntimeException("Error while scanning for workers", e); } }