diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ecca05d7a..1a3317137 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,11 +8,20 @@ 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 }} cancel-in-progress: true +permissions: + contents: read + checks: write + jobs: documentation-validation: runs-on: ubuntu-latest @@ -152,4 +161,60 @@ 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 + 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: | + 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/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/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/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); } } 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..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,9 +36,9 @@ void testEventHandler() { try { eventClient.unregisterEventHandler(EVENT_NAME); } catch (ConductorClientException e) { - if (e.getStatus() != 404) { - 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 1e91169d9..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 @@ -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,9 +39,9 @@ void taskDefinition() { try { metadataClient.unregisterTaskDef(Commons.TASK_NAME); } catch (ConductorClientException e) { - if (e.getStatus() != 404) { - 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)); @@ -54,9 +55,9 @@ void workflow() { try { metadataClient.unregisterWorkflowDef(Commons.WORKFLOW_NAME, Commons.WORKFLOW_VERSION); } catch (ConductorClientException e) { - if (e.getStatus() != 404) { - 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(); @@ -73,6 +74,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 +101,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..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 @@ -19,6 +19,7 @@ import java.util.concurrent.Callable; import java.util.concurrent.TimeoutException; +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; @@ -160,4 +161,31 @@ private static boolean isTerminalFailure(Workflow workflow) { return workflow.getStatus() == Workflow.WorkflowStatus.FAILED || workflow.getStatus() == Workflow.WorkflowStatus.TERMINATED; } + + /** + * 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; + } + } } 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..25d5d303f 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,7 @@ 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; @@ -38,12 +36,12 @@ 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()); + // 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); @@ -57,10 +55,11 @@ public void testCreateWorkflow() { CompletableFuture result = workflow.execute(Map.of("name", "orkes")); Assertions.assertNotNull(result); try { - Workflow executedWorkflow = result.get(3, TimeUnit.SECONDS); + // 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 (InterruptedException | ExecutionException | TimeoutException e) { + } catch (Exception e) { Assertions.fail(e.getMessage()); } }