Files
Naomi Most c8e63df574 feat(rest): add task signal endpoints (#1205)
* feat(rest): add task signal endpoints

Adds the task signal endpoints the Go SDK / CLI already call but OSS never
implemented (conductor-oss/conductor#1197):

  POST /api/tasks/{workflowId}/{status}/signal        (async)
  POST /api/tasks/{workflowId}/{status}/signal/sync   (sync, returns SignalResponse)

Before this, the SDK's SignalAsync hit POST /tasks/{wfId}/{status}/signal, which
had no matching route, so Spring fell through to the all-variable update route
/{workflowId}/{taskRefName}/{status} and tried to coerce the literal "signal"
into TaskResult.Status -> MethodArgumentTypeMismatchException. Adding the literal
/signal segment makes that pattern more specific, so it now wins; a MockMvc
routing test (with PathPatternParser, mirroring production) locks this in.

"Signal" finds the first non-terminal WAIT task in the workflow (descending into
running sub-workflows) and applies the given status + output to it, via the new
TaskService.signalTask. The sync variant then waits for the workflow to settle
into its next blocking/terminal state and renders a SignalResponse per the
returnStrategy, reusing the same poll-to-response logic as executeWorkflow --
extracted into WorkflowSignalResponder so both controllers share it.

Tests: TaskServiceTest (signalTask found / not-found), TaskResourceTest (async,
sync, not-found, route resolution). Full conductor-rest suite green.

* fix(signal): add signalTimeout field and E2E Groovy integration tests

Two issues raised in PR #1205 review:

1. `SignalResponse` was missing the `signalTimeout` boolean that Orkes sets
   when the sync-signal poll times out. Without it a timed-out response looks
   identical to a successful one. Added `signalTimeout` to `SignalResponse`
   (absorbed by `WorkflowRun` and `TaskRun` via inheritance), propagated it
   through `NotificationResult.toResponse()`, and set it to `true` in
   `WorkflowSignalResponder`'s timeout fallback path.

2. Added `SignalTaskSpec` to the test-harness — a Spring Boot integration test
   backed by a real Redis testcontainer that exercises `TaskService.signalTask()`
   without any mocking: direct-WAIT-task signal, signal-with-no-blocker,
   signal-on-nonexistent-workflow, and sub-workflow descent.

* test(signal): remove mocked signal tests — covered by SignalTaskSpec E2E

* fix(test): SignalTaskSpec — expect NotFoundException for missing workflow

ExecutionDAOFacade.getWorkflow() throws NotFoundException for unknown IDs
(does not return null). The test was asserting null — corrected to thrown().

The e2e WorkflowRerunTests failure in the same CI run is pre-existing and
unrelated to signal changes (it was already failing on the prior commit).

* test(e2e): HTTP-level signal endpoint tests (async + sync)

* fix: spotless formatting violations in SignalTaskTest
2026-07-17 16:29:17 -07:00
..
2026-03-26 12:13:56 -07:00
2026-03-26 12:13:56 -07:00
2026-03-26 12:13:56 -07:00
2026-03-26 12:13:56 -07:00
2026-03-26 12:13:56 -07:00

Conductor E2E Tests

End-to-end tests for conductor-oss covering workflow execution, task types, control flow, event handling, metadata operations, and data processing.

Prerequisites

  • Docker and Docker Compose
  • Java 21+
  • Gradle (use the ./gradlew wrapper at the repo root)

Quick Start

The recommended way to run the full suite is via a convenience script. Each script starts the required Docker services, waits for the server to be healthy, runs all tests, then tears down.

From the repo root:

# Postgres + Elasticsearch 7 (recommended for this project)
./e2e/run_tests-postgres.sh

# Other backends:
./e2e/run_tests.sh                  # Redis + Elasticsearch 7 (default)
./e2e/run_tests-es8.sh              # Redis + Elasticsearch 8
./e2e/run_tests-postgres-es7.sh     # Postgres + Elasticsearch 7 (explicit)
./e2e/run_tests-redis-os2.sh        # Redis + OpenSearch 2.x
./e2e/run_tests-redis-os3.sh        # Redis + OpenSearch 3.x
./e2e/run_tests-mysql.sh            # MySQL

The server listens on http://localhost:8000 by default. Override with:

SERVER_ROOT_URI=http://localhost:9090 ./e2e/run_tests-postgres.sh

Manual Setup

If you already have a Conductor server running, skip the scripts and run Gradle directly:

# Using the environment variable (recommended)
RUN_E2E=true SERVER_ROOT_URI=http://localhost:6000 ./gradlew :conductor-e2e:test

# Or using Gradle properties
./gradlew :conductor-e2e:test -PrunE2E -DSERVER_ROOT_URI=http://localhost:8000

Tests are skipped by default during a normal ./gradlew build to avoid requiring a running server. The RUN_E2E=true env var or -PrunE2E flag is required to enable them.

Building a local server image

If you need to test against a locally built server (e.g., after changing core/ code):

# 1. Build the server JAR
./gradlew :conductor-server:build -x test

# 2. Build the Docker image
docker build -t conductor:server -f docker/server/Dockerfile .

# 3. Start with the e2e compose file (maps port 6000 → 8080)
docker compose -f docker/docker-compose-postgres-e2e.yaml up -d

# 4. Run tests
RUN_E2E=true SERVER_ROOT_URI=http://localhost:6000 ./gradlew :conductor-e2e:test

Test Options

Run a specific test class or method

RUN_E2E=true SERVER_ROOT_URI=http://localhost:6000 \
  ./gradlew :conductor-e2e:test --tests "io.conductor.e2e.control.SubWorkflowTests"

# Single method
RUN_E2E=true SERVER_ROOT_URI=http://localhost:6000 \
  ./gradlew :conductor-e2e:test --tests "io.conductor.e2e.control.DoWhileTests.testDoWhileSetVariableFix"

Run the server-backed agent lifecycle suite

RUN_E2E=true SERVER_ROOT_URI=http://localhost:8080 \
  ./gradlew :conductor-e2e:test --tests "io.conductor.e2e.task.AgentTaskTests"

This deterministic suite needs no LLM credentials. It covers basic and nested agent execution, measured callback/poll intervals, manually completed long-running work, multi-turn human input, concurrent calls, parent and explicit CANCEL_AGENT cancellation, child cancellation, wrapper deadlines, agent workflow timeouts, internal failures, and missing agents.

Exclude a test class

./gradlew :conductor-e2e:test -PrunE2E -PexcludeTests="**/HTTPTaskTests*"

# Multiple patterns (comma-separated)
./gradlew :conductor-e2e:test -PrunE2E -PexcludeTests="**/HTTPTaskTests*,**/GraaljsTests*"

Parallelism

Tests run with maxParallelForks = 4 by default. Reduce if the server is under-resourced:

./gradlew :conductor-e2e:test -PrunE2E --max-workers=1

Test Suite Overview

Package What it covers
control DO_WHILE, SWITCH, SUB_WORKFLOW, DYNAMIC_FORK
task AGENT lifecycle, WAIT, HTTP, concurrency limit, task timeout, backoff
workflow Retry, restart, rerun, search, priority, failure workflows
processing GraalJS inline tasks, SET_VARIABLE, JSON_JQ
event Event handlers
metadata Workflow/task definition CRUD, event handler registration

Disabled Tests

18 tests are @Disabled due to conductor-oss behavioural differences or infrastructure constraints. All other skips during ./gradlew build (without RUN_E2E=true) are simply the suite waiting for a server — those tests are not broken.

Rerun behaviour (conductor-oss does not support rerun from non-terminal or complex task states)

Class Method Reason
WorkflowRerunTests testRerunFromWaitTask Rerunning a RUNNING workflow is not allowed; conductor-oss requires a terminal state first
WorkflowRerunTests testRerunForkJoinWorkflow Rerun from a completed fork-branch task does not re-schedule sibling branches
WorkflowRerunTests testRerunForkJoinWorkflowWithLoopTask Rerun from a DO_WHILE task inside a fork terminates the workflow instead of resuming
WorkflowRerunTests testRerunForkJoinWorkflowWithLoopTask2 Rerun from a DO_WHILE task inside a fork terminates the workflow instead of resuming
WorkflowRerunTests testRerunForkJoinWorkflowWithLoopOverTask Rerun from a DO_WHILE iteration task inside a fork terminates the workflow instead of resuming
WorkflowRerunTests testRerunSubWorkflowInsideFork Rerun from a SUB_WORKFLOW task inside a fork terminates the workflow instead of resuming
WorkflowRerunTests testRerunSubWorkflowInsideFork_SequentialBranch Rerun from a SUB_WORKFLOW task inside a fork terminates the workflow instead of resuming
WorkflowRerunTests testDoWhileRerun DO_WHILE task rerun leaves the workflow TERMINATED; sync task status is not reset to SCHEDULED
WorkflowRerunTests testSwitchTaskRerun SWITCH task rerun leaves the workflow TERMINATED; sync task status is not reset to SCHEDULED
WorkflowRerunTests testRerunForkJoinWithWaitAndSwitchTasks Rerun from fork-join with WAIT/SWITCH tasks terminates the workflow instead of resuming
WorkflowRerunTests switchRerunIssue SWITCH task rerun leaves the workflow TERMINATED; sync task status is not reset to SCHEDULED
WorkflowRerunTests switchRerunIssue2 SWITCH inside DO_WHILE rerun leaves the workflow TERMINATED

Infrastructure / external dependencies

Class Method Reason
HTTPTaskTests HTTPAsyncCompleteTest Requires httpbin-server internal service (http://httpbin-server:8081) not in the conductor-oss e2e docker setup
SyncWorkflowExecutionTest testSyncWorkflowExecution6 Depends on external HTTP services (orkes-api-tester.orkesconductor.com) not reliably accessible from conductor-oss e2e
PollTimeoutTests testPollTimeout Postgres-backed queue does not drain tasks from terminated workflows within the required window; cleanup timing is non-deterministic

SDK / test isolation

Class Method Reason
JavaSDKTests testSDK Shared AnnotatedWorkerExecutor thread pool is shut down by SwitchTests.@AfterAll when tests run in the same JVM; subsequent worker tasks are rejected
SwitchTests testSwitchNegetive SDK-based executeDynamic with a switch default case does not complete within the timeout in conductor-oss

Postgres-specific timing

The postgres-backed WAIT task sweeper adds roughly 10 seconds of overhead on top of the configured wait duration. Several tests account for this with extended timeouts (e.g., a "2 second" WAIT task may take up to 15 seconds end-to-end). If tests are flaky on slower machines, check whether a timeout needs to be increased further.